feat(runtime-host): host sandbox boundary interactions - #1935
Merged
M4n5ter merged 2 commits intoAug 3, 2026
Merged
Conversation
M4n5ter
marked this pull request as ready for review
August 3, 2026 07:32
M4n5ter
force-pushed
the
feat/runtime-host-sandbox-boundary-interactions
branch
from
August 3, 2026 08:41
8ab4668 to
edfcc67
Compare
hqhq1025
added a commit
to hqhq1025/maka-agent
that referenced
this pull request
Aug 3, 2026
Three of the sentences this branch rewrote still handed the model a move
it could not make, and the tests could not tell, because they call
tool.impl with a hand-built context: nothing there knows which agent
receives the message, or whether a state survives the turn.
The shell-slot refusals hedged on the wrong thing. "Stop one of yours
with StopBackgroundTask if you started any" hedges on owning a run; the
gap is owning the tool. Built with the real buildChildAgentTools over a
parent set carrying the shell tools, a child agent receives Read, Glob,
Grep, WebSearch, Write, Edit, Bash and ArchiveRead — Bash yes,
StopBackgroundTask and WriteStdin no, because
buildToolsForAgentDefinition is a strict name allowlist. So the caller
that most often reaches the manager-wide cap is precisely the one that
would go looking for a schema entry it does not have. Both sentences now
describe the move instead of naming a tool, and the PTY one leads with
dropping `pty`, which is a parameter every caller already has. The test
asks the real builder which names a child never receives and asserts the
refusal contains none of them, so the next sentence that reaches for one
turns it red.
goal_changed invited a retry that cannot succeed. registration.
observedControlLease is written once at beginObservedTurn and refreshed
only by a mutation that succeeds, so once the lease has moved the turn
declines the same way until it ends. Driven against the real coordinator
— one turn registers, a second arms a goal and clears it — GoalSet
declines goal_changed on the first call and on the fourth, and no call
available to that turn changes the answer. The GoalSet arm of
goal_already_armed is the same shape: reachable only once the armed goal
has left `active`, and by then the controlLease cannot be released,
because releasing it needs a mutation the moved lease also declines.
Both now end by closing the retry. GoalStatus is still offered, because
reading is still worth doing; it is the last step rather than a step
back to the call that just failed.
The agent_swarm resume refusal prescribed spawning. ToolRuntime's
buildChildAgentContext returns {} wholesale when getCurrentRunId() is
falsy, so spawn and resume leave together, and "drop resume_run_ids and
send that work as new items instead" walks the model into the spawn
refusal, which tells it retrying fails the same way. The advice now asks
whether spawning is available before prescribing it — an embedder can
supply one without the other, so it is a real question — and the test
follows the advice rather than reading it.
Also restores what an operator lost. The rejected background-task ref,
and the three capability names the agent-swarm and subagent refusals
stopped mentioning, travel as the Error cause the way builtin-tools.ts
already carries its worker-protocol violations: out of the transcript,
in every stack. And "This is an internal failure, not a problem with
your arguments" could have been deleted from all five filesystem
messages without turning a test red; now it cannot.
Rebased onto main. The tool-runtime.ts conflict is apache#1935's hosted-run
routing against this branch's message: upstream's !hostedRun guard is
kept, so hosted runs still route to the host instead of throwing, and
only the message changes. The comment there now records that this guard,
unlike the one in sandbox-boundary-tool.ts, does fire in production —
the CLI supplies the sandbox store callbacks only on the tui surface.
The two branches that are genuinely unreachable through ToolRuntime say
so where they are.
hqhq1025
added a commit
to hqhq1025/maka-agent
that referenced
this pull request
Aug 3, 2026
Three of the sentences this branch rewrote still handed the model a move
it could not make, and the tests could not tell, because they call
tool.impl with a hand-built context: nothing there knows which agent
receives the message, or whether a state survives the turn.
The shell-slot refusals hedged on the wrong thing. "Stop one of yours
with StopBackgroundTask if you started any" hedges on owning a run; the
gap is owning the tool. Built with the real buildChildAgentTools over a
parent set carrying the shell tools, a child agent receives Read, Glob,
Grep, WebSearch, Write, Edit, Bash and ArchiveRead — Bash yes,
StopBackgroundTask and WriteStdin no, because
buildToolsForAgentDefinition is a strict name allowlist. So the caller
that most often reaches the manager-wide cap is precisely the one that
would go looking for a schema entry it does not have. Both sentences now
describe the move instead of naming a tool, and the PTY one leads with
dropping `pty`, which is a parameter every caller already has. The test
asks the real builder which names a child never receives and asserts the
refusal contains none of them, so the next sentence that reaches for one
turns it red.
goal_changed invited a retry that cannot succeed. registration.
observedControlLease is written once at beginObservedTurn and refreshed
only by a mutation that succeeds, so once the lease has moved the turn
declines the same way until it ends. Driven against the real coordinator
— one turn registers, a second arms a goal and clears it — GoalSet
declines goal_changed on the first call and on the fourth, and no call
available to that turn changes the answer. The GoalSet arm of
goal_already_armed is the same shape: reachable only once the armed goal
has left `active`, and by then the controlLease cannot be released,
because releasing it needs a mutation the moved lease also declines.
Both now end by closing the retry. GoalStatus is still offered, because
reading is still worth doing; it is the last step rather than a step
back to the call that just failed.
The agent_swarm resume refusal prescribed spawning. ToolRuntime's
buildChildAgentContext returns {} wholesale when getCurrentRunId() is
falsy, so spawn and resume leave together, and "drop resume_run_ids and
send that work as new items instead" walks the model into the spawn
refusal, which tells it retrying fails the same way. The advice now asks
whether spawning is available before prescribing it — an embedder can
supply one without the other, so it is a real question — and the test
follows the advice rather than reading it.
Also restores what an operator lost. The rejected background-task ref,
and the three capability names the agent-swarm and subagent refusals
stopped mentioning, travel as the Error cause the way builtin-tools.ts
already carries its worker-protocol violations: out of the transcript,
in every stack. And "This is an internal failure, not a problem with
your arguments" could have been deleted from all five filesystem
messages without turning a test red; now it cannot.
Rebased onto main. The tool-runtime.ts conflict is apache#1935's hosted-run
routing against this branch's message: upstream's !hostedRun guard is
kept, so hosted runs still route to the host instead of throwing, and
only the message changes. The comment there now records that this guard,
unlike the one in sandbox-boundary-tool.ts, does fire in production —
the CLI supplies the sandbox store callbacks only on the tui surface.
The two branches that are genuinely unreachable through ToolRuntime say
so where they are.
hqhq1025
added a commit
that referenced
this pull request
Aug 3, 2026
…ng (#1885) * fix: tell a refused tool call what to do next, not just what went wrong A refusal that names only its cause is a refusal a model re-sends. That was measured, not assumed: across recorded runs, refusals with no next move came back as identical retries, and refusals that named an internal noun the model has no verb for came back as retries of the same call with different arguments. Every sentence changed here answers "so what do I do now": - `agent_spawn` / `agent_list` / `agent_output` said the capability "is unavailable in this runtime context". A model cannot make a runtime context available. They now say retrying will fail the same way and to do the task with the tools already in hand. - The four Goal refusals said "this turn no longer owns Goal activation". Ownership is a host state machine label. They now name `GoalStatus` as the call that shows what the goal actually is. - `agent_output` locator rejections said "its matching identity fields", leaving the model to guess which of four optional id fields to add. They now name the fields for that locator. - `Edit` and `Write` reported an internal filesystem-identity mismatch through the argument-error path, so it read as a complaint about `old_string`. They now say the file is unchanged, that the arguments are not the problem, and what to do. - A malformed background-task ref got its own text echoed back. It now gets the canonical form. - A full shell or PTY slot did not say what frees one. It names `StopBackgroundTask`. The test walks each refusal and asserts on the sentence a model would read. Three of its cases need `node:sqlite` and are skipped on runtimes without it. * test: catch the existing assertions up with the sentences they read CI caught nine tests pinned to wording this change replaces. Every one of them asserted the half of the old sentence that had to go — the host noun the model could not act on — so they had to move rather than be relaxed. They now pin what the new sentence is for: - the six Goal declines assert `Call GoalStatus`, the call that shows what the goal actually is, instead of `no longer owns Goal activation` - `agent_swarm` asserts it names the tool and what to do instead, rather than `spawnChildSession capability is unavailable` - the two capacity refusals assert they name `StopBackgroundTask`, which is the thing that frees a slot and which the old wording never mentioned Relaxing the patterns instead would have left tests that pass whatever the refusal says, which is how a sentence stops being anybody's contract. * fix: stop the new refusals from asserting and prescribing what they cannot Review found that three of these rewrites traded an opaque sentence for a confident wrong one. A refusal is read by a model, so the cost of a claim it cannot support is a decision made on a false picture. The four Goal declines are the worst of it. They asserted one cause — "the session goal changed while this turn was running" — and prescribed a retry. But the gate returns undefined for five reasons and only one of them is a race. A turn that was never registered under a Goal boundary declines permanently: every turn started with goalBoundary 'none', which is the graph supervisor and both session-execution IPC paths, and every turn whose session was closed and removed. The PR's own test file demonstrates the loop. In "GoalSet after permanent removal" the session is gone, so GoalSet says call GoalStatus and retry "only if there is still no active goal", GoalStatus says "No goal set for this session", the condition holds, the model retries, and it receives the identical refusal. Forever. The old text was opaque but it asked for nothing. So the coordinator now says why. `activationStanding` and `mutationStanding` hold the same predicates the two mutators use — one source of truth, not a second copy that can drift — and each names a cause. GoalStatus is prescribed where reading can change what happens next, and where it cannot the sentence says so and asks the model to report instead. One of the five turned out to be misnamed while I was writing it: the lane is looked up from the map before it is checked for currency, so that branch can only fire when the coordinator is disposed, and it is called that now rather than pretending a later turn took over. `internalFilesystemFailure` asserted disk state it never looked at. That branch fires when the worker returns a well-formed result of the wrong operation; real failures throw. A worker that writes the file and then labels the result 'glob' produced "nothing was written to disk", or for Edit "the file is unchanged … a different old_string will not help", and the model went on from there. It is now two helpers. A write says Maka cannot tell whether the change landed and sends the model to read the file. A read says no result came back — and says what that does not mean, because "Grep could not be completed inside Maka, so no matches were produced" reads as a search that ran and found nothing, and a model that takes it that way concludes the pattern is absent from the repository. Glob had the same shape and is fixed with it. The capacity message promised a move that can be unavailable: the reserved-run counters are manager-wide while StopBackgroundTask takes a session-scoped ref, so with several sessions live the one that hits the cap may own none of the runs holding it. It now offers that move as a condition and names waiting, which always works. The review also flagged the printed number as the cap rather than the reserved count; it is both — the throw fires at `reserved >= max` and reserved is only ever incremented after that check passes, so the two are equal here. Left as is. Not changed, but no longer covered silently: the AskUserQuestion and request_sandbox_boundary guards cannot fire in production. ToolRuntime injects both callbacks unconditionally, unlike listChildAgents and readChildAgentOutput beside them, which are spread only when present. Availability is decided when the tool set is assembled — the CLI adds both only on the TUI surface. The guards stay because the context type declares the callbacks optional and an embedder may build one without them, and the tests now say that is the contract they cover rather than implying a reachable path. Every new assertion was negative-controlled. Collapsing the Goal causes back into one retry message turns fifteen of them red, including the two real- coordinator scenarios that reproduce the loop. * fix: stop the refusals prescribing moves the caller does not have Five of the sentences this branch rewrote still asserted or prescribed something the code cannot back. The filesystem-worker refusals ended "use Bash to do the same work". A local_read child is given Read, Glob and Grep and nothing else, so the caller most likely to be running a bare Grep was handed an instruction it cannot carry out. The shell is now offered on a condition the model can check for itself, and the sentence ends on a move every caller has. The worker-protocol violation still reaches an operator: it travels as the Error cause, out of the model's sight and in every stack. coordinator_disposed could not be produced. dispose() sets the flag and clears the lane map in one step, so the lane lookup above the isCurrent check already failed and the turn was told it "does not run under the session goal boundary" — false about a turn that had armed the goal it was trying to clear. Shutdown is now asked about first, and the carefully worded shutdown sentence can be shown. GoalResume goes through the activation gate, so it could be told it had "already armed a goal" and "cannot arm a second one" in reply to a call that asked to arm nothing. The advice now knows which tool it answers. Nor is "already armed" the right noun: beginObservedTurn binds the lease by itself when a goal is already active, and that turn armed nothing. "Goal authority is unavailable." reads as a passing condition. Its only producer is beginDrain, which is one-way, so it now says the door does not reopen. request_sandbox_boundary was rewritten on a path ToolRuntime cannot reach, because ToolRuntime injects the callback unconditionally; the sentence a model actually gets was left alone. Both now share one constant. The retry branch in agent_swarm and the ref message on the Read({ref}) path, which is where a ref is most likely mistyped, get the same treatment. Tests: the fake coordinator answered both authorization gates with the same reason, so a tool wired to the wrong gate stayed green. The two gates now answer separately, and every decline cause is produced by the real coordinator. * fix: stop the refusals naming a tool the reader may not have Three of the sentences this branch rewrote still handed the model a move it could not make, and the tests could not tell, because they call tool.impl with a hand-built context: nothing there knows which agent receives the message, or whether a state survives the turn. The shell-slot refusals hedged on the wrong thing. "Stop one of yours with StopBackgroundTask if you started any" hedges on owning a run; the gap is owning the tool. Built with the real buildChildAgentTools over a parent set carrying the shell tools, a child agent receives Read, Glob, Grep, WebSearch, Write, Edit, Bash and ArchiveRead — Bash yes, StopBackgroundTask and WriteStdin no, because buildToolsForAgentDefinition is a strict name allowlist. So the caller that most often reaches the manager-wide cap is precisely the one that would go looking for a schema entry it does not have. Both sentences now describe the move instead of naming a tool, and the PTY one leads with dropping `pty`, which is a parameter every caller already has. The test asks the real builder which names a child never receives and asserts the refusal contains none of them, so the next sentence that reaches for one turns it red. goal_changed invited a retry that cannot succeed. registration. observedControlLease is written once at beginObservedTurn and refreshed only by a mutation that succeeds, so once the lease has moved the turn declines the same way until it ends. Driven against the real coordinator — one turn registers, a second arms a goal and clears it — GoalSet declines goal_changed on the first call and on the fourth, and no call available to that turn changes the answer. The GoalSet arm of goal_already_armed is the same shape: reachable only once the armed goal has left `active`, and by then the controlLease cannot be released, because releasing it needs a mutation the moved lease also declines. Both now end by closing the retry. GoalStatus is still offered, because reading is still worth doing; it is the last step rather than a step back to the call that just failed. The agent_swarm resume refusal prescribed spawning. ToolRuntime's buildChildAgentContext returns {} wholesale when getCurrentRunId() is falsy, so spawn and resume leave together, and "drop resume_run_ids and send that work as new items instead" walks the model into the spawn refusal, which tells it retrying fails the same way. The advice now asks whether spawning is available before prescribing it — an embedder can supply one without the other, so it is a real question — and the test follows the advice rather than reading it. Also restores what an operator lost. The rejected background-task ref, and the three capability names the agent-swarm and subagent refusals stopped mentioning, travel as the Error cause the way builtin-tools.ts already carries its worker-protocol violations: out of the transcript, in every stack. And "This is an internal failure, not a problem with your arguments" could have been deleted from all five filesystem messages without turning a test red; now it cannot. Rebased onto main. The tool-runtime.ts conflict is #1935's hosted-run routing against this branch's message: upstream's !hostedRun guard is kept, so hosted runs still route to the host instead of throwing, and only the message changes. The comment there now records that this guard, unlike the one in sandbox-boundary-tool.ts, does fire in production — the CLI supplies the sandbox store callbacks only on the tui surface. The two branches that are genuinely unreachable through ToolRuntime say so where they are.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
English
Summary
SessionStoreas the canonical boundary commit point while the Host owns admission, exact-Run continuation capture, first-winner settlement, and stop/restart closure.Verification
npm run build:testnpm run lintnpm run format:checkPart of #1167.
中文
概要
SessionStore作为 boundary 的唯一 canonical commit point;Host 负责准入、精确 Run continuation 捕获、first-winner 结算,以及停止/重启关闭。验证
npm run build:testnpm run lintnpm run format:check属于 #1167 的一部分。