fix(docker): close two driver:docker channels that leaked grading material to the agent - #179
Open
CarlesUIPath wants to merge 8 commits into
Open
CarlesUIPath wants to merge 8 commits into
CarlesUIPath wants to merge 8 commits into
Conversation
…(anti-cheat) Fix A: make /work/input a writable mount (drop :ro; grant input dir writable) and delete the staged task.yaml immediately after load_task in the in-container entry point, gated on IN_CONTAINER_ENV. The staged file is the post-override TaskDefinition with success_criteria, and the agent runs in the same container, so leaving it readable hands over the grading answer key. It is read exactly once; grading reads criteria from memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix B: default-deny mask over an auto-mounted Claude-plugin root. The whole root stays :ro-mounted so the plugin loads, but every child dir outside the keep-set (.claude-plugin + manifest-declared skill dirs) is masked with an empty tmpfs. Colocated eval material — sibling task YAMLs, reference solutions, tests/, node_modules/ — is masked by default so an unknown layout can never leak. New pure helper eval_material.mask_dirs; shared skill-dir resolver manifest_skill_dirs (renamed public in agents/_skills.py) is the SSOT for "what is a skill dir". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix C: a static lint rule (pytest class, CE055 template) that flags the one residual the Fix B allowlist cannot close — a task_id: YAML or a resolved reference.directory colocated INSIDE a skill dir reached through an in-repo agent.plugins[].path / sandbox.template_sources[].path. Masking a skill dir would hide the skill, so such material stays readable and leaks the grading answer key under driver: docker. Reuses the shared manifest_skill_dirs resolver (SSOT, import-identity asserted) with positive+negative sensors. Documents both docker anti-cheat fixes in docs/DOCKER_ISOLATION.md and the CE065 entry in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review found Fix A closed only /work/input/task.yaml but left the identical success_criteria readable in /work/input/context.json via source_yaml (top-level AND inside every config_lineage entry / ConfigLineageEntry.source_yaml) — in a dir this change makes world-readable+writable. The agent could cat context.json to recover its grading answer key. Delete both staged files after they are read into memory (_scrub_staged_task_yaml -> _scrub_staged_inputs); correct the docstring and docs/DOCKER_ISOLATION.md (the 'context.json carries no criteria' claim was false). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L1 — CE065 now flags ANY task_id: YAML or reference dir left readable under a plugin root (not just inside a skill dir): it reuses the runtime mask_dirs (SSOT), walks the readable surface, and catches a loose task_id: YAML *file* at the plugin root too (a tmpfs masks a dir, not a single file). Renamed the class to TestCE065EvalMaterialReadableUnderPluginRoot; added a loose-root-file sensor. L2 — docker_runner resolves a relative agent.plugins[].path against the task-file dir (new _resolve_mount_path), not CWD, matching reference/template/CE065 resolution so the static rule and the runtime inspect the same tree. L3 — eval_material.mask_dirs no longer descends a kept path, so a degenerate manifest skills: '.' masks nothing (fail-safe) instead of hiding the whole skill surface; added a test. Updated CLAUDE.md CE065 entry and docs. make verify green (the one failure is a pre-existing flaky wall-clock datetime test in test_agent_telemetry, passes on rerun, unrelated to these files). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-model review (Opus + Fable + Sonnet) found no Critical/High and no leak bypass; addressed the 3 Mediums and the doc drift: M2 — nested plugin roots (plugin B under plugin A) produced a duplicate Docker mount destination: A's mask emitted --tmpfs <A>/B while B's own mount bound <A>/B:ro. Defer mask emission until all binds are known and drop any mask whose path is also a bind (the bind wins so B loads and masks its own children). Extracted the auto-mount block into _append_auto_mounts to stay under the per-method lint limits. Added a nested-plugin test. M1 — added direct tests for _resolve_mount_path (relative->task-file dir, absolute unchanged, task_file None->cwd, env-var expansion); the L2 fix had no coverage. M3 — warn when a plugin root's mask stands down (manifest skills: '.') so the anti-cheat mask never voids silently. Added a test. Doc drift (L-a/L-b/L-c): corrected the now-false 'task.yaml readable at /work/input' / 'hiding criteria is unsolved' claims in orchestrator.py, fs_permissions.py and CLAUDE.md (Fix A deletes task.yaml + context.json after load); widened DOCKER_ISOLATION.md's residual note to include the loose-root-file case; marked the harness-candidates entry RESOLVED (nothing deferred). make verify green (5758 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-leaks # Conflicts: # CLAUDE.md
CarlesUIPath
requested review from
akshaylive,
bai-uipath,
tmatup and
uipreliga
as code owners
September 15, 2026 15:23
|
Claude finished @CarlesUIPath's task in 1m 21s —— View job Code Review in Progress
|
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, an eval agent could read its own grading material as follows:This PR closes both channels, adds a lint guard so the skills layout can't recur, and is verified end-to-end with a live adversarial agent. That is, a test which asks the agent to cheat by finding reference files.
The existing reference anti-cheat window #106 (mode-000 on the staged reference copy) did not cover either channel: it guards the reference copies, while the criteria sat unshielded at
/work/inputand the plugin mount exposed the originals.The fixes
/work/inputis now writable, and the in-container entry point deletes bothtask.yamlandcontext.jsonimmediately afterload_task(_scrub_staged_inputs), before any agent turn on all paths (normal + regrade). Deleting should be a safe op since graders do not read at task.yaml after agent finishes.isolation/eval_material.py::mask_dirs: the plugin root stays mounted:ro, but every child dir outside the keep-set (.claude-plugin+ the manifest-declared skill dirs) is--tmpfs-masked. Everything that isn't the skill surface is hidden by default, so an unknown/new eval layout can never leak;tests/, references,node_modules/are masked for free. This is stronger than a chmod window. Graders do not look into the mounted tasks, since these resources are present in task dirs from the previous anti-cheat PR feat(reference)!: directory-only references + anti-cheat permission window #106.task_id:YAML orreference.directoryleft readable under an in-repo plugin root (colocated inside a skill dir, or a loose file at the root that a tmpfs can't mask). Reusesmask_dirs+manifest_skill_dirsas the single source of truth, pinned by an import-identity test.Verification
make verify: 5758 passed, 4 skipped (green; 2 pre-existing env-driven failures only appear when a local.envleaksTASK_DIR/model — unrelated files, unchanged from main).find …/tests…andfind … reference→ no output;cat /work/input/task.yamlandcat /work/input/context.json→ No such file or directory;verdict.txt: DENIED; anti-cheat criterion PASS, task SUCCESS.mask_dirs,_resolve_mount_path, nested-plugin dedup, theskills:"."stand-down, and_scrub_staged_inputs.Known residuals (documented defense-in-depth)
chmodback; full containment needs a non-root uid) is unchanged — out of scope.skills:"."stands the mask down (fail-safe, now logged + CE065-flagged).Files
src/:isolation/eval_material.py(new),isolation/docker_runner.py,cli/run_task_internal_command.py,agents/_skills.py(manifest_skill_dirsmade public/SSOT),orchestrator.py,fs_permissions.py.tests/:test_eval_material.py(new),test_run_task_internal.py(new),test_docker_runner_mounts.py,test_custom_lint.py(CE065).Docs:
docs/DOCKER_ISOLATION.md,.claude/architecture-notes.md,CLAUDE.md.🤖 Generated with Claude Code