@deepseek-ai/dsh-fs-local
English | 中文
The local-filesystem implementation of the ctx.fs provider seam (@deepseek-ai/dsh-fs). Backs the eight FileSystem primitives with the host filesystem; loading it as a plugin populates ctx.fs.
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
// ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-policy for the
// freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
Behavior
resolve(path, opts?)— a relativepathresolves againstopts.cwdwhen the caller supplies one (the model-facing tools pass the calling agent's session cwd — see the per-session cwd Agent Note), elseconfig.cwd(defaultprocess.cwd()); an absolutepathignores both.opts.signalis checked before and after local resolution, while a remote sibling backend may use it to abort its round-trip. ThetargetKeyis the file'srealpath, so two input paths reaching the same file through symlinks share one identity, and writes/edits land on the link target (preserving the link). A not-yet-existing path uses the realpathed parent directory plus basename when the parent exists; only an unresolvable parent falls back to the absolute path.displayPathis the absolute (un-resolved) path.stat/lstat— return target metadata orundefinedwhen absent.statreportsFsInfofor an already resolved target (version= an opaque token derived from bigintdev:ino:size:mtimeNs:ctimeNs,typeoffile/directory/other, bytesize); path-shapedlstatreportsFsPathInfowithout following the final symlink and can therefore returnsymlink. Both check cancellation before and after their asynchronous metadata probe, so an abort that lands in flight reportsFS_ABORTEDrather than stale absence.readText/streamText— UTF-8 only.readTextreads the whole file;streamTextstreams it in chunks (cross-chunk decoding) so a huge file never has to be held whole in memory. Both reject invalid UTF-8 and NUL-byte binary samples (FS_NOT_TEXT) and non-regular targets. Thereadtool (@deepseek-ai/dsh-tool-fs) decides which to call by size and owns the line windowing.listDir— lists one directory level in stablename.localeCompare()order. Each entry carries the child basename, type, resolved child target (displayPathunder the listed directory,targetKeyas the realpath identity), and cheap stat metadata (version, plussizefor regular files). It never opens or decodes file contents. Missing targets reportFS_NOT_FOUND, file/special-file targets reportFS_NOT_DIRECTORY, aborted calls reportFS_ABORTED, permission failures reportFS_PERMISSION_DENIED, and other listing or child metadata I/O failures reportFS_IO_ERROR. Broken/disappeared children are returned asotherwithout metadata, but permission/IO failures while resolving a child fail the whole listing with a structuredFsError.writeText— atomic: writes to a temp file opened exclusively (wx,0o600) inside a randomly-named private staging dir (0o700) next to the target, fsyncs, then renames over the target. An existing file's mode is preserved, while new files default to0o600; on Windows a new file inherits the destination directory's DACL, while replacement copies the target DACL onto the empty temp before writing and publishes throughReplaceFileWso the original access policy survives (Windows DACL preservation Agent Note). Theexpectedguard is OPTIONAL: omitting it unconditionally creates-or-overwrites;createIfAbsentcreates a missing target and rejects an existing one (FS_NOT_OBSERVED);replaceIfVersionreplaces only at the observed version (a missing target or mismatch isFS_STALE_VERSION).editText— atomic literal read-modify-write over the same primitive, serialized per target by a mutation lock. Theexpectedguard is OPTIONAL: when supplied it verifies the version BEFORE literal matching (a stale edit reportsFS_STALE_VERSION, neverFS_EDIT_NOT_FOUND/FS_AMBIGUOUS_EDITagainst newer content); omitting it edits the current content unconditionally. A missing target reportsFS_STALE_VERSIONeither way. LF-normalizes for matching, restores the file's dominant CRLF/LF style, and rejects emptyoldString/ zero matches (FS_EDIT_NOT_FOUND) or ambiguous multi-matches withoutreplace_all(FS_AMBIGUOUS_EDIT).
The package-root SDK surface is the default/named LocalFileSystem class plus Config. Raw I/O lives in src/fsio.ts (Cordis-free, independently unit-tested); src/index.ts is the thin service wiring.
Model Experience
Indirectly, through dsh-tool-fs, which renders this provider's line-windowed UTF-8 content, mutation acknowledgements, and exact provider messages in capped retained results while versions, atomic-write mechanics, and directory metadata remain internal.
KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
Known Limitations and Deferred Work
config.cwdis not a sandbox — it is a resolution default, not containment: absolute paths and..escape it. Enforce containment with a stricterctx.fsbackend or a permission plugin on thetools/executewaterfall (capability-seam Agent Note).- An overwrite reads the whole prior file into memory — solely as the UI diff basis; bounding that pre-read above a size threshold is deferred (
TODO(overwrite-diff-bound)). - Version tokens are
mtimeMs:size— an external change that preserves both within the filesystem's timestamp granularity defeats the stale guard. editTextholds the whole file (plus the edited copy) in memory — streaming exists only on the read path.- Binary detection is asymmetric — reads NUL-sample only the first 8192 bytes while edits scan the whole buffer, so a file with a late NUL reads fine but rejects edits.
- The per-target mutation lock is in-process only — a writer in another process is caught only by the optional version guard, never serialized.