docs: qualify plan-mode timing and workspace no-write claims

ds-review-bot round 3:
- set() after a turn's final step lands in a later turn (selection
  never forces continuation) — the same-turn phrasing overstated the
  guarantee; set() JSDoc + subsystem page (both languages).
- Workspace attach/insert/detach no-op claims now carry the
  filtered-candidate-prune caveat: mutate() durably prunes stale
  candidates even when the requested change is a no-op, a state the
  sessionIds contract explicitly permits; Workspace fence re-synced on
  both page sides; catalogs regenerated.
This commit is contained in:
Tianyi Cui
2026-07-28 14:11:18 +08:00
parent 3e1c7351be
commit 44b035c711
30 changed files with 187 additions and 531 deletions

View File

@@ -2,9 +2,9 @@
English | [中文](permission.zh.md)
The permission-preset layer of [dsh-permission](../../packages/ui/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/ui/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale.
The permission-preset layer of [dsh-permission](../../packages/interaction/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/interaction/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale.
Source: [`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts)
Source: [`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts)
## The preset table
@@ -25,7 +25,7 @@ interface PresetSpec {
```
```ts type-equiv
/** The {@link PermissionService} config: the deployment's preset table. */
/** The {@link PermissionService} config: preset table and composition default. */
interface Config {
/**
* The preset table: name → knob bundle. Defaults to `workspace-write`
@@ -33,6 +33,11 @@ interface Config {
* never). The name `custom` is reserved for the derived not-a-preset state.
*/
presets?: Record<string, PresetSpec>
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
defaultPreset?: string
}
```
@@ -51,13 +56,13 @@ interface PresetOption {
value: string
/** The display label. */
name: string
/** One user-facing sentence on what the value means. */
/** One user-facing sentence on what the value means; omitted when not configured. */
description?: string
}
```
## Switching and the `permission/preset` event
`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/ui/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all.
`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/interaction/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all.
`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxpermission--permissionservice).