Commit Graph

1281 Commits

Author SHA1 Message Date
Tianyi Cui
b29a8eca71 fix(review): reconcile sandbox and approval contracts 2026-07-11 21:37:38 +08:00
kingwl
6a13dcb364 test(workflow): give the wedged-child waitFors loaded-runner headroom
The three wedged-child scenarios busy-spin their worker for 1.5s while the
host waits for the start RPC; under a loaded 2-core CI runner (this branch
adds several parallel suites) the RPC lands after vi.waitFor default 1s,
failing the coverage lane three runs in a row at the same three sites. An
explicit 10s waitFor timeout (well inside each test own 15s budget) makes
the assertions load-tolerant without weakening them. Belongs upstream with
dsh-workflow-workerthread; carried here because it gates this PR.
2026-07-10 15:44:38 +08:00
kingwl
ca39fd89b0 feat(example): sandbox-acp-agent — the live composition; RFCs to implemented
The three-entry cordis.yml (dsh-sandbox-local + dsh-bash-sandbox at a
read-only default + dsh-approval) served over ACP: the first live approval
composition. Recorded snapshot scenarios pin the wire end to end —
config-options advertisement, the mode-switching arc as the suite pinned
header (both switches, the prompt-section delta, one changed-by-the-user
notice per knob, a confined write landing under the switched mode), and
both escalation branches over scripted permissionAnswers (a grant runs
confined under workspace-write; a rejection executes nothing and pins the
fail-closed text). The with-key escalation e2e drives a real model +
real runner + the real bridge answerer, world-verified; ci.yml snapshot
lane and e2e.yml install bubblewrap so the confined replays actually
execute.

Both RFCs move to implemented/ (Decision/Consequences form, deferred
phases tracked in their own sections), with every cross-reference flipped.
2026-07-10 15:44:38 +08:00
kingwl
3f663c9154 feat(modes): per-session sandbox/approval switching — the session log as the store, ACP config options
effective(session) = findLast(the session own knob events)?.value ?? the
composition-config default. One log-only event per knob, owned by its
domain (bash/sandbox-mode in dsh-bash, approval/policy in dsh-approval),
each exporting the same three-piece kit: the event declaration, a pure
fold, and THE write path — a switch IS its event; no owner service, no
facts map. Restart immunity and multi-session isolation fall out of the
log replay by construction.

Execution follows the fold on both sides: the bash tool stamps
escalation grant > session override > executor default, and the approval
seam prepends the never-gate that auto-rejects before any interactive
answerer. Visibility is two layers per knob: a per-agent prompt section
states the effective value on every request (logged through
request/header*, so what-the-model-was-told replays from the log), and an
agent/pre-step narrator injects at most one coalesced delta notice with
positional attribution (user switch vs operator/config drift). The ACP
bridge advertises one capability-gated select per composable knob with
currentValue folded per session, validates set_config_option against the
closed vocabularies, and anchors idle switches at the next turn
prompt-submit under the turn-enclosure contract.
2026-07-10 15:44:38 +08:00
kingwl
0e49615a3d feat(tool-bash): sandbox escalation — one approved wider retry after a denial
The tool gate advertises sandbox_permissions (an enum of exactly the modes
STRICTLY WIDER than the mounted executor default — the schema makes a
non-widening request inexpressible) plus a required justification, exactly
when ctx.bash.sandboxMode reports a confining mode at registration:
composition truth, never a dead lever. An escalating call resolves
ctx.approval BEFORE anything executes with the audit-self-contained reason
"escalate sandbox to <mode>: <justification>"; allowed-once stamps the
granted mode onto that one bash request (the seam-level per-call override),
while rejected / cancelled / unavailable and the no-service / no-agent
paths each fail closed with their own error text and execute nothing. The
description teaches the flow only when the fields exist: retry the SAME
command once after a real denial, never preemptively; a rejected
escalation is final. No new session events: the attempt is an ordinary
tool/call, the decision is the approval audit pair, the outcome an
ordinary tool/result whose facts name the mode it ran under.
2026-07-10 15:43:28 +08:00
kingwl
2eed448acf feat(bash): the sandboxed executor — per-call policy carrier, denial facts, runner-failure classification
dsh-bash grows the per-call policy carrier: BashExecRequest.sandboxMode
(request-optional, spec required-but-nullable — the owner pattern; resolve()
is the one explicit defaulting step) and the BashExecutor.sandboxMode
capability fact (undefined in the base class — composition truth the tool
layer can read). dsh-bash-local carries the field verbatim and confines
nothing.

dsh-bash-sandbox extends LocalBashExecutor and hands ctx.sandbox the exact
argv it is about to spawn. A denial is a RESULT FACT (the command RAN;
result.sandbox.denied is orthogonal to exitCode/signal), classified
conservatively against the wrap own dialect; a RUNNER failure outranks
denial — foreground re-throws the structured SANDBOX_UNAVAILABLE, a settled
background task stamps sandbox.runnerFailed — so a broken sandbox never
reads as a failing command and the command never runs unconfined.
dsh-tool-bash renders the markers and teaches the model not to retry around
a policy denial; escalation and per-session switching are staged follow-ups.
2026-07-10 15:43:02 +08:00
kingwl
7b8c3a9b40 feat(sandbox): the confinement seam and the per-platform native runner chains
ctx.sandbox (dsh-sandbox): confine(argv, policy) returns the argv to spawn
instead — wrapped so the process and its children run confined — plus the
enforcement completeness and the backend denial/runner-failure dialects;
no usable backend throws the fail-closed SANDBOX_UNAVAILABLE. Policy rides
per call. dsh-sandbox-local selects by platform and caches the verdict:
multi-candidate chains probe FUNCTIONALLY in preference order (Linux:
bwrap → the registry-installed node-addon-landlock-run launcher), a sole
candidate is selected unprobed (darwin: sandbox-exec/Seatbelt) and fails
closed at execution via runnerFailureSignatures; win32 is a reserved empty
chain. Profile parity is honest per backend (documented temp-area and ABI
differences; enforcement full|partial is a structured result fact).

CI: the sandbox-e2e matrix proves real-kernel confinement per rung (bwrap,
Landlock per architecture through the registry-installed launcher,
Seatbelt), failing on a silent all-skip; the packed-install rehearsal
installs the launcher family from the registry and asserts the binary
executable apart from kernel enforcement.
2026-07-10 15:43:02 +08:00
kingwl
80d8726601 feat(acp): the bridge approval answerer + scripted permission answers
The ACP bridge registers the first real approval answerer: an ask for an
agent it owns becomes session/request_permission attached to the already-
streamed tool call (one-shot allow_once/reject_once only), outcomes map
conservatively (unknown optionId never grants, client cancel → cancelled),
and foreign or call-less requests delegate down the waterfall. The snapshot
harness accepts scripted permissionAnswers (FIFO; an unscripted prompt
answers cancelled, fail closed) so recorded scenarios can drive the wire
keylessly.
2026-07-10 15:43:02 +08:00
kingwl
ef35007d75 feat(approval): the approval seam — one-shot permission decisions over a waterfall of answerers
ctx.approval (dsh-approval): request() dispatches the approval/request
waterfall and always resolves a closed outcome — allowed-once / rejected /
cancelled / unavailable — never rejects; zero listeners fall through to
fail-closed unavailable; abort settles cancelled and discards late answers;
throwing or rogue answerers are contained as unavailable; every ask lands
the log-only approval/asked / approval/decided audit pair. dsh-tools routes
a pre-execute ask through the seam opportunistically (ctx.get) with three
distinct deny reasons, keeping the historical ask→deny degrade when the
seam is absent.

The per-session policy tier, the ACP bridge answerer, and the sandbox
escalation asker are staged follow-ups of the approval-seam RFC.
2026-07-10 15:43:02 +08:00
kingwl
7afdc6e3b9 docs(rfc): escalation targets are a closed static vocabulary; strict widening moves to an execution-time check (review blocker)
The default-relative ladder conflicts with per-session overrides: schemas
are registry-global while the effective mode is switchable, so a session
overridden NARROWER than the default loses exactly the lever it needs — a
workspace-write default with a read-only override could only request
danger-full-access (forced over-escalation), and a danger-full-access
default with a read-only override advertised no fields at all (confined,
no escalation path). The enum now pins the closed target vocabulary
(workspace-write / danger-full-access) whenever the executor confines;
strict widening is enforced per call against the session effective mode
(override ?? default), failing closed without prompting anyone. The
default-relative ladder and per-session dynamic schemas move to
Alternatives; the harmless Risks claim is corrected to name the runtime
check as the safety boundary.
2026-07-10 02:34:11 +08:00
kingwl
d3a833fc4a docs(rfc): speak of the staged example and recordings at plan-time tense (review)
The proposed forms claimed examples/sandbox-acp-agent, its recorded
scenarios, and the approval servicing of ask as already existing; a reader
of this docs-only change would look for a composition and goldens that are
not in the tree. Usage walkthroughs now name the arc the staged scenarios
are to record, both Testing sections open with the plan-time banner, and
the grounding bullet states todays degrade honestly.
2026-07-10 02:34:11 +08:00
kingwl
3191cb9404 docs(rfc): propose the approval seam and the subprocess sandbox 2026-07-10 02:33:22 +08:00
Tianyi Cui
a5c972b7a4 Merge pull request #233 from deepseek-harness/workflow-vm-to-workerthread
workflow: script-driven multi-agent orchestration (workflow seam + node:worker_threads engine + tool)
2026-07-10 00:41:48 +08:00
imccyu
3d141eea57 Merge remote-tracking branch 'origin/master' into workflow-vm-to-workerthread 2026-07-10 00:35:16 +08:00
Tianyi Cui
0cfb657f98 Merge pull request #234 from deepseek-harness/turtle/docs-human
docs: a human touch
2026-07-10 00:31:54 +08:00
imccyu
a53be53d64 workflow: forward TSX_TSCONFIG_PATH through the worker env scrub
The empty-env hardening wiped the one variable the UNBUILT worker's
loader depends on: tsx finds a tsconfig by searching up from the
worker's cwd, and a parent running outside the repo (the ACP snapshot
harness pins the repo tsconfig through TSX_TSCONFIG_PATH exactly
because its child cwd is a temp dir) lost the dsh-* paths map - the
worker then resolved workspace imports to unbuilt lib/ bundles and died
on CI with ERR_MODULE_NOT_FOUND (green locally only because stale built
lib/ masked the wrong resolution).

Forward exactly that variable when the parent carries it - loader
plumbing, not a secret; the built shape stays fully empty - and pin the
whole contract with an escape-based test: the worker env is exactly
{TSX_TSCONFIG_PATH}, the credential canary still never crosses.
2026-07-10 00:23:11 +08:00
Turtle
c60eb87860 make wordcount budget guidance clearer and dedup docs 2026-07-10 00:20:35 +08:00
imccyu
aff657cc28 fix: ci run without build lib for snapshot 2026-07-10 00:16:29 +08:00
Turtle
f77174f13a make it clearer 2026-07-10 00:13:06 +08:00
imccyu
d12cb45838 workflow: spawn the worker with an empty environment
The documented vm escape reaches process, and the worker inherited the
harness's env - so a buggy or prompt-injected script could read and
exfiltrate ambient credentials (DEEPSEEK_API_KEY et al.) without
touching a single file (ds-review-bot finding on #233).

Spawn with env: {} and a hermetic execArgv on both runtime shapes, the
same stance as dsh-code-runtime-worker and stronger than the scrubbed
env the defensive-patterns rule requires for spawned commands (a shell
needs PATH; this worker needs nothing). Ambient-channel hardening only:
an escapee keeps the process-wide privileges the trust premise already
admits - the genuine sandbox remains an engine swap.
2026-07-09 23:57:43 +08:00
Turtle
bf1ab14b78 a human touch
get some headroom
2026-07-09 23:48:29 +08:00
Tianyi Cui
58a5088feb Merge remote-tracking branch 'origin/master' into workflow-vm-to-workerthread
# Conflicts:
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
2026-07-09 23:48:17 +08:00
Tianyi Cui
a205dd9a46 Merge pull request #211 from deepseek-harness/code-mode-tools
feat: Code Mode — the model drives the tool registry by writing TypeScript
2026-07-09 23:24:12 +08:00
imccyu
7d3b16c184 docs: update budget / catalog 2026-07-09 23:10:28 +08:00
imccyu
64d0703c40 workflow: host-guarantee the agent-start/agent-end pairing on every stop path
agent-end was worker-authored only, so a start already forwarded to
observers lost its paired end whenever the worker could no longer speak
- the grace force-settle terminating a wedged script, or an unexpected
worker death - stranding progress consumers with agents that never
finish (ds-review-bot finding on #233).

The host now keeps a ledger of forwarded starts and funnels every
agent-end through one gate: worker-reported ends pair (and clear) their
entry, and both termination paths drain the remainder as synthesized
'cancelled' ends BEFORE the run settles, so ends always precede
workflow/end. A real settlement racing the force-settle loses to the
synthesized cancellation - the same first-wins override onResult applies
to the run's own result.
2026-07-09 23:05:43 +08:00
Tianyi Cui
2da28e89eb Merge remote-tracking branch 'origin/master' into code-mode-tools
# Conflicts:
#	examples/AGENTS.md
#	examples/README.md
#	package.json
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
2026-07-09 22:58:58 +08:00
imccyu
053a2b9403 Merge remote-tracking branch 'origin/master' into workflow-vm-to-workerthread 2026-07-09 22:54:19 +08:00
Tianyi Cui
d58a55ad59 Merge pull request #201 from deepseek-harness/agent-request-messages
feat(agent): the agent/session-prefix seam — a compose-once, header-logged session prefix
2026-07-09 22:46:06 +08:00
Tianyi Cui
c39fe5cd30 Merge remote-tracking branch 'origin/master' into agent-request-messages
# Conflicts:
#	docs/cordis-catalog/services.md
2026-07-09 22:43:26 +08:00
Tianyi Cui
30bc7f6a1d fix: the run_code program IS the execute-card title (root cause: Zed shows nothing else)
Systematic trace through Zed (crates/agent_ui thread_view.rs +
crates/acp_thread): kind:execute routes a tool call onto the
terminal-card layout, whose header (render_collapsible_command) has NO
disclosure toggle, whose body content renders only when is_open — a
flag only a real terminal entity can ever set — and which suppresses
the Raw Input view outright. Every prior attempt (rawInput, pending
content, completed content) targeted slots that layout structurally
never renders; the one slot it always shows is the TITLE, which said
"Run code". codex-acp confirms the idiom: execute cards are titled
with the command itself.

presentCall now titles the card with the program (rawInput kept as the
canonical input slot); presentResult omits the title — an update
replaces only provided fields, so the program header persists — and
carries the captured output as content. Goldens re-recorded; the unit
test pins title-carries-program on both frames.
2026-07-09 22:41:57 +08:00
imccyu
2c812fb84a Merge remote-tracking branch 'origin/master' into workflow-vm-to-workerthread 2026-07-09 22:36:52 +08:00
Tianyi Cui
b472dbcfb1 Merge pull request #225 from deepseek-harness/recall-renderer-shared
refactor(compact): extract the shared transcript renderer into dsh-compact
2026-07-09 22:36:25 +08:00
Tianyi Cui
d976767fa6 Merge branch 'master' into recall-renderer-shared 2026-07-09 22:34:56 +08:00
Tianyi Cui
868b33b8b1 Merge pull request #220 from deepseek-harness/tool-cordis
feat: add the self-referential cordis toolset (@deepseek-ai/dsh-tool-cordis)
2026-07-09 22:32:43 +08:00
imccyu
fe4da9244f fix(tool-cordis): validate a dynamic tool's execute return shape after the realm round-trip
The sandbox execute wrapper JSON round-tripped the return and blindly cast it
to ToolExecuteReturn. A JSON-valid but wrong-shape return — a bare string,
{ content: 'ok' }, blocks without a type tag — sailed through: the registry
spreads result.content, so { content: 'ok' } became ['o','k'], passed the
session log's isJsonValue gate, and the DeepSeek serializer then flattened it
to '(no output)' — silent corruption of the next model request and every
replay, instead of a contained tool error.

The round-tripped value is now shape-checked against the two ToolExecuteReturn
forms (array of content blocks, or { content: blocks, meta? }); block checks
are structural only (plain object + string type tag) because the ContentBlock
union is merge-extensible. A wrong shape — and the formerly cryptic
forgot-return/bare-string cases — fails that one call with a teaching error
echoing a truncated preview of what was returned and the two valid forms.
New specs pin the object-form pass-through (meta included), six rejection
shapes, and the preview truncation; per-file 100% coverage holds.
2026-07-09 22:12:30 +08:00
Tianyi Cui
f505776eee fix: keep the program on the COMPLETED run_code card (agent review follow-up)
The previous commit put the fenced program only on the pending card —
but an ACP tool_call_update's content REPLACES the card content (Zed
truncates to the new list, crates/acp_thread update_fields), so the
code vanished the moment the run completed and was effectively never
visible. presentResult now re-carries the fenced program before the
captured output via a shared fencedProgram helper; the completed card
body is program + output, rendered by Zed as syntax-highlighted
markdown behind the card disclosure. Goldens re-recorded (filtered
this time: DSH_SNAPSHOT=record vitest -u -t mode-turn); unit test pins
the two-block result content.
2026-07-09 22:04:52 +08:00
imccyu
fda5784e16 Merge remote-tracking branch 'origin/master' into tool-cordis
# Conflicts:
#	docs/capability-seams.md
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
#	scripts/gen-doc-graphs.ts
2026-07-09 22:02:06 +08:00
imccyu
27f9c45e6a docs: update budget 2026-07-09 21:38:49 +08:00
imccyu
7f5b990aa1 Merge remote-tracking branch 'origin/master' into workflow-vm-to-workerthread 2026-07-09 21:34:52 +08:00
Tianyi Cui
387f19c7f6 docs: regenerate the module graph for the ask-user merge 2026-07-09 21:23:45 +08:00
imccyu
6f86f6081b workflow: dispose() host-drives child disposal inside the grace 2026-07-09 21:14:30 +08:00
Yichen Jiang
0df497133c Merge branch 'master' into agent-request-messages 2026-07-09 21:12:31 +08:00
Tianyi Cui
19fc33f172 Merge remote-tracking branch 'origin/master' into code-mode-tools
# Conflicts:
#	docs/config-catalog.md
#	docs/module-graph.md
#	docs/tool-catalog.md
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
#	packages/ui/acp-agent/package.json
#	pnpm-lock.yaml
#	scripts/gen-tool-catalog.ts
2026-07-09 21:10:40 +08:00
Tianyi Cui
2c03b2bc29 feat: surface the run_code program in the ACP tool-call card
The generated program rode only rawInput — the detail/expanded slot
many ACP clients never open — so the code a run executed was invisible
in the UI stream. presentCall now also carries it as a fenced ts block
in the card's content, which the bridge already forwards as tool_call
content. The two code-mode snapshot goldens are re-recorded live and
replay green; the presentation unit test pins the fenced block.
2026-07-09 20:56:44 +08:00
Tianyi Cui
137530954a Merge pull request #108 from deepseek-harness/codex/ask-user-question
Add ask_user_question interaction tool
2026-07-09 20:45:46 +08:00
imccyu
f91bfc1fcf test: re-record the header pin and workflow-run for the meta-parameter schema
The workflow tool's wire schema changed (required meta object
parameter; body-only script). Request-header content is pinned by
exactly ONE scenario (text-turn) and scrubbed to {{system}}/{{tools}}
tokens everywhere else, so the schema change re-records exactly two
fixtures:

- text-turn — the pinned header itself (the one committed copy of the
  tool schemas; every other scenario's live header is asserted equal to
  this pin by the uniformity guard).
- workflow-run — its recorded interaction used the removed call shape
  (meta embedded in the script), which the engine now rejects; the
  authored prompt is updated to dictate the new shape (meta as a
  parameter, body-only script) and the scenario re-recorded to a clean
  single call.

Every other fixture stays byte-identical and replays green against the
new pin. Known pre-existing exception: fs-policy-reject's recording
carries a GNU-only sed -i fallback that fails BSD/macOS replay — kept
out of this change deliberately (the snapshot CI lane is ubuntu).
2026-07-09 20:39:28 +08:00
imccyu
0d0f0204f2 workflow: meta rides the seam as data — the engine never evaluates it
P1 review finding: extractMeta timed only the literal's vm evaluation;
materializing the RESULT then read properties ordinarily on the HOST
stack, so a meta literal smuggling a getter (get name() { while(true){} })
could wedge the host outside any timeout — defeating the exact spin
isolation the worker thread exists for.

Rather than harden the evaluator (descriptor walks, AST validation),
delete the mechanism: the workflow's identity now reaches the seam as a
plain JSON field (WorkflowStartRequest.meta), carried by the tool as a
schema-validated `meta` object parameter the model fills directly. The
engine only shape-validates data (validateMeta, every violation named)
and pre-parses the body; the scanner, the vm evaluation, and the
host-side materialization are gone, and with them the hole. A body
still opening with a Claude Code-style `export const meta` statement
gets a pointed SCRIPT_PARSE message (the likeliest authoring slip; a
CC script's body stays drop-in, only its meta header moves into the
parameter). syncTimeoutMs now governs exactly one thing: the initial
synchronous slice inside the worker.

The RFC's decision section is rewritten in place (implemented-RFC
rule); the embedded-meta format moves to alternatives-considered with
the hole as the reason. Tool description, presentation (title now reads
meta.name directly — the textual sniff is gone), seam vocabulary docs,
and catalogs follow.
2026-07-09 20:09:10 +08:00
imccyu
af9616f47d test: let the wedged-worker regression post its child-start first
The regression's script spun immediately after calling agent(), but the
agent() continuation (which posts the child-start RPC) only runs on a
microtask tick — the spin seized the worker's loop before the post, so
the host never saw a child inside the waitFor window. A few await-null
yields before the spin let the RPC out; the posted message needs no
further worker-loop turns to reach the host, so the wedge still holds
for the Cancel message the test is about.
2026-07-09 19:33:08 +08:00
imccyu
0d11a3afc7 workflow: rename dsh-workflow-vm to dsh-workflow-workerthread
Pure mechanical rename now that the package's internals are the
worker-thread engine: directory, package name, spec/e2e filenames,
module tags and logger prefixes, tsconfig/knip/run-gates/AGENTS.md
references, example cordis.yml plugin ids, doc links; catalogs
regenerated and the lockfile refreshed.
2026-07-09 19:33:08 +08:00
imccyu
d5c65e2b4c docs: describe the workflow engine as worker-thread first
The outer ring catches up with the engine swap (the package's own
README/JSDoc rode the port commit):

- Seam module doc and README name the worker-thread engine as THE
  implementation, with isolated-vm/separate-process sandboxing as the
  deferred hardening; the seam service doc states the holder-owned-runs
  contract (engine-fiber disposal deliberately leaves live runs to
  their holders).
- Seam contract precision: agentsStarted documents the termination-path
  degradation to the host-observed count; the events section scopes the
  agent-start/agent-end pair to calls that STARTED a child run;
  WorkflowRun wording drops the vm-era abandonment language.
- The dynamic-workflows RFC is rewritten in place to the shipped
  mechanism (implemented-RFC rule): why worker threads, the thread's
  concrete buys, the in-process node:vm first cut recorded under
  alternatives considered; the tool section describes the usage policy
  as the tool's own prompt section.
- gen-doc-graphs: six workflow/* DYNAMIC_EVENT_DISPATCHERS entries (the
  catalog no longer claims nothing dispatches them) and the seam-note
  wording; core-data-structures gains its workflow.md index row;
  packages/README + AGENTS.md layout line + example cordis.yml comments
  say worker-thread; catalogs regenerated.
2026-07-09 19:31:56 +08:00