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 closedSandboxModeunion, defaultread-only) andworkspaceRoot(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)), andSANDBOX_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 theapproval/*precedent. resolve({ session?, mode? }), which returns a complete per-callSandboxExecutionPolicy: explicit approved mode > the session fold >defaultMode, and the session's immutable cwd > configuredworkspaceRootfallback.defaultMode/workspaceRootaccessors 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-onlydenieswriteText/editTextoutright.workspace-writefences the canonicalized target against the writable-root set —writableRoots(policy)indsh-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/tmpbut 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 (resolverealpaths the deepest existing ancestor) immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught.danger-full-accessdelegates 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.web—SandboxModeclaims file effects only; a web-only network knob while bashcurlruns free would be a false boundary. Revisit when a bash backend enforces network (bwrap--unshare-net, Landlock ABI v4+). - The
subagent-acpconsumer — unchanged deferred phase of the sandbox RFC. - Additional writable roots inside one session — the resolved policy carries one primary
SessionHeader.cwd; ACPadditionalDirectoriesremains 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 directctx.fscallers. 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 beforeresolve(), so it would re-implement cwd defaulting and symlink canonicalization and still race the real resolve. Disqualifying forworkspace-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
modeflag ondsh-fs-localinstead of a sibling backend — rejected: the capability fact must be composition truth the waydsh-bash-localvsdsh-bash-sandboxis; 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 onctx.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-shellasshell/sandbox-mode— rejected: the event is policy state consumed by two families; leaving it bash-named forcesdsh-fs-sandboxto 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 indsh-sandboxwhile 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
signalacross an options bag for mutations while reads keep it positional. A trailing optionalSandboxExecutionPolicymatches bash's carry-and-ignore pattern and keepssignalsymmetric across the seam. - Extra writable-root grants on
SandboxPolicynow — 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/editreturn the[sandbox: file access denied under read-only mode]marker and the disk is untouched;read/listDirbehave identically todsh-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+justificationprompts 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
permissionpreset 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 samesandbox/modefold. - 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.writeTextwith no per-call stamp is confined at the deployment default. - The escalation fields on
write/editexist exactly when the mountedctx.fsconfines, absent underdsh-fs-local. agent-loopis untouched — everything ridesctx.sandboxPolicy, thectx.fsseam,SessionEventMapmerging, 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-sandboxgains a hard dependency onctx.sandboxPolicy. Every sandboxed composition adds onecordis.ymlentry 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-sandboxpins the escalation ladder, the marker builders, the argument-pairing validation, andapproveEscalation's ordered fail-closed sequence (non-widening, no-approval, no-agent, each outcome), pluswritableRoots/canonicalPath.dsh-sandbox-policypins deployment fallback, session mode/root resolution, explicit-mode precedence, the fold/setter, load-time mode rejection, and HMR safety.dsh-fs-sandboxpins 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-fspins 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, anddsh-permission-presetsconsume 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 thesandbox/modeevent 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,即部署沙箱策略的唯一所有者:
Config:mode(封闭的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-bash 与 dsh-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):工作区根加上平台临时目录(/tmp、os.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 在注册时报告一个受限模式,write 与 edit 宣告相同的 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-bash 与 dsh-tool-fs 都使用它们;跨文件重复检测门禁确保单一来源不走样。
examples/acp-agent 组合加载 dsh-sandbox-policy 与 dsh-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所声明的语义只涵盖文件系统效果;在 bashcurl畅通时给一个仅限 web 的网络旋钮会是一道假边界。待某个 bash 后端能强制执行网络策略(bwrap--unshare-net、Landlock ABI v4+)时再议。subagent-acp消费方——沙箱 RFC 中未变的延后阶段。- 单个会话中的额外可写根目录——解析后的策略携带一个主要
SessionHeader.cwd;ACPadditionalDirectories仍是独立的 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-local对dsh-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/listDir与dsh-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.sandboxPolicy、ctx.fsseam、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/canonicalPath。dsh-sandbox-policy钉住部署回退、会话模式/根目录解析、显式模式优先级、折叠/setter、加载期模式拒绝,以及 HMR(热模块替换)安全。dsh-fs-sandbox在真实文件系统上钉住按策略执行的围栏与包含矩阵(内部、临时目录、绝对路径-外部、..、指向外部的符号链接目录、其下的新建文件、路径等于根、文件系统根、以分隔符结尾的根、等价别名形式),外加 per-call 覆盖与 HMR 安全。dsh-tool-fs钉住宣告门控、完整策略解析、拒绝标记映射,以及完整的升级矩阵(授权、拒绝、无服务、无 agent、配对、非受限守卫)。dsh-tool-bash、dsh-bash-sandbox与dsh-permission-presets使用同一套策略工具集。 - 无密钥 e2e:一个真实 Cordis 上下文创建两个 agent,其会话的 cwd 根目录各不相同;系统并发运行正式发布的 bash 与 fs 工具,再通过外部可观察结果验证各自在所属项目中的写入成功,而两次跨项目写入都被拒绝。
- 快照:acp-agent 示例组合
dsh-sandbox-policy+dsh-fs-sandbox;被钉住的 header 携带 fs 升级字段与sandbox/mode事件名,一次性重录。