feat(harbor): bind-mount task.yaml/plugins/templates/extra_mounts, skip Dockerfile when unneeded, translate pre_run - #175
Merged
Conversation
akshaylive
requested review from
CarlesUIPath,
bai-uipath,
tmatup and
uipreliga
as code owners
September 15, 2026 00:08
akshaylive
force-pushed
the
akshaya/mount_plugins
branch
from
September 15, 2026 16:11
6a8d6fd to
ae4d4a6
Compare
…tead of COPY, skip Dockerfile when unneeded, translate pre_run Harbor task exports previously left agent.plugins[] (e.g. a skill plugin directory) and sandbox.docker.extra_mounts (e.g. UiPath CLI credentials) unavailable inside the container -- docker_runner.py auto-mounts these for a normal docker sandbox run, but the Harbor exporter had no equivalent, so an exported agent ran with no skill content and no credentials. - environment/docker-compose.yaml now bind-mounts environment/task.yaml itself, each `type: local` agent.plugins[] dir, each TemplateDirSource in sandbox.template_sources, and each sandbox.docker.extra_mounts entry (its own ro/rw mode preserved) -- all at their own host path, so nothing needs COPYing into the image or rewriting in task.yaml. Mount paths are always emitted POSIX-style (as_posix()) since docker-compose volume specs are POSIX regardless of the host OS running the exporter. - environment/Dockerfile is now written only when sandbox.docker.dockerfile_path is set (real RUN build steps needed); otherwise task.toml's [environment].docker_image points Harbor straight at the pre-built image via its own should_use_prebuilt_docker_image path, and no Dockerfile is written at all. - pre_run commands are now translated into the agent-phase task.yaml -- they run before the agent starts, which coder-eval execute still does for the CoderEvalAgent embed, so this was a fixable gap rather than a fundamental limitation (unlike post_run, which belongs to the grading phase execute never runs, and stays untranslated with a warning). Verified end-to-end against a real harbor install: exported and ran two tasks (uipath-admin audit smoke, uipath-agents antipattern_openai_agents_hitl) through `harbor run -a coder_eval.harbor.agent:CoderEvalAgent`, both scoring 1.0 after these fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
akshaylive
force-pushed
the
akshaya/mount_plugins
branch
from
September 15, 2026 16:59
ae4d4a6 to
83eea1e
Compare
uipreliga
added a commit
that referenced
this pull request
Sep 16, 2026
The bind-mount rewrite (#175) landed on main while this branch was open, so its new prose in `harbor/packager.py` never met the gate this branch introduces. Two docstrings were over the 150-word bar. - The module's emitted-layout diagram carried an eight-line annotation restating what `_write_environment` and `_write_docker_compose_mounts` already say. It is three lines now: Dockerfile only when `dockerfile_path` is set, and everything bind-mounted rather than COPY'd. - `_plugin_volume_specs` keeps its contract and its one real HAZARD -- a plugin path is carried UNEXPANDED and `docker_runner.py` expands it at launch, so the two sides must move together. The rest is a pointer. The rationale moves to `reporting.md § What the export carries, and what it refuses to carry`: why a prebuilt image needs no Dockerfile (Harbor's own `should_use_prebuilt_docker_image`, and `[environment].workdir` reaching `docker exec` independently of the build), why nothing is COPY'd, why a bind mount cannot self-nest into the export's `-o` directory, and the live `harbor run` that surfaced the agentless `initial_prompt` validation error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
uipreliga
added a commit
that referenced
this pull request
Sep 16, 2026
…it (#177) * feat(lint): 1/7 — prose budget ratchet and one home for rationale Adds `tests/lint/prose_budget.py`: a stdlib-only measurement of the essay-shaped prose in `src/coder_eval` — docstrings over 150 words (Typer commands exempt) plus comment runs of 3+ lines — gated against a module baseline of 79,754 words. It also resolves every `Rationale: <path> § <heading>` pointer, and under `--assert-code-unchanged <ref>` proves a commit moved prose only, by comparing the docstring-stripped AST and the multiset of functional directive comments. The gate is wired in both seams: `make docs-budget` / `make verify`, and the `quality-gate` job in pr-checks.yml — CI restates every step and never invokes `make`, so the Makefile line alone would gate nothing on a PR. Relocates the 12 sections of `.claude/architecture-notes.md` verbatim into `.claude/notes/<subsystem>.md` and deletes it, so rationale has exactly one home and the tree has exactly one index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcdMjPKFc2wdg4J6Ezg4E2 * docs: 2/7 — move timing and permissions rationale into .claude/notes Cuts the four densest files in the tree from 6,874 essay words to 533, leaving the caller-facing contract and the couplings a future editor would break, and moving the design rationale to `.claude/notes/{timing,permissions,persistence}.md`. Kept in the source: the `timing.ts` parity claim and its shared fixture, `TurnClock`'s not-for-deadlines hazard, the raw-total ordering hazard and its division guard, the stacked-window contract, and every CE reference whose text stayed. Deleted: history git already holds, and `TurnClock`'s per-harness roster, which `docs/agents/HARNESS_PARITY.md` owns. Also corrects a stale relocated claim: the notes said the task directory was not shielded, but `Orchestrator._communicate_with_retry` chmods it alongside the reference. No executable statement changed — proved per file by `prose_budget --assert-code-unchanged`, which compares the docstring-stripped AST and the functional directive comments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcdMjPKFc2wdg4J6Ezg4E2 * docs: 3/7 — move agent-adapter rationale into .claude/notes Cuts the five harness adapters and their eight helpers from 17,227 essay words to 4,456, and gives `.claude/notes/agents.md` the shared story the adapters kept telling five times over: the turn lifecycle, first-window seeding, per-harness generation marks, token accounting and cost resolution, why a clean exit can still be a crash, and how each CLI is reaped. Every per-harness parity claim either stays in the adapter or is already a row in docs/agents/HARNESS_PARITY.md, which the taxonomy makes the SSOT for cross-harness facts. Both SDK#24168 FIXMEs stay FIXMEs. No executable statement changed — proved per file by `prose_budget --assert-code-unchanged`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcdMjPKFc2wdg4J6Ezg4E2 * docs: 4/7 — move orchestration rationale into .claude/notes Cuts orchestrator.py and orchestration/ from 17,724 essay words to 3,980, extending the four sections Phase 1 relocated into orchestration.md rather than opening rival headings for the same topics: the terminal-status chain and the four grading sites under execute-vs-run, the fired-only gate and the ceiling/floor bounds under early stop. New sections cover what the orchestrator alone owns — recording the task as authored, the three separately-resolved routes, interrupt-proof teardown, restoring a PATH from an untrusted run directory, the dialog loop, embedded commands and experiment resolution. regrade's container rationale went to isolation.md, where detached grading already lives. `prose_budget` now resolves a pointer against `##` or `###`, so appending to an existing section does not force the pointer up to its parent. No executable statement changed — proved by `prose_budget --assert-code-unchanged`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 5/7 — move isolation, sandbox and CLI rationale into .claude/notes 13,289 words of essay-shaped prose across 11 files down to 2,960, with no code change (AST + directive multiset verified against the phase-start SHA). docker_runner.py 4,993 -> ~1,100 and sandbox.py 3,259 -> ~590 are the bulk. The repo's largest comment block, 34 lines on --cap-drop, is now the four COUNTERPART lines that state the coupling plus a pointer; the capability argument it carried is rationale and moved. isolation.md gains Capability drops and the anti-cheat window, What crosses into the container, Trusting what the container sends back, and The sandbox the criteria run in. Detached grading from the CLI joins the existing detached-grading section rather than rivalling it, and the in-container driver rewrite stays in orchestration.md, which already owned it. The Phase-1 relocated bullet covered nine of these topics already, so the single-home rule needed excision as well as writing: seven spans moved out of it, each left as a cross-reference to the section that now owns them. The --cap-drop/FOWNER known gap stays in docs/DOCKER_ISOLATION.md; the notes link it and say so. All eight --help outputs are byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 6/7 — move criteria, routing and judging rationale into .claude/notes 14,406 words across 34 files down to about 3,300, with no code change and no generated surface touched. Two constraints made this phase different from the others, and both were verified by parsing rather than by trusting a downstream check: the first non-blank line of every criterion class docstring, which CE033 copies into the plugin reference, is byte-identical; and no Field(description=...) string under models/ changed, which is what CE030 ripples into the task guide. The LiveVerdict determinism and monotonicity properties stay at the definition site in criteria/base.py, because they are the contract an author has to satisfy. What moved is the derivation: why CE036 replays trajectories, and what CE025 cannot see. contracts.md gains the live_verdict contract, the checker base class, route resolution, judge context and untrusted text, sub-agent judging, and recording a CLI invocation. persistence.md gains judge persistence; agents.md the sdk_options pass-through; orchestration.md the rates, the early-stop guardrail placement, and the armed gate. Docstrings that restated a Field description, or the task guide's own YAML examples, were deleted rather than moved — the guide is the source of truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 7/7 — move reporting, harbor and telemetry rationale into .claude/notes 10,234 words across 30 files down to about 2,900, finishing the plan. The tree goes from 79,754 essay-shaped words to 18,163, and from 126 docstrings over 150 words to one. That one is agent.py::communicate, the plugin SPI contract every third-party agent author reads: the Args/Returns/Raises, the rule that a mid-turn failure sets pending_turn before raising and the caller rolls the counter back, and the streaming event protocol. It is the exemption the plan predicted, and the only one taken, so the 150-word bar held for the whole tree. reporting.md gains the Agent ABC contract, telemetry emission, cost joining, report rollups and the HTML twin, Harbor export, and the ATIF trajectory bridge. Two hazards the plan named had to be ADDED rather than kept: neither the pricing.py/pricing.ts mirror nor the "evalboard's static twin" parity claim was ever stated in its own source file — both lived only in CLAUDE.md. Editing one side of either pair without the other is exactly what they exist to prevent, so they now say so where the editor will be. errors/categories.py is untouched: its per-member retryability notes are the contract errors/categorization.py dispatches on, and they are one-line comments the plan puts out of scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(lint): guard the Rationale pointer's placement, not just its target check_pointers proves a pointer RESOLVES. It cannot see the defect that actually shipped: a block replacement anchored on the wrong line leaves the tail of the replaced prose stranded after the pointer, where the $-anchored regex stops looking. The file parses, the pointer resolves, and the comment carries a severed half-sentence. Three phases of the prose-reduction work shipped that shape to review before this check existed; a fourth ran clean because the check was running from the first edit. It also reports an orphaned docstring terminator, which is worse than it looks: the file stops parsing, and the measurement silently reports it as zero words rather than failing. A docstring may still follow its pointer with Args:/Returns:/Raises: — that is the house shape, and the one case this must not flag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: restore the self-containment clause on reports_html "Self-contained (inline CSS/JS, no external fonts or images)" is caller-facing — it is why the file can be opened offline and uploaded as a CI artifact — and the 7/7 rewrite dropped it while adding the static-twin claim. Both belong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: record the four harness gaps the prose run did not close The fifth was promoted (b8b1dcf). These four are deferred with the reason, so the next person does not rediscover them: the general severed-fragment shape needs an allowlist to be usable, the single-home rule needs a similarity measure, a pointer landing unhelpfully is probably not mechanizable at all, and the generated-surface guard needs a commit-scoped diff the lint harness cannot see. The single-home one is the highest-value unbuilt guard in the notes design — it bit every phase of this run, once against a file the phase never opened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(docs): restore contracts the prose refactor lost or misstated An audit of the finished refactor found nine places where compression either dropped a contract or, worse, replaced it with a plausible wrong reason. The bad one is `_resolve_backend_route`. Its docstring said the function raises "so it must survive -O" — true of a different function, transplanted here — while the actual contract was gone from the source AND from the notes: an explicit backend override that cannot be honored must fail loudly, not degrade to a backend the task author never asked for. A reader told the raise is only about -O can reasonably "improve" it into a fallback, which is exactly the regression the deleted sentence forbade. The rest: - write_text_atomic claimed mode 0o644 as if guaranteed. It is a ceiling; the umask narrows it, and 0o077 yields 0600 — which breaks the docker driver, the failure the directive exists to prevent. - A NOTE in sandbox.py said three methods do not validate path traversal. Two of them go through resolve_files (and so through _within_sandbox and _reject_escaped); only list_files still does not. Someone reasoning about criterion containment from that comment reached the wrong conclusion. - enforces_permission_windows kept the clause making the window SAFE and lost the one making it REAL: without the container's DAC cap drops a mode-000 directory is still readable by root. - capture_to's copytree flags lost the reason one of them is load-bearing. - adopt no longer said criteria can still write to the adopted tree, under a docstring headlined "materializing nothing". - The docker log constants lost why there are two of them, and had no pointer. - armed_criteria_passed said "weighted score", which names a different quantity in the same class; the binarisation that makes gate_threshold=1.0 an exact equivalence was gone. - judge_context said "these tokens" with no list after it. - _build_run_command_env counted eight env facets and sets nine. The baseline rises 18,163 -> 18,549. That is the ratchet working as intended: restoring real contract is a legitimate reason to raise it, and the reason belongs here rather than in a silent constant bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(docs): restore the qualifier that made each absolute true Review B found one shape repeated six times: compression kept a categorical claim and dropped the words that made it true. Each is the only fact its comment now carries, sitting above the code someone would edit. - claude_code_agent: the task-notification guard is checked SECOND, not first. What matters is that it precedes _is_sdk_result_message. - opencode_agent: "the condition is the TELEMETRY, not the event vocabulary" — but one of the two arms IS the vocabulary, and a later comment says so. The dropped word was "alone". Deleting the arm as redundant re-opens scoring SUCCESS 1.0 on zero turns. - batch: the predicate is NOT_GRADED **or** executed. "Executed is a required half" reads as `and`, which would leave zero-iteration execute rows ungraded forever. - reference_comparison: "every failure below raises" — three branches below return a gating 0.0 on purpose, one of them carrying a CE039 noqa. - docker_runner: the stdout limit does not mirror _POST_RUN_STREAM_LIMIT; it is 256x larger, deliberately. Unifying them downward reinstates the mid-stream teardown that lost whole paid tasks. - cli_called: "harness fault, not agent behaviour" contradicted the block 50 lines down explaining that all five paths are agent-reachable. Also restores the judge ignore floor's MECHANISM: it is a copy-time control, the same list passed as copytree's `ignore`, not only an SDK setting. Without that, dropping .claude / .mcp.json from it looks redundant and lets an agent-planted settings file into the judge's own working directory. _resolve_backend_route's contract now has a home in the notes, and the docstring points at it rather than at the LiteLLM section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(docs): stop asserting a tmpfs mask that no longer exists `--tmpfs` is never emitted. `_build_argv` does not construct one, `test_reference_inside_task_dir_needs_no_tmpfs_mask` asserts `not _tmpfs(argv)`, and docker_runner.py's own `_reference_mount_args` docstring opens "No tmpfs mask any more". Three of its neighbours said the opposite, two of them lines this refactor wrote. The claim predates the branch, so this is not a regression — but the refactor read every one of those lines and compressed them instead of questioning them, and it authored two new canonical homes for the falsehood. Re-ratifying it is worse than never having looked. What is actually true: the task dir is a shielded read-write COPY, so a reference embedded in it is covered by the same mode-000 window rather than hidden under a layered filesystem. `orchestration/evaluation.py`'s hard-fail was justified by the mask and by an EROFS on a `:ro` bind — both false now, and the real reason (the fallback resolves to a tree the window was not opened over) is the one that survives. Also restructures `permissions.md`'s Phase-1 blob. Its single 4,450-char line duplicated two of its own sibling sections, restated the FOWNER/CHOWN rationale that isolation.md explicitly delegates to docs/ saying "they are not restated here", and carried the tmpfs claim. All four inbound source pointers still resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(lint): replace the prose baseline with two self-adjusting rules _ESSAY_BASELINE_WORDS was the weakest part of the gate. It was a single tree-wide number, hand-edited eleven times in this work alone, that a reviewer had to take on trust; it let growth in one file hide behind shrinkage in another; and it said nothing at all about a file that did not exist yet. It is gone. Two rules replace it, and neither has a number anyone maintains: own-line comments per file <= MAX(20, 0.15 * file length) no docstring over 150 words of PROSE The comment budget is proportional, so deleting code takes its budget with it and a new file is governed from its first commit. Own-line only: a trailing `# noqa` is a directive and a per-member annotation on an enum is the contract a dispatcher reads — counting either would push against documenting them. The floor is what protects a constants module at one comment per constant, which is where the tree's natural maximum sits. The essay rule now counts prose, not structure: an Args/Returns/Raises block is interface documentation, and counting it pushed exactly the docstrings that document their contract best over the line. Exemptions are categorical rather than numeric — an @AbstractMethod docstring IS the contract implementers read, so the plugin SPI is covered by kind, and a new abstract method is covered automatically. The old "at most two, by fiat" allowance is unnecessary: the tree now has ZERO essays. Getting there took 44 comment lines out of 7 files. Most came out by reflowing two lines into one or cutting a section divider; the one real compression was a 22-line block in early_stop.py restating the floor bound that orchestration.md already owns, and the distractor-exclusion rule it uniquely held moved there rather than being dropped. One reflow silently merged `# pyright: reportImportCycles=false` into the prose line above it, which would have stopped pyright honouring it. --assert-code-unchanged caught it. That is the second time the directive multiset has earned its place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: update CLAUDE.md with communication style and development command clarifications Co-Authored-By: Claude <noreply@anthropic.com> * fix(lint): stop the prose budget penalising usage examples `_TRAILING_SECTIONS` already accepted an `Example:` block after a pointer, but `_DOCSTRING_SECTIONS` counted one against the 150-word bar — the two lists disagreed about the same shape, so a docstring in the house style could fail the gate. Two call examples had been deleted to get under it, one of them carrying the `on_attempt_error` keyword contract for the retry helper. A call example is code, not narrative, so it joins the structure list. Both examples are restored, with `execute_with_retry`'s Args/Returns/Raises block. Also from review: - `_TYPER_COMMANDS` gains a staleness test. An allowlist entry whose function has moved exempts nothing while still reading as a deliberate exemption — the vacuous-guarantee shape CE057's membership test already guards against. - `Agent.communicate` trimmed from 315 to 273 words. Every obligation kept; the turn-lifecycle rationale becomes a pointer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(notes): cut the duplicated catalogue and the unbuilt design Three review cuts, and a readability fix for a fourth. - The CE lint-rule catalogue in README.md: 2,165 words restating each rule's own module docstring, disclaimed as non-authoritative by the two sentences directly above it, and targeted by no pointer. Deleted; the sentences naming where the authority lives stay. README.md drops 2,745 -> 589 words. - "Not wired up yet" in permissions.md: 769 words designing code that does not exist. Deleted — git holds it. - The wall bullets are reflowed to the notes' 88-column prose width. The largest was a single 19,453-character line, which no diff could show usefully. - reporting.md's pseudo-directory-tree, two entries carrying 2,174- and 506-character trailing comments, becomes `###` subsections of ordinary prose. The review also asked for the per-file abstracts to be deleted as restatements of the sections beneath them. Not done: 61 of the isolation abstract's 191 code identifiers appear nowhere else in notes/, docs/ or CLAUDE.md — among them `--allow-recorded-commands`, `task_config.resolved`, `_seed_from_prior_result` with its `early_stop` carry, and `FinalStatus.is_execution_fact`. Deleting them loses rationale rather than a duplicate. Re-homing those claims first is a follow-up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: slim CLAUDE.md, and drop a directory that never existed CLAUDE.md is loaded into every session, so a copy kept there costs context on every turn and goes stale with nothing to sense it. Three sections were copies. - **Directory Structure** was wrong. It listed `optimize/`, which exists on neither this branch nor main, and omitted `errors/` and `plugins.py`, both present since the initial public release. The tree was never the value — the annotations were. It becomes a list of what a filename cannot tell you (the CE048 twins, the CE063 seam, the CE057 sidecar, CE053's filename ownership, CE056's env var, the pricing.ts mirror), opening with "run `ls`". 454 -> 176w. - **Success Criteria** was a third copy of one registry, behind the CE033-generated plugin reference and the Task Definition Guide, with no parity test — the shape CE030's own CLAUDE.md test exists for. Accurate today; stale eventually. Now the shared-field paragraph plus three pointers. 259 -> 76w. - **Extension Points** restated docs/EXTENDING.md's checklists step for step while the section 190 lines above it says "Each entry is a pointer". Keeps the three orientation facts — pkgutil discovery, the plugin SPI with no closed enum, pricing on the same register hook — and the obligations (CE036, CE047, parity). 48 -> 21 lines. Also reverts the run-limit parity sentence claiming an adapter rejects an unsupported field at load time. It does not: opencode_agent.py:785 and pi_agent.py:738 both warn and continue. Neither lint-pinned surface is touched — the CE030 model sentence and the six skill names are unchanged — and nothing links into the cut sections. CLAUDE.md: 2,802 -> 2,028 words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(harbor): apply the branch's prose rules to the bind-mount rewrite The bind-mount rewrite (#175) landed on main while this branch was open, so its new prose in `harbor/packager.py` never met the gate this branch introduces. Two docstrings were over the 150-word bar. - The module's emitted-layout diagram carried an eight-line annotation restating what `_write_environment` and `_write_docker_compose_mounts` already say. It is three lines now: Dockerfile only when `dockerfile_path` is set, and everything bind-mounted rather than COPY'd. - `_plugin_volume_specs` keeps its contract and its one real HAZARD -- a plugin path is carried UNEXPANDED and `docker_runner.py` expands it at launch, so the two sides must move together. The rest is a pointer. The rationale moves to `reporting.md § What the export carries, and what it refuses to carry`: why a prebuilt image needs no Dockerfile (Harbor's own `should_use_prebuilt_docker_image`, and `[environment].workdir` reaching `docker exec` independently of the build), why nothing is COPY'd, why a bind mount cannot self-nest into the export's `-o` directory, and the live `harbor run` that surfaced the agentless `initial_prompt` validation error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
environment/docker-compose.yamlnow bind-mountsenvironment/task.yamlitself, eachtype: localagent.plugins[]dir, eachTemplateDirSourceinsandbox.template_sources, and eachsandbox.docker.extra_mountsentry (its ownro/rwmode preserved) — all at their own host path, instead ofCOPYing content into the image and rewriting paths intask.yaml. Fixes a real bug: an exported agent previously ran with no skill/plugin content and no credentials (e.g.~/.uipath) available inside the container, sincedocker_runner.py's auto-mount for a normal docker sandbox run had no Harbor equivalent. Mount paths are always emitted POSIX-style (as_posix()), since docker-compose volume specs are POSIX regardless of the exporter's host OS.environment/Dockerfileis now written only whensandbox.docker.dockerfile_pathis set (realRUNbuild steps needed); otherwisetask.toml's[environment].docker_imagepoints Harbor straight at the pre-built image via its ownshould_use_prebuilt_docker_imagepath, and no Dockerfile is written at all.pre_runcommands are now translated into the agent-phasetask.yaml— they run before the agent starts, whichcoder-eval executestill does for theCoderEvalAgentembed, so this was a fixable gap rather than a fundamental limitation (unlikepost_run, which belongs to the grading phaseexecutenever runs, and stays untranslated with a warning).Test plan
pytest tests/test_harbor_packager.py tests/test_harbor_export_golden.py tests/test_harbor_experiment_packager.py tests/test_harbor_agent.py— 59/59 passingpytest— 6167 passed, 8 skippeduipath-adminaudit smoke task end-to-end throughharbor run -a coder_eval.harbor.agent:CoderEvalAgent -e docker, reward1.0uipath-agents antipattern_openai_agents_hitl(which needspre_runto seed its fixture) end-to-end, reward1.0🤖 Generated with Claude Code