Repository navigation
feat(computer): computer envs deploy on local Docker - #369
earakely-scale wants to merge 19 commits into
Conversation
496213f to
4ba6146
Compare
8b42cbb to
f9cda77
Compare
4ba6146 to
d79d4ed
Compare
d79d4ed to
9e775d5
Compare
22348da to
572960c
Compare
…e probe boots at the env's settings, no platform on create_container From the review of #369: - teardown ends the run's open recordings, each with a fresh grant whatever env serves it, before any env closes, so a recording on an env whose close doesn't end it keeps its last segment - gate 3 boots a build at the env's computer_settings and display, as a deploy does - a reset the sandbox provider can't carry out fails the step instead of being skipped - a computer skill whose name had to be normalized or shortened takes a digest of the env id - a data/get hook that takes paths gets None when the call names none - a closed-network boot that fails removes its work folder - create_container takes no platform: the deploy and gate 3 choose a provider by native_platforms() before calling it, so the refusal it carried never fired, and the new parameter broke out-of-tree overrides (the plugin-API check) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9e775d5 to
f6e26fe
Compare
…iner's network away create_container takes the platform its image must run as and refuses one the provider doesn't run natively (native_platforms: this machine's for local, linux/amd64 elsewhere). Local Docker enforces an allowlist of nothing for a container: it runs on an internal network of its own, and a relay on that network and the bridge publishes its port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…each build, and deploy_env takes env_params The computer env provider boots the env's first ready build that the sandbox provider runs natively, with its deploy-phase settings and the driver's own, waits for computer-ready, checks the display, hides the env's tools for every role and records the driver's card as the env's. Gate 3 boots each build a configured provider runs natively and probes it at full: ready with its card, or failed at the probe; a build nothing here runs natively stays unverified. deploy_env passes env_params to the env type's deploy() and checks them at save for every env type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…top folders data/get passes `paths` to a get hook that takes them and refuses them otherwise; the client's get_data sends them. A computer returns the files under the paths it's given, or under DEFAULT_DATA_PATHS (Desktop, Documents, Downloads), which its card declares as default_data_paths. ComputerSettings drops data_paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dboxes go Env.close(context) joins the base contract; built-ins take the context and ignore it. teardown_run and the teardown_sandboxes step share one core (task.teardown.tear_down): each env still up closes through close_deployed under the per-sandbox timeout, then every sandbox is terminated by id as before (terminate_sandbox is idempotent, so a close that swallowed a failed terminate is still covered) and a local one's folder removed, which the step now does too. A computer env's close ends its open recordings; before_teardown keeps only the changelog drain. The shared helpers move from the step module to task.teardown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omputer test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Env.reset takes the run's context, which reset_env passes. ComputerEnv.reset ends the env's open recordings, then the computer provider replaces its container with a fresh one of the image it booted, its settings applied as the deploy applied them, on the same sandbox and host port, and readies it as a deploy does (ready, display, hidden tools). SandboxProvider.recreate_container removes and starts the container on a VM-backed sandbox; local Docker also replaces the relay of a closed network, since the fresh container's address there may differ. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refused at start Its driver couldn't reach the store the segments upload to, so the start fails, recorded as any failed start is, rather than recording segments that never arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed by add_skills computer/v1 is served with a skill method: the driver base class writes a SKILL.md under the name it's given from the computer as it booted (platform, session user and home, the live screen size, what its input takes, a per-OS shortcut template, Linux for now, and the driver's new notes). Its params are the driver's features, as before; the declaration keeps recording only those. add_skills takes computer_env_ids: for each, an env this run deployed and bound to the agent (a DeployedAgent now records the env_ids deploy_agent registered), it asks the driver for its skill under computer-<environment name>, installs it, and records the SKILL.md in the run's context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rk folders Gate 3's probe and a boot that fails partway tear down a sandbox no run records, so no run's teardown removes its local work folder; they now discard it as teardown would. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eighth shipped bundle: a computer env of ubuntu-24.04 and reference, a probe task (deploy, probe at full) and an agent task (Claude Code, given the computer skill, makes a folder under a recording; the check reads it back through the driver). The scaffold's probe and agent tasks are the example's; its scripted drive under a recording runs on the example env; the recording chaos tests run on the example's composed image, with the conformance fixture started through computer-exec. The integration tests compose the example env once per module, and one deploys it with no network: reached, reaching nothing, fresh after a reset, gone after teardown. Docs say a driver deploys only as a computer env's driver, and how a computer env deploys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and reset take no context The env, not the run, ends what runs on its computer: computer-recording/v1 gains end_all, which ends every recording not yet ended under the grant each started with, and every end uploads the manifest beside the segments. A computer env's close and reset call it before the computer goes. Teardown then registers the recordings the run left open from their namespaces, reading the manifest the driver uploaded (register_open_recordings), and end_computer_recording does the same when the driver no longer knows a recording, as after a reset. So Env.close and Env.reset keep their signatures, and an out-of-tree override of either is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y its computer and registered whole Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings, one table per layer A deploy sets the base's deploy-phase values in computer_settings, checked as the env's are, and the driver's in driver_settings, checked by the protocol's ComputerSettings and passed in AGENTENV_COMPUTER_SETTINGS; driver_settings replaces spaces, which set two of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l_access Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e probe boots at the env's settings, no platform on create_container From the review of #369: - teardown ends the run's open recordings, each with a fresh grant whatever env serves it, before any env closes, so a recording on an env whose close doesn't end it keeps its last segment - gate 3 boots a build at the env's computer_settings and display, as a deploy does - a reset the sandbox provider can't carry out fails the step instead of being skipped - a computer skill whose name had to be normalized or shortened takes a digest of the env id - a data/get hook that takes paths gets None when the call names none - a closed-network boot that fails removes its work folder - create_container takes no platform: the deploy and gate 3 choose a provider by native_platforms() before calling it, so the refusal it carried never fired, and the new parameter broke out-of-tree overrides (the plugin-API check) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f6e26fe to
7655132
Compare
| its recordings). A type that can't close a deployment leaves them to be terminated by id.""" | ||
| try: | ||
| async with asyncio.timeout(timeout): | ||
| await close_deployed(deployed, type(Env.get(deployed.env_id, deployed.env_version))) |
There was a problem hiding this comment.
Custom env cleanup gets skipped
tear_down sends every env through close_deployed, which reads env._env_provider before calling close. That private field is not part of the Env contract. A custom env can implement from_deployed_env and close and return a DeployedSandboxEnv without defining it. The helper then fails, and teardown stops the sandbox without letting the env save its final data.
Call the reconstructed env's public close method, keeping termination by id as the fallback.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task/teardown.py
Line: 213
Comment:
**Custom env cleanup gets skipped**
`tear_down` sends every env through `close_deployed`, which reads `env._env_provider` before calling `close`. That private field is not part of the `Env` contract. A custom env can implement `from_deployed_env` and `close` and return a `DeployedSandboxEnv` without defining it. The helper then fails, and teardown stops the sandbox without letting the env save its final data.
Call the reconstructed env's public `close` method, keeping termination by id as the fallback.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| "computer_settings": recorded_settings(computer, values), | ||
| "env_params": params.model_dump(exclude_defaults=True)}}, | ||
| ) | ||
| await _require_display(record, values.get("resolution")) |
There was a problem hiding this comment.
A computer that becomes ready but fails _require_name or _require_display leaves its local work folder behind. These checks run after boot returns, outside its discard cleanup. The provider's failure cleanup only calls terminate, which does not remove the folder. The failed deployment also leaves no run record for teardown to reclaim it.
Use discard when these post-boot checks fail so repeated failed deployments do not accumulate abandoned folders.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/env_providers/env_computer_provider.py
Line: 106
Comment:
**Failed deploys leave folders**
A computer that becomes ready but fails `_require_name` or `_require_display` leaves its local work folder behind. These checks run after `boot` returns, outside its `discard` cleanup. The provider's failure cleanup only calls `terminate`, which does not remove the folder. The failed deployment also leaves no run record for teardown to reclaim it.
Use `discard` when these post-boot checks fail so repeated failed deployments do not accumulate abandoned folders.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…erence A secret setting given as its value, on the env or in deploy_env's env_params, was kept wherever what's given is kept: the env, the task, the composed computer's defaults and the deploy record's env_params, which reset reads back. check_computer_settings now refuses anything but a bare reference, the form a stored context keeps, so only names are stored and every boot resolves them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| elif setting.secret and not _names_a_secret(value): | ||
| problems.append(f"computer_settings.{name} is a secret, so it takes a {SECRET_REF_PREFIX}KEY or " | ||
| f"{ENV_REF_PREFIX}NAME reference with no ?default, not its value") |
There was a problem hiding this comment.
A secret setting with an int, float, or bool default can no longer be overridden. The new check rejects literal values, but a valid env:NAME or secret:KEY reference then fails the existing default-type check because it is a string. Setting allows these defaults, so both saving an env and deploying with an override fail for supported settings. Skip the literal-type comparison for secret references.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/env/envs/computer/gate.py
Line: 59-61
Comment:
**Secret overrides get refused**
A secret setting with an `int`, `float`, or `bool` default can no longer be overridden. The new check rejects literal values, but a valid `env:NAME` or `secret:KEY` reference then fails the existing default-type check because it is a string. `Setting` allows these defaults, so both saving an env and deploying with an override fail for supported settings. Skip the literal-type comparison for secret references.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.It resets the container create_container ran to a fresh one of the image, on the same sandbox and port, which is what a computer env's reset uses it for; the provider guide now lists it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…MCP session The fresh driver doesn't know the old one's MCP sessions and answers one with 404, after which an MCP client starts a new session; Claude Code opens a new one every turn anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s env_ids
skill/v1 (SKILL_EXTENSION_URI) answers get {name} with {name, description, skill_md}: the env's
SKILL.md for agents under the name the caller gives. An env built on the SDK serves one by marking
the method that writes it with @agent_skill, which returns its description and body. The computer
driver's skill moves there from computer/v1, which is now declared with the driver's features, and
the conformance probe checks it. add_skills's computer_env_ids becomes env_ids: each env's skill is
installed under its environment name (the agent's MCP alias for it) and recorded in env_skills.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| "deploy_agent") | ||
| name = _env_skill_name((deployed.environment_card or {}).get("name") or env_id, env_id) | ||
| try: | ||
| return await deployed.invoke(SKILL_EXTENSION_URI, "get", {"name": name}) |
There was a problem hiding this comment.
Gateway-backed skills cannot load
An MCPServerEnv using its default gateway provider keeps its server's @agent_skill extension in a child card. _env_skill calls invoke without environment_name, so it checks only the gateway's card and raises “serves no skill.” This fails add_skills even though the server serves the skill.
Select the server's child card when fetching its skill, while keeping support for leaf cards.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task_step/task_steps/add_skills.py
Line: 313
Comment:
**Gateway-backed skills cannot load**
An `MCPServerEnv` using its default `gateway` provider keeps its server's `@agent_skill` extension in a child card. `_env_skill` calls `invoke` without `environment_name`, so it checks only the gateway's card and raises “serves no skill.” This fails `add_skills` even though the server serves the skill.
Select the server's child card when fetching its skill, while keeping support for leaf cards.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
M0.7 of the CUA env epic (FD-4345): computer envs deploy on local Docker, FD-4358, with M0.8 (
deploy_envenv_params, FD-4349), M0.5 (the computer skill, FD-4355) and M0.11 (Env.closeat teardown, FD-4464) folded in. It follows the reviewed design, CUA Env M0.7: Deploying a Computer Env on Local Docker, with its 15 choices confirmed and the changes below decided in review. It builds on #358 (M0.6), now merged, and is rebased ontomain.What's in it
native_platforms():linux/amd64by default, the host's for local Docker. A computer boots only on a provider that runs its arch natively;create_containertakes no platform (decided in review), since that choice is made before it's called.NetworkPolicy.closedis an allowlist of nothing. Local Docker enforces it for a container: it runs on an internal network of its own, with a relay container (providers/sandbox_providers/relay/, built here from its sources) publishing its port to the computer's address there. A VM given a restricting policy is refused.reset_container(sandbox, ...)resets a sandbox's container to a fresh one of the image, on the same sandbox and port; local Docker replaces the relay too. A computer env's reset uses it, since a computer's state is its whole filesystem and running session, with no seed to reload in place as an MCP server does.computerenv provider (providers/env_providers/env_computer_provider.py) deploys a computer env with no gateway in front:readybuild, in the base's order of forms and arches (orenv_params.form's), on a sandbox provider that runs its arch natively;environment_name;display;metadata.computerkeeps the form, arch, provider, image, computer, display, network,computer_settings(a secret's or a generated one's withheld) andenv_params.composer.py): after a build, the composer boots it at the env's settings and display, as a deploy does, on a configured provider that runs its arch natively, and probes it atfull. The build becomesreadywith the driver's card, orfailedat the probe with the report stored as a FileArtifact. A build no configured provider runs natively staysunverified, saying why, andenv buildprobes it once one does. A probe's sandbox, and a boot that fails (a closed-network one whose setup fails too), go with their local work folders.deploy_envenv_params(M0.8), one table per layer: a computer env takesform,computer_settings(deploy-phase values of the base's settings, checked as the env's are; a secret one, here or on the env, only as a baresecret:/env:reference, since the env, the task and the deploy record keep what's given) anddriver_settings(the driver'sComputerSettings, such as its default coordinate space, passed inAGENTENV_COMPUTER_SETTINGS). Preflight now asks every env type'sdeploy_refusal, so another type, or an option a type's deploy doesn't name, is refused before anything deploys.Env.closeat teardown (M0.11):teardown_runand theteardown_sandboxesstep share one core (task.teardown.tear_down). First the recordings the run left open end, each through its driver with a fresh grant, whatever env serves it; then each env still up closes under the per-sandbox timeout; a recording still open after that is registered from its namespace; then every sandbox is terminated by id as before and a local one's folder removed (the step now does both too).Env.closeandEnv.resetkeep their signatures, so out-of-tree overrides are unaffected.computer-recording/v1gainsend_all, which ends every recording not yet ended under the grant each started with, and every end uploads the manifest beside the segments (<recording_id>/manifest.json). A computer env's close and reset call it before the computer goes, so a recording open at a reset, or at a close outside a run's teardown, is saved too. Teardown, andend_computer_recordingwhen the driver no longer knows a recording (as after a reset), register it from what its namespace holds, reading that manifest.skill/v1(SKILL_EXTENSION_URI) answersget {name}with{name, description, skill_md}, written under the name the caller gives, and an env serves it by marking the method that writes it with@agent_skill(description and body). The computer driver's base class serves one written from the computer as it booted, ending with the driver class's newnotes;computer/v1now only declares the seven tools and the driver's features, and the conformance probe checks the skill.add_skillstakesenv_ids: for each env the run deployed and bound to the agent, it installs the env's skill under its environment name (the agent's MCP alias for it, with a digest of the env id when the name had to be normalized or shortened) and records the SKILL.md in the run's context (env_skills).data/gettakespaths(decided in feat(computer): the computer env type and the composer (container forms) #358's review): a computer returns the files under the paths it's given, or under thedefault_data_pathsits card declares (Desktop,Documents,Downloads). A get hook that doesn't takepathsrefuses them; one that does gets None when the call names none.ComputerSettings.data_pathsis gone.desktopexample (src/agent_env/examples/desktop/, the eighth shipped bundle): a computer env ofubuntu-24.04andreference, aprobetask (deploy, probe atfull) and anagenttask (Claude Code, given the computer skill, makes a folder through the file manager under a recording; the check reads it back through the driver).tst/integration/computer/recording/); the recording chaos tests run on the example's composed image, with the conformance fixture started throughcomputer-execas the probe starts it.Where this differs from the design
end_all, instead ofEnv.close(context)andEnv.reset(deployed, context)ending them from the run's context, which would have broken out-of-tree overrides of both. Teardown still ends the run's open recordings first, each with a fresh grant (decided in review of Greptile's finding that a recording served by another env type would lose its last segment). What remains: an end at reset uses the grant the recording started with, so on a store whose grants expire before a long recording is reset, the segments recorded after expiry are lost.terminate_sandboxis idempotent, so the second terminate costs one lookup.failedkeeps meaning "may still be running".env_paramstakescomputer_settingsanddriver_settings(decided in review), one table per layer, instead of settings by layer andspaces.modify_env_tool_access, which works through the driver'sdisable-tool/v1.skill/v1and@agent_skillreplace askillmethod oncomputer/v1, andadd_skillstakesenv_idsinstead ofcomputer_env_ids, so other env types can hand agents a skill the same way.gettakes the name to write into the SKILL.md's frontmatter, soadd_skillsnames it per env.DeployedAgentrecords theenv_idsitsdeploy_agentregistered, since nothing in a run's context said which envs an agent is bound to.data/getpathsand the driver's default replacedata_paths(decided in feat(computer): the computer env type and the composer (container forms) #358's review;snapshot_envgainscomputer_file_pathsin M1.8, FD-4382).create_containertakes noplatform(decided in review). The design passed it so a provider would refuse a platform it would emulate, but both callers, the deploy and gate 3, choose a provider bynative_platforms()first, so that check could never fire, and the new parameter broke out-of-tree overrides ofcreate_container(the plugin-API check).Review
Greptile's review of 9e775d5 raised seven threads. Six are fixed in 7655132, as the bullets above now say: teardown ends the run's open recordings first; gate 3 probes at the env's settings; a reset a provider can't carry out fails; skill names that come out alike get an env-id digest; a get hook that takes
pathsgets None; a failed closed-network boot removes its folder. The seventh, a secret setting given as its value reaching the deploy record, is fixed in 77cbffb (decided in review): a secret setting takes only a baresecret:KEYorenv:NAMEreference. The plugin-API check's break,create_container's newplatform, is gone (difference 10); the check now reports no break. #358's fix for a driver file named outside the build folder (98e139a) applies here too.Testing
main, and again with the secrets change, 11,341 passed; the one failure, the knowntest_deep_nestingtiming flake, passes alone. On the review round before the rebase, 11,288 of 11,292 passed. Three failures were in the round's own new tests, which were fixed and rerun with their files (1,983 passed); the fourth is the knowntest_deep_nestingtiming flake, which passes alone.tst/integration/computer, laptop arm64, real Docker and the local registry), before the review round: 18 passed and 1 skipped (the agent task, which needs a model endpoint). The scripted-drive recording test failed that run: its pixel check compared against the recording's first frame, which can predate the Fixture Desk now that the drive starts it. Fixed to compare against the frame before the click, it passed twice on its own. CI's integration jobs run the same tier on Linux amd64.deploy_chaos.py(scenarios from the design's chaos row; each also checks that no container, relay, network or work folder is left):env buildresumes it toready. This found a hang: the probe had no deadline, and on Docker Desktop a dead computer's connection stays open, so the put waited indefinitely. The probe now has 120 s;env buildresumes it toreadyonce Docker is back. There the probe's computer dying fails fast, since the port closes with the container. The box had no arm64 emulation, so the arm64 pair fails at build, as gate 1 promises.agent-env run desktop --task agent, laptop only, the model key held in-process): passed in 52.7 s. Claude Code, given the computer's skill (then namedcomputer-desktop; it isdesktopsince the review), made the folder through the file manager in 36.4 s, the check confirmed it through the driver, and the recording ended with 4 segments and none missing. Nothing was left.🤖 Generated with Claude Code
This PR needs fixes before merging: gateway-backed env skills fail, and two earlier blocking findings remain.
Fix with agent prompt
Summary
This PR adds computer env deployment on local Docker, fresh-container reset, build probes, recording cleanup, and a desktop example.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A["add_skills env_ids"] --> B["DeployedEnv.invoke"] B --> C["Top-level gateway card"] C --> D["Skill not found"] E["MCP server with @agent_skill"] --> F["Child card with skill/v1"] F -. "Requires environment_name" .-> BReviews (9) · Last reviewed commit: "feat(protocol): any env can serve agents..." · Reviewed by Greptile