docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user