@rulemetric/cli 0.12.8 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-command.js +1 -1
- package/dist/{chunk-CZTTW3MZ.js → chunk-2WQUOC6S.js} +20 -20
- package/dist/chunk-4VN5PG2S.js +1 -0
- package/dist/chunk-5MXZJWQS.js +2 -0
- package/dist/{chunk-NKJTNGXR.js → chunk-5XEGRU2R.js} +1 -1
- package/dist/chunk-6XTLP2G7.js +1 -0
- package/dist/chunk-74QIUVWY.js +1 -0
- package/dist/chunk-7ML2UCI7.js +1 -0
- package/dist/chunk-AHLVY52I.js +1 -0
- package/dist/{chunk-BTQQPQRG.js → chunk-AT2MMCOJ.js} +1 -1
- package/dist/{chunk-GOA5Q6TY.js → chunk-AXJAGWZB.js} +1 -1
- package/dist/{chunk-22ZDUL6H.js → chunk-BVOXWAJO.js} +1 -1
- package/dist/chunk-CT7FWSIU.js +3 -0
- package/dist/{chunk-RFISMWRW.js → chunk-DDYGA2DS.js} +1 -1
- package/dist/{chunk-OOFGV7EX.js → chunk-E5YE25QG.js} +1 -1
- package/dist/chunk-E7GQJML3.js +1 -0
- package/dist/{chunk-KJRMGTAO.js → chunk-E7RH43OS.js} +6 -6
- package/dist/{chunk-HQCPGDQE.js → chunk-ET7753IY.js} +1 -1
- package/dist/{chunk-INSKP5ED.js → chunk-EWGE7YTQ.js} +1 -1
- package/dist/chunk-EXVXKJ7Y.js +1 -0
- package/dist/chunk-FCTFHY3X.js +13 -0
- package/dist/chunk-FJX7WDPG.js +1 -0
- package/dist/{chunk-AGK5AUWT.js → chunk-FK55FXKQ.js} +1 -1
- package/dist/chunk-GH4EZK2O.js +1 -0
- package/dist/chunk-GN46S7T4.js +9 -0
- package/dist/{chunk-AFYZYXQ3.js → chunk-GQGGFHX7.js} +1 -1
- package/dist/chunk-GYNZ3OZF.js +1 -0
- package/dist/{chunk-GHWBW2IW.js → chunk-H4LUPIHJ.js} +1 -1
- package/dist/{chunk-YSLKV4XC.js → chunk-HAYO6JYI.js} +1 -1
- package/dist/{chunk-UKOI2Q4J.js → chunk-HFSISGCJ.js} +1 -1
- package/dist/chunk-HRJDKAJ4.js +1 -0
- package/dist/chunk-IC6VZHE6.js +1 -0
- package/dist/chunk-INAZLRY5.js +1 -0
- package/dist/{chunk-XFAFMYIP.js → chunk-IOX4VJPR.js} +2 -2
- package/dist/chunk-J4W6GNCF.js +2 -0
- package/dist/{chunk-PVPYLKB3.js → chunk-J7UZXRTH.js} +1 -1
- package/dist/{chunk-MILLBGCB.js → chunk-JKWB6UXA.js} +1 -1
- package/dist/chunk-JLBQKV4V.js +3 -0
- package/dist/{chunk-UTAETY5Z.js → chunk-JRAXWASH.js} +1 -1
- package/dist/{chunk-5PM4E27L.js → chunk-JRWQJZYQ.js} +1 -1
- package/dist/chunk-KINIFUZQ.js +1 -0
- package/dist/{chunk-E4RGYYBC.js → chunk-LDUJWJ6A.js} +1 -1
- package/dist/chunk-LMCICVE7.js +12 -0
- package/dist/chunk-MHVHGKDK.js +3 -0
- package/dist/chunk-MMM4B3KH.js +1 -0
- package/dist/{chunk-A2LHEUNW.js → chunk-MX34N7Y5.js} +1 -1
- package/dist/{chunk-TBU5SWA4.js → chunk-N26UEHI3.js} +1 -1
- package/dist/{chunk-DSJNZWCZ.js → chunk-N4RJ6CN7.js} +1 -1
- package/dist/{chunk-CPDMJGTC.js → chunk-N7SZWKCU.js} +1 -1
- package/dist/chunk-NRYVMGJN.js +1 -0
- package/dist/{chunk-FE2UWAIP.js → chunk-NSPDDSRR.js} +1 -1
- package/dist/{chunk-WCIX5KWG.js → chunk-O62OLKQL.js} +1 -1
- package/dist/{chunk-ARC4Q3GL.js → chunk-OLJF4K7L.js} +1 -1
- package/dist/{chunk-GRPUY2EZ.js → chunk-OMRH4JXS.js} +1 -1
- package/dist/{chunk-BJSVATVJ.js → chunk-OO2ZRKZY.js} +5 -5
- package/dist/chunk-OUK2AWEI.js +1 -0
- package/dist/chunk-OYAOLJUH.js +1 -0
- package/dist/chunk-PPAHIVG5.js +1 -0
- package/dist/{chunk-MJ5H7RDK.js → chunk-PPIZMPNI.js} +1 -1
- package/dist/chunk-PUX6P5KC.js +2 -0
- package/dist/chunk-QI3FJKGK.js +1 -0
- package/dist/chunk-RRM4HQG4.js +1 -0
- package/dist/{chunk-EZHL527S.js → chunk-S5CQSP35.js} +1 -1
- package/dist/chunk-S5U76YUI.js +1 -0
- package/dist/{chunk-NF2BIEAF.js → chunk-SBGXIPRN.js} +1 -1
- package/dist/chunk-SE3UKA3M.js +25 -0
- package/dist/chunk-SG4UDDHB.js +4 -0
- package/dist/chunk-SPJ73X7A.js +4 -0
- package/dist/chunk-SV73ASP2.js +1 -0
- package/dist/chunk-SWSOEYC7.js +1 -0
- package/dist/{chunk-DCH3QWB7.js → chunk-TTSDPDRH.js} +1 -1
- package/dist/chunk-TWPG3TEQ.js +48 -0
- package/dist/{chunk-AKXGUQ5B.js → chunk-U7BPWFOP.js} +1 -1
- package/dist/{chunk-L54TWWBY.js → chunk-UFQ43KXE.js} +1 -1
- package/dist/chunk-UJ7EPLKC.js +1 -0
- package/dist/{chunk-5N5S3NKS.js → chunk-UQDOOQCM.js} +1 -1
- package/dist/{chunk-G3HODHFN.js → chunk-V4PUYPTU.js} +1 -1
- package/dist/{chunk-SHYP27B7.js → chunk-VALRPZXH.js} +1 -1
- package/dist/{chunk-DPCAPIQB.js → chunk-VLWNSTYW.js} +1 -1
- package/dist/chunk-WSWZPNLG.js +1 -0
- package/dist/{chunk-UA5GEVGW.js → chunk-XAOLOR4Q.js} +1 -1
- package/dist/chunk-XAXEZI6Z.js +1 -0
- package/dist/{chunk-Z4YVOVMM.js → chunk-XFLKW6SJ.js} +1 -1
- package/dist/chunk-YNLHJHUP.js +1 -0
- package/dist/chunk-YP2CBUXH.js +5 -0
- package/dist/{chunk-VYZKKSX4.js → chunk-YUIRDBQQ.js} +1 -1
- package/dist/{chunk-2KJAM2NG.js → chunk-ZA6NXNPR.js} +1 -1
- package/dist/{chunk-UGACB66X.js → chunk-ZZHIBOMT.js} +1 -1
- package/dist/commands/antigravity/backfill.js +1 -1
- package/dist/commands/auth/create-key.js +1 -1
- package/dist/commands/auth/login.js +1 -1
- package/dist/commands/auth/logout.js +1 -1
- package/dist/commands/auth/revoke-key.js +1 -1
- package/dist/commands/auth/signup.js +1 -1
- package/dist/commands/auth/status.js +1 -1
- package/dist/commands/capture/scope.js +1 -1
- package/dist/commands/catalog/index.js +1 -1
- package/dist/commands/catalog/recommend.js +1 -1
- package/dist/commands/crons/list.js +1 -1
- package/dist/commands/crons/run.js +2 -2
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/diagnostics.js +1 -1
- package/dist/commands/eval-targets/create.js +1 -1
- package/dist/commands/eval-targets/list.js +1 -1
- package/dist/commands/evals/agent.js +3 -3
- package/dist/commands/evals/auto-run.js +1 -1
- package/dist/commands/evals/benchmark.js +1 -1
- package/dist/commands/evals/compare.js +1 -1
- package/dist/commands/evals/create.js +1 -1
- package/dist/commands/evals/optimize-description.js +1 -1
- package/dist/commands/evals/optimize.js +1 -1
- package/dist/commands/evals/refine-cases.js +1 -1
- package/dist/commands/evals/run.js +1 -1
- package/dist/commands/evals/validate-judge.js +1 -1
- package/dist/commands/experiments/list.js +1 -1
- package/dist/commands/gateway/ensure.js +1 -1
- package/dist/commands/hooks/emit-instructions.js +1 -1
- package/dist/commands/hooks/install.js +1 -1
- package/dist/commands/hooks/run.js +1 -1
- package/dist/commands/hooks/uninstall.js +1 -1
- package/dist/commands/instructions/create.js +1 -1
- package/dist/commands/instructions/dedupe.js +1 -1
- package/dist/commands/instructions/delete.js +1 -1
- package/dist/commands/instructions/fork.js +1 -1
- package/dist/commands/instructions/get.js +1 -1
- package/dist/commands/instructions/list.js +1 -1
- package/dist/commands/instructions/promote.js +1 -1
- package/dist/commands/instructions/pull.js +1 -1
- package/dist/commands/instructions/restore.js +1 -1
- package/dist/commands/instructions/retract.js +1 -1
- package/dist/commands/instructions/upstream.js +1 -1
- package/dist/commands/instructions/versions.js +1 -1
- package/dist/commands/launcher.js +1 -1
- package/dist/commands/local/down.js +1 -1
- package/dist/commands/local/up.js +1 -1
- package/dist/commands/org/current.js +1 -1
- package/dist/commands/org/list.js +1 -1
- package/dist/commands/org/switch.js +1 -1
- package/dist/commands/packs/add-instruction.js +1 -1
- package/dist/commands/packs/apply.js +1 -1
- package/dist/commands/packs/create.js +1 -1
- package/dist/commands/packs/fork.js +1 -1
- package/dist/commands/packs/get.js +1 -1
- package/dist/commands/packs/list.js +1 -1
- package/dist/commands/packs/remove-instruction.js +1 -1
- package/dist/commands/proxy/env.js +1 -1
- package/dist/commands/proxy/logs.js +1 -1
- package/dist/commands/proxy/restart.js +1 -1
- package/dist/commands/proxy/setup.js +1 -1
- package/dist/commands/proxy/start.js +1 -1
- package/dist/commands/proxy/status.js +1 -1
- package/dist/commands/proxy/stop.js +1 -1
- package/dist/commands/research/backfill.js +1 -1
- package/dist/commands/research/tail-transcript.js +1 -1
- package/dist/commands/service/install.js +18 -18
- package/dist/commands/service/status.js +1 -1
- package/dist/commands/service/uninstall.js +1 -1
- package/dist/commands/sessions/analyze.js +1 -1
- package/dist/commands/sessions/delete.js +1 -1
- package/dist/commands/sessions/end.js +1 -1
- package/dist/commands/sessions/import.js +1 -1
- package/dist/commands/sessions/list.js +1 -1
- package/dist/commands/sessions/start.js +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/skills/export-approved.js +1 -1
- package/dist/commands/skills/install.js +1 -1
- package/dist/commands/skills/list.js +1 -1
- package/dist/commands/skills/publish.js +1 -1
- package/dist/commands/skills/search.js +1 -1
- package/dist/commands/skills/update.js +1 -1
- package/dist/commands/suggestions/accept.js +1 -1
- package/dist/commands/suggestions/auto.js +1 -1
- package/dist/commands/suggestions/dismiss.js +1 -1
- package/dist/commands/suggestions/list.js +1 -1
- package/dist/commands/suggestions/refresh.js +1 -1
- package/dist/commands/suggestions/undo.js +1 -1
- package/dist/dashboard/Dashboard.js +1 -1
- package/dist/dashboard/data.js +1 -1
- package/dist/dashboard/launcher/LauncherOverlay.js +1 -1
- package/dist/dashboard/launcher/api.js +1 -1
- package/dist/dashboard/launcher/useLaunchJobs.js +1 -1
- package/dist/dist-NHS7HY5Q.js +1 -0
- package/dist/harbor/cost_delta.py +377 -0
- package/dist/harbor/mcp_container_shell.py +156 -0
- package/dist/harbor/provenance.py +361 -0
- package/dist/harbor/rm-batch-task/environment/Dockerfile +6 -0
- package/dist/harbor/rm-batch-task/environment/items.jsonl +60 -0
- package/dist/harbor/rm-batch-task/inject.md +8 -0
- package/dist/harbor/rm-batch-task/instruction.md +8 -0
- package/dist/harbor/rm-batch-task/solution/solve.sh +68 -0
- package/dist/harbor/rm-batch-task/task.toml +30 -0
- package/dist/harbor/rm-batch-task/tests/test.sh +23 -0
- package/dist/harbor/rm-batch-task/tests/test_outputs.py +32 -0
- package/dist/harbor/rm-locality-task/environment/Dockerfile +8 -0
- package/dist/harbor/rm-locality-task/environment/workspace/deploy/prod/ingest.yaml +3 -0
- package/dist/harbor/rm-locality-task/environment/workspace/deploy/staging/ingest.yaml +3 -0
- package/dist/harbor/rm-locality-task/environment/workspace/docs/architecture/ingest.md +6 -0
- package/dist/harbor/rm-locality-task/environment/workspace/legacy/ingest_v1/config.yaml +2 -0
- package/dist/harbor/rm-locality-task/environment/workspace/packages/shared/defaults.py +4 -0
- package/dist/harbor/rm-locality-task/environment/workspace/services/ingest/config/limits.defaults.yaml +4 -0
- package/dist/harbor/rm-locality-task/environment/workspace/services/ingest/config/limits.yaml +3 -0
- package/dist/harbor/rm-locality-task/environment/workspace/services/ingest/loader.py +15 -0
- package/dist/harbor/rm-locality-task/environment/workspace/tests/fixtures/ingest_config.yaml +3 -0
- package/dist/harbor/rm-locality-task/inject.md +12 -0
- package/dist/harbor/rm-locality-task/instruction.md +4 -0
- package/dist/harbor/rm-locality-task/solution/solve.sh +14 -0
- package/dist/harbor/rm-locality-task/task.toml +30 -0
- package/dist/harbor/rm-locality-task/tests/test.sh +23 -0
- package/dist/harbor/rm-locality-task/tests/test_outputs.py +19 -0
- package/dist/harbor/rm-migrations-task/environment/Dockerfile +7 -0
- package/dist/harbor/rm-migrations-task/environment/applied.txt +2 -0
- package/dist/harbor/rm-migrations-task/environment/migrations/00001_init.sql +1 -0
- package/dist/harbor/rm-migrations-task/environment/migrations/00002_add_email.sql +1 -0
- package/dist/harbor/rm-migrations-task/inject.md +7 -0
- package/dist/harbor/rm-migrations-task/instruction.md +8 -0
- package/dist/harbor/rm-migrations-task/solution/solve.sh +8 -0
- package/dist/harbor/rm-migrations-task/task.toml +30 -0
- package/dist/harbor/rm-migrations-task/tests/test.sh +23 -0
- package/dist/harbor/rm-migrations-task/tests/test_outputs.py +36 -0
- package/dist/harbor/rm-pagination-task/environment/Dockerfile +6 -0
- package/dist/harbor/rm-pagination-task/environment/store.py +29 -0
- package/dist/harbor/rm-pagination-task/inject.md +8 -0
- package/dist/harbor/rm-pagination-task/instruction.md +5 -0
- package/dist/harbor/rm-pagination-task/solution/solve.sh +7 -0
- package/dist/harbor/rm-pagination-task/task.toml +30 -0
- package/dist/harbor/rm-pagination-task/tests/test.sh +23 -0
- package/dist/harbor/rm-pagination-task/tests/test_outputs.py +14 -0
- package/dist/harbor/rm_host_claude.py +264 -0
- package/dist/harbor/rm_host_codex.py +208 -0
- package/dist/harbor/run.sh +268 -0
- package/dist/harbor/summarize.py +248 -0
- package/dist/lib/active-org-refresh.js +1 -1
- package/dist/lib/agent-loop.js +1 -1
- package/dist/lib/antigravity-transcript.js +1 -1
- package/dist/lib/api-client.js +1 -1
- package/dist/lib/capture-scope-config.js +1 -1
- package/dist/lib/codex-capture-scope.js +1 -1
- package/dist/lib/codex-exec.js +1 -1
- package/dist/lib/ensure-api-key.js +1 -1
- package/dist/lib/ensure-fresh-token.js +1 -1
- package/dist/lib/eval-accumulation.js +1 -0
- package/dist/lib/eval-analyzer.js +1 -1
- package/dist/lib/eval-benchmark.js +1 -1
- package/dist/lib/eval-candidate-run.js +1 -1
- package/dist/lib/eval-case-generator.js +1 -1
- package/dist/lib/eval-conversation-handler.js +1 -1
- package/dist/lib/eval-executor.js +1 -1
- package/dist/lib/eval-grader.js +1 -1
- package/dist/lib/eval-meta-judge.js +1 -1
- package/dist/lib/eval-resume.js +1 -1
- package/dist/lib/eval-run-batch.js +1 -1
- package/dist/lib/eval-trigger-tester.js +1 -1
- package/dist/lib/evolution-enrolment.js +1 -1
- package/dist/lib/harbor-readiness.js +1 -0
- package/dist/lib/hooks-capture-gate.js +1 -1
- package/dist/lib/insights/generation/installed-asks.js +1 -1
- package/dist/lib/insights/nomination/nominate.js +1 -1
- package/dist/lib/instruction-proposer.js +1 -1
- package/dist/lib/instruction-snapshot.js +1 -1
- package/dist/lib/instructions/acceptance/accept-events.js +1 -1
- package/dist/lib/instructions/acceptance/accept-hook.js +1 -1
- package/dist/lib/instructions/acceptance/agent-instruction-files.js +1 -0
- package/dist/lib/instructions/acceptance/hook-install.js +1 -1
- package/dist/lib/instructions/acceptance/reverse-applications.js +1 -1
- package/dist/lib/instructions/acceptance/suggestion-accept.js +1 -1
- package/dist/lib/jobs/handlers/cron-auto-accept-suggestions.js +1 -1
- package/dist/lib/jobs/handlers/cron-eval-autorun.js +1 -1
- package/dist/lib/jobs/handlers/cron-instruction-evolution.js +1 -1
- package/dist/lib/jobs/handlers/cron-instruction-training.js +1 -1
- package/dist/lib/jobs/handlers/cron-reconcile-attribution.js +1 -1
- package/dist/lib/jobs/handlers/cron-refine-cases.js +1 -1
- package/dist/lib/jobs/handlers/cron-refresh-pricing.js +1 -1
- package/dist/lib/jobs/handlers/cron-retire-unused-skills.js +1 -1
- package/dist/lib/jobs/handlers/cron-suggest-instructions.js +1 -1
- package/dist/lib/jobs/handlers/distill-memories.js +1 -1
- package/dist/lib/jobs/handlers/harbor-ab-verdict.js +1 -0
- package/dist/lib/jobs/handlers/harbor-ab.js +1 -0
- package/dist/lib/jobs/handlers/process-changelog.js +1 -1
- package/dist/lib/jobs/handlers/process-conversation.js +1 -1
- package/dist/lib/jobs/handlers/process-eval.js +1 -1
- package/dist/lib/jobs/handlers/process-insights.js +1 -1
- package/dist/lib/jobs/handlers/process-judge-review.js +1 -1
- package/dist/lib/jobs/handlers/process-launch.js +1 -1
- package/dist/lib/jobs/handlers/process-memory-ab.js +1 -1
- package/dist/lib/jobs/handlers/process-run-cleanup.js +1 -1
- package/dist/lib/jobs/handlers/process-session-goal.js +1 -1
- package/dist/lib/jobs/shed-retry.js +1 -0
- package/dist/lib/judge-review-runner.js +1 -1
- package/dist/lib/llm-client.js +1 -1
- package/dist/lib/llm-json.js +1 -0
- package/dist/lib/llm-reply-parsers.js +1 -0
- package/dist/lib/machine-pause-detector.js +1 -1
- package/dist/lib/manual-tasks-meta.js +1 -1
- package/dist/lib/manual-tasks.js +1 -1
- package/dist/lib/measurement-llm.js +1 -0
- package/dist/lib/pi-exec.js +1 -0
- package/dist/lib/projects/workspace/eval-workspace.js +1 -1
- package/dist/lib/projects/workspace/worktree.js +1 -1
- package/dist/lib/refine-cases.js +1 -1
- package/dist/lib/research-client.js +1 -1
- package/dist/lib/retraction.js +1 -1
- package/dist/lib/reviewer-detect.js +1 -1
- package/dist/lib/service-environment.js +1 -0
- package/dist/lib/setup-steps.js +1 -1
- package/dist/lib/telemetry.js +1 -1
- package/dist/lib/terminal-target-heartbeat.js +1 -1
- package/dist/lib/tool-bin.js +1 -0
- package/dist/lib/tool-cli-map.js +1 -0
- package/dist/lib/transcript-instruction-linking.js +1 -0
- package/dist/lib/transcript-sync.js +1 -1
- package/dist/lib/work-report.js +1 -0
- package/oclif.manifest.json +17 -4
- package/package.json +10 -8
- package/dist/chunk-3DZD322E.js +0 -1
- package/dist/chunk-5OX6YKTR.js +0 -5
- package/dist/chunk-APXQLQZY.js +0 -1
- package/dist/chunk-C6GLNE6X.js +0 -1
- package/dist/chunk-CNQ5DTNC.js +0 -1
- package/dist/chunk-E7XOYGL5.js +0 -20
- package/dist/chunk-EVD5A3VI.js +0 -1
- package/dist/chunk-FUYR7CFD.js +0 -1
- package/dist/chunk-GN3ZHQ46.js +0 -1
- package/dist/chunk-GWDZNGUP.js +0 -2
- package/dist/chunk-LUCVMLHV.js +0 -1
- package/dist/chunk-O65AQZYB.js +0 -1
- package/dist/chunk-PWTTCSQA.js +0 -1
- package/dist/chunk-QBNLZJIR.js +0 -6
- package/dist/chunk-QXPYGN7I.js +0 -1
- package/dist/chunk-RIVEBBZC.js +0 -3
- package/dist/chunk-RLEMDKQ4.js +0 -12
- package/dist/chunk-S2SKT5PV.js +0 -1
- package/dist/chunk-TZA4HAVF.js +0 -1
- package/dist/chunk-UUFCTEBT.js +0 -1
- package/dist/chunk-UXB6DRR7.js +0 -3
- package/dist/chunk-W23MESHM.js +0 -1
- package/dist/chunk-W67DO5PT.js +0 -1
- package/dist/chunk-WMIUFMDI.js +0 -48
- package/dist/chunk-WO6MS654.js +0 -13
- package/dist/chunk-WTMJSL7R.js +0 -1
- package/dist/dist-XEWF3R3N.js +0 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Harbor agent that runs the USER'S OWN Claude Code on the host.
|
|
2
|
+
|
|
3
|
+
No API key or token: the host `claude` CLI uses the user's existing
|
|
4
|
+
subscription login (macOS keychain). The agent downloads the task's /app from
|
|
5
|
+
the environment, runs `claude -p <instruction>` in a host temp dir, and
|
|
6
|
+
uploads the result back for in-container verification.
|
|
7
|
+
|
|
8
|
+
Instruction delivery (the RuleMetric with-arm) via agent kwargs — all
|
|
9
|
+
host-side; the container never sees the injected files:
|
|
10
|
+
|
|
11
|
+
--ak claude_md=/abs/path/CLAUDE.md claude_md delivery (workdir root)
|
|
12
|
+
--ak skills_dirs=/abs/a,/abs/b skill delivery (.claude/skills/<name>)
|
|
13
|
+
--ak append_system_prompt="..." prompt delivery (--append-system-prompt)
|
|
14
|
+
--ak container_shell=true route ALL shell commands into the
|
|
15
|
+
task container via the MCP proxy
|
|
16
|
+
(mcp_container_shell.py); host
|
|
17
|
+
Bash tool is disabled
|
|
18
|
+
|
|
19
|
+
container_shell requires the local docker environment (container resolved by
|
|
20
|
+
compose labels) and imports two harbor internals pinned to 0.22.x:
|
|
21
|
+
_sanitize_docker_compose_project_name and MAIN_SERVICE_NAME.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import provenance
|
|
34
|
+
|
|
35
|
+
from harbor.agents.base import BaseAgent
|
|
36
|
+
from harbor.environments.base import BaseEnvironment
|
|
37
|
+
from harbor.models.agent.context import AgentContext
|
|
38
|
+
|
|
39
|
+
_CONTAINER_SHELL_PROMPT = (
|
|
40
|
+
"Your shell is the task container, not this machine: run every shell "
|
|
41
|
+
"command with the mcp__container__bash tool (the host Bash tool is "
|
|
42
|
+
"disabled). The container's /app is your current working directory: "
|
|
43
|
+
"when using file tools, always use paths RELATIVE to the working "
|
|
44
|
+
"directory (never absolute /app/... paths) — they are synced into the "
|
|
45
|
+
"container's /app before each command runs and when you finish. "
|
|
46
|
+
"Alternatively, write files directly through the container shell."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HostClaudeCode(BaseAgent):
|
|
51
|
+
@staticmethod
|
|
52
|
+
def name() -> str:
|
|
53
|
+
return "host-claude-code"
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
logs_dir: Path,
|
|
58
|
+
model_name: str | None = None,
|
|
59
|
+
claude_md: str | None = None,
|
|
60
|
+
skills_dirs: str | None = None,
|
|
61
|
+
append_system_prompt: str | None = None,
|
|
62
|
+
container_shell: str | bool | None = None,
|
|
63
|
+
agent_timeout_sec: float | None = None,
|
|
64
|
+
**kwargs,
|
|
65
|
+
):
|
|
66
|
+
super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
|
|
67
|
+
self._claude_md = claude_md
|
|
68
|
+
self._skills_dirs = [s for s in (skills_dirs or "").split(",") if s]
|
|
69
|
+
self._append_system_prompt = append_system_prompt
|
|
70
|
+
self._container_shell = str(container_shell).lower() in ("true", "1", "yes")
|
|
71
|
+
self._timeout = agent_timeout_sec or 600.0
|
|
72
|
+
|
|
73
|
+
def version(self) -> str:
|
|
74
|
+
return "0.2.0"
|
|
75
|
+
|
|
76
|
+
def _delivery(self) -> list[str]:
|
|
77
|
+
"""Which delivery channels this arm actually used, for the stamp."""
|
|
78
|
+
used = []
|
|
79
|
+
if self._claude_md:
|
|
80
|
+
used.append("claude_md")
|
|
81
|
+
if self._skills_dirs:
|
|
82
|
+
used.append("skills_dirs")
|
|
83
|
+
if self._append_system_prompt:
|
|
84
|
+
used.append("append_system_prompt")
|
|
85
|
+
if self._container_shell:
|
|
86
|
+
used.append("container_shell")
|
|
87
|
+
return used
|
|
88
|
+
|
|
89
|
+
async def setup(self, environment: BaseEnvironment) -> None:
|
|
90
|
+
if shutil.which("claude") is None:
|
|
91
|
+
raise RuntimeError("host `claude` CLI not found on PATH")
|
|
92
|
+
|
|
93
|
+
def _resolve_container_id(self, environment: BaseEnvironment) -> str:
|
|
94
|
+
# Pinned harbor 0.22.x internals; the compose labels are stable Docker
|
|
95
|
+
# behavior, only the project-name derivation is harbor's.
|
|
96
|
+
from harbor.constants import MAIN_SERVICE_NAME
|
|
97
|
+
from harbor.environments.docker.docker import (
|
|
98
|
+
_sanitize_docker_compose_project_name,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
project = _sanitize_docker_compose_project_name(environment.session_id)
|
|
102
|
+
proc = subprocess.run(
|
|
103
|
+
[
|
|
104
|
+
"docker",
|
|
105
|
+
"ps",
|
|
106
|
+
"-q",
|
|
107
|
+
"--filter",
|
|
108
|
+
f"label=com.docker.compose.project={project}",
|
|
109
|
+
"--filter",
|
|
110
|
+
f"label=com.docker.compose.service={MAIN_SERVICE_NAME}",
|
|
111
|
+
],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=True,
|
|
115
|
+
)
|
|
116
|
+
container_id = proc.stdout.strip().splitlines()
|
|
117
|
+
if len(container_id) != 1:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
f"expected exactly one container for project {project}, "
|
|
120
|
+
f"got {container_id!r} (container_shell needs the local "
|
|
121
|
+
"docker environment)"
|
|
122
|
+
)
|
|
123
|
+
return container_id[0]
|
|
124
|
+
|
|
125
|
+
def _inject(self, workdir: Path) -> list[Path]:
|
|
126
|
+
"""Copy instruction artifacts into the workdir; return paths to strip
|
|
127
|
+
before the final upload so they never count as agent output."""
|
|
128
|
+
injected: list[Path] = []
|
|
129
|
+
if self._claude_md:
|
|
130
|
+
target = workdir / "CLAUDE.md"
|
|
131
|
+
shutil.copyfile(self._claude_md, target)
|
|
132
|
+
injected.append(target)
|
|
133
|
+
if self._skills_dirs:
|
|
134
|
+
skills_root = workdir / ".claude"
|
|
135
|
+
for src in self._skills_dirs:
|
|
136
|
+
src_path = Path(src)
|
|
137
|
+
shutil.copytree(
|
|
138
|
+
src_path,
|
|
139
|
+
skills_root / "skills" / src_path.name,
|
|
140
|
+
dirs_exist_ok=True,
|
|
141
|
+
)
|
|
142
|
+
injected.append(skills_root)
|
|
143
|
+
return injected
|
|
144
|
+
|
|
145
|
+
def _build_command(self, instruction: str, environment: BaseEnvironment,
|
|
146
|
+
workdir: Path) -> list[str]:
|
|
147
|
+
cmd = [
|
|
148
|
+
"claude",
|
|
149
|
+
"-p",
|
|
150
|
+
instruction,
|
|
151
|
+
"--permission-mode",
|
|
152
|
+
"acceptEdits",
|
|
153
|
+
"--output-format",
|
|
154
|
+
"json",
|
|
155
|
+
]
|
|
156
|
+
if self.model_name:
|
|
157
|
+
cmd += ["--model", self.model_name.split("/")[-1]]
|
|
158
|
+
if not self._container_shell:
|
|
159
|
+
# acceptEdits auto-approves file edits but NOT Bash, and a task that
|
|
160
|
+
# invites "write a script, then run it" stalls at "please approve the
|
|
161
|
+
# Bash command" and uploads nothing -- measured 2026-08-31: 12 of 15
|
|
162
|
+
# rm-batch-task with-arm trials died exactly there, reading as rule
|
|
163
|
+
# non-compliance. The workdir is a per-trial temp dir; Bash there is
|
|
164
|
+
# what any real session has. (The container path keeps Bash disallowed
|
|
165
|
+
# by design -- the MCP container shell replaces it.)
|
|
166
|
+
cmd += ["--allowedTools", "Bash"]
|
|
167
|
+
|
|
168
|
+
system_prompt_parts = []
|
|
169
|
+
if self._container_shell:
|
|
170
|
+
container_id = self._resolve_container_id(environment)
|
|
171
|
+
mcp_config = {
|
|
172
|
+
"mcpServers": {
|
|
173
|
+
"container": {
|
|
174
|
+
"command": sys.executable,
|
|
175
|
+
"args": [str(Path(__file__).parent / "mcp_container_shell.py")],
|
|
176
|
+
"env": {
|
|
177
|
+
"RM_CONTAINER_ID": container_id,
|
|
178
|
+
"RM_WORKDIR": str(workdir),
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
mcp_path = self.logs_dir / "mcp-config.json"
|
|
184
|
+
mcp_path.write_text(json.dumps(mcp_config))
|
|
185
|
+
cmd += [
|
|
186
|
+
"--mcp-config",
|
|
187
|
+
str(mcp_path),
|
|
188
|
+
"--strict-mcp-config",
|
|
189
|
+
"--disallowedTools",
|
|
190
|
+
"Bash",
|
|
191
|
+
"--allowedTools",
|
|
192
|
+
"mcp__container__bash",
|
|
193
|
+
]
|
|
194
|
+
system_prompt_parts.append(_CONTAINER_SHELL_PROMPT)
|
|
195
|
+
if self._append_system_prompt:
|
|
196
|
+
system_prompt_parts.append(self._append_system_prompt)
|
|
197
|
+
if system_prompt_parts:
|
|
198
|
+
cmd += ["--append-system-prompt", "\n\n".join(system_prompt_parts)]
|
|
199
|
+
return cmd
|
|
200
|
+
|
|
201
|
+
async def run(
|
|
202
|
+
self,
|
|
203
|
+
instruction: str,
|
|
204
|
+
environment: BaseEnvironment,
|
|
205
|
+
context: AgentContext,
|
|
206
|
+
) -> None:
|
|
207
|
+
workdir = Path(tempfile.mkdtemp(prefix="harbor-host-claude-"))
|
|
208
|
+
try:
|
|
209
|
+
await environment.download_dir("/app", workdir)
|
|
210
|
+
injected = self._inject(workdir)
|
|
211
|
+
cmd = self._build_command(instruction, environment, workdir)
|
|
212
|
+
|
|
213
|
+
env = {
|
|
214
|
+
k: v
|
|
215
|
+
for k, v in os.environ.items()
|
|
216
|
+
# Drop nested-session markers so the child runs as a fresh CLI.
|
|
217
|
+
if not k.startswith(("CLAUDECODE", "CLAUDE_CODE_"))
|
|
218
|
+
}
|
|
219
|
+
proc = await asyncio.create_subprocess_exec(
|
|
220
|
+
*cmd,
|
|
221
|
+
cwd=workdir,
|
|
222
|
+
env=env,
|
|
223
|
+
stdout=asyncio.subprocess.PIPE,
|
|
224
|
+
stderr=asyncio.subprocess.PIPE,
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
stdout, stderr = await asyncio.wait_for(
|
|
228
|
+
proc.communicate(), timeout=self._timeout
|
|
229
|
+
)
|
|
230
|
+
except asyncio.TimeoutError:
|
|
231
|
+
proc.kill()
|
|
232
|
+
raise
|
|
233
|
+
(self.logs_dir / "claude-output.json").write_bytes(stdout)
|
|
234
|
+
(self.logs_dir / "claude-stderr.txt").write_bytes(stderr)
|
|
235
|
+
# Written for EVERY trial, including one about to raise below: the
|
|
236
|
+
# provenance of a failed trial is exactly what tells a later reader
|
|
237
|
+
# whether the failure was the rule or the harness (#415).
|
|
238
|
+
provenance.write_stamp(
|
|
239
|
+
self.logs_dir,
|
|
240
|
+
dict(
|
|
241
|
+
provenance.stamp(
|
|
242
|
+
cmd,
|
|
243
|
+
observed_model=provenance.observed_model_from_claude(stdout),
|
|
244
|
+
engine="claude",
|
|
245
|
+
engine_version=provenance.cli_version("claude"),
|
|
246
|
+
env=env,
|
|
247
|
+
delivery=self._delivery(),
|
|
248
|
+
),
|
|
249
|
+
agent_version=self.version(),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
if proc.returncode != 0:
|
|
253
|
+
raise RuntimeError(
|
|
254
|
+
f"claude exited {proc.returncode}: {stderr.decode()[:2000]}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
for path in injected:
|
|
258
|
+
if path.is_dir():
|
|
259
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
260
|
+
else:
|
|
261
|
+
path.unlink(missing_ok=True)
|
|
262
|
+
await environment.upload_dir(workdir, "/app")
|
|
263
|
+
finally:
|
|
264
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Harbor agent that runs the USER'S OWN Codex CLI on the host.
|
|
2
|
+
|
|
3
|
+
The cross-vendor twin of rm_host_claude.py. No API key: the host `codex` CLI
|
|
4
|
+
uses the user's existing ChatGPT login. The agent downloads the task's /app,
|
|
5
|
+
runs `codex exec` in a host temp dir, and uploads the result back for
|
|
6
|
+
in-container verification.
|
|
7
|
+
|
|
8
|
+
Why a separate agent rather than a flag on the claude one: the two CLIs differ
|
|
9
|
+
in the three things that matter to an A/B arm — which instruction file the
|
|
10
|
+
agent reads (AGENTS.md vs CLAUDE.md), whether there is a system-prompt channel
|
|
11
|
+
(codex has none; the preamble is prepended to the instruction), and how the
|
|
12
|
+
final message comes back (a file via -o, not a JSON blob on stdout). Papering
|
|
13
|
+
over that with `if engine == "codex"` inside one agent is how an arm silently
|
|
14
|
+
delivers an instruction the agent never reads and the trial still reports a
|
|
15
|
+
clean result.
|
|
16
|
+
|
|
17
|
+
Instruction delivery (the RuleMetric with-arm) via agent kwargs:
|
|
18
|
+
|
|
19
|
+
--ak agents_md=/abs/path/AGENTS.md instruction delivery (workdir root)
|
|
20
|
+
--ak append_system_prompt="..." prepended to the instruction
|
|
21
|
+
|
|
22
|
+
Flags verified against codex-cli 0.144.0. `codex -q` was removed from the CLI
|
|
23
|
+
in an earlier version and left a mapping that could never succeed, so every
|
|
24
|
+
flag here was probed against the installed binary rather than remembered.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import tempfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import provenance
|
|
34
|
+
|
|
35
|
+
from harbor.agents.base import BaseAgent
|
|
36
|
+
from harbor.environments.base import BaseEnvironment
|
|
37
|
+
from harbor.models.agent.context import AgentContext
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HostCodex(BaseAgent):
|
|
41
|
+
@staticmethod
|
|
42
|
+
def name() -> str:
|
|
43
|
+
return "host-codex"
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
logs_dir: Path,
|
|
48
|
+
model_name: str | None = None,
|
|
49
|
+
agents_md: str | None = None,
|
|
50
|
+
append_system_prompt: str | None = None,
|
|
51
|
+
skills_dirs: str | None = None,
|
|
52
|
+
container_shell: str | bool | None = None,
|
|
53
|
+
agent_timeout_sec: float | None = None,
|
|
54
|
+
**kwargs,
|
|
55
|
+
):
|
|
56
|
+
super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
|
|
57
|
+
self._agents_md = agents_md
|
|
58
|
+
self._append_system_prompt = append_system_prompt
|
|
59
|
+
self._timeout = agent_timeout_sec or 600.0
|
|
60
|
+
|
|
61
|
+
# Refuse, loudly, rather than ignore. An arm that asked for skill
|
|
62
|
+
# delivery or a container shell and got neither is not a weaker arm —
|
|
63
|
+
# it is a DIFFERENT experiment than the one the operator described, and
|
|
64
|
+
# its result would be compared against a claude arm that did get them.
|
|
65
|
+
if skills_dirs:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"skills_dirs is Claude-Code-specific (.claude/skills) and has no "
|
|
68
|
+
"codex equivalent; deliver the same guidance via --ak agents_md "
|
|
69
|
+
"or --ak append_system_prompt so the arms stay comparable."
|
|
70
|
+
)
|
|
71
|
+
if str(container_shell).lower() in ("true", "1", "yes"):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"container_shell is not implemented for the codex agent (it needs "
|
|
74
|
+
"codex's own MCP config shape, unverified here). Run the codex arm "
|
|
75
|
+
"without it, or use the claude agent for container-shell trials."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def version(self) -> str:
|
|
79
|
+
return "0.1.0"
|
|
80
|
+
|
|
81
|
+
def _delivery(self) -> list[str]:
|
|
82
|
+
"""Which delivery channels this arm actually used, for the stamp."""
|
|
83
|
+
used = []
|
|
84
|
+
if self._agents_md:
|
|
85
|
+
used.append("agents_md")
|
|
86
|
+
if self._append_system_prompt:
|
|
87
|
+
used.append("append_system_prompt")
|
|
88
|
+
return used
|
|
89
|
+
|
|
90
|
+
async def setup(self, environment: BaseEnvironment) -> None:
|
|
91
|
+
if shutil.which("codex") is None:
|
|
92
|
+
raise RuntimeError("host `codex` CLI not found on PATH")
|
|
93
|
+
|
|
94
|
+
def _inject(self, workdir: Path) -> list[Path]:
|
|
95
|
+
"""Copy instruction artifacts into the workdir; return paths to strip
|
|
96
|
+
before the final upload so they never count as agent output."""
|
|
97
|
+
injected: list[Path] = []
|
|
98
|
+
if self._agents_md:
|
|
99
|
+
target = workdir / "AGENTS.md"
|
|
100
|
+
shutil.copyfile(self._agents_md, target)
|
|
101
|
+
injected.append(target)
|
|
102
|
+
return injected
|
|
103
|
+
|
|
104
|
+
def _build_command(self, last_message_file: Path) -> list[str]:
|
|
105
|
+
cmd = [
|
|
106
|
+
"codex",
|
|
107
|
+
"exec",
|
|
108
|
+
# JSONL on stdout so the run has token telemetry at all. Without it
|
|
109
|
+
# codex-output.txt was plain progress text with no usage envelope,
|
|
110
|
+
# every codex arm took summarize.py's null-cost branch, and a codex
|
|
111
|
+
# A/B was indistinguishable from a free run. The ANSWER still comes
|
|
112
|
+
# from --output-last-message; stdout is parsed only for usage.
|
|
113
|
+
"--json",
|
|
114
|
+
# The agent must be able to WRITE — this is a task run, not the
|
|
115
|
+
# read-only analysis path in codex-exec.ts.
|
|
116
|
+
"--sandbox",
|
|
117
|
+
"workspace-write",
|
|
118
|
+
# The per-trial workdir is a temp dir, not a git repo.
|
|
119
|
+
"--skip-git-repo-check",
|
|
120
|
+
# Keep synthetic benchmark trials out of ~/.codex/sessions: they are
|
|
121
|
+
# not the user's work, and letting them land there would contaminate
|
|
122
|
+
# the very session history RuleMetric measures.
|
|
123
|
+
"--ephemeral",
|
|
124
|
+
"--color",
|
|
125
|
+
"never",
|
|
126
|
+
"--output-last-message",
|
|
127
|
+
str(last_message_file),
|
|
128
|
+
]
|
|
129
|
+
if self.model_name:
|
|
130
|
+
cmd += ["--model", self.model_name.split("/")[-1]]
|
|
131
|
+
# Prompt on stdin: instructions embed whole task descriptions and argv
|
|
132
|
+
# has an ARG_MAX ceiling.
|
|
133
|
+
cmd.append("-")
|
|
134
|
+
return cmd
|
|
135
|
+
|
|
136
|
+
async def run(
|
|
137
|
+
self,
|
|
138
|
+
instruction: str,
|
|
139
|
+
environment: BaseEnvironment,
|
|
140
|
+
context: AgentContext,
|
|
141
|
+
) -> None:
|
|
142
|
+
workdir = Path(tempfile.mkdtemp(prefix="harbor-host-codex-"))
|
|
143
|
+
last_message = self.logs_dir / "codex-last-message.txt"
|
|
144
|
+
try:
|
|
145
|
+
await environment.download_dir("/app", workdir)
|
|
146
|
+
injected = self._inject(workdir)
|
|
147
|
+
|
|
148
|
+
# Codex has no --append-system-prompt channel, so the preamble is
|
|
149
|
+
# prepended. Same delivery as the eval path in codex-exec.ts.
|
|
150
|
+
prompt = instruction
|
|
151
|
+
if self._append_system_prompt:
|
|
152
|
+
prompt = f"{self._append_system_prompt}\n\n{instruction}"
|
|
153
|
+
|
|
154
|
+
cmd = self._build_command(last_message)
|
|
155
|
+
env = {
|
|
156
|
+
k: v
|
|
157
|
+
for k, v in os.environ.items()
|
|
158
|
+
# Drop nested-session markers so the child runs as a fresh CLI,
|
|
159
|
+
# and never route a trial through the capture proxy.
|
|
160
|
+
if not k.startswith(("CLAUDECODE", "CLAUDE_CODE_"))
|
|
161
|
+
and k not in ("HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy")
|
|
162
|
+
}
|
|
163
|
+
proc = await asyncio.create_subprocess_exec(
|
|
164
|
+
*cmd,
|
|
165
|
+
cwd=workdir,
|
|
166
|
+
env=env,
|
|
167
|
+
stdin=asyncio.subprocess.PIPE,
|
|
168
|
+
stdout=asyncio.subprocess.PIPE,
|
|
169
|
+
stderr=asyncio.subprocess.PIPE,
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
stdout, stderr = await asyncio.wait_for(
|
|
173
|
+
proc.communicate(prompt.encode()), timeout=self._timeout
|
|
174
|
+
)
|
|
175
|
+
except asyncio.TimeoutError:
|
|
176
|
+
proc.kill()
|
|
177
|
+
raise
|
|
178
|
+
(self.logs_dir / "codex-output.txt").write_bytes(stdout)
|
|
179
|
+
(self.logs_dir / "codex-stderr.txt").write_bytes(stderr)
|
|
180
|
+
# codex states no modelUsage envelope, so the stamp records the
|
|
181
|
+
# REQUESTED model and says so (model_source: 'requested') rather
|
|
182
|
+
# than presenting a flag as an observation.
|
|
183
|
+
provenance.write_stamp(
|
|
184
|
+
self.logs_dir,
|
|
185
|
+
dict(
|
|
186
|
+
provenance.stamp(
|
|
187
|
+
cmd,
|
|
188
|
+
engine="codex",
|
|
189
|
+
engine_version=provenance.cli_version("codex"),
|
|
190
|
+
env=env,
|
|
191
|
+
delivery=self._delivery(),
|
|
192
|
+
),
|
|
193
|
+
agent_version=self.version(),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
if proc.returncode != 0:
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
f"codex exited {proc.returncode}: {stderr.decode()[:2000]}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
for path in injected:
|
|
202
|
+
if path.is_dir():
|
|
203
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
204
|
+
else:
|
|
205
|
+
path.unlink(missing_ok=True)
|
|
206
|
+
await environment.upload_dir(workdir, "/app")
|
|
207
|
+
finally:
|
|
208
|
+
shutil.rmtree(workdir, ignore_errors=True)
|