132 lines
7.5 KiB
Markdown
132 lines
7.5 KiB
Markdown
# Permission Presets
|
|
|
|
English | [中文](permission.zh.md)
|
|
|
|
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/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts)
|
|
|
|
## The preset table
|
|
|
|
A preset is a table key mapping to one sandbox/approval bundle plus optional client presentation; the default table ships `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`).
|
|
|
|
```ts type-equiv
|
|
/** One preset's sandbox/approval bundle and optional client presentation. */
|
|
interface PresetSpec {
|
|
/** The `sandbox/mode` value the preset writes through. */
|
|
sandbox: SandboxMode
|
|
/** The `approval/policy` value the preset writes through. */
|
|
approval: ApprovalPolicy
|
|
/** The display label a client shows for this preset; the raw table key when omitted. */
|
|
name?: string
|
|
/** One user-facing sentence on what the preset means; omitted when not configured. */
|
|
description?: string
|
|
}
|
|
```
|
|
|
|
```ts type-equiv
|
|
/** The {@link PermissionService} config: preset table and composition default. */
|
|
interface Config {
|
|
/**
|
|
* The preset table: name → knob bundle. Defaults to `workspace-write`
|
|
* (workspace-write + ask) and `danger-full-access` (danger-full-access +
|
|
* 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
|
|
}
|
|
```
|
|
|
|
The service requires a confining `ctx.bash` executor and `ctx.approval`, and misconfiguration fails at plugin load: a table entry named `custom` throws (the name is reserved for the derived not-a-preset state), and composing over a bash executor that does not confine (no `sandboxMode` capability fact) throws, because presets bundle a sandbox mode.
|
|
|
|
## Current preset and the derived `custom`
|
|
|
|
`current(events)` derives the effective preset from the knobs, not from its own event alone: it folds the session's effective sandbox mode (falling back to the executor's configured mode) and effective approval policy (falling back to the approval service config, then `ask`), prefers a still-matching recorded selection, then the first matching table entry in declaration order, and otherwise returns `CUSTOM_PRESET` (`'custom'`). `custom` is derived-only: clients may display it as the current value, but it is never a switch target or an event payload.
|
|
|
|
`names` lists the switchable presets in table declaration order; `optionOf(name)` builds the option a client renders for a table key (label falls back to the key) or for `custom`, and throws for any other name.
|
|
|
|
```ts type-equiv
|
|
/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */
|
|
interface PresetOption {
|
|
/** Stable option value: the table key, or `custom`. */
|
|
value: string
|
|
/** The display label. */
|
|
name: string
|
|
/** 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/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](#ctxpermission--permissionservice).
|
|
|
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
|
|
<a id="cordis-surface"></a>
|
|
|
|
## Cordis API
|
|
|
|
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
|
|
|
<a id="ctxpermission--permissionservice"></a>
|
|
|
|
### `ctx.permission` — `PermissionService`
|
|
|
|
Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* Resolve the preset matching the effective knob values. A still-matching
|
|
* last selection wins shared-bundle ties; otherwise the first table match
|
|
* wins, or {@link CUSTOM_PRESET} when no entry matches.
|
|
* @param events - the session's events in log order.
|
|
* @returns the effective preset name, or `custom` when nothing matches.
|
|
*/
|
|
current(events: readonly SessionEvent[]): string
|
|
|
|
/**
|
|
* Build the whole select value for one folded knob state: every table
|
|
* option in declaration order, `custom` appended exactly while derived.
|
|
* @param state - the folded knob overrides.
|
|
* @returns the `permissions` projection payload.
|
|
*/
|
|
selectFor(state: KnobState): PermissionSelect
|
|
|
|
/**
|
|
* Resolve a preset's knob bundle.
|
|
* @param name - the preset name to resolve.
|
|
* @returns the configured bundle.
|
|
* @throws when `name` is not in the table.
|
|
*/
|
|
resolve(name: string): PresetSpec
|
|
|
|
/**
|
|
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
|
|
* missing label falls back to the table key.
|
|
* @param name - a table key, or `custom`.
|
|
* @returns the option a client renders.
|
|
* @throws when `name` is neither a table key nor `custom`.
|
|
*/
|
|
optionOf(name: string): PresetOption
|
|
|
|
/**
|
|
* Record a changed preset, then update each changed knob through its own
|
|
* setter. Selecting the effective preset again appends nothing.
|
|
* @param session - the session the switch belongs to.
|
|
* @param name - the preset to switch to; unknown names throw.
|
|
*/
|
|
set(session: Session, name: string): void
|
|
```
|
|
|
|
Types: [Session](session.md) · [SessionEvent](session.md)
|
|
|
|
Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts)
|
|
<!-- END GENERATED cordis-surface -->
|