Files
deepseek-harness/packages/fs/fs/README.md
T
Tianyi Cui e64623ebfd refactor(fs): prune write-only fields and the dead routing knob from the seam
The fs seam split left four pieces of pre-split surface populated on
every call and read by nobody:

- STREAM_MIN_SIZE + FsIoInternals.streamMinSize in dsh-fs-local: the
  backend has no read routing (readWholeText/streamWholeText are
  separate primitives the caller picks), and the real 10 MiB routing
  constant lives in dsh-tool-fs's read tool. Delete the dead mirror and
  the knob whose JSDoc claimed an override that did not exist; the
  remaining FsIoInternals knobs stay (the atomic-write tests use them).
- FsTarget.inputPath: a "diagnostics only" field every backend and test
  fake had to fabricate, with zero production readers (policy and error
  messages use targetKey/displayPath). listDir gave children the bare
  entry name, which was nobody's input.
- FsEditOutcome.replacements/.replaceAll: replacements had no reader
  (the single-match policy is enforced by the FS_AMBIGUOUS_EDIT /
  FS_EDIT_NOT_FOUND throws, whose message keeps the internal count);
  replaceAll only echoed the replace_all argument back to
  formatEditOutput, which now takes it from the parsed args. The
  outcome shrinks to { version, before, after }, parallel to
  FsWriteOutcome's backend-discovered fields. Emitted text is unchanged
  for both branches (no snapshot churn).
- FileReadOutcome.limit/.version: formatReadOutput renders
  offset/lines/totalLines/truncatedByBytes only, and the fs/observed
  emit uses info.version directly.

Backends shed four fabrication obligations and gain none. Doc pastes
(core-data-structures/filesystem.md), the dsh-fs README resolve row,
and the test fakes shrink with the types. RFC moved to
implemented/simplification and amended to the shipped shape
(FsEditSpec -> FsEditRequest name fix; manifest rows needed no change).
2026-07-04 15:37:43 +08:00

6.7 KiB

@deepseek-ai/dsh-fs

The filesystem provider seam: an abstract FileSystem service (ctx.fs) defining the storage primitives a backend provides — resolve a path, stat metadata, read/stream text, list directories, write atomically, and apply a literal edit — without saying HOW. Both mutations take their version guard optionally, so ctx.fs on its own is a complete, unconstrained text-storage seam. This package also owns the fs/* policy event vocabulary the tool dispatches and the policy plugin listens for.

This package is the provider-seam layer of the four-layer filesystem stack, split so each concern can evolve (and be swapped) independently (see the capability-seam RFC, the filesystem capability-seam RFC, the split-the-filesystem-seam RFC, and the file-context event-gate RFC):

Layer Package Role
tool / executor @deepseek-ai/dsh-tool-fs model-facing read/write/edit schemas + read windowing + text rendering; reads/writes/edits via ctx.fs, dispatches the fs/* events
policy @deepseek-ai/dsh-fs-policy observed-state + read-before-edit + version-guarded write/edit, contributed through the fs/* event gate (no service)
provider seam @deepseek-ai/dsh-fs (this) ctx.fs: text IO + atomic mutation primitives (optional version guard); owns the fs/* event vocabulary
provider @deepseek-ai/dsh-fs-local the host-filesystem implementation

A future sandboxed, virtual, or remote backend implements this interface and the policy/tool layers don't change.

Service API (ctx.fs)

A backend subclasses FileSystem and implements seven primitives.

Member Semantics
resolve(path, opts?) Resolve a path into a stable FsTarget (opaque targetKey, displayPath). opts.cwd is the base a relative path resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same targetKey.
stat(target, signal?) Return FsInfo metadata (version, type, optional size), or undefined when the target is absent. Never content.
readText(target, signal?) Read the whole regular text file as one decoded string. Owns regular-file checks, UTF-8 decoding, binary/NUL rejection (FS_NOT_TEXT).
streamText(target, signal?) Stream the same text as decoded chunks for large files (cross-chunk UTF-8 decoding stays here).
listDir(target, signal?) List direct directory children in stable name order. Returns entry names, entry types, resolved child targets, and cheap metadata (version/file size when available); never reads file contents. Missing targets throw FS_NOT_FOUND, non-directories throw FS_NOT_DIRECTORY, permission failures throw FS_PERMISSION_DENIED, and other backend I/O failures throw FS_IO_ERROR. Broken/disappeared children may be returned as other without metadata; child permission/IO failures fail the whole listing with the same structured codes.
writeText(target, content, expected?, signal?) Atomic create/replace. expected is OPTIONAL: omit ⇒ unconditional create-or-overwrite; supply an FsWriteIntent (createIfAbsent/replaceIfVersion) to guard.
editText(target, edit, expected?, signal?) Literal edit. expected is OPTIONAL: omit ⇒ unconditional edit of the current content; supply { version } to guard (verified BEFORE matching). A missing target reports FS_STALE_VERSION either way. Applies and writes atomically — one mutation critical section.

The mutation runs inside the backend's per-target lock either way, so an unconditional write/edit is still atomic — "unconditional" drops the version precondition, not the atomicity.

The fs/* policy events

This package declares three events (see the generated catalog) so the emitter (@deepseek-ai/dsh-tool-fs) and the policy listener (@deepseek-ai/dsh-fs-policy) share a vocabulary without the emitter depending on the policy plugin. fs/write-intent and fs/edit-intent are single-slot decision waterfalls (the listener fully decides, never calling next()); fs/observed is a fire-and-forget recording event. They carry only dsh-fs vocabulary plus an opaque object actor — no model-facing concepts and no agent/session owner structure.

A provider seam, not the policy layer

ctx.fs is deliberately close to fsspec-style storage primitives — half a level above byte-level cat/open, because it decodes text and rejects binaries so the policy layer never touches raw bytes. It owns UTF-8 decoding, binary rejection, atomic writes, and the literal-edit critical section. It does not own line windows, numbered lines, rendered footers, or observed-state. Observed-state, read-before-edit, and version-guarded write/edit are policy a plugin (@deepseek-ai/dsh-fs-policy) ADDS by supplying the optional guard — not provider behavior — so a sandboxed/remote backend inherits no model-facing observation policy.

editText stays on this seam (not composed in the policy layer from a read plus a write) because version guard + literal match + atomic rewrite must stay inside one critical section for correct error attribution and one-wins/one-stale concurrency, and a remote backend may implement it as a native compare-and-edit.

Vocabulary

FsTargetKey / FsVersion are branded opaque ids (the branded-ids RFC) — consumers must not parse targetKey or interpret version; only displayPath is for model/UI output. FsWriteIntent is the explicit GUARDED write intent (createIfAbsent creates a missing target and rejects an existing one with FS_NOT_OBSERVED; replaceIfVersion replaces only at the observed version, else FS_STALE_VERSION); omitting it from writeText is the third, unconditional state. Failures throw FsError (extends HarnessError, the structured error taxonomy RFC) carrying a stable FsErrorCode (FS_NOT_FOUND, FS_NOT_DIRECTORY, FS_NOT_TEXT, FS_NOT_REGULAR_FILE, FS_PERMISSION_DENIED, FS_IO_ERROR, FS_STALE_VERSION, FS_NOT_OBSERVED, FS_AMBIGUOUS_EDIT, FS_EDIT_NOT_FOUND, FS_ABORTED); the tool registry surfaces { name, code } on isError results. See src/types.ts for the full contracts.