Skip to content

fix(gateway): write the compose file as data and refuse names it can't hold - #382

Merged
earakely-scale merged 4 commits into
mainfrom
edgararakelyan/compose-names-as-data
Oct 29, 2026
Merged

earakely-scale merged 4 commits into
mainfrom
edgararakelyan/compose-names-as-data

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A gateway deploy wrote docker-compose.yml and init-schemas.sql to the VM host with quoted heredocs and spliced each env's environment_name and image reference into them. The snapshot reload passed the same names to docker compose up --force-recreate unquoted. If a name held the heredoc terminator, a newline or a shell metacharacter, it ran as shell on the VM host (the operator's own machine on the local provider) or rewrote the compose file. An env.toml in a bundle is enough to set such a name.

What changes

  • Files as data. EnvironmentGatewayProvider._write_file writes a file as one shlex.quoted printf argument. The gateway's compose and init files and the snapshot reload's compose file use it, keeping the same exec count, with max_retries=2 (the reload's write now retries too; it only replaces the file). The snapshot reload quotes each service name it passes to compose.
  • Names that are names. Two checks, each next to the code it guards:
    • environment_name_problem in env_gateway_provider: an environment name against Compose's service-name characters, with a letter or digit first;
    • image_reference_problem in oci_registry_credentials, next to names_registry: an image against Docker's reference grammar ([registry/]repository[:tag][@digest]), without bracketed IPv6 registries. An unquoted compose value reads those as a YAML list, so nothing deploys with one today.
  • Checked on write.
    • DockerImageArtifact.put_tar and put_ref check before any upload or registry request.
    • Bundle env.toml authoring reports a bad name as a config problem before any write.
    • An env saved through the SDK or CLI with a bad name is stored as on main and refused at its first deploy.
  • Checked at deploy. EnvironmentGatewayProvider.deploy checks before acquiring state or a sandbox; so does the compose renderer (MCP servers, websites, sidecars, the gateway image, the MCP server name) and local_postgres's state services. from_dict is unchanged, so documents already stored still load, and only their deploy is refused.
  • The branch carries test(env): import LocalSandbox from its new module in the artifact staging test #381's one-line LocalSandbox import fix in test_artifact_staging.py, so its unit job runs green before test(env): import LocalSandbox from its new module in the artifact staging test #381 merges.

Testing

  • Unit tests: 11,549 passed, ruff clean. New tests cover:

    • a written file holds its content byte for byte and runs none of it (real bash in tmp_path, for content holding the terminator, $(), backticks and quotes);
    • the VM deploy and the snapshot reload write each file as one quoted argument and name each service as one;
    • the renderer refuses every name and image field that would change what the file says, and takes the names and references Compose and Docker take (registry ports, digests, __ separators);
    • a deploy with a bad name is refused before any state or sandbox is acquired;
    • put_tar, put_ref and bundle authoring refuse on write.
  • Before and after on Linux (EC2 Ubuntu 24.04, Docker 29.9, local provider; main at 1a35e48 vs this branch). Each agent-env run started from a fresh working directory with its own XDG_STATE_HOME. The hostile bundle's env.toml sets environment_name = "items\nCOMPOSE_EOF\ntouch pwned", a harmless marker. After each run, the whole box was searched for pwned* files.

    main this branch
    hostile name the shell ran on the host: pwned, pwned:, pwned:18765 and pwned%22,public in the run directory, then the deploy failed refused at authoring (envs/items: env.toml: environment name 'items\nCOMPOSE_EOF\ntouch pwned' must start with a letter or digit ...), nothing written, no marker
    plain name, same bundle passes passes, deployed through the new write in 22 s
    main's stored hostile env, deployed by this branch (agent-env env deploy) – the document loads; the deploy is refused (Can't deploy '@local/~/e2e/compose-named-env/items' behind a gateway: ...) before any container starts; no marker
  • modal_vm (the remote VM write path, run from a Mac): test_gateway_with_multi_mcp_server, test_gateway_triggers_extension and test_gateway_clock_extension, all 3 passed.

  • Regression and stress on the Linux box: the fast integration tier (175 passed), then the gateway, server-env, MultiEnv, task-step, service-db and bundle integration sets three times in a row: 55 passed each time (706 skipped, every skip a declared agentenv-capability-missing gap), about 30 min a run.

  • CI: all green. One slow shard failed first on recording_chaos_test.py::test_a_restarted_driver_forgets_its_recording_and_keeps_the_segments_it_closed: the computer never became ready after a docker restart. That test runs docker run/docker restart on a computer image and touches nothing this PR changes. It passed on rerun.

Not in this PR: the tarball load check in VmSandbox._load_docker_images still matches image_name's repository as a substring of docker images output instead of the exact tag. With the names now validated, that is an integrity check rather than an injection sink; it can follow separately.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new actionable issues were found.

Summary

The PR writes gateway files as quoted data and rejects names and image references that cannot safely appear in Compose files.

  • Gateway and snapshot reload writes keep file contents and service names as data.
  • Gateway environments accept only names that fit Compose service names.
  • Image references must fit Docker's naming rules before artifact writes or Compose rendering.
  • The artifact-staging test imports LocalSandbox from its current module.

Reviews (4) · Last reviewed commit: "refactor(gateway): write host files thro..." · Reviewed by Greptile

…t hold

A gateway deploy wrote docker-compose.yml and init-schemas.sql through
quoted heredocs and spliced each env's environment_name and image
reference into them, and the snapshot reload passed the names to
`docker compose up` unquoted. A name holding the heredoc terminator, a
newline or a shell metacharacter ran as shell on the VM host (the
operator's machine on the local provider) or rewrote the compose file.

- Both files are written as one shlex-quoted printf argument
  (utils/shell.write_file_script), keeping the exec count and retries;
  the snapshot reload quotes each service name.
- utils/names checks an environment name against Compose's service-name
  characters with a letter or digit first, and an image against Docker's
  reference grammar (without bracketed IPv6 registries, which an
  unquoted compose value reads as a YAML list).
- The names are checked when written (EnvStore.put_document through a
  new Env.put_problem hook, DockerImageArtifact.put_tar/put_ref, bundle
  env.toml authoring) and again at deploy (the gateway topology, the
  compose renderer and the local_postgres state services). from_dict is
  unchanged, so stored documents still load.
- Includes the LocalSandbox import fix in test_artifact_staging.py that
  main needs after the provider move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale and others added 3 commits October 28, 2026 22:04
The gateway refuses a name at deploy and bundle authoring refuses it in
env.toml, so Env.put_problem only moved the SDK and CLI error earlier at
the cost of a base-class hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The environment name rule is the gateway's compose service name rule, so
it lives in env_gateway_provider, which bundle authoring already imports.
The image reference grammar sits with names_registry and
registry_host_from_ref in oci_registry_credentials. utils/names goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot reload already drives the gateway through its private
helpers, so the write is EnvironmentGatewayProvider._write_file beside
_compose_up rather than a utils module. The reload's compose write now
retries like the deploy's; the write only replaces the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit e5f8ba3 into main Oct 29, 2026
25 of 26 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/compose-names-as-data branch October 29, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant