feat(permission): user-facing permission presets — one Permissions select over the two knobs

A preset names a bundle of the two mechanism knobs — request =
workspace-write + ask, yolo = danger-full-access + never — so the editor
shows ONE 'Permissions' select where the sandbox-mode and approval-policy
tiers stay orthogonal capabilities (the Codex /approvals shape: presets over
two dials). ctx.permission (dsh-permission) owns the config-defined table,
validates the default preset's bundle against the composed knob defaults at
load (fails loud), and writes a switch THROUGH: one log-only
permission/preset event (the audit fact reverse-mapping cannot recover —
the planned 'agent' preset shares request's knob values and differs only in
composed policy) plus each knob event via its own setter, deduped — a
net-zero switch appends nothing. Every knob consumer keeps reading its own
fold, untouched.

The current preset DERIVES from the effective knob values — the fold breaks
bundle ties, a knob state outside the table is the reserved 'custom' value
(a state, not an error: shown while it holds, switchable FROM, never a
target), and defaultPreset disappears (zero-event state reverse-maps from
the composition defaults).

The ACP bridge drops the two per-knob selects for the one preset select
(advertised only when ctx.permission is composed); pending/anchor/no-op
semantics carry over unchanged, with the no-op echo acknowledged before
vocabulary validation so a client re-pushing a derived 'custom' current
never errors. The sandbox variant example composes the
service with a workspace-write default; the permission-switching,
escalation-approved and escalation-rejected scenarios are re-recorded under
it (escalations now target an outside-workspace /tmp path under
danger-full-access, self-cleaning) and config-options is re-authored on the
single-select wire.
This commit is contained in:
kingwl
2026-07-12 21:03:41 +08:00
parent 624d8d5df4
commit 95635dfa66
40 changed files with 1724 additions and 1062 deletions
+16 -12
View File
@@ -140,20 +140,24 @@ const SCENARIOS: Scenario[] = [
// prior denial. config-options: the session config-option surface this
// composition adds (both advertised selects, the refreshed state every
// set_config_option answers with, both rejection shapes) — protocol-only,
// replays on runner-less hosts. mode-switching: the runtime switching arc
// and NECESSARILY this class's pinned-header scenario (an approval-policy
// switch rewrites its prompt section; the resulting request/header-delta is
// legal only in the pinning scenario) — the pin commits the full sandbox
// header (persona, tool schemas WITH the escalation fields) plus the
// approval delta and its "changed by the user" notice; the SANDBOX switch
// stays deliberately silent (the visibility asymmetry), proven by BEHAVIOR.
// replays on runner-less hosts. permission-switching: the runtime
// preset-switch arc (request → yolo: one permission/preset event written
// through to both knobs) and NECESSARILY this class's pinned-header
// scenario (yolo's approval=never rewrites the prompt section; the
// resulting request/header-delta is legal only in the pinning scenario) —
// the pin commits the full sandbox header (persona, tool schemas WITH the
// escalation fields) plus the approval delta and its "changed by the user"
// notice; the sandbox half of the bundle stays deliberately silent (the
// visibility asymmetry) and is pinned as its stamped knob event.
// escalation-approved/rejected: the approval wire end-to-end under the
// default read-only/ask — the escalating call streams,
// session/request_permission attaches to it, and the scripted answer drives
// each branch (approved runs CONFINED under the granted workspace-write;
// rejected executes nothing, failing with the deterministic text).
// default request preset (workspace-write/ask) — the escalating call
// (danger-full-access for a user-asserted outside-workspace denial)
// streams, session/request_permission attaches to it, and the scripted
// answer drives each branch (approved runs under the granted mode and
// self-cleans its /tmp target; rejected executes nothing, failing with
// the deterministic text).
{ name: 'config-options', hasModelTurn: false, recorded: false, headerClass: 'sandbox', configPath: SANDBOX_CONFIG },
{ name: 'mode-switching', hasModelTurn: true, recorded: true, pinsHeader: true, expectedHeaderDeltas: 1, headerClass: 'sandbox', configPath: SANDBOX_CONFIG },
{ name: 'permission-switching', hasModelTurn: true, recorded: true, pinsHeader: true, expectedHeaderDeltas: 1, headerClass: 'sandbox', configPath: SANDBOX_CONFIG },
{ name: 'escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox', configPath: SANDBOX_CONFIG },
{ name: 'escalation-rejected', hasModelTurn: true, recorded: true, headerClass: 'sandbox', configPath: SANDBOX_CONFIG },
]