Files
deepseek-harness/docs/core-data-structures/filesystem.md
T
Dudu-0223 ef37ce3b9d refactor(fs): split filesystem seam into provider ctx.fs + policy ctx.fileContext
Implements the split-the-filesystem-seam RFC. ctx.fs shrinks to a text-storage
provider seam (resolve/stat/readText/streamText/writeText/editText with branded
FsTargetKey/FsVersion and an explicit FsWriteExpectation); the new
dsh-file-context package owns the model-facing policy (read windowing,
observed-state, write/edit freshness) as the concrete ctx.fileContext service.

Authorization is now freshness-based rather than full/partial view: a windowed
read records the file version and authorizes a later edit when the file is
unchanged, removing the dead-end where reading lines 100-150 of a large file
could not edit line 120. editText stays a provider primitive so version guard +
literal match + atomic rewrite remain one critical section, and the stale check
runs before matching so a stale edit reports FS_STALE_VERSION. tool-fs injects
fileContext, never reaching around to ctx.fs (the no-bypass contract).
2026-06-26 17:23:18 +08:00

6.0 KiB

Filesystem

The filesystem stack is split across four packages: a provider seam (dsh-fs, ctx.fs, text IO + guarded mutation), a local implementation (dsh-fs-local, local disk), a policy layer (dsh-file-context, ctx.fileContext, read windowing + write/edit freshness), and a consumer (dsh-tool-fs, the model-facing read/write/edit tools). Filesystem access is an optional capability, not part of the agent-loop spine, so its vocabulary lives here rather than in core.md. A sandboxed, remote, virtual, or project-scoped backend can implement the same FileSystem service without changing the policy layer or the tool schemas.

Provider source: packages/fs/fs/src/types.ts and packages/fs/fs/src/index.ts. Policy source: packages/fs/file-context/src/types.ts.

Target identity and metadata (provider seam)

Every operation resolves a user-supplied path to an opaque backend target first. Consumers may display displayPath, but must not parse targetKey (a branded opaque id) or assume it is a local absolute path.

interface FsTarget {
  inputPath: string
  targetKey: FsTargetKey
  displayPath: string
}

The backend owns file-version tokens — the freshness token a write/edit guards against. The policy layer stores them for stale checks; consumers do not interpret them. Both ids are branded opaque strings.

type FsTargetKey = Branded<'FsTargetKey'>
type FsVersion = Branded<'FsVersion'>

stat returns metadata (never content), or undefined when the target is absent. type lets the policy layer reject directories/special files before reading, and size lets it choose readText vs streamText without probing by failure.

interface FsInfo {
  version: FsVersion
  type: 'file' | 'directory' | 'other'
  size?: number
}

Write and edit guards (provider seam)

writeText takes an explicit write expectation rather than inferring intent. createIfAbsent creates a missing target and rejects an existing one with FS_NOT_OBSERVED; replaceIfVersion replaces only when the target exists at the observed version, else FS_STALE_VERSION.

type FsWriteExpectation =
  | { kind: 'createIfAbsent' }
  | { kind: 'replaceIfVersion'; version: FsVersion }
interface FsWriteOutcome {
  operation: 'create' | 'update'
  version: FsVersion
}

editText is a provider-level guarded mutation, not a read plus write composed in the policy layer. It verifies the expected version BEFORE literal matching (so a stale edit reports FS_STALE_VERSION, not a match failure against newer content), then applies the replacement and writes atomically — keeping matching, line-ending handling, stale checks, and atomic replacement inside one mutation critical section.

interface FsEditRequest {
  oldString: string
  newString: string
  replaceAll: boolean
}
interface FsEditOutcome {
  replacements: number
  replaceAll: boolean
  version: FsVersion
}

Execution context and read outcome (policy layer)

The policy layer needs just enough execution context to derive the observed-state owner. ToolExecution satisfies this shape, so dsh-tool-fs passes its execution object through without making dsh-file-context import the tool, agent, or session packages.

interface FileContextExec {
  agent?: {
    session?: object
  }
}

A text read is bounded by line window, byte cap, and backend limits. The outcome the model-facing read tool renders carries the file's version at read time; there is no full/partial view — authorization is freshness-based, so any windowed read can authorize a later write/edit when the file is unchanged.

interface FileReadRequest {
  offset: number
  limit: number
}
interface FileReadOutcome {
  offset: number
  limit: number
  lines: FileTextLine[]
  totalLines: number
  truncatedByBytes?: true
  version: FsVersion
}

Observed-file state (policy layer)

Observed state is a WeakMap<owner, Map<targetKey, { version }>> inside ctx.fileContext. An entry exists iff the owner has read that target through ctx.fileContext.read, so its presence is the read record — there is no separate hasRead flag and no view distinction. The owner is normally exec.agent.session, but the policy layer treats it as opaque and never reads its fields. A successful read/write/edit refreshes the recorded version for that owner; disposal drops everything (HMR safety).

Error taxonomy (provider seam)

Filesystem failures use stable FsErrorCode strings carried by FsError (HarnessError). The tool registry preserves { name, code } on error results, so retry, permission, and UI layers can branch without parsing text.

type FsErrorCode =
  | 'FS_NOT_FOUND'
  | 'FS_NOT_TEXT'
  | 'FS_NOT_REGULAR_FILE'
  | 'FS_STALE_VERSION'
  | 'FS_NOT_OBSERVED'
  | 'FS_AMBIGUOUS_EDIT'
  | 'FS_EDIT_NOT_FOUND'
  | 'FS_ABORTED'

FS_NOT_OBSERVED means no recorded read exists for this owner (or a createIfAbsent hit an existing file). FS_STALE_VERSION means the backend version no longer matches the observed one. Freshness authorization has no partial/full distinction, so there is no FS_PARTIAL_OBSERVATION.

The services

FileSystem (ctx.fs, abstract) owns the provider primitives: resolve, stat, readText, streamText, writeText, and editText. FileContext (ctx.fileContext, concrete) injects fs and owns the model-facing policy: read windows text and records observed state, write/edit derive the freshness expectation and refresh state. The generated wiring catalog shows the exact service signatures on events-and-services.md.