policy: resolve every knob consumer through the shared override chain
Review fixes (ds-review-bot warnings on #623): - One chain, every consumer: the override resolution (own post-seed switches ?? header baseline, closed-vocabulary validated) moves into pure exports (sandboxOverrideOf / approvalOverrideOf); the services delegate, and the permission presets consume them — current(session) and set() now see inherited baselines, so a child inheriting danger-full-access gets REAL knob switches when workspace-write is selected instead of a silent no-op, and a seed-carried preset selection is subsumed by the baseline. current(events) becomes current(session) (pre-release; the only callers were tests). - Unconditional durable validation: a malformed header baseline fails loud on every read, no longer shadowed by an own switch. - The two policy peers are declared optional (peerDependenciesMeta), so a thin spawn/fork deployment without policy plugins can consume the driver; verify-runtime-closure honors the flag. Red-first: inherited-preset derive/switch-away and seeded-selection tests in the permission suite; malformed-baseline-with-own-switch tests in both policy suites.
This commit is contained in:
@@ -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
|
||||
README.md: 537fda6e16c2e21d22a1809e1e91fc0c3ddeee94
|
||||
README.zh.md: 132b145ee3c7f7cea7aa0529358ed4982615499f
|
||||
README.md: f6eaf049a46bc2faa7e0b12809d3d8c63d7792d6
|
||||
README.zh.md: a286d13eb7c5a3e7893a8269c6d50175eff47046
|
||||
|
||||
@@ -8,7 +8,7 @@ Each request must belong to an open agent turn. The service appends a paired `ap
|
||||
|
||||
Answerers are `approval/request` waterfall listeners. Return an outcome to answer for an owned agent or call `next()` to delegate. Agent-scoped listeners receive only that agent's requests; compose one terminal answerer per deployment because sibling listener order is not a policy priority mechanism. The ACP automation bridge supplies one-shot machine decisions for sessions it owns.
|
||||
|
||||
`ApprovalPolicy` is `'ask'` or `'never'`. The effective value is the last `approval/policy` event, falling back to config; `setApprovalPolicy()` is the write path. `'never'` rejects before interactive dispatch and is the only policy stated in the prompt. Switches produce at most one coalesced notice, attributed to the user when the override follows the last `request/header` and to operator/config otherwise. `ctx.approval.overrideOf(session)` resolves the session's override chain, never the configured default: the fold of the session's OWN switches (events past `SessionHeader.seedLength`), else the header's inherited `approvalPolicy` delegation baseline, validated against the closed vocabulary on read; the in-process subagent driver captures this at delegation and writes it into each child's creation-time header, so a `'never'` parent cannot mint prompting children, with no first-turn timing window ([rationale](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)).
|
||||
`ApprovalPolicy` is `'ask'` or `'never'`. The effective value is the last `approval/policy` event, falling back to config; `setApprovalPolicy()` is the write path. `'never'` rejects before interactive dispatch and is the only policy stated in the prompt. Switches produce at most one coalesced notice, attributed to the user when the override follows the last `request/header` and to operator/config otherwise. `ctx.approval.overrideOf(session)` (the pure `approvalOverrideOf` export, also consumed by the permission presets) resolves the session's override chain, never the configured default: the fold of the session's OWN switches (events past `SessionHeader.seedLength`), else the header's inherited `approvalPolicy` delegation baseline, validated against the closed vocabulary on read; the in-process subagent driver captures this at delegation and writes it into each child's creation-time header, so a `'never'` parent cannot mint prompting children, with no first-turn timing window ([rationale](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)).
|
||||
|
||||
The tools pipeline routes `ask` decisions through this seam and fails closed when it is absent; the sandboxed bash tool also uses it for escalated retries. The ACP automation bridge answers calls for its own agents through the client's machine policy. Audit events remain log-only, so the model sees only the asking consumer's result. See the [approval-seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.md) and [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
应答者是 `approval/request` waterfall(瀑布式事件)监听器。要回答所拥有 agent 的请求,请返回一个结果;否则调用 `next()` 委托。限定到 agent 的监听器只接收该 agent 的请求;每项部署应当组合一个终端应答者,因为同级监听器的顺序不是策略优先级机制。ACP(Agent Client Protocol)自动化桥接层为其拥有的会话提供一次性机器决定。
|
||||
|
||||
`ApprovalPolicy` 为 `'ask'` 或 `'never'`。实际值取最后一条 `approval/policy` 事件,并回退到配置;`setApprovalPolicy()` 是写入路径。`'never'` 会在交互式分发之前拒绝请求,也是提示词中唯一声明的策略。切换最多产生一条合并通知:如果覆盖发生在最后一个 `request/header` 之后,则归因于用户;否则归因于操作方/配置。`ctx.approval.overrideOf(session)` 解析会话的覆盖链,绝不包含配置默认值:先折叠会话自己的切换(`SessionHeader.seedLength` 之后的事件),否则取会话头中继承的 `approvalPolicy` 委派基线,读取时按封闭词汇校验;进程内 subagent 驱动器在委派时捕获该值,并写入每个子 agent 创建时的会话头,使 `'never'` 父级无法造出会弹出提示的子 agent,且不存在任何第一轮次的时序窗口(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md))。
|
||||
`ApprovalPolicy` 为 `'ask'` 或 `'never'`。实际值取最后一条 `approval/policy` 事件,并回退到配置;`setApprovalPolicy()` 是写入路径。`'never'` 会在交互式分发之前拒绝请求,也是提示词中唯一声明的策略。切换最多产生一条合并通知:如果覆盖发生在最后一个 `request/header` 之后,则归因于用户;否则归因于操作方/配置。`ctx.approval.overrideOf(session)`(即纯函数导出 `approvalOverrideOf`,也供权限 preset 消费)解析会话的覆盖链,绝不包含配置默认值:先折叠会话自己的切换(`SessionHeader.seedLength` 之后的事件),否则取会话头中继承的 `approvalPolicy` 委派基线,读取时按封闭词汇校验;进程内 subagent 驱动器在委派时捕获该值,并写入每个子 agent 创建时的会话头,使 `'never'` 父级无法造出会弹出提示的子 agent,且不存在任何第一轮次的时序窗口(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md))。
|
||||
|
||||
工具流水线通过此 seam 路由 `ask` 决定,并在该 seam 缺失时以拒绝方式关闭;沙箱 bash 工具也会将它用于升权重试。ACP 自动化桥接层根据客户端的机器策略,回答其自有 agent 的调用。审计事件仍只写入日志,因此模型只会看到发起请求的消费方所返回的结果。详见[审批 seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.md)和[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
|
||||
|
||||
|
||||
@@ -138,6 +138,29 @@ export function effectiveApprovalPolicy(events: readonly SessionEvent[]): Approv
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The session's complete approval-policy OVERRIDE chain — the one home every
|
||||
* consumer (this service's policy tier, the permission presets) resolves
|
||||
* through: the fold of the session's OWN switches (events past the seed
|
||||
* boundary — a fork seed's stale parent switch is subsumed by the delegation
|
||||
* baseline captured after it), else the header's inherited baseline. Never
|
||||
* the configured default. The durable baseline is validated UNCONDITIONALLY —
|
||||
* a corrupt or foreign header must fail loud on every read, not only when no
|
||||
* own switch happens to shadow it.
|
||||
* @param session - the session whose override chain to resolve.
|
||||
* @returns the effective override, or `undefined` for a session following
|
||||
* the configured default.
|
||||
* @throws when the header baseline is outside the closed policy vocabulary.
|
||||
*/
|
||||
export function approvalOverrideOf(session: Session): ApprovalPolicy | undefined {
|
||||
const baseline = session.header.approvalPolicy
|
||||
if (baseline !== undefined && !APPROVAL_POLICIES.includes(baseline as ApprovalPolicy)) {
|
||||
throw new Error(`session header approvalPolicy "${baseline}" is outside the closed policy vocabulary`)
|
||||
}
|
||||
const own = effectiveApprovalPolicy(session.events.slice(session.header.seedLength ?? 0))
|
||||
return own ?? baseline as ApprovalPolicy | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the log currently sits inside an open turn (a `turn/start` not yet
|
||||
* closed by a `turn/end`) — the {@link ApprovalService.request} precondition.
|
||||
@@ -342,14 +365,7 @@ export class ApprovalService extends Service {
|
||||
* vocabulary (a corrupt or foreign log; durable-boundary validation).
|
||||
*/
|
||||
overrideOf(session: Session): ApprovalPolicy | undefined {
|
||||
const own = effectiveApprovalPolicy(session.events.slice(session.header.seedLength ?? 0))
|
||||
if (own !== undefined) return own
|
||||
const baseline = session.header.approvalPolicy
|
||||
if (baseline === undefined) return undefined
|
||||
if (!APPROVAL_POLICIES.includes(baseline as ApprovalPolicy)) {
|
||||
throw new Error(`session header approvalPolicy "${baseline}" is outside the closed policy vocabulary`)
|
||||
}
|
||||
return baseline as ApprovalPolicy
|
||||
return approvalOverrideOf(session)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -636,4 +636,12 @@ describe('delegation inheritance (overrideOf over the header baseline)', () => {
|
||||
|
||||
expect(() => ctx.approval.overrideOf(child)).toThrow(/approvalPolicy/)
|
||||
})
|
||||
|
||||
it('rejects a malformed baseline even when an own switch would win (validation is unconditional)', async () => {
|
||||
const ctx = await mounted()
|
||||
const child = inheritedSession('sess-appr-invalid-own', { approvalPolicy: 'always' })
|
||||
setApprovalPolicy(child, 'never')
|
||||
|
||||
expect(() => ctx.approval.overrideOf(child)).toThrow(/approvalPolicy/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user