Skip to content

fix(env): stage a loaded artifact without passing its names to the shell as code - #374

Merged
earakely-scale merged 1 commit into
mainfrom
edgararakelyan/quote-artifact-load-paths
Oct 29, 2026
Merged

earakely-scale merged 1 commit into
mainfrom
edgararakelyan/quote-artifact-load-paths

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

What and why

When a task loads an environment artifact into an MCP server or a website on a VM sandbox (the gateway deployment, which includes the default local provider), the env stages the file on the sandbox's host with a few bash lines. Those lines spliced in the artifact's file name and the env's name unquoted:

  • MCPServerEnv: the docker exec … mkdir and rm -f <staged file> lines;
  • WebsiteEnv: the docker compose exec, docker compose cp and rm -f lines.

A name holding shell syntax ran as commands on that host. On local, that host is the operator's machine, as the operator. The file name comes from whoever wrote the bundle or the stored artifact, so loading someone else's bundle was enough. The download before these lines already quoted the path, so the file was staged fine and the load still passed: nothing showed that a command had run.

This PR:

  • Quotes the staging lines. MCPServerEnv removes the staged file through docker_cp(..., remove_source=True), which passes paths as arguments. WebsiteEnv quotes the service, the paths and the compose file.
  • Refuses a file name that isn't one plain name before anything runs: one with a / or \, ., .., an empty name, or a control character. The env builds host and container paths from it (/data/<name>), so a name with a / could reach outside the staging directory. Names written by FileArtifact.put and bundle folders are basenames already; put_bytes(filename=...) and stored documents could carry others.

The env name itself isn't validated here. Its other uses, in the compose file and the deploy script, need their own change.

How it was tested

  • Unit: tst/unit/env/envs/test_artifact_staging.py (new). A real LocalSandbox runs each env's staging lines, with a stub docker that logs its arguments. The cases are file and env names with ;, $(), backticks, quotes and spaces: each is staged and copied as one argument, the staged file is removed, and nothing runs. Names that aren't one plain file name are refused before any command runs. On main the same tests fail: the injected touch runs and the copy's arguments split. The full unit suite passes (11,348 passed, 66 skipped).

  • Integration: test_gateway_with_agentenv_environment and test_gateway_with_website_env_v1 now also load a seed named $(touch pwned) \touch pwned`.jsonthrough a real gateway, and check that it loads and that nopwned` file appears.

  • End to end on Linux (Ubuntu 24.04 box, local provider): a bundle whose environment artifact file is named $(touch pwned) \touch pwned`.json, run with agent-env run`:

    • main (33a9eab): the task passes, and pwned appears in the operator's working directory.
    • This branch: the task passes with the seed loaded, and no pwned file appears.
  • Regression on the same box, against this branch: the local-provider integration tests that deploy envs and load artifacts into them. That's env/gateway/gateway_test.py, env/envs/server_env_local_test.py, env/envs/multi_env_local_test.py, env/task_step/task_steps_test.py, cli/run_bundle_built_envs_test.py and cli/run_bundle_cli_folder_test.py.

    • Result: 51 passed, 0 failed, in 33 min.
    • Every one of the 627 skips is a declared capability gap: remote_sandbox (535), mcp_server_sources (64), default_a2a_agent (20) and model_endpoint_configured (8).
    • The two extended gateway tests also ran against main's source, and both fail there. The MCP server load passes, but pwned appears. The website's docker compose cp line splits on the name, and the copy fails.
  • Stress, on a second Linux box (local provider, this branch):

    • The parallel fast integration suite (-m 'not int_test_slow' -n auto) ran 3 times: 175 passed each time.
    • The artifact-load integration set above then ran 3 more times in a row: 51 passed and 0 failed each time, with only the allowed capability skips.
  • modal_vm: test_gateway_with_multi_mcp_server[modal_vm] and test_gateway_triggers_extension[modal_vm] both passed on real Modal VM sandboxes. They load a universe and an environment artifact into MCP servers, so the changed staging path, including docker_cp(..., remove_source=True), runs over modal_vm's exec transport. No existing website test is parametrized by provider, so the website path ran on local only.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

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

Summary

The PR makes artifact staging treat names as data rather than shell commands.

  • MCP servers and website envs keep artifact names literal while staging files.

Scope acknowledgment: earakely-scale explicitly deferred other uses of the environment name in compose generation and deployment scripts.

Reviews (1) · Last reviewed commit: "fix(env): stage a loaded artifact withou..." · Reviewed by Greptile

…ell as code

MCPServerEnv and WebsiteEnv staged a loaded environment artifact on a VM sandbox's host with bash
lines that spliced the artifact's file name and the env's name in unquoted. A name holding shell
syntax ran as commands on that host, which on the local provider is the operator's machine.

- MCPServerEnv quotes its staging lines and removes the staged file through docker_cp's
  remove_source, which passes the paths as arguments.
- WebsiteEnv quotes the compose exec, compose cp and rm lines.
- Both refuse a file name that is not one plain name (a '/', a '\', '.', '..', empty, or a
  control character) before anything runs, since it builds host and container paths.

Tests: a real LocalSandbox runs each env's staging lines with a stub docker, for file and env
names with ';', $(), backticks, quotes and spaces; the gateway integration tests load a
shell-named seed into an MCP server and a website and check it loads and runs nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 28, 2026 21:12
@earakely-scale
earakely-scale merged commit 2f5fa40 into main Oct 29, 2026
28 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/quote-artifact-load-paths branch October 29, 2026 01:01
earakely-scale added a commit that referenced this pull request Oct 29, 2026
…l names its cause

- A secret app setting is checked only as a reference (or null, where it's optional), never against the
  constraints on its value, which its value then never reaches; a required one left out is named.
- A data/add, data/get or run whose connection to the driver breaks fails naming the env, the call and
  the cause, which httpx often leaves empty (found by the chaos run on EC2: a bare "ReadError: ").
- test_artifact_staging imports LocalSandbox from local_docker, where #369 moved it; main's unit tier has
  failed on it since #374.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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