fix(docker): harness isolation via COPY/PRUNE + GRADE-OUTSIDE. Close the criteria/grader leak - #88
Draft
CarlesUIPath wants to merge 1 commit into
Draft
fix(docker): harness isolation via COPY/PRUNE + GRADE-OUTSIDE. Close the criteria/grader leak#88CarlesUIPath wants to merge 1 commit into
CarlesUIPath wants to merge 1 commit 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>
CarlesUIPath
force-pushed
the
fix/docker-isolation-copy-prune
branch
from
August 6, 2026 16:59
68d4e54 to
0cab1e5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Under
--driver docker, the agent-under-test could read its own grading material —success_criteria, the skills-repo graders (check_*.py),RESOLUTION.md, referenceagents, the staged
task.yaml, and the per-task-dir mount — and separately couldoverwrite the operator's
~/.uipath/.auth(it was rw-shared with the host). A nightlyaudit 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 under
tests/(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 cont
ainer 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; `fpost_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 rep o + 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. Nonew knob.
pre_run→ new per-commandruns_in: ho*agentis the opt-in for setup that must bu ild the agent's *runtime environment* in-container (the 7uv synctasks; image-baked fixtures). Ahostpre_run` that 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.