docs: rebalance prose cleanup and add trimming skill

This commit is contained in:
Tianyi Cui
2026-07-13 23:27:00 +08:00
parent fcdc318dda
commit 148046b9c8
392 changed files with 2801 additions and 1754 deletions

View File

@@ -1,20 +1,22 @@
# AGENTS.md — Examples
Runnable harness compositions. **Examples are not workspaces:** their private package stubs are not built; `tsx` and the Cordis Loader resolve package names through the root `tsconfig.json` paths.
Runnable harness compositions. **Examples are not workspaces:** private package stubs are not built. App bins load each `cordis.yml` through `tsx`; package names resolve through root `tsconfig.json` paths, not `node_modules`.
Keep only wiring, demo-only fixtures, and e2e/snapshot scenarios here. Move reusable logic into `packages/`, where coverage and README requirements apply. App-package bins own bootstrapping; examples have no `start.ts`.
Keep wiring, demo fixtures, and e2e/snapshot scenarios here. Move reusable logic into `packages/`, with coverage and a README. App bins own bootstrapping; examples have no `start.ts`.
## Every example ships e2e smokes (keyless + with-key)
## E2E smokes
Each example has both smoke tiers:
Each example has both:
- **Keyless:** boot the real `cordis.yml` through the Loader, drive it, and assert output plus clean exit. This catches Loader/export-shape failures that hand-mounted tests miss ([postmortem](../docs/postmortem/0001-acp-default-export-drops-inject.md)).
- **Keyless:** boot the real `cordis.yml` through the Loader, drive it, and assert output and clean exit. Catches Loader/export-shape failures hand-mounted tests miss ([postmortem](../docs/postmortem/0001-acp-default-export-drops-inject.md)).
- **With-key:** send a live-model prompt and verify external state, not the model's claim. Self-skip without `DEEPSEEK_API_KEY`; see [testing.md](../docs/testing.md).
Mock-only examples need only the keyless tier; state the exception in the test.
Mock-only examples require only the keyless tier; state that exception in the test.
A keyless smoke launched from a temporary cwd sets `TSX_TSCONFIG_PATH` to the root tsconfig and passes `--expose-internals` when loading HMR.
Temp-cwd keyless smokes set `TSX_TSCONFIG_PATH` to the root tsconfig and pass `--expose-internals` when loading HMR.
Do not maintain a prose inventory of example tests here; the `tests/` trees and root scripts are authoritative.
Do not inventory example tests here; the `tests/` trees and root scripts are authoritative.
In `cordis.yml`, comment only non-obvious wiring, load-order consequences, replay, security boundaries, and configuration scope. Do not narrate visible entries; use [dsh-trim-prose](../.agents/skills/dsh-trim-prose/SKILL.md) for example prose.
See [the root AGENTS.md](../AGENTS.md) for repo-wide conventions and [docs/architecture.md](../docs/architecture.md) for the design.

View File

@@ -1,6 +1,6 @@
# Examples
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is now a **thin leaf**: a `cordis.yml` that picks the swappable backends (an LLM adapter, a bash executor), loads ONE app package, and may add optional product tools or demo-only mocks. The composition — the spine, the front-door cluster, and the boot glue — lives in the app packages ([`@deepseek-ai/dsh-stdio-agent`](../packages/ui/stdio-agent), [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent)) and the [`@deepseek-ai/dsh-agent-core`](../packages/core/agent-core) bundle they share. There is no `start.ts`; the `demo:*` scripts invoke each app package's `bin`.
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is a **thin leaf**: a `cordis.yml` that picks the swappable backends (an LLM adapter, a bash executor), loads one app package, and may add optional product tools or demo-only mocks. The composition — the spine, the front-door cluster, and the boot glue — lives in the app packages ([`@deepseek-ai/dsh-stdio-agent`](../packages/ui/stdio-agent), [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent)) and the [`@deepseek-ai/dsh-agent-core`](../packages/core/agent-core) bundle they share. There is no `start.ts`; the `demo:*` scripts invoke each app package's `bin`.
## echo-agent
@@ -35,6 +35,6 @@ Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:code-mo
## sandbox-acp-agent
The coding agent with its bash executor swapped for the sandbox stack ([`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local) + [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox) the one-entry executor swap the `ctx.bash` capability seam exists for), served over ACP with [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval) mounted — the first composition where the approval loop is LIVE: a sandbox denial escalated by the model becomes a `session/request_permission` prompt in the editor, and "Allow once" runs exactly that command under the wider mode.
The coding agent with its bash executor swapped for the sandbox stack ([`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local) + [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox), the one-entry swap supported by the `ctx.bash` capability seam), served over ACP with [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval). A model retry after a sandbox denial becomes a `session/request_permission` prompt, and Allow once” grants only that command the wider mode.
Run with: `pnpm run demo:sandbox-acp` (needs `DEEPSEEK_API_KEY`; bwrap, a Landlock-enforcing kernel, or macOS for confined runs). See [sandbox-acp-agent/README.md](sandbox-acp-agent/README.md).

View File

@@ -1,9 +1,6 @@
# Both-mode REPLAY overlay: the same patched tree as both-mode.cordis.yml
# (registry in `mode: both` + the worker code runtime) with the keyless model
# swap from cordis.snapshot.yml (llm-deepseek disabled, llm-replay serving
# the recorded fixture). Patches do not compose across nested includes —
# an outer include's patch can only target entries in the file IT loads — so
# this file patches ./cordis.yml directly with the union of both overlays.
# Keyless both mode combines the runtime/registry patch with the DeepSeek-to-replay
# swap. Include patches cannot target entries behind a nested include, so this file
# applies both overlays directly to `cordis.yml`.
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,11 +1,7 @@
# Both-mode RECORD overlay: the live acp-agent tree (./cordis.yml) with two
# load-time patches — the app entry's config gains `tools: { mode: both }`
# (every native tool definition stays on the wire AND run_code + the generated
# TypeScript SDK prompt section ride along) and the worker-thread code runtime joins the
# tree as `ctx.codeRuntime`. The dsh-acp-agent bin boots this file when the
# snapshot harness records the both-mode scenario; DSH_SNAPSHOT=replay swaps
# it for the sibling both-mode.cordis.snapshot.yml. A config patch REPLACES
# the entry's whole config, so the base entry's fields are restated verbatim.
# Both mode adds `ctx.codeRuntime` while keeping native tools on the wire and
# adding `run_code` plus its generated TypeScript SDK prompt. The app bin selects
# this overlay for snapshot recording and the sibling overlay for replay. A config
# patch replaces the whole app config, so unchanged base fields are restated below.
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,9 +1,6 @@
# Code Mode REPLAY overlay: the same patched tree as code-mode.cordis.yml
# (registry in `mode: code` + the worker code runtime) with the keyless model
# swap from cordis.snapshot.yml (llm-deepseek disabled, llm-replay serving
# the recorded fixture). Patches do not compose across nested includes —
# an outer include's patch can only target entries in the file IT loads — so
# this file patches ./cordis.yml directly with the union of both overlays.
# Keyless Code Mode combines the runtime/registry patch with the DeepSeek-to-replay
# swap. Include patches cannot target entries behind a nested include, so this file
# applies both overlays directly to `cordis.yml`.
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,12 +1,8 @@
# Code Mode overlay: the live acp-agent tree (./cordis.yml) with two
# load-time patches — the app entry's config gains `tools: { mode: code }`
# (the registry offers exactly one wire tool, run_code, plus the generated
# TypeScript SDK prompt section) and the worker-thread code runtime joins the
# tree as `ctx.codeRuntime`. The dsh-acp-agent bin boots this file for
# `pnpm run demo:code-mode acp` and when the snapshot harness records the
# code-mode scenarios; DSH_SNAPSHOT=replay swaps it for the sibling
# code-mode.cordis.snapshot.yml. A config patch REPLACES the entry's whole
# config, so the base entry's fields are restated verbatim.
# Code Mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
# `run_code`, plus its generated TypeScript SDK prompt. The app bin selects this
# overlay for `demo:code-mode acp` and snapshot recording, and selects the sibling
# replay overlay for `DSH_SNAPSHOT=replay`. A config patch replaces the whole app
# config, so unchanged base fields are restated below.
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,30 +1,17 @@
# Snapshot-test REPLAY overlay: the SAME app tree as cordis.yml, derived from
# it by an include — the one difference is the model backend. A keyless replay
# run cannot boot the real adapter (llm-deepseek's apply() throws without
# DEEPSEEK_API_KEY), so the include patches the live tree at load time: the
# llm-deepseek entry is disabled by id, and the llm-replay entry (which serves
# a recorded session JSONL — no API key, no network) is inserted. Every other
# entry — the app, the bash executor, the fs/subagent/todo tools, both hook
# bridges, the system prompt — IS the live tree, so replay exercises exactly
# what ships and an app-shape change lands once, in cordis.yml.
#
# The dsh-acp-agent bin selects this file for DSH_SNAPSHOT=replay. The replay
# fixture path comes from $DSH_SNAPSHOT_FILE (and an optional
# $DSH_SNAPSHOT_OVERRIDE sidecar), set by the snapshot harness. stdout stays
# reserved for the ACP JSON-RPC protocol (the app package loads no stdout
# logger). Patches apply when the include loads the file — a one-shot replay
# boot, so the load-time-only patch semantics are exactly enough.
# Keyless replay includes the live `cordis.yml`, disables the key-requiring
# DeepSeek adapter, and inserts `llm-replay` to serve recorded JSONL without a key
# or network; every other app entry remains shared.
# With `DSH_SNAPSHOT=replay`, the app bin reads `DSH_SNAPSHOT_FILE` and optional
# `DSH_SNAPSHOT_OVERRIDE` from the harness. The one-shot patch applies at include
# load time, and stdout remains reserved for ACP JSON-RPC.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
# The name is an assertion, not an override: the include skips the patch
# (warning if a logger exists) when the id points at a different plugin,
# so this can never disable the wrong entry. If cordis.yml ever RENAMES
# the id, the patch degrades to a skip — replay output stays correct
# (llm-replay still short-circuits the stream) but the stale patch and a
# futile keyless adapter entry linger until review catches them.
# `name` asserts the target: a mismatch skips the patch and warns only when
# a logger exists. A renamed id leaves a stale adapter entry, but replay still
# short-circuits through `llm-replay`.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true

View File

@@ -1,17 +1,8 @@
# The acp-agent plugin tree: the ACP server. Also the snapshot RECORD config
# (the dsh-acp-agent bin selects it for DSH_SNAPSHOT=record): a real llm-deepseek
# run whose persisted log the snapshot harness harvests. The swappable DeepSeek
# adapter, local bash/filesystem executors, the ACP server app
# (@deepseek-ai/dsh-acp-agent), and the optional model-facing fs/subagent/todo
# tools loaded below.
#
# CRITICAL: this tree loads NO stdout logger and NO hmr — stdout is reserved for
# the ACP JSON-RPC protocol (see packages/ui/acp). That guarantee is now a
# property of @deepseek-ai/dsh-acp-agent (it contains no logger entry), not a
# leaf convention: there is no logger here to get wrong.
#
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) — the
# dsh-acp-agent bin loads the gitignored repo-root .env first (on STDERR only).
# ACP server and snapshot-record composition. With `DSH_SNAPSHOT=record`, the
# app bin runs the real DeepSeek adapter and the harness harvests its persisted log.
# `dsh-acp-agent` loads no stdout logger or HMR because stdout carries ACP JSON-RPC.
# It loads the gitignored root `.env` on stderr before reading `DEEPSEEK_API_KEY`
# and optional `DEEPSEEK_BASE_URL` here.
# The DeepSeek adapter.
- id: llm-deepseek
@@ -23,8 +14,7 @@
- deepseek-v4-flash
- deepseek-v4-pro
# Local bash executor for agent-core's tool-bash schema (one of several tool
# stacks in this tree: filesystem, subagent, and todo_write load below).
# Local executor for the app bundle's bash tool.
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
@@ -38,22 +28,16 @@
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
# The persona: identity + behavior only, nothing about transports or
# tooling — tool guidance lives with each tool plugin (descriptions +
# prompt sections). {{model}} and {{cwd}} are prompt variables the agent
# loop resolves per session (every ACP session carries the client's cwd,
# so the persona can state the workspace).
# Keep the persona to identity and behavior; tool plugins own tool guidance.
# The loop resolves {{model}} and each ACP session's client-supplied {{cwd}}.
persona: |
You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
Verify your work by running the code or tests. Keep answers brief and factual.
# The subagent seam + both in-process backends + two model-facing tools, as leaf
# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
# child) and fork (a child seeded with the parent's completed-turn prefix) are
# both reachable by the model: dsh-tool-subagent is loaded once per backend with
# a distinct toolName (subagent → spawn, subagent_fork → fork), so a multi-child
# scenario can exercise both transports.
# Expose fresh-child `spawn` and completed-prefix `fork` through separate tool
# names so multi-child scenarios exercise both transports. These leaves follow
# the app because it provides `ctx.agents` and `ctx.tools`.
- id: subagent
name: '@deepseek-ai/dsh-subagent'
@@ -80,10 +64,8 @@
toolName: subagent_fork
# Dynamic workflows: the worker-thread engine (ctx.workflows) over the spawn
# subagent backend above, plus the model-facing `workflow` tool. The model
# writes a JavaScript orchestration script (meta + body); the engine runs it
# in its own worker thread and fans agent() calls out as spawn children.
# The worker-thread workflow engine fans a model-written JavaScript script's
# `agent()` calls out through the spawn backend; the adjacent tool exposes it to the model.
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
@@ -91,23 +73,18 @@
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
# The model-facing todo_write tool: whole-list task tracking written to the
# session log (todo/write), surfaced to the ACP client as a `plan` update.
# `todo_write` replaces the logged whole list and surfaces an ACP `plan` update.
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
# The repeat-tool-call guard: advisory reminders (injected context, never a
# block) when the model re-issues the same tool call with identical arguments;
# defaults [3, 5, 8]. Loaded here so the snapshot tier exercises the reminder
# transcript (the repeat-tool-guard scenario) — no other scenario repeats a
# call three times, so it is inert everywhere else.
# Identical repeat calls trigger advisory context, never a block, at the default
# thresholds [3, 5, 8]. Only the repeat-tool-guard snapshot scenario reaches them.
- id: repeat-tool-guard
name: '@deepseek-ai/dsh-repeat-tool-guard'
# Filesystem capability stack: local provider, read-before-write/edit policy
# gate, then the model-facing read/write/edit tools. Relative filesystem paths
# resolve from the server launch cwd; the documented Zed setup launches this
# demo from the harness checkout with `pnpm --dir`.
# Policy loads before the model-facing filesystem tools so writes and edits require
# an observed file. Relative paths use the server launch cwd; the Zed setup launches
# this demo from the harness checkout with `pnpm --dir`.
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
@@ -119,28 +96,19 @@
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
# The Claude Code hook bridge. `configPath` is PROCESS-LEVEL: it is read ONCE at
# load and the relative `./hooks.json` resolves against the ACP server's launch
# cwd, NOT each `session/new.cwd`. So a single `hooks.json` next to where the
# server starts applies to every session; a project-local, per-session hooks.json
# is NOT discovered (per-session config resolution is a TODO — see the bridge
# README). With no file present the parse fails-soft and the bridge registers
# nothing (a silent no-op). Hooks THEMSELVES run in the session cwd (the bridge
# passes it as the workdir); only WHERE the config is read from is process-level.
# stdout is the ACP JSON-RPC channel — the bridge's warnings go through ctx.logger
# (no exporter here), never to stdout.
# `configPath` is read once at load and resolves from the server launch cwd, not
# `session/new.cwd`; one `hooks.json` therefore applies to every session and a
# project-local file is not discovered. Missing config registers nothing. Hook
# commands still run in the session cwd. Warnings use `ctx.logger`, never stdout;
# see packages/hooks/hooks-claude/README.md for the deferred per-session design.
- id: hooks-claude
name: '@deepseek-ai/dsh-hooks-claude'
config:
configPath: ./hooks.json
# The Codex hook bridge, loaded alongside the Claude one. It reads its OWN config
# file (`./codex-hooks.json`, Codex's snake_case five-event dialect) — the two
# bridges cannot share one file, so each owns a distinct path. Same process-level
# read-once semantics and same fails-soft-when-absent contract: a launch cwd with
# no `codex-hooks.json` registers nothing (a silent no-op through ctx.logger, never
# stdout). The example ships both bridges so a scenario can exercise EITHER dialect
# end-to-end by seeding the matching file in its workspace/.
# Codex uses its own `codex-hooks.json` and snake_case five-event dialect; it
# cannot share Claude's file. It has the same process-level, read-once, missing-is-no-op,
# logger-only contract. Shipping both bridges lets a scenario seed and exercise either dialect.
- id: hooks-codex
name: '@deepseek-ai/dsh-hooks-codex'
config:

View File

@@ -26,12 +26,13 @@ import {
* WITHOUT a key, since it only needs the server to boot and answer initialize.
*/
// The dsh-acp-agent bin (the demo:acp entry) and this example's cordis.yml.
// The child runs from a temp cwd, so its bin and config path are absolute.
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
// Resolve tsx absolutely because the subprocess runs outside the repo.
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// Absolute path to the repo-root tsconfig.
// The root tsconfig supplies unbuilt workspace `paths`; making it explicit
// avoids accidental resolution through stale built output.
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
interface Spawned {

View File

@@ -75,12 +75,15 @@ const SCENARIOS: Scenario[] = [
// child runs as a spawn subagent under the worker-thread engine (its session is the
// child fixture), and the tool result carries the script's return value.
{ name: 'workflow-run', hasModelTurn: true, recorded: true, childSessions: 1 },
// Hook matrix — one scenario per hook point × its headline Decision outcome, across BOTH
// bridges (Claude `hooks.json`, Codex `codex-hooks.json`, seeded in workspace/).
// Prompt-submit blocks are authored keylessly: they persist a rejected turn
// and hook events without starting a model step, so their logs still compare.
{ name: 'hook-cc-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false },
{ name: 'hook-codex-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false },
// The mid-turn seams fire during a real model turn, so each is recorded with its hook active
// (the model's reaction to a deny/block/force-continue is part of the captured transcript).
// SessionStart/SubagentStart are excluded because detached injection races log
// order; SubagentStop writes no transcript, so a golden could not prove it ran.
// Unit tests cover those points; the hook-snapshot-matrix RFC owns the rationale.
{ name: 'hook-cc-promptsubmit-context', hasModelTurn: true, recorded: true },
{ name: 'hook-cc-pretool-deny', hasModelTurn: true, recorded: true },
{ name: 'hook-cc-pretool-ask', hasModelTurn: true, recorded: true },
@@ -97,7 +100,7 @@ const SCENARIOS: Scenario[] = [
{ name: 'hook-codex-stop-continue', hasModelTurn: true, recorded: true },
// Code Mode: the registry in `mode: code` — the wire tool list collapses to [run_code], the
// tools:sdk section rides in the prompt, and the program's tool calls land as
// tool/code-dispatch events.
// tool/code-dispatch events. Each overlay composes and pins its own header class.
{ name: 'code-mode-turn', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'code', configPath: CODE_MODE_CONFIG },
{ name: 'both-mode-turn', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'both', configPath: BOTH_MODE_CONFIG },
]

View File

@@ -17,8 +17,10 @@ import {
} from '@agentclientprotocol/sdk'
/**
* With-key e2e: the Claude Code hook bridge running against the real acp-agent subprocess and
* the real model.
* With-key e2e for the Claude hook bridge. The process-level `./hooks.json` is
* resolved from a temporary launch cwd and blocks all PreToolUse calls; a real
* model is asked to write there, and absence of the file proves interception.
* The test owns and disposes the ACP subprocess.
*/
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
@@ -76,7 +78,8 @@ afterEach(async () => {
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: a PreToolUse hook blocks bash (real model)', () => {
it('denies every bash command, so the requested file is never written (verified on disk)', async () => {
workdir = await mkdtemp(join(tmpdir(), 'acp-hooks-e2e-'))
// A PreToolUse hook that blocks every tool (exit 2, no matcher = match-all).
// `configPath` is process-relative, so placing the match-all hook in the
// launch cwd selects it; hook commands themselves run in the session cwd.
await writeFile(join(workdir, 'hooks.json'), JSON.stringify({
hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'echo "bash blocked by policy" >&2; exit 2' }] }] },
}))

View File

@@ -1,13 +1,8 @@
# Code Mode overlay: the live coding-agent tree (./cordis.yml) with two
# load-time patches — the app entry's config gains `tools: { mode: code }`
# (the registry offers exactly one wire tool, run_code, plus the generated
# TypeScript SDK prompt section declaring bash/read/write/edit/subagent/
# todo_write) and the worker-thread code runtime joins the tree as
# `ctx.codeRuntime`. The dsh-stdio-agent bin boots this file for
# `pnpm run demo:code-mode` (the acp-agent example carries the same-shaped
# overlay for the `acp` UI). A config patch REPLACES the entry's whole
# config, so the base entry's fields are restated verbatim; only `tools`,
# the welcome, and the persona's second paragraph are Code Mode deltas.
# Code Mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
# `run_code`, plus a generated SDK for bash/read/write/edit/subagent/todo_write.
# `demo:code-mode` selects this overlay; the ACP example has the same UI-specific
# shape. A config patch replaces the whole app config, so unchanged base fields
# are restated; only `tools`, `welcome`, and the persona's second paragraph differ.
- id: base
name: '@cordisjs/plugin-include'
config:

View File

@@ -1,15 +1,8 @@
# The coding-agent plugin tree: the REPL agent demo. The two swappable
# backends — the DeepSeek adapter and the local bash executor — plus `hmr` for
# the dev/demo reload loop, then the stdio chat app (@deepseek-ai/dsh-stdio-
# agent), which bundles the whole agent-core spine (timer, llm, sessions,
# system-prompt, tools, agents, invariants, tool-bash, agent-loop), the console
# logger, JSONL persistence, the readline UI, and a pre-created `main` agent.
#
# `hmr` is a leaf entry (not baked into dsh-stdio-agent): it is a Loader-only
# dev plugin that needs `--expose-internals` — the `demo:repl` script passes
# it. Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the
# environment — the dsh-stdio-agent bin loads the gitignored repo-root .env
# first. cordis.yml reads them via the `!!js` tag.
# REPL agent with swappable DeepSeek and local-bash backends. `dsh-stdio-agent`
# supplies the agent-core spine, logging, JSONL persistence, readline UI, and `main` agent.
# HMR remains a leaf because it requires Loader internals; `demo:repl` passes
# `--expose-internals`. The app bin loads the gitignored root `.env`; this file
# reads `DEEPSEEK_API_KEY` and optional `DEEPSEEK_BASE_URL` through `!!js`.
# Hot-module reload for the dev/demo loop (needs `node --expose-internals`).
- id: hmr
@@ -28,15 +21,13 @@
- deepseek-v4-pro
- deepseek-v4-flash
# Local bash executor for agent-core's tool-bash schema (one of several tool
# stacks in this tree: filesystem, subagent, and todo_write load below).
# Local executor for the app bundle's bash tool.
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
# The stdio chat app: the whole spine + front-door cluster, configured for a
# REPL agent demo driving a pre-created `main` agent.
# The app bundle pre-creates the REPL's `main` agent.
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:
@@ -46,20 +37,16 @@
resumeSessionId: !!js process.env.RESUME_SESSION_ID
persistenceRoot: './.sessions'
welcome: 'agent REPL ready. Give it a coding task.'
# The persona: identity + behavior only, nothing about transports or
# tooling — tool guidance lives with each tool plugin (descriptions +
# prompt sections). {{model}} is the prompt variable the agent loop
# resolves from this agent's configured model.
# Keep the persona to identity and behavior; tool plugins own tool guidance.
# The loop resolves {{model}} from this agent's configuration.
persona: |
You are coding-agent, a coding assistant powered by the {{model}} model.
Verify your work by running the code or tests. Keep answers brief and
factual.
# Automatic context compaction: when the derived history approaches the model's
# context window, summarize an older range into a checkpoint so a long-running
# or tool-heavy session keeps fitting. A leaf entry (needs ctx.llm + the
# agent-loop's `agent/pre-step` seam from the app above).
# Summarize an older range when derived history approaches the context window.
# This leaf consumes `ctx.llm` and the app's `agent/pre-step` seam.
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
config:
@@ -70,13 +57,9 @@
maxTokens: 8192
compactionRetries: 1
# The subagent seam + BOTH in-process backends + two model-facing tools, as leaf
# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
# child) and fork (a child seeded with the parent's completed-turn prefix) are
# independent backends over the shared dsh-subagent-inprocess driver. Exposing
# both transports is pure config: load each backend, then load dsh-tool-subagent
# once per backend with a distinct toolName (the tool registry rejects a
# duplicate name) — no code change.
# Expose fresh-child `spawn` and completed-prefix `fork` through independent
# in-process backends. Each tool instance needs a distinct `toolName`; the registry
# rejects duplicates. These leaves follow the app because it provides `ctx.agents` and `ctx.tools`.
- id: subagent
name: '@deepseek-ai/dsh-subagent'
@@ -103,10 +86,8 @@
toolName: subagent_fork
# Dynamic workflows: the worker-thread engine (ctx.workflows) over the spawn
# subagent backend above, plus the model-facing `workflow` tool. The model
# writes a JavaScript orchestration script (meta + body); the engine runs it
# in its own worker thread and fans agent() calls out as spawn children.
# The worker-thread workflow engine fans a model-written JavaScript script's
# `agent()` calls out through the spawn backend; the adjacent tool exposes it to the model.
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
@@ -114,14 +95,12 @@
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
# The model-facing todo_write tool: whole-list task tracking written to the
# session log (todo/write), rendered as a stdio checklist / ACP plan.
# `todo_write` replaces the logged whole list and renders as a stdio checklist or ACP plan.
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
# Filesystem capability stack: local provider, read-before-write/edit policy
# gate, then the model-facing read/write/edit tools. stdio-agent is a single
# session, so relative paths resolve from the process cwd (the workspace).
# Policy loads before the model-facing filesystem tools so writes and edits require
# an observed file. This single-session app resolves relative paths from the process cwd.
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:

View File

@@ -10,7 +10,8 @@ import { afterEach, describe, expect, it } from 'vitest'
* `@deepseek-ai/dsh-stdio-agent` bin against `code-mode.cordis.yml` (the cordis Loader,
* `unwrapExports`, the include patches over ./cordis.yml, the worker-thread code runtime, and
* the registry in `mode: code`), then close stdin with no prompt and assert the Code Mode
* banner + a clean exit.
* banner + a clean exit. A dummy key satisfies adapter boot, but no prompt means
* no model call; the with-key proof lives in `code-mode.e2e.ts`.
*/
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
@@ -20,7 +21,8 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
// the repo, so point it at the repo tsconfig.
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// The real-API workflow runs up to 14 e2e files at once.
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
// 30s still detects a wedged child.
const PROCESS_TIMEOUT_MS = 30_000
// Leave enough room for the process-owned timeout to report captured output
// before Vitest aborts the test itself.

View File

@@ -16,8 +16,9 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { WorkerCodeRuntime } from '@deepseek-ai/dsh-code-runtime-worker'
/**
* With-key Code Mode proof: a real model composes tool calls, writes a file, and
* returns curated output while the log records `run_code` and its sub-dispatches.
* With-key Code Mode proof: a real model receives only `run_code`, composes two
* sub-calls, writes a file, and returns curated output while the log records
* each `tool/code-dispatch`. The keyless Loader smoke is in the sibling test.
*/
const PERSONA = 'You are coding-agent. You work by writing TypeScript programs for run_code: '

View File

@@ -6,8 +6,12 @@ import type { Context } from 'cordis'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.ts'
/** Key-gated smoke for mid-session compaction and continued agent progress. */
// FIXME(compaction-snapshot): replay cannot serve the unlogged summarization model call.
/**
* Key-gated smoke for mid-session compaction. It verifies the compact event
* pair, replacement of older surface nodes, and a final answer after compaction.
*/
// FIXME(compaction-snapshot): this is the only full compaction coverage because
// replay cannot serve the summarizer's unlogged model call.
let workdir: string | undefined
let ctx: Context | undefined

View File

@@ -10,10 +10,11 @@ import { afterEach, describe, expect, it } from 'vitest'
* `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the cordis Loader,
* `unwrapExports`, the full plugin tree incl. the `@deepseek-ai/dsh-agent-core` bundle and the
* app's in-package readline UI module), then close stdin with no prompt and assert the ready
* banner + a clean exit.
* banner + a clean exit. A dummy key satisfies adapter boot, but no prompt means
* no network call; with-key suites own the product behavior.
*/
// The dsh-stdio-agent bin (the demo:repl entry) and this example's cordis.yml.
// The temp-cwd child needs absolute bin and config paths.
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
@@ -21,7 +22,8 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
// the repo, so point it at the repo tsconfig (root is four levels up).
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// The real-API workflow runs up to 14 e2e files at once.
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
// 30s still detects a wedged child.
const PROCESS_TIMEOUT_MS = 30_000
// Leave enough room for the process-owned timeout to report captured output
// before Vitest aborts the test itself.

View File

@@ -30,4 +30,4 @@ Ask for `cordis_inspect` with `what: "api"` or `what: "events"` to see the gener
## End-to-end tests
`tests/keyless-smoke.e2e.ts` boots the real `cordis.yml` through the Loader with a dummy key and asserts the banner + clean EOF exit (the export-shape / real-load-path guard, now across the package-name resolution). `tests/cordis-tools.e2e.ts` is the with-key smoke: a real model mounts a status listener (asserting the tagged console line actually fires — the world, not the agent's claim), builds itself a `reverse_text` tool and uses it, and composes two mounts via provide/inject. The tool logic itself is unit-tested in [`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) under the per-file 100% coverage gate.
`tests/keyless-smoke.e2e.ts` boots the real `cordis.yml` through the Loader with a dummy key and asserts the banner, package-name resolution, and clean EOF exit. `tests/cordis-tools.e2e.ts` is the with-key smoke: a real model mounts a status listener and the test verifies its tagged console line, creates and uses a `reverse_text` tool, and composes two mounts through provide/inject. [`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) carries the unit coverage under the per-file 100% gate.

View File

@@ -1,17 +1,11 @@
# The cordis-agent plugin tree: the SELF-REFERENTIAL harness demo. Same spine
# as coding-agent (DeepSeek V4 + local bash on @deepseek-ai/dsh-stdio-agent),
# plus @deepseek-ai/dsh-tool-cordis, which gives the model three tools over the
# live cordis runtime it is running inside: cordis_inspect (services / plugin
# tree / tools / dynamic mounts / api / events), cordis_mount (evaluate
# model-written code in a vm sandbox and mount the returned plugin under the
# `cordis-dynamic` group), and cordis_unmount (dispose one mount by id).
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) — the
# dsh-stdio-agent bin loads the gitignored repo-root .env first.
#
# Trust stance (docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md):
# the mounted code gets the REAL ctx — the
# vm sandbox only prevents accidental global pollution. Load the toolset as
# deliberately as you would grant a bash tool.
# Self-referential stdio demo: the coding spine plus tools to inspect the live
# service/plugin/tool/mount/API/event state, mount a model-written plugin under
# `cordis-dynamic`, and quiescently unmount it. The app bin loads the gitignored
# root `.env` before reading the required DeepSeek key and optional base URL.
# Trust stance: the vm and context façade limit accidental global/framework
# access but are not a security boundary; mounted code can reach live capabilities
# such as `ctx.bash`. Grant this toolset like bash access. See
# ../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
# Hot-module reload for the dev/demo loop (needs `node --expose-internals`).
- id: hmr
@@ -53,8 +47,7 @@
- id: web-fetch-local
name: '@deepseek-ai/dsh-web-fetch-local'
# The stdio chat app: the whole spine + front-door cluster, configured for the
# self-referential demo driving a pre-created `main` agent.
# The app bundle pre-creates the self-referential demo's `main` agent.
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:

View File

@@ -79,7 +79,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
await waitForIdle(ctx, agent)
// World checks: the tool exists in the registry, was invoked as a real tool call, and its
// RESULT (the self-made execute actually running) is the reversed string.
// RESULT (the self-made execute actually running) is the reversed string. Model prose is only
// self-report and is deliberately not asserted.
expect(ctx.tools.get('reverse_text')).toBeDefined()
const events = [...agent.session.events]
const calls = events.filter(event => event.type === 'tool/call')

View File

@@ -11,10 +11,11 @@ import { afterEach, describe, expect, it } from 'vitest'
* `unwrapExports`, the full plugin tree INCLUDING the `@deepseek-ai/dsh-tool-cordis` package
* resolved by name (whose `inject` would crash a collapsed export shape at load, see
* docs/postmortem/0001) — then close stdin with no prompt and assert the ready banner + a
* clean exit.
* clean exit. A dummy key satisfies adapter boot, but no prompt means no network
* call; `cordis-tools.e2e.ts` owns the with-key product proof.
*/
// The dsh-stdio-agent bin (the demo:cordis entry) and this example's cordis.yml.
// The temp-cwd child needs absolute bin and config paths.
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
@@ -22,7 +23,8 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
// the repo, so point it at the repo tsconfig (root is three levels up).
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// The real-API workflow runs up to 14 e2e files at once.
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
// 30s still detects a wedged child.
const PROCESS_TIMEOUT_MS = 30_000
// Leave enough room for the process-owned timeout to report captured output
// before Vitest aborts the test itself.

View File

@@ -1,9 +1,5 @@
# The echo-agent plugin tree: the stdio chat app with its LLM backend swapped to
# the local `mock-echo` mock and the local `echo` tool added. The clean
# demonstration of "swap the backend, keep the app" — every service the agent
# needs lives in @deepseek-ai/dsh-stdio-agent (which bundles @deepseek-ai/dsh-
# agent-core); this leaf only picks the backends, `hmr`, and the app config.
#
# Stdio agent with the network-free `mock-echo` adapter and example-local `echo`
# tool. The app bundle supplies the spine; this leaf selects backends, HMR, and app config.
# No API key: the `mock-echo` adapter never touches the network.
# Hot-module reload for the dev/demo loop (a leaf entry, not baked into
@@ -13,8 +9,7 @@
config:
root: ['.']
# The mock model (registers the `mock-echo` adapter) and the demo `echo` tool —
# example-local teaching plugins, resolved relative to THIS file's directory.
# Example-local model and tool plugins resolve relative to this file.
- id: mock-llm
name: './src/mock-llm.ts'
@@ -27,8 +22,7 @@
- id: bash
name: '@deepseek-ai/dsh-bash-local'
# The stdio chat app: console logger + the agent-core spine (pre-creating the
# `main` agent on the mock model) + JSONL persistence + the readline UI.
# The app pre-creates `main` on the mock model and supplies logging, persistence, and readline UI.
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:

View File

@@ -9,17 +9,19 @@ import { afterEach, describe, expect, it } from 'vitest'
* Keyless Loader-path smoke for examples/echo-agent: boot the real example through the
* `@deepseek-ai/dsh-stdio-agent` bin against this example's `cordis.yml` (the cordis Loader,
* `unwrapExports`, the whole plugin tree), pipe a script of stdin lines, and assert the
* rendered stdout.
* rendered stdout. The mock adapter is network-free, making this the complete
* smoke; inputs cover both the echo-tool round trip and direct-reply branch.
*/
// The dsh-stdio-agent bin (the demo:echo entry) and this example's cordis.yml.
// The temp-cwd child needs absolute bin and config paths.
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// Dev/test run UNBUILT: `@deepseek-ai/dsh-*` imports resolve through the root tsconfig `paths`
// map, which tsx finds by searching UP from cwd.
// The temp cwd is outside the repo, so point tsx at the root config that resolves
// unbuilt workspace packages through `paths`.
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// The real-API workflow runs up to 14 e2e files at once.
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
// 30s still detects a wedged child.
const PROCESS_TIMEOUT_MS = 30_000
// Leave enough room for the process-owned timeout to report captured output
// before Vitest aborts the test itself.

View File

@@ -1,6 +1,6 @@
# sandbox-acp-agent
The coding agent with its bash executor swapped for the sandbox stack ([`@deepseek-ai/dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) + [`@deepseek-ai/dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) — the one-entry executor swap the `ctx.bash` capability seam exists for), served over the **Agent Client Protocol**, plus [`@deepseek-ai/dsh-user-approval`](../../packages/ui/user-approval/) — which makes this the first composition where the approval loop is LIVE end to end: bash runs under `read-only`, a denial comes back as the structured marker, the model retries once with `sandbox_permissions` + `justification`, the ACP bridge's answerer turns that ask into a `session/request_permission` prompt in your editor, and "Allow once" runs exactly that command under the wider mode ([sandbox RFC § Escalation](../../docs/rfc/implemented/feature/2026-07-06-sandbox.md)).
An ACP coding agent composed with [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/), [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/), and [`dsh-user-approval`](../../packages/ui/user-approval/). Bash defaults to `read-only`; after a denial, the model may retry once with `sandbox_permissions` and `justification`. The ACP bridge presents that call as `session/request_permission`, and Allow once” grants only that command the wider mode. See the [sandbox escalation contract](../../docs/rfc/implemented/feature/2026-07-06-sandbox.md#escalation-one-approved-wider-retry-after-a-denial).
```sh
pnpm run demo:sandbox-acp # needs DEEPSEEK_API_KEY; drive it from Zed or any ACP client
@@ -13,4 +13,4 @@ Zed setup is the same as [acp-agent](../acp-agent/README.md) with this example's
- **The write boundary is config-fixed**: an escalated `workspace-write` run may write under the launch directory (`workspaceRoot: process.cwd()`) plus the platform temp area — a per-session root is config-phase future work in the [sandbox RFC](../../docs/rfc/implemented/feature/2026-07-06-sandbox.md).
- **No usable runner fails closed per command** (structured `SANDBOX_UNAVAILABLE`), and the filesystem tools stay unloaded for the same reason as `sandbox-agent`: they would bypass the bash sandbox.
`tests/escalation.e2e.ts` boots the real composition keylessly and exercises config-option advertisement, updates, and validation; with a key and usable runner it also world-verifies an allowed escalation. `tests/acp.snapshot.ts` pins config exchange, mode switching, and allowed and rejected approval branches through the shared snapshot kit. Replay executes recorded bash calls on the host runner, so Linux needs bubblewrap or Landlock while macOS uses Seatbelt. Fixtures avoid real denial stderr because that dialect is platform-specific.
`tests/escalation.e2e.ts` boots the real composition keylessly and exercises config-option advertisement, updates, and validation; with a key and usable runner it also world-verifies an allowed escalation. `tests/acp.snapshot.ts` pins config exchange, mode switching, and allowed and rejected approval branches through the [shared snapshot kit](../../packages/support/acp-snapshot/). Replay executes recorded bash calls on the host runner, so Linux needs bubblewrap or Landlock while macOS uses Seatbelt. Fixtures avoid real denial stderr because that dialect is platform-specific.

View File

@@ -1,27 +1,15 @@
# Snapshot-test REPLAY overlay for the sandboxed composition: the SAME app
# tree as cordis.yml, derived from it by an include — the one difference is
# the model backend. A keyless replay run cannot boot the real adapter
# (llm-deepseek's apply() throws without DEEPSEEK_API_KEY), so the include
# patches the live tree at load time: the llm-deepseek entry is disabled by
# id, and the llm-replay entry (which serves a recorded session JSONL — no
# API key, no network) is inserted. Every other entry — the sandbox provider,
# the confined bash executor, the approval seam, the app — IS the live tree.
# The sandbox provider probes for a platform runner per EXECUTION, not at
# boot, so a protocol-only scenario (session config options) replays on hosts
# with no runner at all.
#
# The dsh-acp-agent bin selects this file for DSH_SNAPSHOT=replay (the
# sibling-swap of whatever config path it was handed). The replay fixture
# path comes from $DSH_SNAPSHOT_FILE, set by the snapshot harness. stdout
# stays reserved for the ACP JSON-RPC protocol.
# Keyless replay includes the live sandbox composition, disables the DeepSeek
# adapter, and inserts `llm-replay` to serve `DSH_SNAPSHOT_FILE` without a key or
# network. The sandbox, bash, approval, and app entries remain shared. Because
# runner probing happens per execution, protocol-only scenarios work without a
# platform runner. The app bin selects this sibling for `DSH_SNAPSHOT=replay`;
# stdout remains reserved for ACP JSON-RPC.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
# The name is an assertion, not an override: the include skips the patch
# (warning) when the id points at a different plugin, so this can never
# disable the wrong entry.
# `name` asserts the target; a mismatch skips and warns instead of disabling another entry.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true

View File

@@ -1,16 +1,9 @@
# The sandbox-acp-agent plugin tree: the sandboxed coding agent served over the
# Agent Client Protocol, with the approval seam composed — the first LIVE
# approval composition. A sandbox denial escalated by the model
# (sandbox_permissions + justification) reaches the EDITOR as a
# session/request_permission prompt through the ACP bridge's answerer, and an
# "Allow once" runs exactly that command under the wider mode.
#
# CRITICAL: this tree loads NO stdout logger and NO hmr — stdout is reserved
# for the ACP JSON-RPC protocol (a property of @deepseek-ai/dsh-acp-agent,
# same as examples/acp-agent).
#
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) — the
# dsh-acp-agent bin loads the gitignored repo-root .env first (on STDERR only).
# ACP coding agent with sandbox and approval composed. A retry carrying
# `sandbox_permissions` and `justification` becomes `session/request_permission`;
# “Allow once” grants only that command the wider mode.
# `dsh-acp-agent` loads no stdout logger or HMR because stdout carries ACP JSON-RPC.
# It loads the gitignored root `.env` on stderr before reading `DEEPSEEK_API_KEY`
# and optional `DEEPSEEK_BASE_URL` here.
# The DeepSeek adapter.
- id: llm-deepseek
@@ -21,13 +14,11 @@
models:
- deepseek-v4-flash
# The sandbox stack: the platform-runner provider (bwrap → per-platform
# Landlock launcher → Seatbelt, functionally probed), then the confined bash executor.
# read-only is the fail-safe default; the write boundary for an escalated
# workspace-write run is workspaceRoot + the platform's temp area. NOTE: the
# workspace root is CONFIG-FIXED for the executor's lifetime (the launch dir
# here), while each ACP session has its own cwd — a per-session root is config-
# phase future work in the sandbox RFC.
# The provider probes bwrap, Landlock, then Seatbelt for each run. `read-only`
# is the fail-safe default; `workspace-write` permits `workspaceRoot` and the
# platform temp area. This root is fixed for the executor lifetime even though
# ACP sessions have distinct cwd values; per-session roots remain deferred in
# ../../docs/rfc/implemented/feature/2026-07-06-sandbox.md.
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: bash
@@ -36,16 +27,12 @@
mode: read-only
workspaceRoot: !!js process.cwd()
# The approval seam (ctx.approval — mechanism only, no config): with it
# mounted, the bash tool's escalation gate has a channel, and the ACP bridge
# inside dsh-acp-agent answers for the sessions it owns by prompting the
# editor. Without an editor attached nothing can answer, and every ask fails
# closed.
# `ctx.approval` supplies the escalation channel; the ACP bridge answers for its
# sessions by prompting the editor. Without an attached editor, every ask fails closed.
- id: approval
name: '@deepseek-ai/dsh-user-approval'
# The ACP server app: the agent-core spine + JSONL persistence + the ACP
# bridge (whose approval answerer completes the loop).
# The app bundles the agent spine, JSONL persistence, ACP bridge, and approval answerer.
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
config:

View File

@@ -3,20 +3,25 @@ import { fileURLToPath } from 'node:url'
import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot'
/**
* Snapshot suite for the sandboxed composition (`../cordis.yml`, swapped to the sibling
* `cordis.snapshot.yml` replay overlay by the bin under `DSH_SNAPSHOT=replay`).
* Snapshot suite for the sandboxed composition. Replay swaps only the model;
* bash still runs under the host's Seatbelt/bwrap backend, so fixtures use
* portable `cat`/`printf` commands. Real denial stderr is deliberately absent
* because its wording varies by backend and platform; unit and kernel e2e tests
* own that path, while escalation fixtures start from a user-stated denial.
*/
const SCENARIOS: Scenario[] = [
// Protocol-only (keyless, authored): the session config-option surface this composition adds
// — both advertised selects on session/new, the complete refreshed state every
// session/set_config_option answers with, and both rejection shapes — as committed wire
// bytes.
// bytes. It runs no bash and therefore works without a sandbox runner.
{ name: 'config-options', hasModelTurn: false, recorded: false },
// The runtime mode-switching arc, and NECESSARILY the pinned-header scenario: an
// approval-policy switch rewrites its prompt section, and the resulting request/header-delta
// is legal only in the pinning scenario (the factory's uniformity guard).
// is legal only in the pinning scenario. The pin includes that delta and
// notice; the sandbox switch stays prompt-silent and is proven by a confined write.
{ name: 'mode-switching', hasModelTurn: true, recorded: true, pinsHeader: true, expectedHeaderDeltas: 1 },
// Pin both approval branches under the default read-only/ask policy.
// Under default read-only/ask, approval executes a confined retry; rejection
// executes nothing and returns deterministic text.
{ name: 'escalation-approved', hasModelTurn: true, recorded: true },
{ name: 'escalation-rejected', hasModelTurn: true, recorded: true },
]

View File

@@ -17,7 +17,10 @@ import {
} from '@agentclientprotocol/sdk'
/**
* examples/sandbox-acp-agent end to end.
* Sandbox ACP end to end. The keyless leg boots the real composition through
* initialize and session/new without a model or runner. With a key and runner,
* a scripted client grants a read-only denial's escalation once and the retried
* write must land on disk; unavailable prerequisites self-skip.
*/
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
@@ -27,8 +30,8 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// tsconfig so the unbuilt `paths` map resolves (see examples/AGENTS.md).
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// A usable confining runner, probed the same way the executor suites do: bwrap on Linux,
// Seatbelt's sandbox-exec on macOS.
// Without a usable bwrap/Seatbelt runner, the strict attempt fails closed with
// SANDBOX_UNAVAILABLE instead of producing the denial this flow requires.
const hasBwrap = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {
timeout: 5_000,
stdio: 'ignore',