feat(modes): per-session sandbox/approval switching — the session log as the store, ACP config options

effective(session) = findLast(the session own knob events)?.value ?? the
composition-config default. One log-only event per knob, owned by its
domain (bash/sandbox-mode in dsh-bash, approval/policy in dsh-approval),
each exporting the same three-piece kit: the event declaration, a pure
fold, and THE write path — a switch IS its event; no owner service, no
facts map. Restart immunity and multi-session isolation fall out of the
log replay by construction.

Execution follows the fold on both sides: the bash tool stamps
escalation grant > session override > executor default, and the approval
seam prepends the never-gate that auto-rejects before any interactive
answerer. Visibility is two layers per knob: a per-agent prompt section
states the effective value on every request (logged through
request/header*, so what-the-model-was-told replays from the log), and an
agent/pre-step narrator injects at most one coalesced delta notice with
positional attribution (user switch vs operator/config drift). The ACP
bridge advertises one capability-gated select per composable knob with
currentValue folded per session, validates set_config_option against the
closed vocabularies, and anchors idle switches at the next turn
prompt-submit under the turn-enclosure contract.
This commit is contained in:
kingwl
2026-07-09 16:41:03 +08:00
parent 0e49615a3d
commit 3f663c9154
24 changed files with 1149 additions and 43 deletions

View File

@@ -23,7 +23,7 @@ An approval question was put to the answerer chain — log-only audit (like `hoo
Types: [CallId](core-data-structures/core.md)
Source: [`packages/approval/approval/src/index.ts:66`](../packages/approval/approval/src/index.ts)
Source: [`packages/approval/approval/src/index.ts:77`](../packages/approval/approval/src/index.ts)
#### `approval/decided` — log-only
@@ -33,7 +33,17 @@ The outcome of a prior `approval/asked` (same `id`) — log-only audit. Exactly
'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome }
```
Source: [`packages/approval/approval/src/index.ts:77`](../packages/approval/approval/src/index.ts)
Source: [`packages/approval/approval/src/index.ts:88`](../packages/approval/approval/src/index.ts)
#### `approval/policy` — log-only
The session's approval policy was switched — log-only, durable, replayable, never in the model transcript (the model learns the policy from the prompt section and the narrator's notices). The LAST such event is the session's override (effectiveApprovalPolicy); who asked for it is derivable from position (an event after the log's last `request/header*` was a runtime switch by the user).
```ts persistence-catalog
'approval/policy': { policy: ApprovalPolicy }
```
Source: [`packages/approval/approval/src/index.ts:100`](../packages/approval/approval/src/index.ts)
### `assistant/*`
@@ -61,6 +71,18 @@ Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-st
Source: [`packages/core/session/src/types.ts:320`](../packages/core/session/src/types.ts)
### `bash/*`
#### `bash/sandbox-mode` — log-only
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 (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).
```ts persistence-catalog
'bash/sandbox-mode': { mode: SandboxMode }
```
Source: [`packages/bash/bash/src/session-mode.ts:31`](../packages/bash/bash/src/session-mode.ts)
### `compact/*`
#### `compact/end` — log-only