DSH / Atlas
2026-07-14implementedfeature

Cross-family file sandbox — one policy home, a sandboxed fs provider, and fs escalation parity

跨家族文件沙箱——统一策略归属、沙箱化 fs 提供方、fs 升级对等

`SandboxMode` claims file effects, but originally only `ctx.shell` enforced it. The fs tools (`write`/`edit`) mutate the host filesystem in-process through `ctx.fs`, where an OS argv wrapper is mechanically meaningless — [the sandbox Agent Note](2026-07-06-sandbox.md) § In-process tools records this and left cross-family enforcement as a deferred phase with an open question: whether in-process enforcement stays per-s

English

Problem

SandboxMode claims file effects, but originally only ctx.shell enforced it. The fs tools (write/edit) mutate the host filesystem in-process through ctx.fs, where an OS argv wrapper is mechanically meaningless — the sandbox Agent Note § In-process tools records this and left cross-family enforcement as a deferred phase with an open question: whether in-process enforcement stays per-seam or becomes a uniform harness capability. This Agent Note is that phase, and answers it: one shared policy home, per-seam enforcement at each family's correct altitude.

The gap was not read-only-shaped. A confined coding agent's product mode is workspace-write: bash may already write under the workspace root while everything outside is denied, so an fs enforcement that could only deny-all would be strictly worse than disabling the fs tools — the model would attempt an in-workspace write, be denied, and learn to detour through bash heredocs. Cross-family enforcement therefore speaks the full mode ladder, including the path-containment judgment workspace-write requires (canonical targets; ../symlink/absolute-path escapes) and the same escalation lever bash carries.

A second enforcing family also exposed an ownership problem in the original layout. The deployment default (mode + workspaceRoot) was configured on dsh-bash-sandbox, and the per-session override event was shell/sandbox-mode, folded and written by dsh-shell's session-mode kit. With fs enforcing the same policy, either fs reads bash's config and events (a capability family depending on a sibling's plugin config) or each family carries its own copy — and two copies of workspaceRoot drift into exactly the split world the sandbox RFC warns about: bash confined to one root while fs fences another.

Decision

Three coordinated pieces, all composed from the leaf cordis.yml, none touching agent-loop.

ctx.sandboxPolicy — one home for mode and workspace root

packages/sandbox/sandbox-policy/ (@deepseek-ai/dsh-sandbox-policy) registers ctx.sandboxPolicy, the single owner of the deployment's sandbox policy:

  • Config: mode (the closed SandboxMode union, default read-only) and workspaceRoot (default the process cwd, resolved absolute). Misconfiguration fails loud at load.
  • The per-session override event sandbox/mode, with its pure fold (effectiveSandboxMode(events)), its write path (setSandboxMode(session, mode)), and SANDBOX_MODES. The event is policy state — consumed by two families — so it lives here, not in either capability's seam. Its shape and log-only semantics match the approval/* precedent.
  • resolve({ session?, mode? }), which returns a complete per-call SandboxExecutionPolicy: explicit approved mode > the session fold > defaultMode, and the session's immutable cwd > configured workspaceRoot fallback.
  • defaultMode / workspaceRoot accessors retained as deployment fallbacks and the capability-advertisement fact.

dsh-bash-sandbox carries no sandbox config of its own — it injects sandboxPolicy and uses its deployment fallback only for direct calls. dsh-tool-bash and dsh-tool-fs pass the active session to ctx.sandboxPolicy.resolve(), so both receive the same effective mode and cwd root on every call; dsh-permission-presets presets and the ACP bridge write through the relocated setter. The seams that own bash and fs execution remain session-free — the session dependency lives in the policy package and tool consumers.

dsh-fs-sandbox — enforcement inside the provider

packages/fs/fs-sandbox/ (@deepseek-ai/dsh-fs-sandbox) mirrors the bash-local/bash-sandbox split: SandboxedFileSystem extends LocalFileSystem, registered as ctx.fs, injecting sandboxPolicy. Reads (resolve/stat/readText/streamText/listDir) pass through untouched — every mode permits reading. The two mutations enforce by mode before delegating to the inherited atomic write:

  • read-only denies writeText/editText outright.
  • workspace-write fences the canonicalized target against the writable-root set — writableRoots(policy) in dsh-sandbox: the workspace root plus the platform temp areas (/tmp, os.tmpdir()), each realpathed — the SAME set the Seatbelt profile grants, so the fs fence is the fourth dialect of one mode meaning alongside the bwrap/Landlock/Seatbelt profiles, and "the write tool cannot write /tmp but bash can" asymmetries cannot arise. Canonical spellings take a lexical containment fast path; when Windows exposes one directory through different casing or long-name/8.3 spellings, an ancestor walk compares filesystem identity rather than weakening the boundary to textual prefix guesses. The target is re-canonicalized (resolve realpaths the deepest existing ancestor) immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught.
  • danger-full-access delegates unfenced.

A denial is the structured FS_SANDBOX_DENIED carrying the effective mode — distinct from FS_PERMISSION_DENIED (a host EACCES is the world refusing; this is policy refusing). No text inference: an in-process fence knows exactly what it denied. The per-call carrier is a trailing optional SandboxExecutionPolicy on writeText/editText (the filesystem twin of ShellExecRequest.sandboxPolicy); the seam stays session-free, and the bare local backend ignores it. FileSystem.sandboxMode is the capability fact (undefined on the base and fs-local, the default on SandboxedFileSystem), so the tool layer advertises escalation from composition truth.

The threat model is stated in the package README: a policy fence in trusted code over model-controlled paths, not a kernel boundary — the operations are the seam's own, only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface (the code-runtime "containment, not a security boundary" precedent). Kernel-grade isolation of untrusted CODE stays ctx.shell's job. The residual resolve-to-syscall race is narrowed by the in-place re-canonicalization and eliminated only by platform primitives (openat2 RESOLVE_BENEATH) not worth their portability cost here.

Tool parity — one denial marker, one escalation flow

dsh-tool-fs resolves the active session's complete policy onto each mutation and maps FS_SANDBOX_DENIED to the marker the model already knows from bash: [sandbox: file access denied under <mode> mode]. When ctx.fs.sandboxMode reports a confining mode at registration, write and edit advertise the same sandbox_permissions + justification fields, teach the same same-turn retry, and resolve the same ctx.approval request before executing — the four outcomes and their verbatim fail-closed texts carried over from the sandbox Agent Note § Escalation (strict widening checked at execution against the call's effective mode; a grant changes only that call's mode and retains its session root; no new session events).

The shared pieces live in dsh-sandbox, which owns the mode types: WIDER_MODES, the escalation-target enum, the argument-pairing validation, the denial/hint marker builders, and approveEscalation — the ordered fail-closed choreography. approveEscalation takes a minimal STRUCTURAL approver (EscalationApprover, generic over the agent and call-id types), not the approval service type, so dsh-sandbox gains no dependency on the approval or agent packages: each tool passes its own ctx.approval, agent, call id, and tool name as ingredients. dsh-tool-bash and dsh-tool-fs both use these; the cross-file duplication gate holds the single-sourcing honest.

The examples/acp-agent composition loads dsh-sandbox-policy and dsh-fs-sandbox, moves the mode/workspaceRoot config to the policy entry, and drops the old gating that disabled the fs stack under confined modes; fs-observation-policy (read-before-edit) composes orthogonally on top. The system prompt still states no sandbox mode — the marker teaches the boundary at the moment it matters, per the sandbox Agent Note's live evidence.

The enforcement point: provider, not intent gate

The sandbox Agent Note's original cross-family sketch put fs enforcement on the fs/write-intent/fs/edit-intent events. This Agent Note enforces in the provider instead, on two mechanical facts: the intent slots are single-decision first-wins (occupied by dsh-fs-observation-policy, whose contract names a second decider a misconfiguration), and the intent events are dispatched only by dsh-tool-fs — a direct ctx.fs caller (a cordis-mounted plugin, a custom tool) bypasses them, where provider-level enforcement covers every caller by construction.

Out of scope

  • Network policy for ctx.webSandboxMode claims file effects only; a web-only network knob while bash curl runs free would be a false boundary. Revisit when a bash backend enforces network (bwrap --unshare-net, Landlock ABI v4+).
  • The subagent-acp consumer — unchanged deferred phase of the sandbox RFC.
  • Additional writable roots inside one session — the resolved policy carries one primary SessionHeader.cwd; ACP additionalDirectories remains a separate bridge and policy design.
  • A uniform per-tool sandbox runtime — remains rejected for the reasons in the sandbox RFC.

Alternatives considered

  • Enforce on the fs/* intent events (the sandbox Agent Note's original sketch) — rejected on the two mechanical facts in § The enforcement point: single-slot first-wins already occupied, and a bypass for direct ctx.fs callers. Provider-level enforcement covers every caller and mirrors bash's swap-the-implementation shape.
  • Enforce in tools/pre-execute — rejected: the listener sees the model's raw path string before resolve(), so it would re-implement cwd defaulting and symlink canonicalization and still race the real resolve. Disqualifying for workspace-write, a judgment over canonical paths.
  • Inline checks in dsh-tool-fs — rejected: covers only the tool path (same bypass as the intent events) and duplicates resolve knowledge one layer above where the canonical target already exists.
  • A mode flag on dsh-fs-local instead of a sibling backend — rejected: the capability fact must be composition truth the way dsh-bash-local vs dsh-bash-sandbox is; a config flag makes the tool's advertisement conditional on configuration, and the bash family already establishes the sibling-package shape.
  • Kernel-enforced fs mutations via a confined helper subprocess — rejected: a process per write; editText's read-match-write critical section would have to move wholesale into the child to stay atomic; and the threat surface (trusted operations, untrusted path argument) does not need a kernel — the fence in trusted code is the complete answer, while untrusted-code isolation stays on ctx.shell.
  • Per-family policy config with a load-time consistency check — rejected: two homes for one fact, patched by a check that must enumerate every future enforcing family; the policy service makes drift inexpressible instead of detected.
  • Keep the override event in dsh-shell as shell/sandbox-mode — rejected: the event is policy state consumed by two families; leaving it bash-named forces dsh-fs-sandbox to depend on bash vocabulary. Pre-release, the rename is a same-change move with snapshot re-records, no shims.
  • Escalation choreography imported from the approval/agent packages into dsh-sandbox — rejected: it would invert the layering (a base vocabulary package depending on UI/agent packages). The structural approver keeps the logic single-sourced in dsh-sandbox while the dependencies stay in the tool layer that already holds them.
  • A consolidated mutation-options object on the fs seam (the shape first sketched for the per-call carrier) — rejected on friction: it splits signal across an options bag for mutations while reads keep it positional. A trailing optional SandboxExecutionPolicy matches bash's carry-and-ignore pattern and keeps signal symmetric across the seam.
  • Extra writable-root grants on SandboxPolicy now — deferred unchanged: writableRoots() derives from the mode meaning today; ad-hoc grants are an escalation-scope question the sandbox RFC left open.

Consequences

What shipped — the tiers in § Testing hold each:

  • Under read-only, write/edit return the [sandbox: file access denied under read-only mode] marker and the disk is untouched; read/listDir behave identically to dsh-fs-local.
  • Under workspace-write, mutations land under the workspace root and the temp areas and are denied outside; the containment matrix — .. traversal, absolute paths outside, a pre-existing symlinked directory inside pointing out, a new file created under such a symlink, and alias-equivalent root spellings — denies every escape while admitting the same directory identity on real disks.
  • A denied fs mutation retried once with sandbox_permissions + justification prompts through the composed approval chain; a grant runs exactly that call under the wider mode and the write lands; rejected/cancelled/unavailable each produce their verbatim fail-closed text and mutate nothing.
  • One permission preset switch governs both families: after a session switches modes, the next bash call and the next fs mutation both honor the new mode from the same sandbox/mode fold.
  • Concurrent sessions with different cwd roots carry different policies through the same service instances; neither family caches one session's root for the next call.
  • A direct ctx.fs.writeText with no per-call stamp is confined at the deployment default.
  • The escalation fields on write/edit exist exactly when the mounted ctx.fs confines, absent under dsh-fs-local.
  • agent-loop is untouched — everything rides ctx.sandboxPolicy, the ctx.fs seam, SessionEventMap merging, and the tool-execution pipeline.

Costs and accepted limits:

  • The fs fence is a policy boundary, not a kernel one. Its threat surface is model-chosen paths, not adversarial host processes; the residual resolve-to-syscall TOCTOU is narrowed, not eliminated, and the README says so. Kernel boundaries remain bash's.
  • dsh-bash-sandbox gains a hard dependency on ctx.sandboxPolicy. Every sandboxed composition adds one cordis.yml entry or fails loud at load — the intended pre-release foundation move; the examples update in the same change.
  • Fence-vs-runner parity is derived, not asserted. The fs fence and the Seatbelt profile both take their writable set from writableRoots, and a parity unit test pins the sets; a runner profile changing its writable set without that function would drift.
  • The marker and escalation teaching now serve two families. A wording change is a coordinated edit behind one builder in dsh-sandbox; the duplication gate and pinned snapshots hold it single-sourced, at the cost that fs and bash cannot deliberately diverge in phrasing without splitting the builder.

Testing

  • Unit: dsh-sandbox pins the escalation ladder, the marker builders, the argument-pairing validation, and approveEscalation's ordered fail-closed sequence (non-widening, no-approval, no-agent, each outcome), plus writableRoots/canonicalPath. dsh-sandbox-policy pins deployment fallback, session mode/root resolution, explicit-mode precedence, the fold/setter, load-time mode rejection, and HMR safety. dsh-fs-sandbox pins the per-policy fence and containment matrix (inside, temp area, absolute-outside, .., symlinked-out directory, new file under one, path-equals-root, filesystem-root, root-ending-in-separator, and alias-equivalent spelling) on a real filesystem, plus per-call override and HMR safety. dsh-tool-fs pins advertisement gating, complete policy resolution, denial-marker mapping, and the full escalation matrix (grant, reject, no-service, no-agent, pairing, non-confining guard). dsh-tool-bash, dsh-bash-sandbox, and dsh-permission-presets consume the same policy kit.
  • Keyless e2e: one real Cordis context creates two agents with different session cwd roots, runs the shipped bash and fs tools concurrently, and world-verifies that own-project writes land while both cross-project writes are denied.
  • Snapshot: the acp-agent example composes dsh-sandbox-policy + dsh-fs-sandbox; the pinned header carries the fs escalation fields and the sandbox/mode event name, re-recorded once.

中文

问题

SandboxMode 所声明的语义涵盖文件系统效果,但最初只有 ctx.shell 强制执行该策略。fs 工具(write/edit)在进程内经由 ctx.fs 变更宿主文件系统,那里的 OS argv 包装在机制上毫无意义——沙箱 Agent Note § 进程内工具记录了这一点,并把跨家族强制执行留作一个暂缓阶段,附带一个未决问题:进程内强制执行是各 seam 各自表达,还是变成一个统一的 harness 能力。本 Agent Note 就是那个阶段,并给出答案:一个共享的策略归属,在每个家族各自正确的层级上做 per-seam 强制执行。

这个缺口并不只有 read-only 一种形态。一个受限编码 agent(智能体)的产品模式是 workspace-write:bash 已经可以在工作区根目录下写入,而其外的一切都被拒绝,所以只能全部拒绝的 fs 强制执行会严格劣于禁用 fs 工具——模型会尝试在工作区内 write,被拒,然后学会绕道 bash heredoc。因此跨家族强制执行必须涵盖完整的模式阶梯,包括 workspace-write 要求的路径包含判定(规范化目标;../符号链接/绝对路径逃逸),以及与 bash 相同的升级手段。

第二个强制执行家族还暴露了原布局中的一个归属问题。部署默认值(mode + workspaceRoot)配置在 dsh-bash-sandbox 上,而 per-session 覆盖事件是 shell/sandbox-mode,由 dsh-shell 的 session-mode 工具集折叠与写入。当 fs 强制执行同一套策略时,要么 fs 读取 bash 的配置与事件(一个能力家族依赖同级插件的配置),要么各家族各持一份副本——两份 workspaceRoot 会漂移进沙箱 RFC 警告过的割裂世界:bash 受限于一个根,而 fs 围住另一个根。

决策

三个相互协调的部分全部在叶子 cordis.yml 中组合,均不触及 agent-loop

ctx.sandboxPolicy——mode 与工作区根的统一归属

packages/sandbox/sandbox-policy/@deepseek-ai/dsh-sandbox-policy)注册 ctx.sandboxPolicy,即部署沙箱策略的唯一所有者:

  • Configmode(封闭的 SandboxMode 联合,默认 read-only)与 workspaceRoot(默认进程 cwd,解析为绝对路径)。配置错误会在加载时明确报错。
  • per-session 覆盖事件 sandbox/mode,连同它的纯折叠(effectiveSandboxMode(events))、写入路径(setSandboxMode(session, mode))与 SANDBOX_MODES。该事件是策略状态——被两个家族消费——所以它归于此处,而不归于任一能力的 seam。它的形状与仅日志(log-only)语义遵循 approval/* 的先例。
  • resolve({ session?, mode? }) 返回完整的单次调用 SandboxExecutionPolicy:显式批准的模式 > 会话折叠结果 > defaultMode,而会话中不可变的 cwd > 配置的 workspaceRoot 回退值。
  • 保留 defaultMode / workspaceRoot 访问器,作为部署回退值与能力宣告依据。

dsh-bash-sandbox 自身不再携带任何沙箱配置——它注入 sandboxPolicy,仅在直接调用时使用其中的部署回退值。dsh-tool-bashdsh-tool-fs 把当前会话传给 ctx.sandboxPolicy.resolve(),因此两者每次调用都会取得相同的生效模式与 cwd 根目录;dsh-permission-presets 预设与 ACP(Agent Client Protocol)bridge 经由迁移后的 setter 写入。拥有 bash 与 fs 执行的 seam 仍不依赖会话——会话依赖归策略包与工具消费方所有。

dsh-fs-sandbox——在提供方内部强制执行

packages/fs/fs-sandbox/@deepseek-ai/dsh-fs-sandbox)镜像 bash-local/bash-sandbox 的拆分:SandboxedFileSystem extends LocalFileSystem,注册为 ctx.fs,注入 sandboxPolicy。读取(resolve/stat/readText/streamText/listDir)原样透传——每种模式都允许读。两个变更操作在委托给继承来的原子写之前按模式强制执行:

  • read-only 直接拒绝 writeText/editText
  • workspace-write 把规范化后的目标围栏于可写根集合——dsh-sandbox 中的 writableRoots(policy):工作区根加上平台临时目录(/tmpos.tmpdir()),各自 realpath——与 Seatbelt profile 授予的是同一个集合,所以 fs 围栏是这一个模式含义在 bwrap/Landlock/Seatbelt profile 之外的第四种方言,因此不会出现「write 工具不能写 /tmp 而 bash 能」的不对称。规范化路径写法采用词法包含的快速路径;当 Windows 以大小写不同的路径、长文件名或 8.3 短文件名表示同一目录时,系统会逐级遍历祖先目录并比较文件系统身份,而不会把边界弱化为依据文本前缀猜测包含关系。目标在委托前被立即重新规范化(resolve 对最深的既有祖先做 realpath),因此自工具解析该目标以来被换出的祖先符号链接会被捕获。
  • danger-full-access 不加围栏地委托。

拒绝是结构化的 FS_SANDBOX_DENIED,携带生效模式——区别于 FS_PERMISSION_DENIED(宿主 EACCES 是世界在拒绝;这里是策略在拒绝)。无文本推断:进程内围栏确切知道它拒绝了什么。per-call 载体是 writeText/editText 上一个末尾可选的 SandboxExecutionPolicy(文件系统侧对应 ShellExecRequest.sandboxPolicy);该 seam 保持无会话依赖,而裸的本地后端会忽略它。FileSystem.sandboxMode 是能力事实(在基类与 fs-local 上为 undefined,在 SandboxedFileSystem 上为默认值),所以工具层按组合真相来宣告升级。

威胁模型写在包 README 里:一道位于可信代码中、针对模型可控路径的策略围栏,而非内核边界——操作是 seam 自身的,只有目标路径不可信,所以「先规范化再判包含」足以完整覆盖这一调用面(code-runtime 的「containment, not a security boundary」先例)。对不可信代码的内核级隔离仍是 ctx.shell 的职责。resolve 到系统调用之间残留的竞态被就地重新规范化收窄,只有平台原语(openat2 RESOLVE_BENEATH)能彻底消除它,而在此不值得为其付出可移植性代价。

工具对等——一个拒绝标记、一条升级流程

dsh-tool-fs 把当前会话解析成完整策略,并传给每次变更,同时将 FS_SANDBOX_DENIED 映射为模型已从 bash 认识的标记:[sandbox: file access denied under <mode> mode]。当 ctx.fs.sandboxMode 在注册时报告一个受限模式,writeedit 宣告相同的 sandbox_permissions + justification 字段,向模型说明同样的同一轮次重试方式,并在执行前处理同样的 ctx.approval 请求——四种结果及其逐字的 fail-closed 文案沿用自沙箱 Agent Note § 升级(执行时根据调用的生效模式检查是否严格加宽;授权只改变当前调用的模式,并保留其会话根目录;不产生任何新会话事件)。

共享部分住在 dsh-sandbox,它拥有模式类型:WIDER_MODES、升级目标枚举、参数配对校验、拒绝/提示标记构造器,以及 approveEscalation——有序的 fail-closed 编排。approveEscalation 接收一个最小的结构式 approver(EscalationApprover,对 agent 与 call-id 类型泛型化),而非审批服务类型,所以 dsh-sandbox 不获得对 approval 或 agent 包的依赖:每个工具把自己的 ctx.approval、agent、call id 与工具名作为原料传入。dsh-tool-bashdsh-tool-fs 都使用它们;跨文件重复检测门禁确保单一来源不走样。

examples/acp-agent 组合加载 dsh-sandbox-policydsh-fs-sandbox,把 mode/workspaceRoot 配置移到策略条目,并去掉在受限模式下禁用整个 fs 栈的旧门控;fs-observation-policy(read-before-edit)正交地叠加其上。系统提示仍然不陈述沙箱模式——标记会在真正重要的那一刻教会模型边界,遵循沙箱 Agent Note 所述的实时证据原则。

强制执行点:提供方,而非 intent gate

沙箱 Agent Note 最初的跨家族草图把 fs 强制执行放在 fs/write-intent/fs/edit-intent 事件上。本 Agent Note 改为在提供方中强制执行,基于两个机制性事实:intent slot是单决策、先到先得(已被 dsh-fs-observation-policy 占据,其约定明确规定,出现第二个决策者即属配置错误),且 intent 事件只由 dsh-tool-fs 派发——一个直连 ctx.fs 的调用方(一个 Cordis 挂载插件、一个自定义工具)会绕过它们,而提供方级强制执行按构造覆盖每一个调用方。

范围之外

  • ctx.web 的网络策略——SandboxMode 所声明的语义只涵盖文件系统效果;在 bash curl 畅通时给一个仅限 web 的网络旋钮会是一道假边界。待某个 bash 后端能强制执行网络策略(bwrap --unshare-net、Landlock ABI v4+)时再议。
  • subagent-acp 消费方——沙箱 RFC 中未变的延后阶段。
  • 单个会话中的额外可写根目录——解析后的策略携带一个主要 SessionHeader.cwd;ACP additionalDirectories 仍是独立的 bridge 与策略设计问题。
  • 统一的 per-tool 沙箱运行时——因沙箱 RFC 中的理由继续否决。

考虑过的替代方案

  • fs/* intent 事件上强制执行(沙箱 Agent Note 的原始草图)——因 § 强制执行点 中的两个机制性事实被否决:单一 slot、先到先得且已被占据,以及对直连 ctx.fs 调用方的绕过。提供方级强制执行覆盖每一个调用方,并镜像 bash 的换实现形态。
  • tools/pre-execute 中执行——否决:监听器在 resolve() 之前看到模型的原始路径字符串,因此它会重新实现 cwd 默认化与符号链接规范化,并且仍与真正的 resolve 竞态。这使其不适用于 workspace-write,因为后者需要对规范路径作出判定。
  • dsh-tool-fs 中做内联检查——否决:只覆盖工具路径(与 intent 事件同样的绕过),并在规范目标已存在之上重复了一层 resolve 知识。
  • dsh-fs-local 上加一个 mode 标志而非同级后端——否决:能力事实必须是组合真相,正如 dsh-bash-localdsh-bash-sandbox;一个配置标志会让工具的宣告取决于配置,而 bash 家族已经确立了同级包形态。
  • 经受限 helper 子进程做内核级 fs 变更——否决:每次写入都要启动一个进程;editText 的读-匹配-写临界区不得不整体搬进子进程才能保持原子;而威胁面(可信操作、不可信路径参数)不需要内核——可信代码中的围栏就是完整答案,而不可信代码隔离仍在 ctx.shell
  • 带加载期一致性校验的 per-family 策略配置——否决:一个事实两个归属,靠一个必须枚举每个未来的强制执行家族的校验来打补丁;策略服务让漂移不可表达,而非被检测到。
  • 把覆盖事件留在 dsh-shell 里作 shell/sandbox-mode——否决:该事件是被两个家族消费的策略状态;保留 bash 命名会迫使 dsh-fs-sandbox 依赖 bash 词汇。预发布阶段,该改名是同一变更内的迁移,附带快照重录,无任何 shim。
  • 把升级编排从 approval/agent 包导入 dsh-sandbox——否决:那会倒置分层(一个基础词汇包依赖 UI/agent 包)。结构式 approver 让逻辑单一来源于 dsh-sandbox,而依赖留在本就持有它们的工具层。
  • fs seam 上一个合并的 mutation-options 对象(per-call 载体最初草拟的形状)——因摩擦被否决:它会把 signal 拆进变更专用的选项包,而读取仍保持位置参数。一个末尾可选的 SandboxExecutionPolicy 匹配 bash 的携带并忽略模式,并使 signal 在整个 seam 上保持对称。
  • 现在就在 SandboxPolicy 上加额外的可写根授权——照旧延后:writableRoots() 如今由模式含义推导;临时授权是沙箱 RFC 留下的升级作用域问题。

后果

已交付的部分——§ 测试的各层各自钉住:

  • read-only 下,write/edit 返回 [sandbox: file access denied under read-only mode] 标记,磁盘不受触动;read/listDirdsh-fs-local 行为一致。
  • workspace-write 下,变更落在工作区根与临时目录下,其外被拒;包含矩阵——.. 穿越、指向外部的绝对路径、一个既有的、指向外部的工作区内符号链接目录、在这样一个符号链接下新建的文件,以及根路径的等价别名形式——在真实磁盘上拒绝每一种逃逸,同时允许文件系统认定为同一目录的路径。
  • 一个被拒的 fs 变更,携带 sandbox_permissions + justification 重试一次,会经组合的审批链提示;一次授权让恰好那一次调用在更宽的模式下运行且写入落盘;rejected/cancelled/unavailable 各自产生其逐字的 fail-closed 文案且不做任何变更。
  • 一次 permission 预设切换同时管辖两个家族:会话切换模式后,下一次 bash 调用与下一次 fs 变更都从同一个 sandbox/mode 折叠遵循新模式。
  • cwd 根目录不同的并发会话通过同一组服务实例携带不同策略;两个家族都不会缓存某个会话的根目录供下一次调用使用。
  • 一次无 per-call 盖章的直连 ctx.fs.writeText 会被围栏于部署默认值。
  • write/edit 上的升级字段恰好在被挂载的 ctx.fs 受限时存在,在 dsh-fs-local 下不存在。
  • agent-loop 未被触动——一切都依托于 ctx.sandboxPolicyctx.fs seam、SessionEventMap 合并以及工具执行流水线。

代价与接受的限制:

  • fs 围栏是策略边界,而非内核边界。 它的威胁面是模型选定的路径,而非对抗性宿主进程;resolve 到系统调用之间残留的 TOCTOU 被收窄而非消除,README 已如实声明。内核边界仍属 bash。
  • dsh-bash-sandbox 获得对 ctx.sandboxPolicy 的硬依赖。 每个沙箱化组合要么加一个 cordis.yml 条目,要么在加载时明确报错——这是有意的预发布奠基之举;示例在同一变更内更新。
  • 围栏与 runner 的对等是推导出来的,而非断言的。 fs 围栏与 Seatbelt profile 都从 writableRoots 取其可写集合,一个对等单元测试钉住这些集合;一个 runner profile 若在不经该函数的情况下改变其可写集合便会漂移。
  • 标记与升级教学如今服务于两个家族。 措辞改动是 dsh-sandbox 中一个构造器背后的协调编辑;重复检测门禁与钉住的快照维持单一来源,代价是 fs 与 bash 无法在不拆分该构造器的情况下有意地在措辞上分道。

测试

  • 单元:dsh-sandbox 钉住升级阶梯、标记构造器、参数配对校验,以及 approveEscalation 的有序 fail-closed 序列(非加宽、无 approval、无 agent、各结果),外加 writableRoots/canonicalPathdsh-sandbox-policy 钉住部署回退、会话模式/根目录解析、显式模式优先级、折叠/setter、加载期模式拒绝,以及 HMR(热模块替换)安全。dsh-fs-sandbox 在真实文件系统上钉住按策略执行的围栏与包含矩阵(内部、临时目录、绝对路径-外部、..、指向外部的符号链接目录、其下的新建文件、路径等于根、文件系统根、以分隔符结尾的根、等价别名形式),外加 per-call 覆盖与 HMR 安全。dsh-tool-fs 钉住宣告门控、完整策略解析、拒绝标记映射,以及完整的升级矩阵(授权、拒绝、无服务、无 agent、配对、非受限守卫)。dsh-tool-bashdsh-bash-sandboxdsh-permission-presets 使用同一套策略工具集。
  • 无密钥 e2e:一个真实 Cordis 上下文创建两个 agent,其会话的 cwd 根目录各不相同;系统并发运行正式发布的 bash 与 fs 工具,再通过外部可观察结果验证各自在所属项目中的写入成功,而两次跨项目写入都被拒绝。
  • 快照:acp-agent 示例组合 dsh-sandbox-policy + dsh-fs-sandbox;被钉住的 header 携带 fs 升级字段与 sandbox/mode 事件名,一次性重录。