docs(policy): align every precedence statement with the override chain

Review fix (ds-review-bot on #623): the READMEs and JSDoc still stated the
pre-baseline formulas — resolve() outranking 'the session's last
sandbox/mode event', 'effective = explicit grant ?? fold(events) ??
deployment default', and the approval README's 'last approval/policy event'
opener — which contradict the shipped semantics for a delegated fork whose
seed tail differs from its header baseline. Every statement now names the
override chain (own post-seed switches ?? inherited header baseline): both
READMEs (both languages), resolve()'s JSDoc, the session-mode module and
event-declaration docs, the raw folds re-scoped as building blocks, and
the regenerated catalogs.
This commit is contained in:
kingwl
2026-07-27 13:45:09 +08:00
parent 68d59a5414
commit 10bb6dc4fe
11 changed files with 65 additions and 54 deletions

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
README.md: ca6ee67ab0cdb645bba135b3ca4f542844306857
README.zh.md: a0c43235de97872ef734f0bef3cc6ed16930e232
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-policy/README.md
README.md: be1125b1ec30e1db10223fe1023cc5dd7a57a3a7
README.zh.md: 3ef9e5310570b1f2597fde99ff5cfcebb14096c1

View File

@@ -15,9 +15,9 @@ Two families enforce the same mode vocabulary: the sandboxed bash executor (`@de
## Surface
- `ctx.sandboxPolicy.resolve({ session?, mode? })` — resolves one complete per-call policy. An explicit approved mode outranks the session's last `sandbox/mode` event, which outranks `defaultMode`; the session's immutable `cwd` is canonicalized with filesystem semantics before becoming `workspaceRoot`, otherwise the configured fallback applies. Canonicalization precedes lexical normalization so `symlink/..` agrees with process working-directory resolution.
- `ctx.sandboxPolicy.resolve({ session?, mode? })` — resolves one complete per-call policy. An explicit approved mode outranks the session's override chain (`overrideOf`, below), which outranks `defaultMode`; the session's immutable `cwd` is canonicalized with filesystem semantics before becoming `workspaceRoot`, otherwise the configured fallback applies. Canonicalization precedes lexical normalization so `symlink/..` agrees with process working-directory resolution.
- `ctx.sandboxPolicy.defaultMode` / `ctx.sandboxPolicy.workspaceRoot` — the deployment default and fallback root used by `resolve()`.
- `effectiveSandboxMode(events)` — the pure fold of a session's `sandbox/mode` events (the last switch wins, or `undefined`), used inside `resolve()`.
- `effectiveSandboxMode(events)` — the pure fold of a slice of `sandbox/mode` events (the last switch wins, or `undefined`), the building block `sandboxOverrideOf` composes with the seed boundary and the header baseline.
- `setSandboxMode(session, mode)` — THE write path for a per-session override: appends exactly one `sandbox/mode` event. The switch IS its event; nothing mutates the mode out of band.
- `ctx.sandboxPolicy.overrideOf(session)` (the pure `sandboxOverrideOf` export, also consumed by the permission presets) — the session's override chain, never the deployment default: with an inherited `sandboxMode` header baseline (a delegation child), the fold of the session's OWN switches past `SessionHeader.seedLength`, else the baseline, validated against the closed vocabulary on read (throws on foreign values — a durable boundary); without one (a top-level session or a generic `SessionStore.fork` child), the whole-log fold, so seed-carried switches remain the replayed inherited truth. The in-process subagent driver captures this at delegation and writes it into each child's creation-time header, so a delegating parent's tightened mode binds its children with no first-turn timing window ([rationale](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)).
- `SANDBOX_MODES` — every mode, for option advertisement and runtime validation.
@@ -26,7 +26,7 @@ The optional `./invariant` companion rejects a forged durable `sandbox/mode` eve
## The per-session store
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event.
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? override chain ?? deployment default`, where the override chain is `sandboxOverrideOf`'s fold of the session's OWN post-seed switches, else the inherited header baseline — so an override survives restart by replay, a delegation child starts under its parent's captured policy, and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event.
## Model Experience

View File

@@ -15,9 +15,9 @@
## 表层
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话的覆盖链(见下文 `overrideOf`,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.defaultMode``ctx.sandboxPolicy.workspaceRoot``resolve()` 使用的部署默认值与回退根。
- `effectiveSandboxMode(events)`会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined` `resolve()` 内使用
- `effectiveSandboxMode(events)`对一段 `sandbox/mode` 事件切片的纯折叠(最后一次切换胜出,没有则为 `undefined` `sandboxOverrideOf` 与种子边界和会话头基线进行组合时所用的基础构件
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
- `ctx.sandboxPolicy.overrideOf(session)`(即纯函数导出 `sandboxOverrideOf`,也供权限 preset 消费):会话的覆盖链,绝不包含部署默认值:当存在继承的 `sandboxMode` 会话头基线时(即委派子 agent先折叠会话自己在 `SessionHeader.seedLength` 之后的切换,否则取该基线,读取时按封闭词汇校验(遇到词汇之外的值即抛出异常——这是一条持久边界);没有基线时(顶层会话或通用的 `SessionStore.fork` 子会话),折叠覆盖完整日志,因此种子携带的切换仍是回放所得的继承事实。进程内 subagent 驱动器在委派时捕获该值,并写入每个子 agent 创建时的会话头,使发起委派的父级收紧后的模式约束其子 agent且不存在任何第一轮次的时序窗口参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md))。
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
@@ -26,7 +26,7 @@
## 逐会话 store
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`因此覆盖会通过回放跨重启保留两个会话也绝不会看到彼此状态。Workspace 标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件只进入日志(沿用 `approval/*` 先例):模型通过强制执行工具的拒绝标记获知模式,绝不会从事件获知。
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? override chain ?? deployment default`其中覆盖链override chain`sandboxOverrideOf` 折叠会话自己在种子之后的切换所得,否则取继承的会话头基线——因此覆盖会通过回放跨重启保留,委派子 agent 会在其父级捕获的策略下启动两个会话也绝不会看到彼此状态。Workspace 标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件只进入日志(沿用 `approval/*` 先例):模型通过强制执行工具的拒绝标记获知模式,绝不会从事件获知。
## 模型体验

View File

@@ -90,10 +90,11 @@ export class SandboxPolicyService extends Service {
/**
* Resolve the complete policy for one capability call. An approved explicit
* mode outranks the session's last `sandbox/mode` event, which outranks the
* deployment default. A session cwd is its workspace-write boundary; the
* configured root is the fallback for agentless calls and sessions without a
* cwd.
* mode outranks the session's override chain ({@link overrideOf}: own
* post-seed switches, else the inherited header baseline), which outranks
* the deployment default. A session cwd is its workspace-write boundary;
* the configured root is the fallback for agentless calls and sessions
* without a cwd.
* @param request - optional session and approved mode override.
* @returns the fully resolved per-call mode and absolute workspace root.
*/

View File

@@ -1,15 +1,19 @@
/**
* Per-session sandbox-mode override: the session log as the store. A runtime
* switch (a UI policy control or test scenario) is recorded as one
* `sandbox/mode` event on the session it applies to;
* `effective = fold(events) ?? the deployment default`, so an override
* survives restart by replay, two sessions can never see each other's state,
* and there is no external config store. The event is log-only (the
* `approval/*` precedent): the model learns the mode from the boundary
* markers in the enforcing tools, never from the event itself. EXECUTION
* honors the fold through `ctx.sandboxPolicy.resolve()` — it stamps the mode
* together with the calling session's workspace root onto each capability
* call, weakest-precedence beneath an escalation grant.
* Per-session sandbox-mode override: the session log as the store, layered
* over the header's delegation baseline. A runtime switch (a UI policy
* control or test scenario) is recorded as one `sandbox/mode` event on the
* session it applies to; `effective = override chain ?? the deployment
* default`, where the override chain ({@link sandboxOverrideOf}) is the fold
* of the session's OWN post-seed switches, else the inherited
* `SessionHeader.sandboxMode` baseline. An override survives restart by
* replay, a delegation child starts under its parent's captured policy, two
* sessions can never see each other's state, and there is no external config
* store. The event is log-only (the `approval/*` precedent): the model
* learns the mode from the boundary markers in the enforcing tools, never
* from the event itself. EXECUTION honors the chain through
* `ctx.sandboxPolicy.resolve()` — it stamps the mode together with the
* calling session's workspace root onto each capability call,
* weakest-precedence beneath an escalation grant.
*
* The override is policy state shared by every enforcing family (bash and
* filesystem alike), so it lives here in the policy package rather than in any
@@ -26,10 +30,11 @@ declare module '@deepseek-ai/dsh-session' {
/**
* The session's sandbox mode was switched — log-only (like `approval/*`;
* NOT a surface event, carries no `surfaceOp`): durable and replayable,
* never in the model transcript. The LAST such event is the session's
* override ({@link effectiveSandboxMode}); who asked for it is derivable
* from position (an event after the log's last `request/header*` was a
* runtime switch by the user; see the tool layer's narrator).
* never in the model transcript. The last such OWN (post-seed) event is
* the session's override ({@link sandboxOverrideOf}); who asked for it is
* derivable from position (an event after the log's last
* `request/header*` was a runtime switch by the user; see the tool
* layer's narrator).
*/
'sandbox/mode': { mode: SandboxMode }
}
@@ -39,10 +44,11 @@ declare module '@deepseek-ai/dsh-session' {
export const SANDBOX_MODES: readonly SandboxMode[] = ['read-only', 'workspace-write', 'danger-full-access']
/**
* The session's sandbox-mode override: the last `sandbox/mode` event in the
* log, or undefined when the session never switched (callers apply the
* deployment default). The pure fold — resume needs no catch-up machinery
* because replaying the log IS the state.
* The pure fold of a slice of `sandbox/mode` events: the last switch wins,
* or undefined without one. The building block {@link sandboxOverrideOf}
* composes with the seed boundary and the header baseline — consumers
* resolving a SESSION's policy go through that chain, not this raw fold.
* Resume needs no catch-up machinery because replaying the log IS the state.
* @param events - session events in log order (other event types are skipped).
* @returns the mode of the last switch event, or undefined without one.
*/