…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>
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.