fix(docker): harness isolation via COPY/PRUNE + GRADE-OUTSIDE. Close the criteria/grader leak - #88
fix(docker): harness isolation via COPY/PRUNE + GRADE-OUTSIDE. Close the criteria/grader leak#88CarlesUIPath wants to merge 8 commits into
Conversation
…e the criteria/grader leak Under --driver docker the agent could read its own grading material (success_criteria, skills-repo graders check_*.py, RESOLUTION.md, reference agents, the staged task.yaml, the per-task-dir mount). A nightly audit measured ~2.4% of replicates reading the suite instead of solving the task; an adversarial "check /work" prompt drives it to ~100%. Prior denylist mitigations did not hold, and a uid-permission barrier (PR #85, closed) corrupted the host checkout on Linux and was defeated by the macOS uid-remap. This closes the leak by ABSENCE, not permissions: - Agent container mounts ONLY a sanitized :ro skill bundle (project_plugin_for_agent: skills/.claude-plugin/commands/agents/hooks — no tests/, graders, or reference) + its throwaway workspace. The raw skills repo, reference, and task-dir are NOT mounted. - The staged task.yaml is criteria-stripped (agent_safe_dump: success_criteria=[], reference=None) and context.json.source_yaml is nulled; no task_full.json is staged. - Grading runs on the HOST after the container exits (regrade_on_host, the evaluate-only Orchestrator seam), against the full criteria the host holds — so criteria/graders and the agent never share a filesystem or a moment in time. Nothing chmods a host mount. Hardening found via multi-model review + real containerized runs (codex/claude/gemini/kimi): - regrade switches the sandbox driver off 'docker' (Sandbox.setup rejects driver=docker) — without it every docker task ERRORed. - regrade seeds the container's turns (existing_turns, deep-copied) so trajectory-based criteria (skill_triggered / command_executed / agent_judge / llm_judge transcript) grade against the REAL trajectory — without it skill_triggered reported ~0 activation. - _copy_claude_home ignores jobs/ (operator session/conversation no longer copied in). - ~/.uipath forwarded as a throwaway rw COPY (never the host original). - pre_run/post_run not re-run on the host re-grade; template_sources/system_prompt_file auto-mount rejected if it overlaps the host task dir; authored empty success_criteria rejected at load (container re-parse bypasses via allow_empty_criteria); re-grade fail-safe never leaves a false SUCCESS on disk. Early-stop is a documented no-op under docker (criteria are stripped from the container, so the in-container watcher cannot arm); DockerRunner warns, verdict is unaffected (host grades the full criteria). The leak-free follow-up is a host-side watcher over the live event stream — see docs/DOCKER_ISOLATION.md § Limitations. Verification: make test-docker-detectors (absence + host-unchanged proxy + baked-image scan + no-uid-machinery) and the -m live host-unchanged sensor (a real docker run leaves the host byte + metadata identical) — both wired into the docker-isolation CI job. Backwards-compat confirmed on real runs: llm_judge / skill_triggered / agent_judge / simulation / early-stop all intact under grade-outside across claude/codex/gemini/kimi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68d4e54 to
0cab1e5
Compare
Under --driver docker the agent container no longer holds the skills tests/ tree, so pre_run/post_run helper scripts can't run inside it. Move them host-side: post_run runs after the container exits over the copied-out workspace; pre_run runs before the container into a staging dir that is seeded (files + directory trees) into the agent workspace. The container suppresses both. A guard redirects tasks whose pre_run must build an in-container env (uv sync / uip codedagent setup) to --driver tempdir for now, skipped per-task so the rest of a suite runs. Verified end-to-end under docker on Bedrock: host pre_run seed -> agent consumes it -> capture -> host post_run reads the output -> host grading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
…platform byod_smoke_test checked /opt/byod_marker directly, but under grade-outside the host grader can't see absolute container paths outside /work. Have the agent (which runs in the container) surface the image-baked marker into its workspace and grade that host-side — verified SUCCESS under --driver docker on Bedrock. test_host_commands::test_cwd_accepts_str parsed `pwd` output, which git-bash POSIX-ifies on Windows (/c/Users vs C:\Users), failing the raw Path compare. Assert the command's cwd via a file it creates there instead; drop the now-unused Path import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes harness-outside. Under --driver docker, pre_run runs host-side by default (seeded into the agent workspace); a command that must run INSIDE the container (e.g. a uv sync venv build) sets runs_in: agent, and the in-container orchestrator runs that subset before the agent. post_run stays host-only, and default host leaves existing tasks unchanged. A guard flags an in-container-only pre_run (uv sync / uip codedagent setup) left runs_in: host under docker, redirecting the author to mark it runs_in: agent. Verified end-to-end under docker on Bedrock: host-marked step runs host-side, agent-marked step runs in-container, in-container venv builds against the host-seeded fixture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 3 —
|
|
Claude finished @CarlesUIPath's task in 1m 31s —— View job Code Review in Progress
|
…ontent test The overlap guard rejected any auto-mount / extra_mounts source *contained by* the task (grader) dir, which false-blocked clean child template_sources (e.g. `fixtures/`) — ~358/1147 skills tasks errored at docker staging with no task.json, so they were invisible on the evalboard. A docker `-v child:child:ro` mounts only that subtree, so it never exposes the parent's sibling `check_*.py`. Narrow `_overlaps_grader_dir` to a content test: block only when the source is the grader dir, an ancestor of it, or a descendant whose subtree overlaps a real grader artifact (a `check_*.py` or the task's resolved `reference.file`/ `.directory`). A new pure helper `_grader_artifacts` computes the set once in `_build_argv` and threads it into both call sites. Adds argv-level leak-detector coverage (a clean child mounts while the grader dir and `check_*.py` stay absent from every `-v` source) plus the reference-directory-descendant case, updates the stale descendant-blocks tests, and corrects the DOCKER_ISOLATION.md contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-runs Under docker DIRECT_WRITE the agent writes into the reused host run-dir, so a stale `.venv` from a prior run can remain. `uv venv` then refuses with "... are the same file" (exit 2) because `.venv/bin/python` resolves to the same interpreter, and sandbox setup fails after retries (seen as ~14 spurious ERRORs on a re-run). Remove any pre-existing `.venv` before creating so venv setup is idempotent. Regrade mode reuses an existing venv and never calls this path, so the agent's own produced venv is untouched. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntrolled env Under grade-outside, regrade_on_host re-grades agent artifacts on the HOST with graders running in the agent-controlled environment: the agent's `.venv/bin` and `node_modules/.bin` on PATH, `os.environ.copy()` carrying operator credentials, and cwd = the agent artifacts dir. An agent could plant `.venv/bin/python` (or a `node_modules/.bin` tool, or a cwd module shadowing stdlib for a `python -c` grader) and get code execution AS THE OPERATOR with operator credentials — a category beyond the answer-disclosure leak grade-outside already closes. Harden the host-side untrusted-artifacts grader env only: drop agent-writable dirs from PATH (trusted interpreter), set PYTHONSAFEPATH and strip PYTHONPATH/PYTHONSTARTUP/PYTHONHOME (defeats the cwd-relative import hijack), and scrub operator credentials via a denylist + keep-set. The in-container/tempdir grader path is byte-for-byte unchanged. Venv-dependent graders (`uv run uipath eval`) opt back in per task via `sandbox.docker.regrade_trusts_agent_env` (default false); that host-only flag is stripped from the staged task.yaml so it never crosses the container boundary or trips extra="forbid" on an older image. Adds fail-before/pass-after security detectors (planted interpreter not executed, operator secret scrubbed, cwd-import hijack blocked, host-only flag not staged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Summary
Under
--driver docker, the agent-under-test could read its own grading material —success_criteria, the skills-repo graders (check_*.py),RESOLUTION.md, reference agents, the stagedtask.yaml, and the per-task-dir mount — and separately could overwrite the operator's~/.uipath/.auth(it was rw-shared with the host). A nightly audit measured ~2.4% of replicates reading the suite instead of solving the task; an adversarial "check /work" prompt drives it to ~100%.This closes the leak by absence, not permissions: the agent container never contains grading material, grading moves to the host after the container exits, and host state (auth, checkout) is never rw-shared with the agent.
How it works — before (main) vs. after (this PR)
Before —
main: agent and grader share one container/filesystem, and the criteria are staged verbatim into/work/input.%%{init: {'flowchart': {'useMaxWidth': false, 'padding': 20, 'nodeSpacing': 60, 'rankSpacing': 55}}}%% flowchart TB s1["HOST · stage — task.yaml = FULL criteria; mount RAW skills repo (tests/, check_*.py, reference); ~/.uipath rw-shared"]:::host c1["CONTAINER · AGENT + GRADER — same filesystem<br/>agent turn, then grades in-container"]:::leak n1["During its turn the agent can read its own success_criteria,<br/>graders, and reference — and overwrite ~/.uipath/.auth"]:::note s1 -->|"docker run (TASK_DIR / SKILLS_REPO_PATH point at the above)"| c1 c1 -.-> n1 classDef host fill:#eef2ff,stroke:#5566aa,color:# classDef leak fill:#ffe0e0,stroke:#cc0000,color:#111; classDef note fill:#fff3cd,stroke:#cc9900,color:#After — this PR: phase 2 is agent-only; grading is a separate phase 3 on the host.
%%{init: {'flowchart': {'useMaxWidth': false, 'padd'rankSpacing': 55}}}%% flowchart TB s2["HOST · stage (agent-only) — task.yaml strippeared); sanitized skill bundle only — NO raw repo /tests/ / graders / reference / task-dir; ~/.uipath & ~/.claude are throwaway copies"]:::host a2["CONTAINER · AGENT ONLY<br/>sees: skill docs ( workspace<br/>cannot see: criteria, check_*.py,tests/, reference, host auth"]:::safe g2["HOST · GRADE-OUTSIDE (regrade_on_host)<br/>fua TASK_DIR — agent never shared theirfilesystem"]:::host s2 -->|"docker run"| a2 a2 -->|"container exits → artifacts copied out"| g2 classDef host fill:#eef2ff,stroke:#5566aa,color:# classDef safe fill:#e0ffe0,stroke:#0aa00a,color:#111;In one line: split the fused container into ade-on-host (phase 3), and stop mounting the answerkey — or host auth — into the agent's box.
Leaks closed
main)/work/input/task.yaml(fullsuccess_criteria) +context.json.source_yamltask.yamlcriteria-stripped (agent_safe_dump),source_yamlnulled, notask_full.jsonstaged; full criteria stay on the host for gradingtests/,check_*.py,RESOLUTION.md, reference agents (whole repo mounted):robundle (skills/.claude-plugin/commands/agents/hooks)host_task_dir:host_task_dir)TASK_DIRreference.file/reference.directorytemplate_sources/system_prompt_fileauto-mount rejected if it overlaps the task dirTASK_DIR/SKILLS_REPO_PATH~/.uipath/.auth(rw-shared → agent could overwrite operator credentials)~/.uipath(incl..auth) is never mounted~/.claude/jobs— operator session timelines / conversation copied into every container~/.claudecopy denylistVerification
make test-docker-detectors(criteria-absence + host-unchanged proxy + baked-image scan) and the-m livehost-unchanged sensor — a real--driver dockerrun leaves the host byte + metadata identical (also proves~/.uipath/checkout are untouched).llm_judge,skill_triggered,agent_judge, andsimulation(3-turn dialog) all confirmed intact under grade-outside. Early stop is intentionally DISABLED under docker for now (see Limitations).make check/typecheck/lintgreen; fullmake test= pass modulo pre-existing/environmental failures.Limitations & follow-ups
EarlyStopWatchercan't arm.DockerRunnerlogs a warning; the verdict is unaffected (the host grades the full criteria, and a completed run gates strict-AND). The leak-free follow-up is a host-side watcher over the live event stream (the host already receives the tool-call stream + can signal the container via the heartbeat channel) — seedocs/DOCKER_ISOLATION.md§ Limitations.tests/by construction) — cross-repo follow-up.~/.claude/~/.uipathcopies use a denylist; a follow-up should flip them to an allowlist so new dirs default to excluded.pre_run/post_runharness scripts under--driver dockerContext: PR #88 (docker harness isolation, "COPY/PRUNE + GRADE-OUTSIDE"). Kept as draft because of this blocker.
Summary
Grade-outside makes the docker driver leak-safe by no longer mounting the skills-repo
tests/tree into the agent container (that tree holds the graders/criteria — the leak we're closing). But many tasks'pre_run/post_runcommands invoke helper scripts that live undertests/(e.g.python3 $SKILLS_REPO_PATH/tests/tasks/.../seed.py,cleanup_*.py). Those commands currently run inside the container, so under docker they can no longer find their script and fail.Grading itself is unaffected — the skills suite grades workspace-relative (no criterion references an absolute container path). This is purely a setup/teardown problem.
Impact
Reproducible counts (grep over
$SKILLS_REPO_PATH/tests/tasks):pre_run/post_runreferences$SKILLS_REPO_PATH(the affected set)post_runpre_runuv sync(must stay in-container)243 is ~21% of the full tree; as a share of the actively-run cloud/e2e suites (
uipath-platform,maestro-flow,maestro-case,agents,ixp,admin,governance,data-fabric) it is a large fraction.Category split (estimate — derived from reading each distinct script, not a mechanical count):
pre_runfail_on_error=True(default) → loud task ERROR;fail_on_error: false→ agent runs unseeded →post_runpre_runuv syncvenv build (7 tasks)Does this happen without docker? No —
tempdiris fineIt's docker-only, and the reason is where the harness runs, not concurrency:
tempdir: no container. The sandbox is a plain host tempdir, andpre_run/post_runrun on the host (inheriting the host env), so$SKILLS_REPO_PATHdisk, and creds are present → works.docker:pre_run/post_runrun inside the container, which grade-outside strips of thetests/tree → scripts missing.Both drivers run a single task's `pre_run → y; the difference is the *isolation boundary
*. So this isn't a new class of bug — docker regressed the location where the harness runs.
Root cause
pre_run/post_runare harness operations (setup/teardown), but they currently execute in the agent's container. Grade-outside correctly removes the `testsand the harness scripts vanish with it.Proposed fix — "harness-outside"
Extend the grade-outside principle to the wh
/post_runon the host**, where the full repo + creds live. This does **not** re-open the leak — the scripts never enter the container. In effect, it makes docker do whattempdir` already does.post_run→ host-only. Teardown/inspepace + cloud is inherently a host concern. No new knob.pre_run→ new per-commandruns_in: host/agentis the opt-in for setup that must build the agent's runtime environment in-container (the 7uv synctasks; image-baked fixtures). Ahostpre_runthat writes workspace files runs into a staginr workspace.hostas default, ~236 of the affected tasks need zero change; only the ~7uv synctasks add one line (runs_in: agent)ma + a ~7-line skills-repo edit.