Merge remote-tracking branch 'origin/master' into cross-family-fs-sandbox
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # examples/acp-agent/cordis.yml # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/permission-switching/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # packages/bash/bash-sandbox/src/index.ts # packages/bash/bash/src/types.ts # packages/bash/tool-bash/src/index.ts # packages/fs/fs/src/index.ts # packages/sandbox/sandbox-policy/src/session-mode.ts # packages/ui/permission/src/index.ts # packages/ui/permission/tests/permission.spec.ts # scripts/doc-budgets.manifest.json
This commit is contained in:
@@ -1,16 +1,9 @@
|
||||
/**
|
||||
* User-facing PERMISSION PRESETS: one product-level knob over the two
|
||||
* mechanism knobs. A preset names a bundle — its sandbox mode
|
||||
* (`sandbox/mode`) and its approval policy (`approval/policy`) — so a
|
||||
* user picks `workspace-write` or `danger-full-access` while the mechanism
|
||||
* tiers stay orthogonal capabilities. Switching a preset WRITES THROUGH: one `permission/preset` event
|
||||
* records the chosen bundle (the audit fact reverse-mapping cannot recover —
|
||||
* two presets may share knob values and differ only in composed policy, the
|
||||
* planned `agent` preset being the standing example), then each knob event
|
||||
* follows through its own THE-write-path setter, skipping values the session
|
||||
* already effectively has. Every existing consumer (executor stamping, the
|
||||
* approval gate, narrators, resume) keeps reading its own knob fold,
|
||||
* untouched.
|
||||
* User-facing permission presets over the independent sandbox-mode and
|
||||
* approval-policy knobs. A switch records the selected preset, then writes
|
||||
* changed knobs through their canonical setters. Execution, prompt narration,
|
||||
* and replay keep reading their knob folds. The preset event preserves user
|
||||
* intent when two presets share a bundle.
|
||||
*
|
||||
* @module dsh-permission
|
||||
*/
|
||||
@@ -35,21 +28,16 @@ declare module 'cordis' {
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* The session's permission preset was switched — log-only (the
|
||||
* `sandbox/mode` precedent): durable and replayable, never in the
|
||||
* model transcript. The LAST such event is the session's preset
|
||||
* ({@link effectivePermissionPreset}); the knob events the switch wrote
|
||||
* through follow it in the same turn, and they — not this record of the
|
||||
* user's choice — are what execution reads.
|
||||
* Records the selected preset as durable, log-only user intent. The knob
|
||||
* events follow in the same turn and control execution; this event stays
|
||||
* out of the model transcript and lets {@link effectivePermissionPreset}
|
||||
* preserve a selection when bundles match.
|
||||
*/
|
||||
'permission/preset': { preset: string }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One preset's knob bundle — the sandbox mode and approval policy a session
|
||||
* runs under while the preset is active — plus its presentation.
|
||||
*/
|
||||
/** One preset's sandbox/approval bundle and optional client presentation. */
|
||||
export interface PresetSpec {
|
||||
/** The `sandbox/mode` value the preset writes through. */
|
||||
sandbox: SandboxMode
|
||||
@@ -72,21 +60,16 @@ export interface PresetOption {
|
||||
}
|
||||
|
||||
/**
|
||||
* The derived not-a-preset state: the session's effective knob values match
|
||||
* no table entry (composition defaults outside the table, or a knob moved
|
||||
* out from under the last-chosen preset). Never a switch target and never
|
||||
* an event payload — {@link PermissionService.current} derives it, and the
|
||||
* presentation layer shows it as a selectable-FROM-only current value.
|
||||
* Returned when effective knob values match no table entry. Clients may show
|
||||
* it as the current value, but it is never a switch target or event payload.
|
||||
*/
|
||||
export const CUSTOM_PRESET = 'custom'
|
||||
|
||||
/**
|
||||
* The session's permission-preset override: the last `permission/preset` event in the
|
||||
* log, or undefined when the session never switched (callers apply the
|
||||
* plugin's configured default). The pure fold — resume needs no catch-up
|
||||
* machinery because replaying the log IS the state.
|
||||
* @param events - session events in log order (other event types are skipped).
|
||||
* @returns the preset of the last switch event, or undefined without one.
|
||||
* Fold the last selected preset from the durable log; replay needs no catch-up
|
||||
* state.
|
||||
* @param events - session events in log order; other event types are ignored.
|
||||
* @returns the last selected preset, or undefined when none was recorded.
|
||||
*/
|
||||
export function effectivePermissionPreset(events: readonly SessionEvent[]): string | undefined {
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
@@ -107,13 +90,9 @@ export interface Config {
|
||||
}
|
||||
|
||||
/**
|
||||
* The permission service (`ctx.permission`). Owns the deployment's preset
|
||||
* table and THE write path for preset switches; presentation layers (the ACP
|
||||
* bridge's single `Permissions` select) advertise {@link names} and call
|
||||
* {@link set}. Composing it REQUIRES both mechanism knobs — a confining
|
||||
* `ctx.bash` executor and the `ctx.approval` seam. A knob state matching no
|
||||
* table entry is not an error but the derived {@link CUSTOM_PRESET} state:
|
||||
* shown as the current value, never a switch target.
|
||||
* 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 {@link CUSTOM_PRESET}, not an error.
|
||||
*/
|
||||
export class PermissionService extends Service {
|
||||
// Inline schema call: the config catalog walks `static Config` statically.
|
||||
@@ -124,14 +103,13 @@ export class PermissionService extends Service {
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
})).default({
|
||||
// Keep the user-facing preset names explicit about filesystem reach.
|
||||
'workspace-write': {
|
||||
sandbox: 'workspace-write', approval: 'ask',
|
||||
name: 'workspace-write', description: 'Write inside the workspace; anything wider asks for your approval.',
|
||||
name: 'workspace-write', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.',
|
||||
},
|
||||
'danger-full-access': {
|
||||
sandbox: 'danger-full-access', approval: 'never',
|
||||
name: 'danger-full-access', description: 'Full file access, no approval prompts.',
|
||||
name: 'danger-full-access', description: 'Full file access without approval prompts.',
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -161,11 +139,9 @@ export class PermissionService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset a session is on right now, derived from the EFFECTIVE knob
|
||||
* values (fold ?? composition default per knob): the last-chosen preset
|
||||
* when its bundle still matches (presets may share bundles — the fold
|
||||
* breaks the tie), else the first table entry that matches, else
|
||||
* {@link CUSTOM_PRESET} — a mismatch is a state, not an error.
|
||||
* 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.
|
||||
*/
|
||||
@@ -185,10 +161,10 @@ export class PermissionService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* A preset's knob bundle, for consumers presenting or validating one.
|
||||
* Resolve a preset's knob bundle.
|
||||
* @param name - the preset name to resolve.
|
||||
* @returns the bundle; throws on a name outside the table (fails loud —
|
||||
* an unvalidated caller handed the service an unknown preset).
|
||||
* @returns the configured bundle.
|
||||
* @throws when `name` is not in the table.
|
||||
*/
|
||||
resolve(name: string): PresetSpec {
|
||||
const spec = this.presets[name]
|
||||
@@ -199,28 +175,25 @@ export class PermissionService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* The select-option presentation of one advertisable value: a table entry
|
||||
* (label/description from its spec, the raw key standing in for a missing
|
||||
* label) or the derived {@link CUSTOM_PRESET} with its fixed presentation.
|
||||
* 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 on any other name.
|
||||
* @returns the option a client renders.
|
||||
* @throws when `name` is neither a table key nor `custom`.
|
||||
*/
|
||||
optionOf(name: string): PresetOption {
|
||||
if (name === CUSTOM_PRESET) {
|
||||
return { value: CUSTOM_PRESET, name: 'Custom', description: 'A hand-set knob combination outside the preset table.' }
|
||||
return { value: CUSTOM_PRESET, name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' }
|
||||
}
|
||||
const spec = this.resolve(name)
|
||||
return { value: name, name: spec.name ?? name, ...spec.description !== undefined ? { description: spec.description } : {} }
|
||||
}
|
||||
|
||||
/**
|
||||
* THE write path for a preset switch: appends one `permission/preset` event when
|
||||
* `name` differs from the session's current preset, then writes each knob
|
||||
* through its own setter, skipping values the session already effectively
|
||||
* has — a net-zero switch appends nothing (the log records switches, not
|
||||
* select clicks).
|
||||
* 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 (validated via {@link resolve}).
|
||||
* @param name - the preset to switch to; unknown names throw.
|
||||
*/
|
||||
set(session: Session, name: string): void {
|
||||
const spec = this.resolve(name)
|
||||
|
||||
Reference in New Issue
Block a user