Merge remote-tracking branch 'origin/master' into codex/trim-ai-prose

# Conflicts:
#	docs/config-catalog.md
#	docs/development.i18n.yaml
#	docs/development.zh.md
#	docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	docs/rfc/implemented/feature/2026-07-06-sandbox.md
#	examples/AGENTS.md
#	examples/README.md
#	examples/acp-agent/README.md
#	examples/acp-agent/cordis.yml
#	examples/acp-agent/tests/acp.e2e.ts
#	examples/acp-agent/tests/escalation.e2e.ts
#	examples/sandbox-acp-agent/README.md
#	examples/sandbox-acp-agent/cordis.snapshot.yml
#	examples/sandbox-acp-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/acp.snapshot.ts
#	packages/ui/acp-agent/src/bin.ts
#	packages/ui/acp/README.md
#	packages/ui/jsonrpc-agent/README.md
#	packages/ui/jsonrpc-agent/src/bin.ts
#	scripts/verify-translation-pairing.ts
This commit is contained in:
Tianyi Cui
2026-07-14 12:34:14 +08:00
180 changed files with 3874 additions and 1935 deletions

View File

@@ -0,0 +1,7 @@
# @deepseek-ai/dsh-permission
User-facing permission presets through `ctx.permission` ([`PermissionService`](src/index.ts)). Each configured name bundles `bash/sandbox-mode` with `approval/policy`; the defaults are `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`). The ACP bridge exposes them as one `Permissions` select, while sandbox execution and approval continue to consume their own knobs.
`set(session, name)` records a changed selection in a log-only `permission/preset` event, then calls each knob's setter only when its effective value changes. The selection event precedes the knob events and preserves user intent when presets share a bundle; a net-zero selection appends nothing. `current(events)` prefers a still-matching recorded selection, then the first matching table entry, and otherwise returns `custom`. Clients may display `custom` as the current value, but cannot select it.
The service requires a confining `ctx.bash` executor and `ctx.approval`. A table entry named `custom` throws at load; composition defaults outside the table instead make a zero-event session derive `custom`. See [the acp-agent example](../../../examples/acp-agent/) for the composition and [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) for the switching design.

View File

@@ -0,0 +1,41 @@
{
"name": "@deepseek-ai/dsh-permission",
"description": "User-facing permission presets (ctx.permission) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -0,0 +1,210 @@
/**
* 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
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-bash'
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
declare module 'cordis' {
interface Context {
permission: PermissionService
}
}
declare module '@deepseek-ai/dsh-session' {
interface SessionEventMap {
/**
* 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 sandbox/approval bundle and optional client presentation. */
export interface PresetSpec {
/** The `bash/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
}
/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */
export interface PresetOption {
/** The machine value (`session/set_config_option` vocabulary): the table key, or `custom`. */
value: string
/** The display label. */
name: string
/** One user-facing sentence on what the value means. */
description?: string
}
/**
* 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'
/**
* 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) {
const event = events[index] as SessionEvent
if (event.type === 'permission/preset') return event.data.preset
}
return undefined
}
/** The {@link PermissionService} config: the deployment's preset table. */
export 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>
}
/**
* 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.
static Config: z<Config> = z.object({
presets: z.dict(z.object({
sandbox: z.union(SANDBOX_MODES as SandboxMode[]).required(),
approval: z.union(APPROVAL_POLICIES as ApprovalPolicy[]).required(),
name: z.string(),
description: z.string(),
})).default({
'workspace-write': {
sandbox: 'workspace-write', approval: 'ask',
name: 'workspace-write', description: 'Write inside the workspace; wider retries require approval.',
},
'danger-full-access': {
sandbox: 'danger-full-access', approval: 'never',
name: 'danger-full-access', description: 'Full file access without approval prompts.',
},
}),
})
static inject = ['bash', 'approval']
private readonly presets: Record<string, PresetSpec>
constructor(ctx: Context, config: Config) {
super(ctx, 'permission')
// The schema defaulted the table — the cast records that runtime fact.
this.presets = config.presets as Record<string, PresetSpec>
if (CUSTOM_PRESET in this.presets) {
throw new Error(`permission: "${CUSTOM_PRESET}" is reserved for the derived not-a-preset state and cannot name a table entry`)
}
if (ctx.bash.sandboxMode === undefined) {
throw new Error('permission: the mounted bash executor does not confine (no sandboxMode) — presets bundle a sandbox mode, so composing this plugin over an unconfined executor is a misconfiguration')
}
}
/**
* The advertised preset names, in the preset table's declaration order.
* @returns every switchable preset name.
*/
get names(): readonly string[] {
return Object.keys(this.presets)
}
/**
* 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 {
const sandbox = effectiveSandboxMode(events) ?? this.ctx.bash.sandboxMode
const approval = effectiveApprovalPolicy(events) ?? this.ctx.approval.config.policy ?? 'ask'
const matches = (spec: PresetSpec): boolean => spec.sandbox === sandbox && spec.approval === approval
const folded = effectivePermissionPreset(events)
if (folded !== undefined) {
const spec = this.presets[folded]
if (spec !== undefined && matches(spec)) return folded
}
for (const [name, spec] of Object.entries(this.presets)) {
if (matches(spec)) return name
}
return CUSTOM_PRESET
}
/**
* 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 {
const spec = this.presets[name]
if (spec === undefined) {
throw new Error(`permission: unknown preset "${name}" (known: ${Object.keys(this.presets).join(', ')})`)
}
return spec
}
/**
* 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 {
if (name === CUSTOM_PRESET) {
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 } : {} }
}
/**
* 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 {
const spec = this.resolve(name)
if (this.current(session.events) !== name) {
session.append('permission/preset', { preset: name })
}
const events = session.events
if (spec.sandbox !== (effectiveSandboxMode(events) ?? this.ctx.bash.sandboxMode)) {
setSandboxMode(session, spec.sandbox)
}
if (spec.approval !== (effectiveApprovalPolicy(events) ?? this.ctx.approval.config.policy ?? 'ask')) {
setApprovalPolicy(session, spec.approval)
}
}
}
export default PermissionService

View File

@@ -0,0 +1,140 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import PermissionService, { CUSTOM_PRESET, effectivePermissionPreset } from '@deepseek-ai/dsh-permission'
import type { Config } from '@deepseek-ai/dsh-permission'
async function mounted(options: {
config?: Config
bashDefault?: SandboxMode | undefined
approvalDefault?: ApprovalPolicy | undefined
} = {}): Promise<Context> {
const ctx = new Context()
ctx.provide('bash', { sandboxMode: 'bashDefault' in options ? options.bashDefault : 'workspace-write' })
ctx.provide('approval', { config: { policy: 'approvalDefault' in options ? options.approvalDefault : 'ask' } })
await ctx.plugin(PermissionService, options.config ?? {})
return ctx
}
function freshSession(id: string): Session {
return new Session(SessionId(id))
}
describe('effectivePermissionPreset', () => {
it('folds to the last event, or undefined without one', () => {
const session = freshSession('sess-fold')
expect(effectivePermissionPreset(session.events)).toBeUndefined()
session.append('permission/preset', { preset: 'danger-full-access' })
session.append('permission/preset', { preset: 'workspace-write' })
expect(effectivePermissionPreset(session.events)).toBe('workspace-write')
})
})
describe('PermissionService', () => {
it('advertises the preset table in declaration order and resolves bundles', async () => {
const ctx = await mounted()
expect(ctx.permission.names).toEqual(['workspace-write', 'danger-full-access'])
expect(ctx.permission.resolve('danger-full-access')).toMatchObject({ sandbox: 'danger-full-access', approval: 'never' })
expect(() => ctx.permission.resolve('plan')).toThrow(/unknown preset "plan"/)
})
it('current() derives from the effective knobs: composition defaults hit workspace-write, a switch hits its preset', async () => {
const ctx = await mounted()
const session = freshSession('sess-current')
expect(ctx.permission.current(session.events)).toBe('workspace-write')
ctx.permission.set(session, 'danger-full-access')
expect(ctx.permission.current(session.events)).toBe('danger-full-access')
})
it('a knob state matching no table entry derives custom — a state, not an error', async () => {
const ctx = await mounted()
const session = freshSession('sess-custom')
session.append('bash/sandbox-mode', { mode: 'read-only' })
expect(ctx.permission.current(session.events)).toBe(CUSTOM_PRESET)
ctx.permission.set(session, 'danger-full-access')
expect(ctx.permission.current(session.events)).toBe('danger-full-access')
expect(() => ctx.permission.resolve(CUSTOM_PRESET)).toThrow(/unknown preset/)
})
it('composition defaults outside the table derive custom at zero events', async () => {
const ctx = await mounted({ approvalDefault: 'never' })
const session = freshSession('sess-defaults-custom')
expect(ctx.permission.current(session.events)).toBe(CUSTOM_PRESET)
})
it('the fold breaks bundle ties; a stale fold no longer matching falls back to table order', async () => {
const ctx = await mounted({ config: { presets: {
'workspace-write': { sandbox: 'workspace-write', approval: 'ask' },
agentish: { sandbox: 'workspace-write', approval: 'ask' },
'danger-full-access': { sandbox: 'danger-full-access', approval: 'never' },
} } })
const session = freshSession('sess-tie')
ctx.permission.set(session, 'agentish')
expect(ctx.permission.current(session.events)).toBe('agentish')
session.append('approval/policy', { policy: 'never' })
session.append('bash/sandbox-mode', { mode: 'danger-full-access' })
expect(ctx.permission.current(session.events)).toBe('danger-full-access')
})
it('set() writes through: one preset event plus both knob events', async () => {
const ctx = await mounted()
const session = freshSession('sess-set')
ctx.permission.set(session, 'danger-full-access')
expect(session.events.map(e => [e.type, e.data])).toEqual([
['permission/preset', { preset: 'danger-full-access' }],
['bash/sandbox-mode', { mode: 'danger-full-access' }],
['approval/policy', { policy: 'never' }],
])
})
it('set() to the current preset is a no-op when the knobs already match (clicks are not switches)', async () => {
const ctx = await mounted()
const session = freshSession('sess-noop')
ctx.permission.set(session, 'workspace-write')
expect(session.events).toHaveLength(0)
})
it('re-asserting a preset from a drifted (custom) state re-records the choice and repairs the knob', async () => {
const ctx = await mounted()
const session = freshSession('sess-drift')
ctx.permission.set(session, 'danger-full-access')
// Re-selecting from a drifted state records the choice and repairs only
// the changed knob.
session.append('bash/sandbox-mode', { mode: 'read-only' })
ctx.permission.set(session, 'danger-full-access')
const tail = session.events.slice(4)
expect(tail.map(e => [e.type, e.data])).toEqual([
['permission/preset', { preset: 'danger-full-access' }],
['bash/sandbox-mode', { mode: 'danger-full-access' }],
])
})
it('rejects composition over a non-confining executor at load', async () => {
await expect(mounted({ bashDefault: undefined }))
.rejects.toThrow(/does not confine/)
})
it('optionOf() presents shipped labels/descriptions, falls back to the raw key, and fixes custom', async () => {
const ctx = await mounted()
expect(ctx.permission.optionOf('danger-full-access')).toEqual({ value: 'danger-full-access', name: 'danger-full-access', description: 'Full file access without approval prompts.' })
expect(ctx.permission.optionOf('custom')).toEqual({ value: 'custom', name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' })
const bare = await mounted({ config: { presets: { plain: { sandbox: 'workspace-write', approval: 'ask' } } } })
expect(bare.permission.optionOf('plain')).toEqual({ value: 'plain', name: 'plain' })
expect(() => ctx.permission.optionOf('plan')).toThrow(/unknown preset/)
})
it('rejects a table entry named custom (reserved for the derived state)', async () => {
await expect(mounted({ config: { presets: { custom: { sandbox: 'read-only', approval: 'ask' } } } }))
.rejects.toThrow(/reserved for the derived not-a-preset state/)
})
it('reads a schema-less approval stand-in as the ask default', async () => {
const ctx = await mounted({ approvalDefault: undefined })
const session = freshSession('sess-standin')
ctx.permission.set(session, 'workspace-write')
expect(session.events).toHaveLength(0)
expect(ctx.permission.current(session.events)).toBe('workspace-write')
})
})

View File

@@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/session"
},
{
"path": "../../sandbox/sandbox"
},
{
"path": "../../bash/bash"
},
{
"path": "../user-approval"
}
]
}