Repository navigation
feat(computer)!: only agent-env reaches a computer's framework-only routes - #430
Merged
Merged
Conversation
- An app declares the paths it keeps its data in under the session user's home (@linux(paths=...)). With no data hooks, they serve its data plane: data/add writes files under them, data/get returns them as a file tree (declared on its card), data/reset puts back the copy its server keeps before it first serves on a boot, the app as installed. - @add_data, @get_data and @reset_data hooks override paths. The base class serves either around the app's contract: a load and a reset stop the app it launched (its pid kept in the app's folder) and relaunch it after, a load that fails is followed by a reset, and each call runs within data_timeout_s. - An app serves its skill file through skill/v1, unless it writes its skill itself. - computer-app/v1's params carry the app's paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nments - The driver's card lists each app composed onto the computer as a child environment, named for its folder: its own server's card with every path under /svc/<name>/, or its name and route alone while its server isn't answering. A new AgentEnvEnvironment hook, children_environments(), lists them each time the card is served. - The driver forwards every request under /svc/<name>/ to that app's server on its socket, with no timeout of its own; a name it has no app for is a 404. - The driver's own data/get leaves out the files under any path an app keeps its data in, as the apps' cards say. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s apps by name A load sends each environment to the data plane its name reaches: the computer's own to the driver, an app's to the app's route. A computer's snapshot keeps its files beside each app's export, with data/get params given per environment, and leaves out an app that keeps nothing yet. add_skills installs the computer's skill and each app's. Gate 1 refuses apps whose paths overlap and an app named like its computer; gate 3's probe asks each app for its export once; boot records the card read after ready, with every app on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut safely Data calls and launches take one turn at a time, so two loads never overlap and a launch never starts the app mid-load. A sync hook runs in a thread, timed from the start; a call past data_timeout_s fails then, and its reset and relaunch, and the next call, wait for its work to end. Stopping an app ends its whole process group, though its first process exited. An app's paths may not overlap, and a load may not write through a link out of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing for its threads A shielded async hook that never ended held the app's turn for good, so every later data call and launch timed out. A call past data_timeout_s is now cancelled: what awaits stops at once, and a step run in a thread, a sync hook's or the defaults' disk work, is still waited for to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent the app data plane An app's @add_data hook gets its parts as they were sent, inline or as a read grant; agentenv_protocol.computer.part_bytes returns a part's bytes either way, as the paths defaults read them. The protocol README now says how an app's server serves its data plane and skill and how the driver fronts its apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s apps A computer env now checks its env.toml before anything is written (accept_toml), and a computer env's and a computer app env's environment_name default to their folder's there too, so the bundle check knows a computer env's name and its apps' and refuses a seed named after none of them, as it does for an MCP server's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outes agent-env boots each computer with a per-deploy control token and keeps it on the deployment record. With the token set, the SDK refuses a call that doesn't carry it on every route but the MCP endpoint and the card: the data plane, every extension route, tool access, and the driver's app routes. agent-env's own calls send it; env_outcome_verifier runs a task's verify() under a grant of it; a run_code script inside the computer gets it as input["env"]["headers"]; the conformance probe takes it and checks a framework-only route refuses a call without it. An env booted without a token is served as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oting computer Run alone the restart test passes in ~55 s on this branch and on main; after the other computer suites on a loaded machine, its 60 s wait ran out (once on #426's CI, once on an EC2 box). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame the token's size Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erations and the driver's app errors in one shape The data-call logic (turns, stop and relaunch, the reset after a failed load, timeouts) moves out of ComputerAppEnvironment, the class app authors subclass, into app_data.AppDataPlane, which the app's handlers and launch hand their calls to. client.operations(card) replaces each hand-rolled read of a card's capabilities.operations. The driver answers a request for an app it doesn't have, or one whose server doesn't answer, with the protocol's errors (target_unavailable, delivery_failed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, the driver's app errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from
edgararakelyan/fd-4369-m14-app-data-planes-through-the-driver
to
main
November 1, 2026 16:08
… a 401 is unknown Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ol headers A protocol-client call a step makes to a deployed env's address, naming no headers of its own, sends that env's control headers, so plugin steps and verifiers reach a computer's framework-only routes without passing the token. The grant now carries headers, not a token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pass the token, not its variable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… address, as a step's grant DeployedEnv.control_headers_for(url) gives the token only to a URL at or under the env's address, through protocol_v1.granted_headers, the rule request_headers applies to a grant. Snapshot captures and service_state use it instead of checking for a gateway_url override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
@greptileai review |
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.
What
Only agent-env reaches a computer's framework-only routes. Before this, a computer's driver served
computer-exec, the data plane, tool access, recordings and the display setter on the same port as its MCP endpoint, with no auth. An agent with a shell couldcurlcomputer-exec/runand solve a GUI task from the command line, re-enable a tool its task disabled, or end a recording.Now, when agent-env deploys a computer, it boots it with a per-deploy control token, and the computer refuses every framework-only call that doesn't carry it:
The token. It's made per deploy (
secrets.token_urlsafe(32)) and passed to the computer asAGENTENV_CONTROL_TOKEN, beside its settings.DeployedEnv.control_token), so a resumed run or another worker can still reach the computer.What's guarded. Everything the protocol SDK mounts except the MCP endpoint and the card:
POST /agentenv);@extensionroute;/svc/<app>/routes to its apps.A call without the token, or with another, is answered 401
unauthorizedbefore the route runs. An agent reaches MCP and the card exactly as before.Unchanged without a token. An env run by hand, or deployed by an older agent-env, is served exactly as before.
agent-env's own calls carry it:
DeployedEnv.invoke;inside);Every step gets it without asking.
Task.runruns each step under a grant of each deployed env's control headers (protocol_v1.authorized). A protocol-client call a step makes to a deployed env's address, naming no headers of its own, carries that env's token, and no other address gets it. So a verifier like the shipped desktop example's reachescomputer-execas written, and so does a plugin step. The grant ends with the step.Scripts inside the computer. A
run_codescript that runs inside the computer gets the token asinput["env"]["headers"]. A script that runs in an agent's sandbox never gets it.The conformance probe sends the token it's given (
run_probe(..., token=), or--token, defaulting to$AGENTENV_CONTROL_TOKEN). Given one, it also checks that a framework-only route refuses a call without it and with a wrong one.Issue: FD-4612 (M2.8). The plan is on the issue. Follow-up for the gateway and for MCP server and website envs: FD-4913.
Breaking: a script running inside a computer that calls the driver's framework-only routes now has to send
input["env"]["headers"]. Reading the card, and the MCP endpoint, need nothing.Builds on #426 (M1.4, merged), whose
/svc/<app>/routes and app servers this guards.How
CONTROL_TOKEN_ENVlives intypes.py.control_guard()inagent_env_environment.pywraps a handler: with the variable set, it compares theAuthorizationheader toBearer <token>withhmac.compare_digest, and otherwise answers 401 with the protocol's error body.add_routes_to_appwraps the data plane, each extension route and the tool-access routes with it; the card and MCP aren't wrapped./svc/{name}/{path}route the same way.serve_app) drops the variable before it mounts. It serves on a Unix socket only the computer reaches, and the menu launcher and the driver's readiness checks call it there without one.reset_data,add_data,get_data,invoke_extensionandget_interface_manifesttakeheaders.control_headers(token)builds them.authorized({url: headers})is a context-variable grant: a call that names no headers sends those of the granted address it's at or under.request_headers(url, headers)decides which, throughgranted_headers(url, grants), the one address rule. The grant carries headers, not a token, so onlycontrol_headersknows the token's form.Drivertakes a token, and falls back to a grant without one, as the recording test's verifier needs.DeployedEnv.control_token(stored, read back by_fields_from),control_headers, andcontrol_headers_for(url), the token only for a URL at or under the env's address. Snapshot captures andservice_stateuse it, so agateway_urloverride never gets the token.Task.runruns each step under the grant. Core still passesdeployed.control_headersexplicitly, since its helpers also run outside a step (deploy,DeployedEnv.invoke, tests that call a step directly).env_computer_providermakes the token at deploy, boots with it, records it, and reboots with the record's.boot(..., token=)hands the computer its token (control_environment) and asksreadywith it;boot.control_token()makes one.verify_env_core_protocolprobes the data plane with the token, and reads a 401 as unknown rather than as support.AGENTS.mdandrun_code's docstring say where the token comes from.Not in this PR
agent-env task run's local output file, like the env's address. It's useless once the computer is torn down.Testing
Unit (on an agent-environments-admin c6i.4xlarge, Ubuntu 24.04, on this PR's head, ed9e0bf): 12,460 passed. The three that failed fail on main on the same box too:
run_docker_container_testneedsunzip, and twotest_mcp_cli_buildertests needhttpxon the system Python. New and extended tests:test_control_token.py:invoke_extensionand the data plane send them.legacy_protocol.service_statereads a child's state with the deployment's token (Greptile).run_codescript inside the computer gets it ininput["env"].Task.run(control_grant_test.py), a step's calls carry each deployed env's own token, an env without one gets nothing, an env an earlier step deployed is granted to the steps after it, a call's own headers win, and the grant ends with the step.data/getand on a trajectory read from the card's address, and not to an override.Integration (local Docker on the same box): on ed9e0bf,
desktop_example_test.py,computer_env_test.pyandcomputer_app_test.pypassed, 13 tests, with 1 skip formodel_endpoint_configured. The desktop example's and the recording task's verifiers run throughagent-env run, so they reach the computer through the step grant. On the first head,base_computer_test.pyandrecording_chaos_test.pypassed too, all but one test (see below). They include the newtest_only_agent_env_reaches_a_deployed_computers_framework_only_routes_while_its_mcp_and_card_stay_open:computer-exec run,computer-ready status,computer-display get,computer-recording end_all,disable-toolanddata/get. The refused command left nothing behind.computer-exec.computer_*tools.The app data-plane test passes with the token through load, snapshot, the in-computer reset of each app over
/svc/<app>/, and snapshot.The one integration failure.
recording_chaos_test::test_a_restarted_driver_forgets_its_recording_and_keeps_the_segments_it_closedrestarts a desktop container started with plaindocker run. That container has no token, so the guard is off there. It then waits 60 s for it to be ready again, and timed out. It also failed once on #426's CI and passed on rerun. Run alone on the same box, it passed 3 of 3 on this branch and 3 of 3 on main, in the same ~55 s. So it's a timing flake, not this change: after the other suites, its 60 s wait ran out. This PR now gives the restarted desktop the 180 s a deploy gives a booting computer (boot.READY_TIMEOUT_S); a desktop that never comes back still fails.Chaos (a second agent-environments-admin box, terminated; about $1 of the $20). Seven scenarios on real computers:
host.docker.internal, as local agents reach envs) and on the host network,computer-execgot 401, while the card and MCPinitializegot 200.supervisorctl restart): the token kept working and none kept being refused. Commands ran after./svc/notesand/svc/clockgave 401 without the token or with a wrong one, and 200 with it. The menu launcher still launches an app over its socket.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge within its documented rollout limits.Summary
Adds per-deploy control tokens to protect a computer's framework-only routes while leaving MCP and the card open.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Framework["agent-env calls"] --> Headers["Deployment's control headers"] Headers --> Guard["control_guard"] Agent["Agent without token"] --> Guard Guard -->|Matching token| Routes["Framework-only routes"] Guard -->|Missing or wrong token| Refused["401 unauthorized"] Agent --> Public["MCP and card"]Reviews (5) · Last reviewed commit: "refactor(env): one rule for where a cont..." · Reviewed by Greptile