Merge commit '70396085b141370ce32de1be4e225b4384eaf46d' into HEAD

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md
#	.agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.i18n.yaml
#	docs/config-catalog.i18n.yaml
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	docs/tool-catalog.i18n.yaml
#	docs/tool-catalog.md
#	docs/tool-catalog.zh.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
#	examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
#	examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json
#	packages/core/tools/README.i18n.yaml
#	packages/core/tools/README.zh.md
#	packages/core/tools/src/code-mode.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/plugin-inventory/tests/inventory.spec.ts
#	packages/mcp/mcp-client/tests/mcp-client.e2e.ts
#	packages/mcp/mcp-client/tests/mcp-client.spec.ts
#	packages/self-modification/tool-cordis/src/api-catalog.ts
#	packages/test-support/acp-snapshot/README.i18n.yaml
#	pnpm-lock.yaml
This commit is contained in:
Tianyi Cui
2026-08-17 11:31:59 +08:00
3845 changed files with 62208 additions and 100402 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/README.md
README.md: c48039f25ce234ce8b16d9829668224156855f31
README.zh.md: 20ecf1ee854b6f301fa6057bf4442bf82d6e9b54
README.md: 344ebc309769290064952bbfac2faacabc339ea3
README.zh.md: c36317a61c16e99a314b5d21e8c7abb0ba202fdb

View File

@@ -10,11 +10,11 @@ The filesystem stack: a provider contract (execution-world paths, bounded text I
| `fs-local/` | Local-filesystem `FileSystem` implementation | (registers `ctx.fs`) |
| [`e2b/fs-e2b`](../e2b/fs-e2b/README.md) | E2B-backed `FileSystem` implementation sharing the remote runtime owned by `ctx.e2b` | (registers `ctx.fs`) |
| `fs-sandbox/` | Sandbox-enforcing `FileSystem`: extends `fs-local` and fences write/edit by the per-call mode + workspace root policy (read-only denies, workspace-write contains to the session workspace + temp roots), reads pass through | (registers `ctx.fs`) |
| `fs-policy/` | Policy gate plugin: observed-state + read-before-edit + version-guarded write/edit, via the `fs/*` event gate | (no service — `fs/*` listeners) |
| `fs-observation-policy/` | Policy gate plugin: observed-state + read-before-edit + version-guarded write/edit, via the `fs/*` event gate | (no service — `fs/*` listeners) |
| `tool-fs/` | Model-facing `read`/`write`/`edit` tools AND the executor (reads via `ctx.fs`, owns read windowing, dispatches `fs/*`); preserves filesystem semantics for session-cwd-relative paths and advertises sandbox escalation fields when the mounted `ctx.fs` confines | (registers on `ctx.tools`) |
| `tool-fs-search/` | Model-facing `glob`/`grep` discovery tools backed by the packaged `@vscode/ripgrep` binary spawned through `ctx.subprocess`, NOT by `ctx.fs` provider methods | (registers on `ctx.tools`) |
The Service Definition lives at `fs/fs/`. A sandboxed, remote, or project-scoped filesystem backend can replace `fs-local` without touching the Service Definition, policy gate, or model-facing tool schemas: `fs-sandbox` provides an in-process path fence over the shared sandbox mode ([decision](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)), while `fs-e2b` places file state in the remote execution world shared with the E2B subprocess provider ([decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md)). The policy (`fs-policy/`) is a plugin that participates only through the `fs/*` event gate, not a service the tool injects — so dropping it gracefully loses the policy and leaves the unconstrained bare provider rather than breaking the tool. A deployment that loads `tool-fs/` is expected to also load it. The mode fence and the read-before-edit gate are orthogonal and compose. Discovery (`tool-fs-search/`) deliberately does NOT extend the provider contract: search is a process-backed `rg` workflow (the packaged `@vscode/ripgrep` binary spawned through `ctx.subprocess`), so filesystem backends stay free of a universal search contract; its tools register unconditionally, and its results are follow-up-readable when the search workdir and the `read` root are the same workspace (the co-located deployment its README documents).
The Service Definition lives at `fs/fs/`. A sandboxed, remote, or project-scoped filesystem backend can replace `fs-local` without touching the Service Definition, policy gate, or model-facing tool schemas: `fs-sandbox` provides an in-process path fence over the shared sandbox mode ([decision](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)), while `fs-e2b` places file state in the remote execution world shared with the E2B subprocess provider ([decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md)). The policy (`fs-observation-policy/`) is a plugin that participates only through the `fs/*` event gate, not a service the tool injects — so dropping it gracefully loses the policy and leaves the unconstrained bare provider rather than breaking the tool. A deployment that loads `tool-fs/` is expected to also load it. The mode fence and the read-before-edit gate are orthogonal and compose. Discovery (`tool-fs-search/`) deliberately does NOT extend the provider contract: search is a process-backed `rg` workflow (the packaged `@vscode/ripgrep` binary spawned through `ctx.subprocess`), so filesystem backends stay free of a universal search contract; its tools register unconditionally, and its results are follow-up-readable when the search workdir and the `read` root are the same workspace (the co-located deployment its README documents).
## No timeouts on file IO

View File

@@ -10,11 +10,11 @@
| `fs-local/` | 本地文件系统 `FileSystem` 实现 | (注册 `ctx.fs` |
| [`e2b/fs-e2b`](../e2b/fs-e2b/README.md) | 以 E2B 为后端的 `FileSystem` 实现,共享由 `ctx.e2b` 拥有的远程运行时 | (注册 `ctx.fs` |
| `fs-sandbox/` | 强制沙箱的 `FileSystem`:扩展 `fs-local`,并按每次调用的模式与工作区根政策约束写入/编辑(只读模式拒绝,工作区写入模式限制在会话工作区与临时根目录内);读取直接通过 | (注册 `ctx.fs` |
| `fs-policy/` | 政策门禁插件:通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑 | (无服务,仅有 `fs/*` 监听器) |
| `fs-observation-policy/` | 政策门禁插件:通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑 | (无服务,仅有 `fs/*` 监听器) |
| `tool-fs/` | 面向模型的 `read`/`write`/`edit` 工具以及执行器(通过 `ctx.fs` 读取,拥有读取窗口逻辑,分派 `fs/*`);为会话 cwd 相对路径保留文件系统语义,并在已挂载的 `ctx.fs` 实施约束时声明沙箱升级字段 | (注册到 `ctx.tools` |
| `tool-fs-search/` | 面向模型的 `glob`/`grep` 发现工具,由经 `ctx.subprocess` spawn 的打包 `@vscode/ripgrep` 二进制文件支持,而不是使用 `ctx.fs` 提供方方法 | (注册到 `ctx.tools` |
Service Definition 位于 `fs/fs/`。沙箱化、远程或限定项目作用域的文件系统后端可以替换 `fs-local`,而无需更改 Service Definition、政策门禁或面向模型的工具 schema`fs-sandbox` 基于共享沙箱模式提供进程内路径围栏([决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)),而 `fs-e2b` 则把文件状态置于与 E2B 子进程提供方共享的远程执行世界中([决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md))。政策(`fs-policy/`)是一个只通过 `fs/*` 事件门禁参与的插件,不是工具注入的服务;因此移除它会平稳失去政策,留下不受约束的裸提供方,而不会破坏工具。加载 `tool-fs/` 的部署也应加载该插件。模式围栏与编辑前读取门禁彼此正交,可以组合。发现(`tool-fs-search/`)有意不扩展提供方约定:搜索是由进程支持的 `rg` 工作流(经 `ctx.subprocess` spawn 的打包 `@vscode/ripgrep` 二进制文件),因此文件系统后端无需承担通用搜索约定;其工具会无条件注册。如果搜索工作目录与 `read` 根目录是同一工作区,结果就能继续读取,这也是其 README 所述的共置部署。
Service Definition 位于 `fs/fs/`。沙箱化、远程或限定项目作用域的文件系统后端可以替换 `fs-local`,而无需更改 Service Definition、政策门禁或面向模型的工具 schema`fs-sandbox` 基于共享沙箱模式提供进程内路径围栏([决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)),而 `fs-e2b` 则把文件状态置于与 E2B 子进程提供方共享的远程执行世界中([决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md))。政策(`fs-observation-policy/`)是一个只通过 `fs/*` 事件门禁参与的插件,不是工具注入的服务;因此移除它会平稳失去政策,留下不受约束的裸提供方,而不会破坏工具。加载 `tool-fs/` 的部署也应加载该插件。模式围栏与编辑前读取门禁彼此正交,可以组合。发现(`tool-fs-search/`)有意不扩展提供方约定:搜索是由进程支持的 `rg` 工作流(经 `ctx.subprocess` spawn 的打包 `@vscode/ripgrep` 二进制文件),因此文件系统后端无需承担通用搜索约定;其工具会无条件注册。如果搜索工作目录与 `read` 根目录是同一工作区,结果就能继续读取,这也是其 README 所述的共置部署。
## 文件 I/O 不设超时

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
README.md: 6b1e91425c07a073aaa8cbc00fd24318bf369d3c
README.zh.md: 13a5f13da0917664fe50d0d4758dd8669f97b546
README.md: 4d5c42945b86ccdc8b041d9d7f99a067ab9a37f5
README.zh.md: 0e69817a154c3c1f5d4d76d658d8097feffebab0

View File

@@ -8,7 +8,7 @@ The **local-filesystem implementation** of the `ctx.fs` provider contract ([`@de
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
// ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-observation-policy for the
// freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
```

View File

@@ -8,7 +8,7 @@
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
// ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-observation-policy for the
// freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
```

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-fs-local",
"description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -3,7 +3,7 @@
* file/streamed text reads, atomic guarded writes (createIfAbsent /
* replaceIfVersion), version-guarded literal edits, concurrency races, symlink
* identity, and HMR/disposal. Read WINDOWING is policy and lives in
* `dsh-fs-policy`, so it is not exercised here.
* `dsh-fs-observation-policy`, so it is not exercised here.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

View File

@@ -2,7 +2,7 @@
* Cordis-free tests for the raw local-filesystem I/O: path resolution, probe,
* whole-file/streamed text reads, binary/UTF-8 rejection, atomic-write temp
* safety, literal edit matching, and line-ending handling. Line WINDOWING is
* policy and lives in `dsh-fs-policy`, so it is not tested here.
* policy and lives in `dsh-fs-observation-policy`, so it is not tested here.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

View File

@@ -22,7 +22,7 @@
"path": "../fs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/fs-policy/README.md
README.md: 59865d063095d666244c2ec86b8604f006f70554
README.zh.md: 05ae12eae39c314d15dd0f86ba21772463b38e51
# pnpm run verify-translation-pairing --write packages/fs/fs-observation-policy/README.md
README.md: a126c7d0e3c806e3ce22eedfb05b21d8fd5aba60
README.zh.md: 829893028e388380e33b35ec9d5bf4c9d0a58757

View File

@@ -1,12 +1,12 @@
# @deepseek-ai/dsh-fs-policy
# @deepseek-ai/dsh-fs-observation-policy
English | [中文](README.zh.md)
The **fs-policy plugin**: it records observed presence or absence and adds read-before-edit plus guarded write/edit on top of the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)) — through the `fs/*` event gate, **NOT** through a method service. This plugin registers **no** `ctx.fsPolicy` service and has no public `read`/`write`/`edit`/`resolve` methods. It is the policy third of the filesystem stack: not a swappable seam, but the policy that does not belong on the `FileSystem` provider base class.
The **fs-observation-policy plugin**: it records observed presence or absence and adds read-before-edit plus guarded write/edit on top of the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)) — through the `fs/*` event gate, **NOT** through a method service. This plugin registers **no** `ctx.fsPolicy` service and has no public `read`/`write`/`edit`/`resolve` methods. It is the policy third of the filesystem stack: not a swappable seam, but the policy that does not belong on the `FileSystem` provider base class.
```ts
import type { Context } from '@deepseek-ai/cordis'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
declare const ctx: Context
@@ -23,7 +23,7 @@ await ctx.plugin(FsPolicy)
| Layer | Package | Role |
|---|---|---|
| tool / executor | `@deepseek-ai/dsh-tool-fs` | model-facing schemas + read windowing + text rendering; reads/writes/edits via `ctx.fs`, dispatches the `fs/*` events |
| policy | `@deepseek-ai/dsh-fs-policy` (this) | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
| policy | `@deepseek-ai/dsh-fs-observation-policy` (this) | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
| provider contract | `@deepseek-ai/dsh-fs` | `ctx.fs`: text IO + atomic mutation primitives (optional version guard); owns the `fs/*` event vocabulary |
| provider | `@deepseek-ai/dsh-fs-local` | local implementation of `ctx.fs` |

View File

@@ -1,12 +1,12 @@
# @deepseek-ai/dsh-fs-policy
# @deepseek-ai/dsh-fs-observation-policy
[English](README.md) | 中文
**fs-policy 插件**:它记录观测到的存在或缺失状态,并在 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))之上增加编辑前读取和带防护的写入/编辑;它通过 `fs/*` 事件门禁参与,**不是**通过方法服务。该插件**不**注册 `ctx.fsPolicy` 服务,也没有公开的 `read`/`write`/`edit`/`resolve` 方法。它是文件系统栈的政策层:不是可替换 seam而是不应位于 `FileSystem` 提供方基类上的政策。
**fs-observation-policy 插件**:它记录观测到的存在或缺失状态,并在 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))之上增加编辑前读取和带防护的写入/编辑;它通过 `fs/*` 事件门禁参与,**不是**通过方法服务。该插件**不**注册 `ctx.fsPolicy` 服务,也没有公开的 `read`/`write`/`edit`/`resolve` 方法。它是文件系统栈的政策层:不是可替换 seam而是不应位于 `FileSystem` 提供方基类上的政策。
```ts
import type { Context } from '@deepseek-ai/cordis'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
declare const ctx: Context
@@ -23,7 +23,7 @@ await ctx.plugin(FsPolicy)
| 层 | 包 | 角色 |
|---|---|---|
| 工具/执行器 | `@deepseek-ai/dsh-tool-fs` | 面向模型的 schema、读取窗口和文本渲染通过 `ctx.fs` 读取/写入/编辑,并分派 `fs/*` 事件 |
| 策略 | `@deepseek-ai/dsh-fs-policy`(本包) | 通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑(无服务) |
| 策略 | `@deepseek-ai/dsh-fs-observation-policy`(本包) | 通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑(无服务) |
| 提供方约定 | `@deepseek-ai/dsh-fs` | `ctx.fs`:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 事件词汇 |
| 提供方 | `@deepseek-ai/dsh-fs-local` | `ctx.fs` 的本地实现 |

View File

@@ -1,14 +1,14 @@
{
"name": "@deepseek-ai/dsh-fs-policy",
"name": "@deepseek-ai/dsh-fs-observation-policy",
"description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/fs/fs-policy"
"directory": "packages/fs/fs-observation-policy"
},
"type": "module",
"main": "lib/index.js",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -4,15 +4,15 @@
* guards from that state, and the provider performs the atomic freshness/no-clobber check. Without
* this plugin, tools retain the bare provider's unconditional mutation behavior. See the package
* README for composition rules.
* @module @deepseek-ai/dsh-fs-policy
* @module @deepseek-ai/dsh-fs-observation-policy
*/
import type { Context } from '@deepseek-ai/cordis'
import { FsError } from '@deepseek-ai/dsh-fs'
import type { FsObservation, FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs'
import type { FsPolicyExec } from './types.ts'
import type { FsObservationActor } from './types.ts'
export type { FsPolicyExec } from './types.ts'
export type { FsObservationActor } from './types.ts'
/**
* Per-context observed-file state and the three `fs/*` decisions over it. One
@@ -34,10 +34,10 @@ class ObservedStateGate {
* the write/edit prior-observation policy.
*/
private owner(actor: object | undefined): object | undefined {
// tsgolint treats object as assignable to weak FsPolicyExec, while tsc still requires the structural cast for property access.
// tsgolint treats object as assignable to weak FsObservationActor, while tsc still requires the structural cast for property access.
// See the analyzer-divergence consequence in .agents/notes/implemented/process/2026-07-29-oxlint-linter.md.
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- The analyzers disagree on this weak type.
return (actor as FsPolicyExec | undefined)?.agent?.session
return (actor as FsObservationActor | undefined)?.agent?.session
}
private get(owner: object, targetKey: string): FsObservation | undefined {
@@ -95,7 +95,7 @@ class ObservedStateGate {
}
/** Cordis plugin name used by loader diagnostics. */
export const name = 'fs-policy'
export const name = 'fs-observation-policy'
/**
* Register the three `fs/*` listeners. No `inject` this plugin reads no
@@ -111,7 +111,7 @@ export function apply(ctx: Context): void {
// (HMR safety). The WeakMap itself would be GC'd, but replacing it makes the
// release observable and immediate for tests.
gate.clear()
}, 'fs-policy observed-state teardown')
}, 'fs-observation-policy observed-state teardown')
// fs/write-intent: occupy the single decision slot — do NOT call next().
// Deferred through Promise.resolve().then so the declared Promise return type

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-policy`.
* @module @deepseek-ai/dsh-fs-policy/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-observation-policy`.
* @module @deepseek-ai/dsh-fs-observation-policy/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-policy'
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-observation-policy'
/** Cordis companion plugin name. */
export const name = 'fs-policy-invariant'
export const name = 'fs-observation-policy-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -1,5 +1,5 @@
/**
* Vocabulary for the fs-policy plugin: the minimal execution-context
* Vocabulary for the fs-observation-policy plugin: the minimal execution-context
* fields used to derive an observed-state owner by narrowing the opaque `object`
* actor the `fs/*` events carry.
*
@@ -7,7 +7,7 @@
* re-used from `@deepseek-ai/dsh-fs`; this package owns only the observed-state
* owner structure on top of it.
*
* @module @deepseek-ai/dsh-fs-policy/types
* @module @deepseek-ai/dsh-fs-observation-policy/types
*/
/**
@@ -15,12 +15,12 @@
* an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` contains
* these fields, so the tool passes its `exec` straight through as the opaque
* `object` actor on the `fs/*` events; this plugin narrows that actor to
* `FsPolicyExec` without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
* `FsObservationActor` without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
*
* The owner is `agent.session` when present. It is treated as an opaque object
* identity (a `WeakMap` key); this package never reads any of its fields.
*/
export interface FsPolicyExec {
export interface FsObservationActor {
/** The agent on whose behalf the call runs, when there is one. */
agent?: {
/** The session that owns observed-file state, used as an opaque key. */

View File

@@ -4,13 +4,13 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type { FsObservation, FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import type { FsPolicyExec } from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import type { FsObservationActor } from '@deepseek-ai/dsh-fs-observation-policy'
function target(path: string): FsTarget {
return { targetKey: FsTargetKey(path), displayPath: path }
}
const ownerExec = (session: object): FsPolicyExec => ({ agent: { session } })
const ownerExec = (session: object): FsObservationActor => ({ agent: { session } })
const present = (version: string): FsObservation => ({ kind: 'present', version: FsVersion(version) })
const absent: FsObservation = { kind: 'absent' }
@@ -184,10 +184,10 @@ describe('single-slot, first-wins', () => {
expect(defaultRan).toBe(false)
})
it('a SECOND decider registered AFTER fs-policy is not reached (first-wins short-circuit)', async () => {
it('a SECOND decider registered AFTER fs-observation-policy is not reached (first-wins short-circuit)', async () => {
const { ctx } = await setup()
let secondRan = false
// Registered after fs-policy, so it dispatches second; fs-policy does
// Registered after fs-observation-policy, so it dispatches second; fs-observation-policy does
// not call next(), so this never runs. (A decider registered BEFORE — or with
// prepend — would instead win: first-wins is by convention, not enforced.)
ctx.on('fs/edit-intent', () => {
@@ -200,7 +200,7 @@ describe('single-slot, first-wins', () => {
expect(secondRan).toBe(false)
})
it('a SECOND write-intent decider registered AFTER fs-policy is not reached', async () => {
it('a SECOND write-intent decider registered AFTER fs-observation-policy is not reached', async () => {
const { ctx } = await setup()
let secondRan = false
ctx.on('fs/write-intent', () => {

View File

@@ -19,7 +19,7 @@
"path": "../fs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/fs-sandbox/README.md
README.md: ae1fd746c711a86e308a02e0054ba478e8d913c0
README.zh.md: e25a3467c06fbd93de9bb75d6b364e8da5a451fe
README.md: 73df31d0f98b602c57a3e86340c3dfe0ee836cd0
README.zh.md: f4000b200979e951f7e10d452a11773114484185

View File

@@ -18,7 +18,7 @@ The per-call policy carries the effective mode (session override or escalation g
## Threat model: a policy fence, not a kernel boundary
The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface. This mirrors the `code-runtime` stance: containment, not a security boundary. Kernel-grade isolation of untrusted CODE stays `ctx.bash`'s job ([`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md)). The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs `openat2`-class primitives not worth their portability cost here.
The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface. This mirrors the `code-runtime` stance: containment, not a security boundary. Kernel-grade isolation of untrusted CODE stays `ctx.shell`'s job ([`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md)). The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs `openat2`-class primitives not worth their portability cost here.
A denial is a structured `FsError` (`FS_SANDBOX_DENIED`, carrying the effective mode) — no stderr text inference (unlike bash's kernel denials), because an in-process fence knows exactly what it refused. The model-facing `[sandbox: file access denied under <mode> mode]` marker and the one-approved-wider retry live in the tool layer (`dsh-tool-fs`), exactly as bash's do. See [the cross-family fs sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md).
@@ -40,6 +40,6 @@ A standing-policy change appends an owner-rendered superseding runtime-context s
## Known Limitations and Deferred Work
- **A policy fence, not a kernel boundary** — the check is trusted code over a model-controlled path, so the residual resolve-to-syscall TOCTOU is narrowed (by the in-place re-canonicalization) but not eliminated; adversarial host processes are out of scope. Kernel-grade isolation of untrusted code stays `ctx.bash`'s.
- **A policy fence, not a kernel boundary** — the check is trusted code over a model-controlled path, so the residual resolve-to-syscall TOCTOU is narrowed (by the in-place re-canonicalization) but not eliminated; adversarial host processes are out of scope. Kernel-grade isolation of untrusted code stays `ctx.shell`'s.
- **Fence-vs-runner parity is derived from one owner** — the writable set comes from `writableRoots`, shared with the Seatbelt profile; a runner profile that defines its writable set elsewhere would drift.
- **Requires `ctx.sandboxPolicy`** — tools use it to resolve each session policy and the backend uses it for agentless-call fallbacks; the backend does not confine without it composed.

View File

@@ -18,7 +18,7 @@
## 威胁模型:策略围栏,而非内核边界
围栏是在可信代码中检查模型控制的路径。操作本身属于 seamopen、rename只有目标路径不可信因此「规范化后检查包含关系」就是该接口的完整答案。这与 `code-runtime` 的立场相同:提供约束,但不是安全边界。不可信代码的内核级隔离仍由 `ctx.bash` 负责([`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md))。剩余 TOCTOU在包含关系复查与系统调用之间替换祖先符号链接会通过写入前立即重新规范化来缩小并为该威胁模型所接受内核严密边界需要 `openat2` 一类原语,其可移植性成本在此不值得。
围栏是在可信代码中检查模型控制的路径。操作本身属于 seamopen、rename只有目标路径不可信因此「规范化后检查包含关系」就是该接口的完整答案。这与 `code-runtime` 的立场相同:提供约束,但不是安全边界。不可信代码的内核级隔离仍由 `ctx.shell` 负责([`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md))。剩余 TOCTOU在包含关系复查与系统调用之间替换祖先符号链接会通过写入前立即重新规范化来缩小并为该威胁模型所接受内核严密边界需要 `openat2` 一类原语,其可移植性成本在此不值得。
拒绝是结构化 `FsError``FS_SANDBOX_DENIED`,携带有效模式),不通过 stderr 文本推断(不同于 bash 的内核拒绝),因为进程内围栏准确知道自己拒绝了什么。面向模型的 `[sandbox: file access denied under <mode> mode]` 标记以及唯一一次获批的更宽权限重试位于工具层(`dsh-tool-fs`),与 bash 完全相同。见[跨能力族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)。
@@ -40,6 +40,6 @@
## 已知限制与暂缓事项
- **策略围栏,而非内核边界**:该检查是可信代码处理模型控制的路径,因此解析到系统调用之间残留的 TOCTOU 会被原位重新规范化缩小,但不会消除;对抗性宿主进程不在范围内。不可信代码的内核级隔离仍属于 `ctx.bash`
- **策略围栏,而非内核边界**:该检查是可信代码处理模型控制的路径,因此解析到系统调用之间残留的 TOCTOU 会被原位重新规范化缩小,但不会消除;对抗性宿主进程不在范围内。不可信代码的内核级隔离仍属于 `ctx.shell`
- **围栏与 runner 的一致性由单一所有方派生**:可写集合来自 `writableRoots`,该函数与 Seatbelt profile 共享;在其他位置定义可写集合的 runner profile 会发生漂移。
- **要求 `ctx.sandboxPolicy`**:工具使用它解析每个会话策略,后端用它处理无 agent智能体调用的回退未组合该服务时后端不会实施约束。

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-fs-sandbox",
"description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",

View File

@@ -11,7 +11,7 @@
* NOT a kernel boundary — the operations are the seam's own (open, rename),
* and only the target path is untrusted, so canonicalize-then-contain is the
* complete answer to this surface. Kernel-grade isolation of untrusted CODE
* stays `ctx.bash`'s job (`@deepseek-ai/dsh-bash-sandbox`). This mirrors the
* stays `ctx.shell`'s job (`@deepseek-ai/dsh-bash-sandbox`). This mirrors the
* `code-runtime` stance: containment, not a security boundary. The residual
* TOCTOU (an ancestor symlink swapped between the containment re-check and the
* syscall) is narrowed by re-canonicalizing immediately before delegating and

View File

@@ -27,7 +27,7 @@
"path": "../../sandbox/sandbox-policy"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/fs/README.md
README.md: 197114c02280f9ece81fa4b97af7e67aa39d77c4
README.zh.md: b972da91ec9abfee84b8606be5b3f024a26a7708
README.md: 7bde7d4d64005a6bdd0f0ac974bf45e5450e4207
README.zh.md: 7efb03609f862f6d4ee6d796cc099df4a8ef7287

View File

@@ -9,7 +9,7 @@ This package owns the Service Definition and provider contract layer of the four
| 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) |
| policy | `@deepseek-ai/dsh-fs-observation-policy` | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
| provider contract | `@deepseek-ai/dsh-fs` (this) | `ctx.fs`: execution-world paths, text IO, and atomic mutation primitives (optional version guard); owns the `fs/*` event vocabulary |
| provider | `@deepseek-ai/dsh-fs-local` | the host-filesystem implementation |
@@ -38,11 +38,11 @@ The mutation runs inside the backend's per-target lock either way, so an uncondi
## The `fs/*` policy events
This package declares three events (see the generated region of [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface)) 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 carrying an `FsObservation` discriminated union: present with a version or confirmed absent. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
This package declares three events (see the generated region of [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface)) so the emitter (`@deepseek-ai/dsh-tool-fs`) and the policy listener (`@deepseek-ai/dsh-fs-observation-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 carrying an `FsObservation` discriminated union: present with a version or confirmed absent. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
## A provider contract, 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.
`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-observation-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.

View File

@@ -9,7 +9,7 @@
| 层 | 包 | 角色 |
|---|---|---|
| 工具/执行器 | `@deepseek-ai/dsh-tool-fs` | 面向模型的 `read`/`write`/`edit` schema、读取窗口和文本渲染通过 `ctx.fs` 读取/写入/编辑,并分派 `fs/*` 事件 |
| 政策 | `@deepseek-ai/dsh-fs-policy` | 已观察状态、编辑前读取和版本防护的写入/编辑,通过 `fs/*` 事件门禁贡献(无服务) |
| 政策 | `@deepseek-ai/dsh-fs-observation-policy` | 已观察状态、编辑前读取和版本防护的写入/编辑,通过 `fs/*` 事件门禁贡献(无服务) |
| 提供方约定 | `@deepseek-ai/dsh-fs`(本包) | `ctx.fs`:执行世界路径、文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 事件词汇 |
| 提供方 | `@deepseek-ai/dsh-fs-local` | 宿主文件系统实现 |
@@ -38,11 +38,11 @@
## `fs/*` 政策事件
本包声明三个事件(见 [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface) 的生成区块),使发出方(`@deepseek-ai/dsh-tool-fs`)和政策监听器(`@deepseek-ai/dsh-fs-policy`)共享词汇,而无需让发出方依赖政策插件。`fs/write-intent``fs/edit-intent` 是单槽决策 waterfall瀑布式事件监听器完整决策绝不调用 `next()``fs/observed` 是发后即忘的记录事件,携带 `FsObservation` 可辨识联合:存在并带有版本,或确认缺失。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent智能体/会话所有者结构。
本包声明三个事件(见 [filesystem.md](../../../docs/subsystems/filesystem.md#cordis-surface) 的生成区块),使发出方(`@deepseek-ai/dsh-tool-fs`)和政策监听器(`@deepseek-ai/dsh-fs-observation-policy`)共享词汇,而无需让发出方依赖政策插件。`fs/write-intent``fs/edit-intent` 是单槽决策 waterfall瀑布式事件监听器完整决策绝不调用 `next()``fs/observed` 是发后即忘的记录事件,携带 `FsObservation` 可辨识联合:存在并带有版本,或确认缺失。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent智能体/会话所有者结构。
## 提供方约定,不是政策层
`ctx.fs` 有意接近 fsspec 风格的存储原语,比字节级 `cat`/`open` 高半层,因为它会解码文本并拒绝二进制,使政策层绝不接触原始字节。它负责 UTF-8 解码、二进制拒绝、原子写入和字面量编辑临界区。它**不** 负责行窗口、编号行、渲染 footer 或已观察状态。已观察状态、编辑前读取和版本防护的写入/编辑属于插件(`@deepseek-ai/dsh-fs-policy`)通过提供可选防护而添加的政策,并非提供方行为,因此沙箱化/远程后端不会继承任何面向模型的观察政策。
`ctx.fs` 有意接近 fsspec 风格的存储原语,比字节级 `cat`/`open` 高半层,因为它会解码文本并拒绝二进制,使政策层绝不接触原始字节。它负责 UTF-8 解码、二进制拒绝、原子写入和字面量编辑临界区。它**不** 负责行窗口、编号行、渲染 footer 或已观察状态。已观察状态、编辑前读取和版本防护的写入/编辑属于插件(`@deepseek-ai/dsh-fs-observation-policy`)通过提供可选防护而添加的政策,并非提供方行为,因此沙箱化/远程后端不会继承任何面向模型的观察政策。
`editText` 留在该 seam 上,不由政策层通过读取加写入组合,因为版本防护、字面量匹配和原子重写必须处于同一临界区内,才能正确归因错误并实现一方胜出/一方陈旧的并发;远程后端也可以将其实现为原生比较并编辑操作。

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-fs",
"description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -92,7 +92,7 @@ export abstract class FileSystem extends Service {
* The sandbox mode this backend enforces on mutations BY DEFAULT, or
* `undefined` when it does not confine at all — the capability fact the tool
* layer reads to advertise the escalation fields honestly (mirrors
* `BashExecutor.sandboxMode`). The base class and the bare local backend
* `ShellExecutor.sandboxMode`). The base class and the bare local backend
* report `undefined`; a sandboxing backend (`@deepseek-ai/dsh-fs-sandbox`)
* overrides it with the deployment default. A session override may make the
* effective mode narrower or wider, so strict escalation widening is checked

View File

@@ -3,11 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type { FsTarget } from '@deepseek-ai/dsh-fs'
import * as FsInvariant from '@deepseek-ai/dsh-fs/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(FsInvariant)
return ctx
}

View File

@@ -1,7 +1,7 @@
/**
* Tests for the filesystem Service Definition: registration, duplicate-service
* behavior, disposal, and the branded id factories. The provider primitives and
* policy live in `dsh-fs-local` and `dsh-fs-policy`; this seam owns only the
* policy live in `dsh-fs-local` and `dsh-fs-observation-policy`; this seam owns only the
* abstract service contract, so a minimal fake backend exercises it.
*/

View File

@@ -19,7 +19,7 @@
"path": "../../llm/llm"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../sandbox/sandbox"

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/tool-fs-search/README.md
README.md: 7803ce52c1b9d2d858dadb718edd163585360aef
README.zh.md: b4f04017ee178217a5b98427ae2a5335e697c598
README.md: 83290df98260e977a8cd3ea808f491ea63e75857
README.zh.md: b33fe002fc4ccc2bfe5fefec342f2044e3511078

View File

@@ -6,13 +6,13 @@ The **model-facing filesystem discovery tools**—`glob`, `grep`—are backed by
```ts ignore-check
// A deployment chooses how over-cap glob pages are selected.
await ctx.plugin(LocalSubprocessService) // @deepseek-ai/dsh-subprocess-local
await ctx.plugin(LocalSubprocessRuntime) // @deepseek-ai/dsh-subprocess-local
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: false })
// Optional: a spill backend makes capped results fully recoverable.
await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-spill-local
```
Why spawn-backed: local workspace discovery is naturally a process-backed `rg` workflow, and putting search on `ctx.fs` would force every filesystem backend to grow a search API. The subprocess seam owns spawn execution, process-tree termination, environment scrubbing, and bounded output capture; this package owns schemas, argument validation, argv construction, parsing, retention, formatted-result spill, and timeout declaration. The tools never expose a background task — the call returns only after `rg` exits, is terminated by the cooperative timeout, is aborted, or fails.
Why spawn-backed: local workspace discovery is naturally a process-backed `rg` workflow, and putting search on `ctx.fs` would force every filesystem backend to grow a search API. The subprocess seam owns spawn execution, process-tree termination, environment scrubbing, and bounded output capture; this package owns schemas, argument validation, argv construction, parsing, retention, formatted-result spill, and timeout declaration. The tools never expose a background job — the call returns only after `rg` exits, is terminated by the cooperative timeout, is aborted, or fails.
## Deployment requirement: no host rg, co-located workdir/filesystem
@@ -29,7 +29,7 @@ The binary ships with the package on every supported platform (macOS/Linux/Windo
| `grepMaxMatches` | `250` | Max flat matches one `grep` call retains inline (matches Claude Code's `GrepTool` `head_limit`); later matches go to the formatted spill artifact. |
| `grepMaxLineBytes` | `2000` | Byte cap per matched-line preview; the cut preserves UTF-8 boundaries and is marked `(line truncated)`. |
| `rawOutputMaxBytes` | `20000000` | Max complete raw `rg` stdout a search will parse (matches Claude Code's ripgrep raw buffer); larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. |
| `timeoutMs` | `30000` | Cooperative tool-call budget attached to both tool definitions, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`; the subprocess seam's terminate escalation is the hard kill. |
| `timeoutMs` | `30000` | Cooperative tool-call budget attached to both tool definitions, enforced by `@deepseek-ai/dsh-tool-call-timeout-policy` through `exec.signal`; the subprocess seam's terminate escalation is the hard kill. |
| `graceMs` | `3000` | Positive terminate-escalation grace the subprocess seam grants past `timeoutMs` before the search fails as `SEARCH_ABORTED`; it cannot exceed [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md). |
| `stderrMaxBytes` | `65536` | Diagnostic-tail budget for `rg` stderr, captured through the subprocess seam's collect disposition; a lossy read keeps only the tail (marked `[stderr truncated]`). |

View File

@@ -6,7 +6,7 @@
```ts ignore-check
// A deployment chooses how over-cap glob pages are selected.
await ctx.plugin(LocalSubprocessService) // @deepseek-ai/dsh-subprocess-local
await ctx.plugin(LocalSubprocessRuntime) // @deepseek-ai/dsh-subprocess-local
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: false })
// Optional: a spill backend makes capped results fully recoverable.
await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-spill-local
@@ -29,7 +29,7 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
| `grepMaxMatches` | `250` | 一次 `grep` 调用内联保留的最大平铺匹配数(与 Claude Code 的 `GrepTool` `head_limit` 相同);后续匹配写入格式化 spill 产物。 |
| `grepMaxLineBytes` | `2000` | 每条匹配行预览的字节上限;截断会保留 UTF-8 边界,并标记为 `(line truncated)`。 |
| `rawOutputMaxBytes` | `20000000` | 搜索将解析的完整原始 `rg` stdout 上限(与 Claude Code 的 ripgrep 原始 buffer 相同);更大的原始输出以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败。 |
| `timeoutMs` | `30000` | 附加到两个工具定义上的协作式工具调用预算,由 `@deepseek-ai/dsh-timeout-policy` 通过 `exec.signal` 强制执行subprocess seam 的终止升级提供硬终止。 |
| `timeoutMs` | `30000` | 附加到两个工具定义上的协作式工具调用预算,由 `@deepseek-ai/dsh-tool-call-timeout-policy` 通过 `exec.signal` 强制执行subprocess seam 的终止升级提供硬终止。 |
| `graceMs` | `3000` | subprocess seam 在 `timeoutMs` 之外授予的终止升级宽限期须为正值;超过后搜索以 `SEARCH_ABORTED` 失败;该宽限期不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md)。 |
| `stderrMaxBytes` | `65536` | `rg` stderr 的诊断尾部预算,经 subprocess seam 的 collect 形态捕获lossy 读取只保留尾部(标记 `[stderr truncated]`)。 |

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-tool-fs-search",
"description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"@vscode/ripgrep": "^1.18.0",
"@deepseek-ai/schemastery": "workspace:^"
@@ -38,7 +38,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-retention": "workspace:^",
"@deepseek-ai/dsh-output-retention": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-spill": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
@@ -53,7 +53,7 @@
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-retention": "workspace:^",
"@deepseek-ai/dsh-output-retention": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-spill": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",

View File

@@ -14,7 +14,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { GenericCallView, SearchResultView, ToolResult } from '@deepseek-ai/dsh-tools'
import type { RetainedItems } from '@deepseek-ai/dsh-retention'
import type { RetainedItems } from '@deepseek-ai/dsh-output-retention'
import type { SpillRef } from '@deepseek-ai/dsh-spill'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type { GrepMatch } from './search-core.ts'

View File

@@ -8,7 +8,7 @@
*
* Local workspace discovery is a process-backed `rg` workflow, so these tools
* execute through `ctx.subprocess.spawn()` with fixed ripgrep argv templates —
* never `ctx.bash`, never `ctx.bash.start()`, never a model-visible background
* never `ctx.shell`, never `ctx.shell.start()`, never a model-visible background
* task. The tool layer owns schemas, argument validation, argv construction
* ({@link module:@deepseek-ai/dsh-tool-fs-search/glob} /
* {@link module:@deepseek-ai/dsh-tool-fs-search/grep}), result parsing,
@@ -87,7 +87,10 @@ export interface Config {
graceMs?: number
/** Max bytes retained for one search's stderr tail; the excerpt is embedded in `SEARCH_*` error messages, never shown on success. */
stderrMaxBytes?: number
/** Cooperative tool-call timeout budget (ms) on both tools, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`. */
/**
* Cooperative tool-call timeout budget (ms) on both tools, enforced by
* `@deepseek-ai/dsh-tool-call-timeout-policy` through `exec.signal`.
*/
timeoutMs?: number
}

View File

@@ -31,7 +31,7 @@ import type {
SearchLineMatch,
SearchResultView,
} from '@deepseek-ai/dsh-tools'
import type { RetainedItems } from '@deepseek-ai/dsh-retention'
import type { RetainedItems } from '@deepseek-ai/dsh-output-retention'
import type { GrepMatch } from './search-core.ts'
/**

View File

@@ -6,7 +6,7 @@
* and workdir-relative path display.
*
* Both tools execute as ordinary foreground spawns through `ctx.subprocess` —
* never `ctx.bash`, never `ctx.bash.start()`, never a model-visible background
* never `ctx.shell`, never `ctx.shell.start()`, never a model-visible background
* task. The ripgrep binary ships inside the npm package, so no system `rg`
* install is required, and no shell layer exists between the argv vector and
* ripgrep, so no shell quoting is involved. Raw `rg` stdout is an internal
@@ -22,8 +22,8 @@
import { isAbsolute, relative, sep } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { ItemRetainer, TextRetainer } from '@deepseek-ai/dsh-retention'
import type { RetainedItems } from '@deepseek-ai/dsh-retention'
import { ItemRetainer, TextRetainer } from '@deepseek-ai/dsh-output-retention'
import type { RetainedItems } from '@deepseek-ai/dsh-output-retention'
import type { SubprocessHandle, SubprocessOutcome, SubprocessOutputRead, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
@@ -37,7 +37,7 @@ export const RAW_OUTPUT_MAX_BYTES = 20_000_000
/**
* Default cooperative tool-call timeout budget in milliseconds (the `timeoutMs`
* config), attached to both tool definitions for
* `@deepseek-ai/dsh-timeout-policy` to enforce through `exec.signal`.
* `@deepseek-ai/dsh-tool-call-timeout-policy` to enforce through `exec.signal`.
*/
export const SEARCH_TIMEOUT_MS = 30_000
@@ -178,7 +178,7 @@ export function resolveRgPath(): Promise<string> {
* complete raw stdout. The working directory is the calling agent's session
* cwd (`exec.agent.session.header.cwd`) when available, else
* `process.cwd()`. `exec.signal` is forwarded so the cooperative tool timeout
* (`@deepseek-ai/dsh-timeout-policy`) and caller cancellation terminate the
* (`@deepseek-ai/dsh-tool-call-timeout-policy`) and caller cancellation terminate the
* process tree.
*
* The spawn is unconfined (a plain `ctx.subprocess` call), so `--no-config`

View File

@@ -17,8 +17,8 @@ import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
const testToolSignal = new AbortController().signal
@@ -62,8 +62,8 @@ describe('search tools over the real subprocess service + the packaged rg', () =
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: true })
})

View File

@@ -18,8 +18,8 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as toolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
describe('dsh-tool-fs-search real-load-path guard', () => {
@@ -38,8 +38,8 @@ describe('dsh-tool-fs-search real-load-path guard', () => {
it('boots over ctx.subprocess through the unwrapped module without an inject error', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(toolFsSearch) as Parameters<Context['plugin']>[0]

View File

@@ -6,7 +6,7 @@
* abort/timeout kills, signal kills, ripgrep exit codes — so these tests
* verify schemas, argument validation, argv construction, workdir derivation,
* signal forwarding, `SEARCH_*` error classification, retention,
* formatted-result spill handoff, and the no-background-task invariant.
* formatted-result spill handoff, and the no-background-job invariant.
* Real-`rg` behavior is pinned separately in integration.spec.ts.
*/
@@ -15,8 +15,8 @@ import { Context } from '@deepseek-ai/cordis'
import { join, sep } from 'node:path'
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH, type ToolExecution, type ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH, type ToolExecution, type ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessCollectedOutputs, SubprocessHandle, SubprocessOutcome, SubprocessOutputRead, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { rgPath } from '@vscode/ripgrep'
@@ -146,7 +146,7 @@ class FakeHandle implements SubprocessHandle {
* never spawn outside a single awaited foreground call, so every test can
* assert on the exact spawn specs and settled handles.
*/
class FakeSubprocess extends SubprocessService {
class FakeSubprocess extends SubprocessRuntime {
spawns: SubprocessSpawnSpec[] = []
override async resolveExecutable(command: string): Promise<string> { return command }
override spawnTerminal(): Promise<never> { throw new Error('search tools spawn pipes, never terminals') }
@@ -192,7 +192,7 @@ async function setup(options: SetupOptions = {}) {
const warnings: string[] = []
ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeSubprocess)
const subprocess = ctx.subprocess as FakeSubprocess
if (options.spill === true) await ctx.plugin(FakeSpill)
@@ -250,7 +250,7 @@ describe('registration', () => {
it('stays pending until ctx.subprocess exists (inject)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(ToolFsSearch, DEFAULT_CONFIG) // no subprocess service
expect(ctx.tools.schemas()).toHaveLength(0)
})
@@ -308,7 +308,7 @@ describe('config validation', () => {
] as const)('rejects a non-positive or fractional %s at load', async (name, config) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeSubprocess)
await expect(ctx.plugin(ToolFsSearch, { ...DEFAULT_CONFIG, ...config })).rejects.toThrow(new RegExp(`tool-fs-search: ${name} must be a positive integer`))
})
@@ -316,7 +316,7 @@ describe('config validation', () => {
it('rejects a grace beyond the Node timer range at load', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeSubprocess)
await expect(ctx.plugin(ToolFsSearch, {
...DEFAULT_CONFIG,
@@ -1090,7 +1090,7 @@ describe('rg --json transport failures (SEARCH_FAILED)', () => {
})
})
describe('the no-background-task invariant', () => {
describe('the no-background-job invariant', () => {
it('settles every spawned search handle across successful and failed searches', async () => {
const { ctx, subprocess } = await setup()
subprocess.handler = () => runResult('a.ts\n')

View File

@@ -16,7 +16,7 @@
"path": "../../../vendor/schemastery"
},
{
"path": "../../util/retention"
"path": "../../util/output-retention"
},
{
"path": "../../llm/llm"
@@ -31,7 +31,7 @@
"path": "../../core/system-prompt"
},
{
"path": "../../bash/bash"
"path": "../../shell/shell"
},
{
"path": "../../spill/spill"
@@ -40,7 +40,7 @@
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/tool-fs/README.md
README.md: 7e334f886747cd8dc566a572c699cd80c7cf62fe
README.zh.md: 269992c86d46744bf11a9360202767ac1243dc91
README.md: 71dc4134feee6bf1b5d17eef5f62e587653adf6d
README.zh.md: dff4b838aea4c6fa50a92ce2102bfdd2a96150a5

View File

@@ -2,17 +2,17 @@
English | [中文](README.zh.md)
The **model-facing filesystem tools**`read`, `read_image`, `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 contract ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool exposes escalation for filesystem mutations.
The **model-facing filesystem tools**`read`, `read_image`, `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 contract ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-observation-policy`](../fs-observation-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool exposes escalation for filesystem mutations.
```ts ignore-check
// 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(FsPolicy) // @deepseek-ai/dsh-fs-observation-policy (policy gate)
await ctx.plugin(LocalAttachmentStore, { dshHome }) // optional — enables durable read_image results
await ctx.plugin(ToolFs) // this package — read/write/edit, plus read_image with attachments
```
`@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.
`@deepseek-ai/dsh-fs-observation-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.
`read_image` registers only while a durable `ctx.attachments` service is mounted — without one the deployment cannot commit image bytes, so the tool never appears. Execution additionally requires the exact routed model to declare `image` input (resolved through `ctx.llm.resolveModelInfo` from the session's latest request header, falling back to agent options); an unknown or text-only route gets a refusal result before any filesystem I/O, so a text route's durable history stays free of image blocks.
@@ -49,13 +49,13 @@ The tools do **not** inject a policy service or inspect any cache. Each tool res
- **write** — `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.writeText(target, content, intent)`, then `fs/observed`. (0 stat.)
- **edit** — `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.editText(target, edit, intent)`, then `fs/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.
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-observation-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 `ToolRuntime.execute()` and become `isError` tool results with their `{ name, code }` attached.
When `ctx.fs.sandboxMode` reports confinement, write/edit advertise `sandbox_permissions` and `justification` and resolve approved retries through `ctx.approval`. The policy owner contributes capability-neutral standing policy; the tool results retain operation-specific denial and retry guidance.
## `fs/observed` is fire-and-forget
`fs/observed` fires AFTER the read/read_image/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.
`fs/observed` fires AFTER the read/read_image/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-observation-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.
`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
@@ -78,13 +78,13 @@ Use the read tool — not shell commands like cat — to inspect text files. Res
##### Write guidance
```markdown
Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.
```
##### Edit guidance
```markdown
Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.
```
#### Token effect

View File

@@ -2,17 +2,17 @@
[English](README.md) | 中文
**面向模型的文件系统工具**`read``read_image``write``edit`)及其**执行器**。这是文件系统栈的消费方层拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
**面向模型的文件系统工具**`read``read_image``write``edit`)及其**执行器**。这是文件系统栈的消费方层拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-observation-policy`](../fs-observation-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
```ts ignore-check
// 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(FsPolicy) // @deepseek-ai/dsh-fs-observation-policy (policy gate)
await ctx.plugin(LocalAttachmentStore, { dshHome }) // optional — enables durable read_image results
await ctx.plugin(ToolFs) // this package — read/write/edit, plus read_image with attachments
```
`@deepseek-ai/dsh-fs-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供写入/编辑前读取行为。
`@deepseek-ai/dsh-fs-observation-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供写入/编辑前读取行为。
`read_image` 只在持久 `ctx.attachments` 服务已挂载时注册:没有它,部署无法持久提交图像字节,工具就不会出现。执行时还要求确切路由的模型声明 `image` 输入(通过 `ctx.llm.resolveModelInfo` 从会话最新请求 header 解析,缺失时回退到 agent 选项);未知或纯文本路由在任何文件系统 I/O 之前就得到拒绝结果,因此文本路由的持久历史不会出现图像块。
@@ -49,13 +49,13 @@ await ctx.plugin(ToolFs) // this package — re
- **write**:调用 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.writeText(target, content, intent)`,再发出 `fs/observed`。0 次 stat。
- **edit**:调用 `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.editText(target, edit, intent)`,再发出 `fs/observed`。0 次 stat。
工具在每次分派中把 `exec`(工具执行上下文)作为不透明 `actor` 传入。默认 thunk 返回 `undefined`(不受约束的裸提供方)。加载 `@deepseek-ai/dsh-fs-policy` 后,它会占用单个决策槽:返回 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`,并在 `fs/observed` 时记录。后端错误(`FsError`)和抛出的 `FS_NOT_OBSERVED` 会流经 `ToolRegistry.execute()`,变成 `isError` 工具结果,并附带 `{ name, code }`。
工具在每次分派中把 `exec`(工具执行上下文)作为不透明 `actor` 传入。默认 thunk 返回 `undefined`(不受约束的裸提供方)。加载 `@deepseek-ai/dsh-fs-observation-policy` 后,它会占用单个决策槽:返回 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`,并在 `fs/observed` 时记录。后端错误(`FsError`)和抛出的 `FS_NOT_OBSERVED` 会流经 `ToolRuntime.execute()`,变成 `isError` 工具结果,并附带 `{ name, code }`。
当 `ctx.fs.sandboxMode` 表明提供方施加沙箱限制时write/edit 会公开 `sandbox_permissions` 与 `justification`,并通过 `ctx.approval` 处理获批后的重试。策略归属方会贡献与具体能力无关的常驻策略;工具结果仍保留针对具体操作的拒绝与重试引导。
## `fs/observed` 发后即忘
`fs/observed` 在 read/read_image/write/edit 已经成功之后,通过普通 `ctx.emit` 发出。监听器的约定是同步且只有副作用的记录器(`@deepseek-ai/dsh-fs-policy` 使用 `WeakMap.set`);工具不保护这次发出,因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。
`fs/observed` 在 read/read_image/write/edit 已经成功之后,通过普通 `ctx.emit` 发出。监听器的约定是同步且只有副作用的记录器(`@deepseek-ai/dsh-fs-observation-policy` 使用 `WeakMap.set`);工具不保护这次发出,因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。
`read` 允许并发调度,因为它唯一会改变状态的操作是同步记录版本。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此即使记录器发生竞态,系统也会安全地拒绝操作;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
@@ -78,13 +78,13 @@ Use the read tool — not shell commands like cat — to inspect text files. Res
##### Write 指导
```markdown
Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.
Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.
```
##### Edit 指导
```markdown
Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.
Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.
```
#### Token 影响

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-tool-fs",
"description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"diff": "^9.0.0",
"@deepseek-ai/schemastery": "workspace:^"
@@ -55,7 +55,7 @@
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-fs-policy": "workspace:^",
"@deepseek-ai/dsh-fs-observation-policy": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",

View File

@@ -77,7 +77,7 @@ export function applyEditTool(ctx: Context, sandbox: FsSandboxController): void
ctx.systemPrompt.section({
name: 'tool:edit',
order: 102,
text: 'Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.',
text: 'Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.',
})
ctx.tools.register(defineTool({

View File

@@ -111,7 +111,7 @@ export class FsSandboxController {
* Map a thrown provider error for the model: a `FS_SANDBOX_DENIED` becomes a
* `FsError` whose text is the shared `[sandbox: …]` denial marker plus the
* same-turn escalation hint, so a policy denial reads identically to bash's
* WHILE keeping the structured `FS_SANDBOX_DENIED` code — `ToolRegistry`
* WHILE keeping the structured `FS_SANDBOX_DENIED` code — `ToolRuntime`
* populates `result.error` only for `HarnessError` instances, so a plain
* `Error` would strip the code retry/observers key off. Any other error
* passes through unchanged. A `FS_SANDBOX_DENIED` only arises under a

View File

@@ -63,7 +63,7 @@ export function applyWriteTool(ctx: Context, sandbox: FsSandboxController): void
ctx.systemPrompt.section({
name: 'tool:write',
order: 101,
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.',
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.',
})
ctx.tools.register(defineTool({

View File

@@ -3,7 +3,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'

View File

@@ -12,9 +12,9 @@ import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
const testToolSignal = new AbortController().signal
@@ -48,12 +48,12 @@ afterEach(async () => {
// --------------------------------------------------------------------------
// DEFAULT deployment: the policy gate plugin is loaded.
// --------------------------------------------------------------------------
describe('default deployment (with dsh-fs-policy)', () => {
describe('default deployment (with dsh-fs-observation-policy)', () => {
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'dsh-tool-fs-'))
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalFileSystem, { cwd: dir })
await ctx.plugin(FsPolicy)
fiber = await ctx.plugin(ToolFs)
@@ -310,12 +310,12 @@ describe('default deployment (with dsh-fs-policy)', () => {
// --------------------------------------------------------------------------
// BARE deployment: the tool suite WITHOUT the policy gate.
// --------------------------------------------------------------------------
describe('bare provider (no dsh-fs-policy)', () => {
describe('bare provider (no dsh-fs-observation-policy)', () => {
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'dsh-tool-fs-bare-'))
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalFileSystem, { cwd: dir })
fiber = await ctx.plugin(ToolFs)
})
@@ -383,7 +383,7 @@ describe('per-session cwd', () => {
sessionDir = await mkdtemp(join(tmpdir(), 'dsh-tool-fs-session-'))
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalFileSystem, { cwd: dir }) // config.cwd = dir, NOT sessionDir
await ctx.plugin(FsPolicy)
fiber = await ctx.plugin(ToolFs)
@@ -428,7 +428,7 @@ describe('signal, concurrency, and the fs/observed contract', () => {
dir = await mkdtemp(join(tmpdir(), 'dsh-tool-fs-'))
ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalFileSystem, { cwd: dir })
await ctx.plugin(FsPolicy)
fiber = await ctx.plugin(ToolFs)

View File

@@ -12,13 +12,13 @@ import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import { CallId, LlmAdapter, LlmService } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import type { Config as ToolConfig } from '@deepseek-ai/dsh-tools'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import { AttachmentId, AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
@@ -100,7 +100,7 @@ interface SetupOptions {
async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry, { mode: options.toolMode ?? 'native' })
await ctx.plugin(ToolRuntime, { mode: options.toolMode ?? 'native' })
if (options.toolMode === 'code' || options.toolMode === 'both') {
await ctx.plugin(FakeRuntime)
}
@@ -110,7 +110,7 @@ async function setup(options: SetupOptions = {}) {
await ctx.plugin(LocalAttachmentStore, { dshHome: home, ...options.storeConfig })
}
if (options.llm !== false) {
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['visual'], new CatalogAdapter(options.models ?? [
{ provider: 'visual', id: 'vision-model', name: 'Vision', inputModalities: ['text', 'image'] },
{ provider: 'visual', id: 'text-model', name: 'Text', inputModalities: ['text'] },
@@ -444,7 +444,7 @@ describe('registration surface', () => {
it('withdraws read_image when the tool-fs fiber or the attachment store is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry, { mode: 'native' })
await ctx.plugin(ToolRuntime, { mode: 'native' })
await ctx.plugin(LocalFileSystem, { cwd: dir })
await ctx.plugin(FsPolicy)
const attachmentsFiber = await ctx.plugin(LocalAttachmentStore, { dshHome: home })

View File

@@ -10,7 +10,7 @@ import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolResult } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { type ToolResult } from '@deepseek-ai/dsh-tools'
import { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
FsDirEntry,
@@ -22,7 +22,7 @@ import type {
FsWriteIntent,
FsWriteOutcome,
} from '@deepseek-ai/dsh-fs'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import { STREAM_MIN_SIZE } from '../src/read.ts'
import { formatReadOutput } from '../src/read-render.ts'
@@ -101,7 +101,7 @@ class FakeFs extends FileSystem {
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeFs)
await ctx.plugin(FsPolicy)
await ctx.plugin(ToolFs)
@@ -177,7 +177,7 @@ describe('registration', () => {
it('stays pending until ctx.fs exists (inject)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(ToolFs) // no fs provider
expect(ctx.tools.schemas()).toHaveLength(0)
})
@@ -185,7 +185,7 @@ describe('registration', () => {
it('unregisters everything on fiber disposal (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeFs)
await ctx.plugin(FsPolicy)
const fiber = await ctx.plugin(ToolFs)
@@ -692,7 +692,7 @@ describe('read caps are plugin config', () => {
async function setupWith(config: ToolFs.Config) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeFs)
await ctx.plugin(FsPolicy)
await ctx.plugin(ToolFs, config)
@@ -749,7 +749,7 @@ describe('read caps are plugin config', () => {
] as const)('rejects a non-positive or fractional %s at load', async (name, config) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeFs)
await expect(ctx.plugin(ToolFs, config)).rejects.toThrow(new RegExp(`tool-fs: ${name} must be a positive integer`))
})
@@ -791,7 +791,7 @@ describe('sandbox escalation API (write/edit)', () => {
async function setupConfining(opts: { approval?: boolean } = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write' })
await ctx.plugin(SandboxingFakeFs)
await ctx.plugin(FsPolicy)
@@ -821,7 +821,7 @@ describe('sandbox escalation API (write/edit)', () => {
it('fails load when a confining filesystem has no shared sandbox-policy resolver', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SandboxingFakeFs)
await expect(ctx.plugin(ToolFs)).rejects.toThrow('tool-fs: the mounted filesystem confines but ctx.sandboxPolicy is missing')
})

View File

@@ -28,10 +28,10 @@
"path": "../fs"
},
{
"path": "../fs-policy"
"path": "../fs-observation-policy"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../sandbox/sandbox"

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-tool-str-replace-editor",
"description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -29,7 +29,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -45,7 +45,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-fs-policy": "workspace:^",
"@deepseek-ai/dsh-fs-observation-policy": "workspace:^",
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -9,11 +9,11 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor'
const contexts: Context[] = []
@@ -71,7 +71,7 @@ async function setup(
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
if (options.sandboxMode === undefined) {
await ctx.plugin(LocalFileSystem, { cwd: root })
@@ -440,7 +440,7 @@ describe('tool-str-replace-editor', () => {
})).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } })
})
it('delegates read-before-edit decisions to fs-policy', async () => {
it('delegates read-before-edit decisions to fs-observation-policy', async () => {
const { ctx, root, owner } = await setup({}, { fsPolicy: true })
const existing = join(root, 'existing.txt')
const created = join(root, 'created.txt')
@@ -531,7 +531,7 @@ describe('tool-str-replace-editor', () => {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalFileSystem, { cwd: root })
Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' })

View File

@@ -11,6 +11,6 @@
{ "path": "../fs" },
{ "path": "../../sandbox/sandbox" },
{ "path": "../../sandbox/sandbox-policy" },
{ "path": "../../support/invariants" }
{ "path": "../../runtime-diagnostics/invariants" }
]
}