Repository navigation
Description
#515 closed the bash half of this issue: a tui stop now raises a per-turn Cancel that reaches kill_group for the running command, waits for the call to settle, and starts none behind it. What is left is the six in-process tools.
An in-process tool call cannot be cancelled
read, write, edit, ls, grep and find run through BuiltinTool::execute on tokio::task::spawn_blocking (crates/sandbx-turn/src/tools.rs), which cannot be cancelled: dropping the JoinHandle leaves the task running. So a stop that lands in one of them waits for it, and its effect stays — a write under way lands, and a read on a stalled filesystem holds the stop for as long as the filesystem takes.
There is no kill to reach for as there is with bash: these tools run inside sandbx's own process, so there is no process group to signal. Cancelling one means the tool itself observing the token between units of work — between files for grep and find, and not at all inside a single write or read.
Constraints any design must keep
- The gate stays ahead of the spawn.
approveis asked between resolving the name andspawn_blocking, andcontext/decision-approval-gate.mdties that position to blocking tasks being uncancellable. A tool that can be cancelled mid-write also has to say what a half-appliedwriteoreditleaves behind; stopping partway is a different hazard from finishing. tokio::process::Commandwas rejected and the reasoning stands (it would rewrite the verified spawn path for a disclosed non-weakness), and it does not apply here anyway: these tools do not spawn.- The bounds No run_turn: every consumer will reassemble the event stream itself, and the blocking boundary has no owner #58 shipped stay:
max_rounds,stream_timeout, and a sequentialanswer_calls.
Where this is disclosed
SECURITY.md — "Only a spawned command can be interrupted.", and the wall-clock bullet's "on a thread that cannot be cancelled". Both stay accurate until a tool can observe the token, and want a look when one does.
Cancel and ExecutionContext::cancelled are already in place for a tool to poll, so the open question is per tool, not mechanism: which of the six has a safe point to stop at, and is a stop worth the partial state it can leave.
Not labelled security
Per #22: a limitation of the harness, not a weakness in the sandbox boundary.
Activity
danczw commentedon Oct 1, 2026
2a is decided:
spawn_blocking. #58 was the Phase 5 caller this issue said to waitfor, and it landed in #86 with the boundary owned by
run_turnand nowhere else.The reasoning, so it does not have to be re-derived:
tokio::process::Commandwould buy cancellation and parallelism, but it would do so byrewriting the spawn path — the machinery behind the process-group SIGKILL, the PID
namespace reaping, and
PR_SET_PDEATHSIG. Both this issue and #22 are explicit thatwhat that buys is not a sandbox-boundary weakness: it is a denial-of-service surface
against the harness, which SECURITY.md already discloses honestly in its ninth
does not claim bullet. Rewriting verified enforcement to fix a disclosed
non-weakness is a net loss, so the restrictive-by-omission option won.
What #58 shipped instead, which narrows this issue rather than closing it:
bound
anthropic.rshands upwardanswer_calls, with a comment naming this issue as the reason it issequential
So a turn's cost is now derivable —
max_rounds * (stream_timeout + calls * tool timeout)— without anything becoming cancellable.Still open here, unchanged: a running tool call cannot be killed from outside, and
tool calls do not run in parallel. The ordering semantics of two tools sharing one
ExecutionContextare also still undefined;run_turnsidesteps the question byrunning calls in order rather than answering it.
danczw commentedon Oct 2, 2026
Concrete consequence found while reviewing
feat/run-turn, recorded here rather than fixed, since it needs the cancellation this issue tracks.answer_callsruns each tool viatokio::task::spawn_blocking(..).await(crates/sandbx-agent/src/turn.rs:226). That is not cancel-safe: dropping therun_turnfuture drops theJoinHandlewhile the blocking task runs to completion. So a turn abandoned mid-tool — a TUI cancel, a losingselect!branch, an outer deadline — still:writeoreditafter the caller stopped waiting, orbashcommand run out its full timeout (90s by default).The transcript that would have named the call is dropped with the future, so the model-facing record of it is gone. The side effect itself is still in the audit trail (
AuditEvent::allowedfromFsGuard::permit,AuditEvent::spawnedfromSandboxedCommand), which is what keeps this an observability gap rather than a silent one.No fix attempted: an outer
tokio::time::timeoutaround thespawn_blockingwould bound when the caller gives up, not when the tool stops, and would leave runaway work parked on the blocking pool — so a time bound here is misleading until tool calls can actually be cancelled.Documented in the meantime, so nothing claims otherwise:
run_turn’s “blocking boundary” section now states it (c7762ae).SECURITY.md’s wall-clock bullet now says tools run on a blocking thread that cannot be cancelled.danczw commentedon Oct 5, 2026
Triage against
44fc1ea(2026-10-05) — still open and unchanged in substance; one line reference in the comment above has drifted.The remaining scope is intact and nothing has fixed it:
run_with_deadlinestill exists (crates/sandbx-core/src/command.rs:194), still pollstry_wait()(:234) and still SIGKILLs the process group (killpg(Pid::from_raw(pid), Signal::SIGKILL),:341).answer_callsstill runs tools one at a time throughtokio::task::spawn_blocking(move || tool.execute(input, &context)).await, and its doc still names this issue as the reason: "Sequential: concurrency would need the ordering semantics of two tools sharing oneExecutionContextsettled first (An in-process tool call cannot be cancelled #26)."run_turn's doc still states the cancel-safety gap (turn.rs:203-208): "spawn_blockingcannot be cancelled: dropping this future drops theJoinHandlewhile the blocking task runs to completion, so a turn abandoned mid-tool still applies thewrite, recorded only in the audit trail (An in-process tool call cannot be cancelled #26)."SECURITY.mdstill discloses it: "…no deadline anywhere cuts it short, because tools run on a blocking thread that cannot be cancelled."TurnLimits::max_rounds(turn.rs:130, default 8), enforced at:245, withTurnError::RoundLimitat:333;stream_timeout(:144, default 300s) applied at:274/:279.Correction to the comment above: the
spawn_blockingcall site iscrates/sandbx-agent/src/turn.rs:364, not:226.answer_callsitself begins at:341. Line 226 now falls insiderun_turn's round loop, near the compaction code. The drift is from94e5001/55b2211/dd5717a(doc compression and test renames), none of which changed behaviour.Also note
context/TOOLS.mdis nowcontext/guide-tools.md. It still commits tospawn_blocking(:126) and carries the honest row| cancellable from outside | **partly** — the turn can be abandoned; the running tool still completes (#26) |(:134).On the relationship to #128: that issue originally listed this one as blocked on a
run_turncaller. That claim has been withdrawn there. Of the three parts left here, parallelism and theExecutionContextordering semantics need no caller at all — they are design questions answerable today — and cancellation needs an interactive caller, which #128's single-shot non-interactive subcommand is explicitly not. So nothing here is waiting on #128.docs(agent): trim the branch's comments to budget
docs(agent): trim the branch's comments to budget
docs(agent): trim the branch's comments to budget
danczw commentedon Oct 6, 2026
The precondition this issue deferred on is now met, and the gate added a constraint the redesign has to keep.
"Decide it at Phase 5 when
sandbx-agentexists to exercise it." It exists, and #128 gave it a caller in the binary. So the async-tool-API question is live rather than speculative — but neither half has been built, and the gate (#106, PR #170) is now a second caller whose correctness depends on the current shape.The constraint.
approveis asked betweenBuiltinTool::from_nameandspawn_blocking, never concurrently with it, because of this issue: dropping aJoinHandleleaves the blocking task running, so a gate raced against the call would answer "denied" about awritethat had already landed.context/decision-approval-gate.mdrecords it. If tool execution moves totokio::process::Commandwith a cancellable future, the refusal stops depending on position — but any intermediate shape that keepsspawn_blockingmust keep the gate ahead of it.The parallelism half gained a reason to stay sequential.
answer_callsruns calls in the order the model asked for them, and its doc cites this issue: concurrency needs the ordering semantics of two tools sharing oneExecutionContextsettled first. A gate decision per call is unaffected by ordering, so the gate does not make this harder — but a parallelanswer_callswould callapprovefor several calls before any runs, which an operator prompt (#165) would have to render as a batch rather than one at a time. Worth deciding with #165 in view.Still not labelled
security: unchanged by the gate, which refuses before a call starts and so needs no cancellation to be effective.[-]Tool calls cannot be cancelled or run in parallel[/-][+]A running tool call cannot be cancelled[/+]danczw commentedon Oct 8, 2026
Triage at
aae4bd0. Narrowed to cancellation; the parallelism half is now #242.This issue keeps its number because eleven of the twelve in-tree citations of #26 are
about cancellation — including
SECURITY.md:323-325and a shipped release note, whichshould not be repointed. The one exception,
crates/sandbx-agent/src/turn/tools.rs:14-15,is about parallelism and wants the new number.
Title and body rewritten to match. What changed in substance since the last triage:
The precondition this issue deferred on — "decide it at Phase 5 when
sandbx-agentexists to exercise it" — is discharged twice over, and the interrupt landed reaching
nothing. #133 shipped the
tuiinterrupt: keys on a detached OS thread, becauseevent::readcannot be cancelled (crates/sandbx-tui/src/input.rs:27-29, thread at:34-39); ctrl-C or Escape, latched (:78,interruptsat:99-105); raw mode takenbefore the reader or ctrl-C would arrive as a signal
(
crates/sandbx-cli/src/agent/tui.rs:154-156). The keystroke then drops the turn's futureand stops there (
tui.rs:176-179). So the shape to validate against now exists, and thegap is no longer "no caller" but "nothing in
run_turnfor the caller to reach".Every line reference in this issue and its comments is stale.
run_with_deadlinecommand.rs:194→:279; thetry_waitpoll:234→:316-325;killpg:341→:423, insidekill_groupat:415.answer_callsand thespawn_blockingsite leftturn.rsentirely — they arecrates/sandbx-agent/src/turn/tools.rs:21and:113, soboth the
:226and the corrected:364in the comments above are wrong now.run_turn'sdoc
turn.rs:203-208→:255-257.max_rounds:130→:191(default 8 at:215),enforced
:245→:297.stream_timeout:144→:201(300s at:217), applied:274/:279→:337-343.guide-tools.md:126→:187-189, and the honest table row:134→:195.And a type correction:
TurnError::RoundLimitdoes not exist. It isTurnStop::RoundLimit(turn.rs:167-170, constructed at:422-424) — a stop carrying thework the turn did, not an error. The 2026-10-05 comment's
turn.rs:333citation is for aname that was never there.
Two things found while verifying, recorded rather than fixed here:
gate.settlednever fires for the in-flight call.tools.rs:121-141is past the point the dropped future stops at, so the operator gets noline at all for the one call still running: not drawn, not on stderr, not in the audit
trail. Tracked on A screen-owning TUI displaces the stderr account and nothing tracks it #224, whose fix is an account rather than a cancellation.
is then interrupted is released to work nobody can see or stop. For an unprivileged tool
that is a wasted round; for a credentialed one it is a secret in flight past the point
the operator believed they had stopped it. Noted on A built-in tool could perform a credentialed operation without the child holding the secret #186 as a constraint on that design.
Confirmed unchanged: the mechanism is still there, the bounds #58 shipped are all still
present, and nothing has made a running call killable.
6 remaining items
test(helper): a cancel kills a descendant that left the process group
docs: say what a cancel leaves out
docs: trim two comments the cancel branch repeated
feat(core): cancel a running command from a token
feat(tools): stop a running bash call from the context's token
feat(turn): kill a running bash call when the turn is stopped
refactor(cli): drop the unseen-call account a stop no longer owes
docs: a stop kills a running bash call
test(helper): a cancel kills a descendant that left the process group
docs: say what a cancel leaves out
docs: trim two comments the cancel branch repeated
[-]A running tool call cannot be cancelled[/-][+]An in-process tool call cannot be cancelled[/+]