The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
5.0 KiB
@deepseek-ai/dsh-tool-fs
The model-facing filesystem tools — read, write, edit — and their executor. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, read windowing, and result formatting. It reads/writes/edits through the ctx.fs provider seam (@deepseek-ai/dsh-fs) directly — it injects fs (plus tools/systemPrompt), not a policy service. The freshness/observation policy is contributed by a separate plugin (@deepseek-ai/dsh-fs-policy) through the fs/* event gate; the tool is not method-coupled to it.
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local
await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-policy (policy gate)
await ctx.plugin(ToolFs) // this package — registers read/write/edit
@deepseek-ai/dsh-fs-policy is optional: omit it and the tools run against the bare provider (unconditional write/overwrite/edit, no observed-state). A deployment that loads these tools is expected to also load it, so the behavior is read-before-write/edit.
Config
All keys are optional; the defaults are the shipped read caps.
| Key | Default | Meaning |
|---|---|---|
readLimit |
2000 |
Default and maximum lines returned by one read call (the tool schema advertises it as the limit default). |
readMaxLineLength |
2000 |
Characters kept per line before truncation (the suffix names the cap). |
readMaxBytes |
51200 |
Byte cap on one read call's selected lines; overflow ends the window with a "capped" footer. |
readStreamMinSize |
10485760 |
Files at or above this size (or with unknown size) stream instead of loading whole into memory. |
Tools (schemas per the filesystem tool schemas RFC)
| Tool | Arguments | Behavior |
|---|---|---|
read |
file_path, offset?, limit? |
Line-numbered UTF-8 content with a pagination footer. offset is 1-based; limit defaults to and caps at the configured readLimit (2000). |
write |
file_path, content |
Create or fully replace a file. With the policy plugin: overwriting an existing file requires a prior read at the unchanged version; creating a new file does not. Without it: unconditional. |
edit |
file_path, non-empty old_string, new_string, replace_all? |
Literal replacement; unique match required unless replace_all is true. With the policy plugin: requires a prior read (any window) and the file unchanged since. Without it: unconditional. |
Field names are snake_case to match Claude Code and existing harness tool schemas.
The tool is the executor; policy is an event gate
The tools do not inject a policy service or inspect any cache. Each tool resolves the path via ctx.fs.resolve(path, { cwd }) — passing the calling agent's session cwd (exec.agent.session.header.cwd) so a relative path resolves against the session's workspace, matching dsh-tool-bash (see the per-session cwd RFC) — then:
- read — one
ctx.fs.stat(type + size routing + version), thenreadText/streamText, then builds the line window, then emitsfs/observedwith a plainctx.emit. (1 stat.) - write —
ctx.waterfall('fs/write-intent', target, exec, () => undefined)for the optional guard, thenctx.fs.writeText(target, content, intent), thenfs/observed. (0 stat.) - edit —
ctx.waterfall('fs/edit-intent', target, exec, () => undefined)for the optional guard, thenctx.fs.editText(target, edit, intent), thenfs/observed. (0 stat.)
The tool passes exec (the tool-execution context) as the opaque actor on every dispatch. The default thunks return undefined (the unconstrained bare provider). When @deepseek-ai/dsh-fs-policy is loaded it occupies the single decision slot — returning createIfAbsent/replaceIfVersion/{ version } or throwing FS_NOT_OBSERVED — and records on fs/observed. Backend errors (FsError) and a thrown FS_NOT_OBSERVED flow through ToolRegistry.execute() and become isError tool results with their { name, code } attached.
fs/observed is fire-and-forget
fs/observed fires AFTER the read/write/edit already succeeded, via a plain ctx.emit. A listener is contractually a synchronous, side-effect-only recorder (@deepseek-ai/dsh-fs-policy's is a WeakMap.set); the tool does not guard the emit, so a listener that throws would surface as the tool's isError result — async or fallible observation does not belong on this event.
The read rendering (line windowing + output formatting) lives in src/read-render.ts (Cordis-free, independently unit-tested); src/read.ts/write.ts/edit.ts are the tool executors and src/index.ts composes them.