Rename per review naming decisions: - package dsh-file-context → dsh-fs-policy (dir, package name, plugin name, tsconfig refs, importers, type-equiv manifest, generated catalog + module-graph) - events fs/write-expectation → fs/write-intent, fs/edit-expectation → fs/edit-intent (fs/observed unchanged); type FsWriteExpectation → FsWriteIntent, "expectation" wording → "intent" throughout - exported FileContextExec → FsPolicyExec Make the implemented RFCs describe what shipped, not the superseded designs: the 2026-06-17 capability-seam + tool-schemas RFCs no longer place policy on ctx.fs or use full/partial-view authorization, and the fsspec RFC's ctx.fileContext service prose is rewritten to the fs/* event-gate reality (freshness-based auth). Sharpen docs/rfc/implemented/AGENTS.md: a rename is a fact to fix IN PLACE — the "new RFC" escape hatch is for macro decision reversals only, not renames. Code fixes from review: - fsio.ts resolveLocalTarget/probe translate ENOTDIR (a parent path segment is a file) into the structured FsError taxonomy instead of leaking a raw Node error; resolve reports FS_NOT_FOUND, probe reports absent. Regression tests proven to fail on the unfixed code. - tool-fs HMR test now asserts prompt sections (not just tool schemas) are withdrawn on disposal. - fs/observed is a plain (unguarded) ctx.emit: correct the fs-policy comment, filesystem.md, and tool-fs module doc that wrongly claimed the tool "contains" a throwing listener; a throw surfaces as the tool's isError result. - drop the false "loaded by the default product config" claim (no config wires the fs tools yet), the duplicate ctx.bash service-map row, the stale FileReadRequest catalog link-map entry, and the fs/fs README EOF blank line; correct the dsh-fs package.json description.
@deepseek-ai/dsh-fs
The filesystem provider seam: an abstract FileSystem service (ctx.fs) defining the text-storage primitives a backend provides — resolve a path, stat metadata, read/stream text, 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 six primitives.
| Member | Semantics |
|---|---|
resolve(path) |
Resolve a path into a stable FsTarget (inputPath, opaque targetKey, displayPath). 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). |
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_TEXT, FS_NOT_REGULAR_FILE, 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.