fix(tools): a sandbox's third-party tools are the item's, not the waker's - #708
Merged
Conversation
…he wake
My P1 threaded the turn's bundles into one of the FOUR things that create a
sandbox. The other three carry nothing: the human terminal
(`POST …/exec`), a workflow's deterministic node, and `rebuild_io_handle`.
A sandbox mounts once, at create, and is never repaired — so after a restart
wipes the sessions, whichever entry point happens to wake an item first
decides whether it has third-party tools for that sandbox's whole life. Open
a terminal before your first message and the tools are gone until recycle.
P2 then made that failure louder rather than rarer: the session recorded the
tool-less sandbox as known-empty, so the next turn refused the tools instead
of offering them. `No such file or directory` became "the tool is not there
at all", which is what a redeploy looked like from the outside.
The model was wrong, not just incomplete. What to mount is a property of the
ITEM — its App's declared `external_tools` — and a turn is only special in
that it has already resolved, so its shas are pinned to the schemas the model
was handed. Everyone else should ask the same question rather than answer it
by omission. So the registry gains `tools_for`, used when a caller states
nothing; a caller's `{}` stays an ANSWER, so an app with no third-party tools
pays for no resolve.
The lookup itself is now written once (`resolve_item_tools`) and read by both
callers. Two places answering "which bundles does this item get" is exactly
the rule that drifts, and its way of drifting is a tool that exists in some
sandboxes and not others depending on who opened them.
Resolving is best effort here: an artifact store that is down must not stop
someone opening a terminal. The sandbox then comes up without that tool and
the session records known-empty, so the next turn still says WHY rather than
handing over a launcher that isn't there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVqSrLupJQTJ4Wp1exBKnJ
§15 described registering a tool and changing its version, and never said the one fact both of those depend on: mounting happens inside `create` and never again. Without it, "the bundle is cached on the host" reads as "the tool is available", and every consequence below looks arbitrary — why a newly registered tool does nothing for an item already open, why a release only reaches the next sandbox, why a workspace can hold a tool at a version the resolve no longer returns. New §15.3 separates resolve (per turn, fills the host's cache, mounts nothing) from mount (once, at create), names the four things that create a sandbox, and states that all four must carry the item's tools — with the symptom of getting that wrong, because we shipped it: whichever entry point woke the item first decided whether it had third-party tools at all. §15.4 now also says that a live sandbox keeps serving the older build on purpose, and what that costs. Subsections renumbered from 15.3 down; cross-references followed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVqSrLupJQTJ4Wp1exBKnJ
Renumbering §15's subsections updated every cross-reference inside `deployment.md` and none of the one in `extending-the-platform.md`, which still sent readers to §15.6 for issuing a certificate — now the disk section. The kind of break that stays correct-looking: the number resolves to a real heading, just the wrong one. Also points at the new §15.3 from there, since "when does a tool actually get into a sandbox" is the question that page's third-party section raises and does not answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVqSrLupJQTJ4Wp1exBKnJ
CI caught what my local targeted run did not: three assertions about the certificate runbook sliced `docs/deployment.md` between `### 15.6` and `### 15.7`. Inserting one subsection above moved that window onto the disk section, so they failed reporting that the runbook no longer says the name is ours to choose — true of the text they were reading, and nothing to do with the text they are about. A section number is not part of what a runbook says. The heading is the handle a reader would use, so the tests use it too: `_section(doc, "工具憑證")`. `####` children do not terminate the slice, and the result stays bounded to one section rather than silently widening to the whole file, which would make every one of these assertions pass for the wrong reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KVqSrLupJQTJ4Wp1exBKnJ
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.
Follow-up to #707, which fixed half the problem and made the other half worse.
What #707 got wrong
It threaded the turn's resolved bundles into one of the four things that
create a sandbox. The other three carry nothing:
turn_context)POST /a/{slug}/items/{id}/execregistry.rebuild_io_handle(file op after a reaped sandbox)A sandbox mounts its bundles inside
createand never again. So once arestart clears the in-memory sessions, whichever entry point happens to wake
an item first decides whether it has third-party tools for that sandbox's
entire life. Open the terminal before your first message and they are gone
until the sandbox is recycled.
#707's second commit then made that louder instead of rarer: the session
recorded the tool-less sandbox as known-empty, so the next turn refused the
tools rather than offering them.
No such file or directorybecame "the toolisn't there at all" — which is what a redeploy looked like from outside.
The model was wrong, not just incomplete
What a sandbox mounts is a property of the item — its App's declared
external_tools. A turn is only special in that it has already resolved, soits shas are pinned to the schemas the model was handed. Everyone else should
ask the same question rather than answer it by omission.
InvestigationRegistry.tools_foranswers "what does this item mount", usedwhenever a caller states nothing.
{}stays an answer, not a gap, so an app with nothird-party tools pays for no resolve.
resolve_item_tools) and read by both callers.Two places answering "which bundles does this item get" is precisely the
rule that drifts, and its way of drifting is a tool that exists in some
sandboxes and not others depending on who opened them.
someone opening a terminal. The sandbox then comes up without that tool and
the session records known-empty, so the next turn still says why instead
of handing over a launcher that isn't there.
Docs
New
docs/deployment.md§15.3 states the fact everything else depends on andthat §15 never said: resolve (per turn, fills the host's cache, mounts
nothing) is not mount (once, at create). It names the four creators and
records the symptom of getting this wrong, since we shipped it. §15.4 now also
says a live sandbox keeps serving the older build deliberately, and what that
costs. Subsections renumbered from 15.3 down, cross-references followed.
Verification
Targeted:
test_turn_external_tools(2 new),test_registry,tooling/test_external, plus app-construction smoke tests(
test_api_namespace,test_app_version_header,test_apps_routes,test_turn_queue) — 93 passed.ruff check+format --check,ty check,and
mkdocs build --strictall pass. Full suite left to CI.Deploying
App-side only, as before — no sandbox-host or mcp-runner rebuild.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KVqSrLupJQTJ4Wp1exBKnJ