Skip to content

Commit 9caffcb

Browse files
akshayliveclaude
andauthored
feat(harbor): task.yaml/experiment.yaml → Harbor export + coder-eval as a Harbor agent (#166)
* feat(harbor): export coder-eval tasks to Harbor, with coder-eval as the grader Implements C0/C1.1/C1.4/C2/C3(golden) of the Harbor interop plan (tmp/harborframework.md): `coder-eval export --format harbor <task.yaml> -o <dir>` emits a Harbor task directory whose `tests/test.sh` grades with coder-eval's own criteria via a two-line verifier shim. - `coder_eval.harbor.reward` — translates a graded run's task.json into Harbor's reward.json contract. A missing/unmeasured weighted_score writes no reward file (never coalesced to 0.0), so Harbor's own RewardFileNotFoundError path masks the trial instead of scoring it zero. - `coder_eval.harbor.portability` — classifies all 15 criterion types so an unsupported criterion is refused at export time, not as a mystery low reward at verify time. Registry-derived coverage over the real SuccessCriterion union. - `coder_eval.harbor.packager` — the export writer itself: task.toml, instruction.md, environment/, tests/test.sh, tests/task.yaml, tests/reference/. New dependency: tomli-w. - `coder-eval harbor reward` / `coder-eval export` CLI commands. - CE004 widened to scope harbor/ as a core layer (must not import coder_eval.cli). Verified live against real Harbor 0.22.0 + real docker (not just unit tested): exported tasks scored reward 1.0 (correct solution), 0.5 (wrong content, genuinely measured not masked), and correctly masked with no solution present. Two bugs the live run found and fixed that no unit test caught: test.sh referenced tests/task.yaml as cwd-relative when Harbor uploads the whole tests/ tree to the absolute /tests/; and `set -e` in test.sh was silently discarding every real failing score as if it were unmeasured, since `coder-eval evaluate` exits non-zero on a failed criterion (a measured verdict, not an infra failure). v1 assumes the exported task's Dockerfile is FROM coder-eval-agent:<tag> (confirmed live) so the verifier's `coder-eval` binary is present; the packager warns, non-blocking, when it can't confirm that. Full design, verification log and known follow-up (C1.2/C1.3, solution/ auto-derivation, network:none, docs) recorded in tmp/harborframework.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(harbor): derive a prebuilt image's real WORKDIR instead of guessing /app Converting a real in-tree task (hello_date_smoke_test) through the export pipeline surfaced a live bug in the bare-`docker.image` branch of `_write_environment`: it defaulted `workdir` to the hardcoded `DEFAULT_WORKDIR = "/app"` whenever `sandbox.docker.working_dir` was unset, regardless of the image's actual WORKDIR. `coder-eval-agent:latest`'s real WORKDIR is `/work`. Harbor's shared verifier runs `docker exec -w <workdir> <container> ...`, and confirmed live that `docker exec -w` -- unlike `docker run -w`, which auto-creates the directory -- hard-fails to chdir into a path that doesn't already exist in the image ("no such file or directory"), surfacing as agent exit 127 with no verifier output at all. The dockerfile_path branch never had this bug (it appends a WORKDIR line and the directory exists once Docker builds it); the bare-image branch had no Dockerfile to fix and no way to introspect the image, so it just guessed. Fix: `_inspect_image_workdir()` runs `docker image inspect <image> --format '{{.Config.WorkingDir}}'` (best-effort, 30s timeout, returns None on any failure) and the bare-image branch now tries, in order: explicit `working_dir` -> inspected WORKDIR -> `DEFAULT_WORKDIR`, warning (non-blocking) only on the last fallback. Re-verified live after the fix: `harbor run -p <dir> -a oracle` against the converted hello_date task scored reward: 1.0. Test hygiene: added an autouse fixture stubbing `_inspect_image_workdir` across `test_harbor_packager.py` so packager unit tests never depend on what happens to be cached in the local docker daemon -- a real image named `byod-custom-image:0.1.0` (this file's own placeholder name), built by an unrelated docker-integration test elsewhere in the suite, silently flipped a DEFAULT_WORKDIR assertion the first time this ran locally. New tests (TestPrebuiltImageWorkdirInspection) cover the inspection helper directly and its three-way fallback order. Full narrative in tmp/harborframework.md (gitignored), including a scoping note for the next piece (coder-eval as Harbor's *agent*, not just its verifier) -- not started, planning only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(harbor): export experiment.yaml variants to Harbor task directories Extends the single-task Harbor exporter to experiment.yaml: each resolved (task, variant, replicate[, dataset row]) combination gets its own Harbor directory, since Harbor's task.toml has no variant concept. Reuses resolve_all_tasks (the same pipeline `run -e` uses) for resolution, and skips (rather than silently drops) a variant whose agent/simulation override Harbor's verifier-only export cannot honor, detected precisely via config_lineage so a task's own agent config never false-triggers it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(harbor): coder-eval as a Harbor agent (C1.2) Adopts ATIF (Agent Trajectory Interchange Format) as coder-eval's trajectory interchange layer so a Harbor trial can run coder-eval itself as the agent, not just as the grader. - harbor/atif_models.py + atif_emit.py: vendored ATIF schema + a pure EvaluationResult -> Trajectory converter, opt-in via `--format harbor` on `execute`/`run` (writes a trajectory.json sibling to task.json; never changes default output). - harbor/atif_hydrate.py: the reverse direction (Trajectory -> TurnRecords) so `evaluate --format harbor --trajectory <path>` can grade trajectory-dependent criteria against an ATIF file that didn't originate from an in-process run. - harbor/agent.py: CoderEvalAgent(BaseInstalledAgent) — a real Harbor agent that shells out to `coder-eval execute --format harbor` inside the container and lets Harbor discover the resulting trajectory.json via `populate_context_post_run`, matching Harbor's own ClaudeCode agent's exact convention. Verified end-to-end against a real harbor==0.22.0 install: a full export -> docker build -> `harbor run` -> grade round trip produces correct rewards for both a passing and a deliberately failing criterion. - packager.py: bakes a criteria-free agent-phase task.yaml into the image at a fixed path (agent_paths.py) via a Dockerfile COPY line, so CoderEvalAgent always has a task to execute without ever seeing the real success_criteria. - run_command.py: when `--format harbor` writes exactly one trajectory (the shape a CoderEvalAgent invocation always produces), it is also copied to <run_dir>/trajectory.json so a fixed-path caller (Harbor's agent log dir) can find it without knowing coder-eval's internal variant/task/replicate nesting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(harbor): fix agent workspace alignment, env passthrough, and template sources Several fixes to the Harbor export/agent pipeline, each verified against a real `harbor==0.22.0` install with real Docker and (where relevant) real Bedrock credentials: - CoderEvalAgent now passes --workspace-dir "$(pwd)" to `coder-eval execute`, and BatchRunConfig/run_batch/run+execute CLIs gained a matching --workspace-dir flag (Orchestrator already supported this internally for DockerRunner). Without it, the agent phase wrote to a throwaway tempdir the verifier phase never looked at, silently scoring every criterion 0. - packager.py now emits [environment.env]/[verifier.env] in task.toml, derived from the resolved task's own sandbox.docker.env_passthrough (the same allowlist DockerRunner already uses) rather than requiring the operator to hand-edit task.toml. HOME is excluded (that entry only makes sense paired with DockerRunner's ~/.claude bind-mount). Values are `${VAR:-}` templates (empty default) so Harbor doesn't hard-require every var to be present. - environment/Dockerfile is now always written (synthesized from sandbox.docker.image when no dockerfile_path is set), so the CoderEvalAgent embed always has somewhere to COPY task.yaml into. - instruction.md is now a fixed placeholder pointing at environment/task.yaml and the CoderEvalAgent agent, instead of leaking the real prompt to a stock Harbor agent that isn't the one this export is built for. - environment/task.yaml's success_criteria is now a real `[]` instead of a placeholder criterion, now that TaskDefinition.validate_success_criteria (the "at least one criterion" check) has been removed. - TemplateDirSource directories are now copied into environment/templates/<n>-<name>/ with their path rewritten to the in-container copy; environment/task.yaml's sandbox block is field-merged (driver forced to tempdir) instead of being replaced wholesale, so python.env_packages/limits/etc. survive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ci(harbor): add Harbor E2E workflow (export + CoderEvalAgent round trip) Installs harbor==0.22.0 alongside coder-eval in the same venv, builds the coder-eval-agent and byod-custom-image Docker images, then exports and runs four scenarios via `hb run -a coder_eval.harbor.agent:CoderEvalAgent`: baseline (plain docker driver, default image), llm_judge (real judge call inside the verifier phase), docker_custom_image (reuses tasks/byod_smoke_test.yaml's dockerfile_path + custom image), and template_sources (TemplateDirSource copy-in). Each scenario asserts reward.json == 1.0, a real ATIF trajectory.json, and both agent- and verifier-side task.json. Verified locally end-to-end against real Docker + real Bedrock credentials before committing (all four scenarios: reward 1.0, no exceptions). Triggered on workflow_dispatch/schedule/push-to-main only (not pull_request) so it's informational rather than a required PR check for now — it depends on live model calls and Docker builds, and Harbor's own release cadence is independent of this repo's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(harbor): suppress pyright reportMissingImports for the harbor package `harbor` is intentionally not a project dependency -- coder_eval.harbor.agent only ever runs inside a Harbor trial container, where it's already present (see the module's own ImportError message). CI's Quality Gate installs the codex/litellm extras, which cleared those pre-existing pyright errors, so these 3 harbor.* import errors were the only real failures blocking PR #166 -- scoped `# pyright: ignore[reportMissingImports]` on the same 3 lines, matching the existing pattern for openai_codex's optional import in codex_agent.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(tests): harden harbor CLI-error assertions and Windows chmod checks Two more CI-only failures found running PR #166's full Quality Gate/Windows Smoke Test jobs (masked previously by the typecheck failure that aborted before the test step ran): - test_execute_format_harbor.py / test_evaluate_format_harbor.py assert a plain substring ("Unsupported --format", "--trajectory") against a raised Click/Typer error's rendered output. CI renders that error box with color, which splits "--format"/"--trajectory" across ANSI escape sequences and silently breaks a naive substring check -- reproduced locally with FORCE_COLOR=1. Fixed with click.unstyle(result.output) before asserting. - Two harbor packager tests assert the exported test.sh's chmod executable bit, which is meaningless on NTFS (Windows Smoke Test runs on Windows). Skipped/guarded the same way test_sandbox.py already does for other POSIX-only behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(harbor): address code review findings from full 8-axis review Fixes surfaced by /coder-eval-code-review-full over two iterations: - Refuse a task with empty success_criteria under `run`/`evaluate` (grade=True) -- it previously loaded and would silently score FinalStatus.SUCCESS at weighted_score 0.0 since validate_success_criteria was removed to let the criteria-free Harbor agent-phase task.yaml round-trip through `execute`. `execute` (grade=False) still accepts it. Scoped to the post-`--resume` task set so an already-finalized row is never blocked. - Refuse a path-traversal-shaped variant_id/task_id/row_id in experiment export (`_out_subdir`) instead of silently writing outside `-o`'s output directory. - Convert `--workspace-dir` misuse (docker driver, >1 task) from an unhandled ValueError/traceback into a clean typer.BadParameter. - Escalate a missing `template_sources` directory during export from a warning to a hard failure -- the agent-phase task.yaml still references it, so a silently-skipped copy shipped an export with no starter code. - Add a `harbor` optional extra (pinned harbor==0.22.0) so harbor/agent.py type-checks against real types instead of scoped pyright ignores; wire it into the two CI jobs that run pyright and simplify harbor-e2e.yml's install step to use it. - New test coverage: harbor/agent.py's run()/populate_context_post_run (previously zero coverage), the empty-criteria and workspace_dir guards, the path-traversal refusal, and the template-source warn vs. hard-fail branches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(test): strip ANSI color before asserting --workspace-dir in CLI output CI renders Click's error box with color, splitting "--workspace-dir" across escape sequences and breaking the new test_workspace_dir_with_docker_driver_is_a_clean_cli_error test that raw-substring-matched it. Same fix as the existing --format/--trajectory tests: assert against click.unstyle(result.output) instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(harbor): address PR review blockers (score-integrity, security, drops) Closes out the reviewer's Top-5 priority actions from the full 8-axis review of this branch, plus the two CodeQL findings: - Restore the empty-`success_criteria` refusal at the layers `run --to_run`'s guard didn't cover: `regrade_in_place` (shared by `evaluate <run_dir>` and `run --resume`'s `to_grade` set), the orchestrator-direct `evaluate` branch, and the resume `to_grade` partition itself. Without this, a criteria-less task graded SUCCESS at weighted_score 0.0 and exited 0. - Widen `write_reward`'s unmeasured-row guard to also refuse `final_status.category == "error"`, since a grading-time infra crash (JudgeInfrastructureError/CheckerMisuseError/ReferenceTamperedError) finalizes ERROR with weighted_score 0.0, not None -- publishing a crash to Harbor as a measured zero. - Read `result_status`/`duration_ms` back out of ATIF's ToolCall/ ObservationResult `extra` in atif_hydrate, so `command_executed (require_success: true)` no longer silently scores 0.0 on the `evaluate --format harbor` path for a command that succeeded. - Replace the two hand-picked export payloads: carry `run_limits` (both agent-phase and verifier task.yaml) and `checker_context` (verifier) through instead of dropping them, and refuse exporting a `dataset:`- backed task (unexpanded `${row.*}` placeholders) or an enabled `simulation:` block. - `_unhonorable_override_reason` no longer skips agent-override variants: CoderEvalAgent (C1.2) now carries `task.agent` verbatim and executes it, so `export -e experiment.yaml` exports one directory per variant instead of exiting with zero. - shlex.quote the task-YAML-controlled `working_dir` interpolated into the generated Harbor verifier `tests/test.sh` (command injection via a crafted `sandbox.docker.working_dir`). - Drop symlinks (never dereference) when copying template/reference trees into an export, matching every other task-authored-tree copy in `src/`; wrap the reference copy's OSError as TaskNotExportableError. - `--` before the task-controlled image name in `docker image inspect`. - Fix the SubagentTrajectoryRef vendored ATIF model to match harbor==0.22.0 exactly (verified against the installed package): all four fields optional, `session_id` added -- the old required-`trajectory_id` shape rejected two of harbor's own valid document forms. - `evaluate --format harbor`'s write-back now checks `target.mode is RUN_DIR`, not just `prior is not None`: a synthetic prior seeded from `--trajectory` on the WORK_DIR shape was planting a spurious task.json into the Harbor-synced work dir. - Narrow the DIRECT_WRITE stale-artifact warning suppression for `workspace_dir` to `IN_CONTAINER_ENV` (CE056) instead of the field itself: `--workspace-dir` is now also a host-reachable CLI flag, where the named directory persists across runs exactly like DIRECT_WRITE's own target. - Remove an unnecessary `del environment` (CodeQL) and fix a test's dual import style (`import X.Y` + `from X.Y import Z`, CodeQL) for the same module. Regenerated the golden export fixture for the `run_limits` addition. Full suite green (5706 passed), ruff/pyright/lint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * build(docker): bake the harbor extra into the coder-eval-agent image Lets the same per-task agent image double as a Harbor agent image (coder_eval.harbor.agent:CoderEvalAgent, a harbor.agents.installed.base.BaseInstalledAgent subclass) without a separate build, matching the harbor extra already wired into both pyright CI jobs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 87102a3 commit 9caffcb

55 files changed

Lines changed: 6310 additions & 52 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/harbor_e2e.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env python3
2+
"""Harbor end-to-end smoke test.
3+
4+
Exports a handful of coder-eval tasks to Harbor (`coder-eval export --format
5+
harbor`), runs each with `hb run -a coder_eval.harbor.agent:CoderEvalAgent`
6+
against real Docker, and asserts that:
7+
8+
- the verifier phase wrote `reward.json` with `reward == 1.0`
9+
- the agent phase wrote a real ATIF `trajectory.json`
10+
- both the agent and verifier phases left a `task.json` behind
11+
12+
Invoked by `.github/workflows/harbor-e2e.yml` -- deliberately NOT part of
13+
`make test`: it shells out to a real `hb` CLI, real Docker builds, and (for
14+
the llm_judge scenario) a real model call, none of which belong in the fast
15+
unit-test suite.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import json
21+
import shutil
22+
import subprocess
23+
import sys
24+
from dataclasses import dataclass
25+
from pathlib import Path
26+
27+
28+
REPO_ROOT = Path(__file__).resolve().parents[2]
29+
WORK_DIR = REPO_ROOT / "tmp" / "harbor_e2e"
30+
AGENT_IMPORT_PATH = "coder_eval.harbor.agent:CoderEvalAgent"
31+
32+
33+
@dataclass(frozen=True)
34+
class Scenario:
35+
"""One (task.yaml, export flags) pair to round-trip through Harbor."""
36+
37+
name: str
38+
task_file: Path
39+
allow_credentials: bool = False
40+
41+
42+
SCENARIOS: list[Scenario] = [
43+
# "tmpdir" baseline: plain docker driver, default coder-eval-agent image,
44+
# no dockerfile_path / template_sources / llm_judge -- exercises the bare
45+
# export -> CoderEvalAgent -> --workspace-dir -> verifier round trip.
46+
Scenario("baseline", REPO_ROOT / "tests/harbor_e2e/fixtures/docker_baseline.yaml"),
47+
# llm_judge: real model call inside the VERIFIER phase, not just the agent.
48+
Scenario("llm_judge", REPO_ROOT / "tests/harbor_e2e/fixtures/llm_judge.yaml", allow_credentials=True),
49+
# Custom (BYOD) Docker image via dockerfile_path -- reuses the in-tree
50+
# byod_smoke_test task/image rather than duplicating it.
51+
Scenario("docker_custom_image", REPO_ROOT / "tasks/byod_smoke_test.yaml"),
52+
# template_sources: TemplateDirSource copy-in + rewritten path, plus
53+
# sandbox.python.env_packages surviving the agent-phase task.yaml merge.
54+
Scenario("template_sources", REPO_ROOT / "tests/harbor_e2e/fixtures/template_sources.yaml"),
55+
]
56+
57+
58+
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
59+
print(f"+ {' '.join(cmd)}", flush=True)
60+
return subprocess.run(cmd, check=False, text=True, capture_output=True)
61+
62+
63+
def export_task(scenario: Scenario, out_dir: Path) -> None:
64+
if out_dir.exists():
65+
shutil.rmtree(out_dir)
66+
cmd = ["coder-eval", "export", str(scenario.task_file), "-o", str(out_dir)]
67+
if scenario.allow_credentials:
68+
cmd.append("--allow-credentials")
69+
result = _run(cmd)
70+
print(result.stdout)
71+
print(result.stderr, file=sys.stderr)
72+
if result.returncode != 0:
73+
raise RuntimeError(f"[{scenario.name}] `coder-eval export` failed (exit {result.returncode})")
74+
75+
76+
def run_harbor(scenario: Scenario, export_dir: Path, jobs_dir: Path) -> Path:
77+
if jobs_dir.exists():
78+
shutil.rmtree(jobs_dir)
79+
cmd = [
80+
"hb",
81+
"run",
82+
"-p",
83+
str(export_dir),
84+
"-a",
85+
AGENT_IMPORT_PATH,
86+
"--jobs-dir",
87+
str(jobs_dir),
88+
"-n",
89+
"1",
90+
"-y",
91+
]
92+
result = _run(cmd)
93+
print(result.stdout)
94+
print(result.stderr, file=sys.stderr)
95+
if result.returncode != 0:
96+
raise RuntimeError(f"[{scenario.name}] `hb run` failed (exit {result.returncode})")
97+
98+
# <jobs_dir>/<job_timestamp>/<trial_name>/{agent,verifier}/... -- glob for
99+
# the one directory two levels down that actually holds a verifier/ output,
100+
# rather than assuming a fixed trial-name shape Harbor doesn't guarantee.
101+
trial_dirs = [d for d in jobs_dir.glob("*/*/") if (d / "verifier").is_dir()]
102+
if len(trial_dirs) != 1:
103+
raise RuntimeError(
104+
f"[{scenario.name}] expected exactly one trial directory under {jobs_dir}, found {len(trial_dirs)}"
105+
)
106+
return trial_dirs[0]
107+
108+
109+
def assert_scenario_artifacts(scenario: Scenario, trial_dir: Path) -> None:
110+
reward_path = trial_dir / "verifier" / "reward.json"
111+
if not reward_path.is_file():
112+
raise RuntimeError(f"[{scenario.name}] missing {reward_path}")
113+
reward = json.loads(reward_path.read_text(encoding="utf-8"))
114+
if reward.get("reward") != 1.0:
115+
raise RuntimeError(f"[{scenario.name}] expected reward 1.0, got {reward!r} ({reward_path})")
116+
117+
trajectory_path = trial_dir / "agent" / "trajectory.json"
118+
if not trajectory_path.is_file():
119+
raise RuntimeError(f"[{scenario.name}] missing {trajectory_path}")
120+
trajectory = json.loads(trajectory_path.read_text(encoding="utf-8"))
121+
if "schema_version" not in trajectory:
122+
raise RuntimeError(f"[{scenario.name}] {trajectory_path} is missing 'schema_version'")
123+
124+
agent_task_jsons = list((trial_dir / "agent").glob("**/task.json"))
125+
if not agent_task_jsons:
126+
raise RuntimeError(f"[{scenario.name}] no task.json found under {trial_dir / 'agent'}")
127+
verifier_task_json = trial_dir / "verifier" / "task.json"
128+
if not verifier_task_json.is_file():
129+
raise RuntimeError(f"[{scenario.name}] missing {verifier_task_json}")
130+
131+
print(
132+
f"[{scenario.name}] OK: reward=1.0, trajectory.json present, "
133+
+ f"{len(agent_task_jsons)} agent task.json + verifier/task.json present"
134+
)
135+
136+
137+
def main() -> int:
138+
WORK_DIR.mkdir(parents=True, exist_ok=True)
139+
failures: list[str] = []
140+
for scenario in SCENARIOS:
141+
export_dir = WORK_DIR / scenario.name / "export"
142+
jobs_dir = WORK_DIR / scenario.name / "jobs"
143+
print(f"\n=== {scenario.name} ===", flush=True)
144+
try:
145+
export_task(scenario, export_dir)
146+
trial_dir = run_harbor(scenario, export_dir, jobs_dir)
147+
assert_scenario_artifacts(scenario, trial_dir)
148+
except Exception as exc:
149+
print(f"[{scenario.name}] FAILED: {exc}", file=sys.stderr)
150+
failures.append(scenario.name)
151+
152+
print("\n=== Summary ===")
153+
for scenario in SCENARIOS:
154+
print(f" {scenario.name}: {'FAILED' if scenario.name in failures else 'OK'}")
155+
156+
if failures:
157+
print(f"\n{len(failures)}/{len(SCENARIOS)} scenario(s) failed: {', '.join(failures)}", file=sys.stderr)
158+
return 1
159+
return 0
160+
161+
162+
if __name__ == "__main__":
163+
raise SystemExit(main())

.github/workflows/harbor-e2e.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Harbor E2E
2+
3+
# Deliberately NOT triggered on pull_request: this exercises real Docker
4+
# builds, a real `harbor` install, and (for the llm_judge scenario) a real
5+
# model call, so it is informational rather than a required PR check for now
6+
# (see .github/scripts/harbor_e2e.py's module docstring). workflow_dispatch
7+
# lets a maintainer run it on demand; the nightly schedule catches drift
8+
# between coder-eval's own release and Harbor's own upstream releases without
9+
# blocking anyone's PR.
10+
on:
11+
workflow_dispatch:
12+
schedule:
13+
- cron: "17 5 * * *" # nightly, off the hour to avoid GitHub's peak-load pile-up
14+
push:
15+
branches: [main]
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
permissions:
22+
contents: read
23+
24+
env:
25+
TELEMETRY_ENABLED: "false"
26+
# Route through Bedrock, mirroring pr-checks.yml's smoke-pass job -- keeps
27+
# Anthropic-credit spend off this path; DirectRoute is exercised elsewhere.
28+
API_BACKEND: "bedrock"
29+
CLAUDE_CODE_USE_BEDROCK: "1"
30+
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
31+
AWS_REGION: ${{ secrets.AWS_REGION }}
32+
BEDROCK_MODEL: ${{ secrets.BEDROCK_MODEL }}
33+
34+
jobs:
35+
harbor-e2e:
36+
name: Harbor export + CoderEvalAgent round trip
37+
runs-on: uipath-ubuntu-latest
38+
timeout-minutes: 20
39+
40+
steps:
41+
- name: Checkout code
42+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
43+
44+
- name: Set up Python 3.13
45+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
46+
with:
47+
python-version: "3.13"
48+
49+
- name: Set up Node.js 20
50+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
51+
with:
52+
node-version: "20"
53+
54+
- name: Install Claude CLI
55+
run: npm install -g @anthropic-ai/claude-code
56+
57+
- name: Cache dependencies
58+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
59+
with:
60+
path: |
61+
~/.cache/uv
62+
~/.cache/pip
63+
key: ${{ runner.os }}-py3.13-harbor-e2e-${{ hashFiles('pyproject.toml', 'uv.lock') }}
64+
restore-keys: |
65+
${{ runner.os }}-py3.13-harbor-e2e-
66+
67+
- name: Install uv
68+
run: |
69+
python -m pip install --upgrade "pip>=26.2"
70+
pip install uv
71+
72+
- name: Install project dependencies (hash-verified from uv.lock)
73+
# --extra harbor installs `harbor` into the SAME venv as coder-eval:
74+
# CoderEvalAgent is resolved by `hb run -a
75+
# coder_eval.harbor.agent:CoderEvalAgent` on the HOST process, so
76+
# `harbor` and `coder_eval` must be importable from the same
77+
# interpreter (see harbor/agent.py's module docstring). The version is
78+
# pinned once, in pyproject.toml's `harbor` extra -- bump it there.
79+
run: uv sync --frozen --extra dev --extra harbor
80+
81+
- name: Put the project venv on PATH
82+
run: echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH"
83+
84+
- name: Build coder-eval-agent base Docker image
85+
run: make docker-image
86+
87+
- name: Build BYOD template Docker image
88+
run: docker build -t byod-custom-image:0.1.0 templates/byod_smoke_test/
89+
90+
- name: Run Harbor E2E scenarios
91+
run: python .github/scripts/harbor_e2e.py

.github/workflows/pr-checks.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ jobs:
8080
pip install uv
8181
8282
- name: Install project dependencies (hash-verified from uv.lock)
83-
run: uv sync --frozen --extra dev --extra uipath --extra codex --extra litellm
83+
# --extra harbor: pyright below type-checks src/coder_eval/harbor/agent.py
84+
# against harbor's real types, not a scoped ignore.
85+
run: uv sync --frozen --extra dev --extra uipath --extra codex --extra litellm --extra harbor
8486

8587
# PHASE 1: Fast checks (fail early)
8688
- name: Check code formatting (ruff format)
@@ -385,7 +387,9 @@ jobs:
385387
pip install uv
386388
387389
- name: Install project dependencies (hash-verified from uv.lock)
388-
run: uv sync --frozen --extra dev --extra uipath --extra codex --extra litellm
390+
# --extra harbor: pyright below type-checks src/coder_eval/harbor/agent.py
391+
# against harbor's real types, not a scoped ignore.
392+
run: uv sync --frozen --extra dev --extra uipath --extra codex --extra litellm --extra harbor
389393

390394
- name: Check code formatting (ruff format)
391395
run: .venv/Scripts/ruff format --check src/ tests/

docker/Dockerfile

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,19 @@ COPY src/ ./src/
5454
# experiments/default.yaml is force-included by hatchling per pyproject.toml.
5555
COPY experiments/default.yaml ./experiments/default.yaml
5656

57-
# Codex, Antigravity, and litellm are always baked into the image -- codex/
58-
# antigravity are peers to the claude-code agent installed above; litellm backs
59-
# the `checker_context.api_route.route: litellm` judge dispatch (see
60-
# pyproject.toml's `litellm` extra), which every DockerRunner-isolated task
61-
# needs available IN-container since the checker runs there too, not just on the
62-
# host. `openai-codex` (+ its pinned cli-bin), `google-antigravity` (which
63-
# bundles its `localharness` binary as a manylinux wheel), and `litellm` all
57+
# Codex, Antigravity, litellm, and harbor are always baked into the image --
58+
# codex/antigravity are peers to the claude-code agent installed above;
59+
# litellm backs the `checker_context.api_route.route: litellm` judge dispatch
60+
# (see pyproject.toml's `litellm` extra), which every DockerRunner-isolated
61+
# task needs available IN-container since the checker runs there too, not
62+
# just on the host; harbor lets this same image double as a Harbor agent
63+
# image (`coder_eval.harbor.agent:CoderEvalAgent`, a
64+
# `harbor.agents.installed.base.BaseInstalledAgent` subclass -- see
65+
# src/coder_eval/harbor/agent.py) without a separate build. `openai-codex`
66+
# (+ its pinned cli-bin), `google-antigravity` (which bundles its
67+
# `localharness` binary as a manylinux wheel), `litellm`, and `harbor` all
6468
# come from public PyPI, so this needs no private-index credentials. The RUN
65-
# below always passes `--extra codex --extra antigravity --extra litellm`.
69+
# below always passes `--extra codex --extra antigravity --extra litellm --extra harbor`.
6670
#
6771
# `pi` IS baked above (pinned PI_VERSION) and its OPENROUTER_API_KEY provider
6872
# credential is in DockerDriverConfig.env_passthrough, so `--driver docker --type pi`
@@ -90,7 +94,7 @@ ARG SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS="openai-codex-cli-bin,openai-codex
9094
# All extras (codex, antigravity, litellm, and the opt-in uipath) resolve from
9195
# public PyPI per uv.lock, so the build needs no private-index credentials.
9296
RUN export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS="${SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS}" && \
93-
uv export --frozen --extra codex --extra antigravity --extra litellm ${CODER_EVAL_UV_EXTRAS} | uv pip install --system -r /dev/stdin
97+
uv export --frozen --extra codex --extra antigravity --extra litellm --extra harbor ${CODER_EVAL_UV_EXTRAS} | uv pip install --system -r /dev/stdin
9498

9599
# Sanity check: the in-container entrypoint subcommand must be wired up.
96100
RUN coder-eval _run-task-internal --help > /dev/null

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"pydantic>=2.12.5",
3131
"pydantic-settings>=2.14.2",
3232
"pyyaml>=6.0.3",
33+
"tomli-w>=1.0.0",
3334
"typer>=0.24.1",
3435
"click>=8.3.3",
3536
"rich>=14.3.3",
@@ -160,6 +161,23 @@ opencode = []
160161
# the CLI on PATH the framework still installs and runs; Pi tasks fail at start()
161162
# with a clear hint pointing here. See docs/agents/PI.md.
162163
pi = []
164+
# Optional extra for the Harbor-interop direction: coder-eval AS a Harbor agent
165+
# (`coder_eval.harbor.agent:CoderEvalAgent`, a `harbor.agents.installed.base.BaseInstalledAgent`
166+
# subclass) run via `harbor run -a coder_eval.harbor.agent:CoderEvalAgent`. `harbor` itself
167+
# is never imported by anything OUTSIDE `coder_eval.harbor` (the export/packager side of
168+
# the integration reads/writes Harbor's task.toml directly and needs no `harbor` import at
169+
# all), so this extra exists only to satisfy that one module -- a task-image author (or a CI
170+
# job driving `hb run`) installs it with `pip install 'coder-eval[harbor]'` into the SAME
171+
# interpreter that also has `coder_eval`, since Harbor resolves the agent class on the host
172+
# process (see harbor/agent.py's module docstring). Pinned exactly, mirroring the other
173+
# harness pins: Harbor's own trial/environment/agent-context models are a load-bearing
174+
# runtime contract for this class, not a loose API. Without this extra the framework still
175+
# installs and runs; `coder_eval.harbor.agent` fails to import with a clear hint pointing
176+
# back here (it is not needed to EXPORT a task to Harbor format, only to run coder-eval
177+
# itself as Harbor's agent).
178+
harbor = [
179+
"harbor==0.22.0",
180+
]
163181

164182
[project.scripts]
165183
coder-eval = "coder_eval.cli:app"

src/coder_eval/cli/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from .console import console
99
from .evaluate_command import evaluate_command
1010
from .execute_command import execute_command
11+
from .export_command import export_command
12+
from .harbor_command import harbor_app, reward_command
1113
from .plan_command import plan_command
1214
from .report_command import report_command
1315
from .run_command import run_command
@@ -82,6 +84,9 @@ def main(
8284
app.command(name="evaluate")(track_command("evaluate")(evaluate_command))
8385
app.command(name="report")(track_command("report")(report_command))
8486
app.command(name="aggregate")(track_command("aggregate")(aggregate_command))
87+
app.command(name="export")(track_command("export")(export_command))
88+
harbor_app.command(name="reward")(track_command("harbor-reward")(reward_command))
89+
app.add_typer(harbor_app, name="harbor")
8590
# Hidden internal command invoked inside the Docker container only — UNWRAPPED
8691
# (it runs inside the run-task subprocess and would double-count / pollute events).
8792
app.command(name="_run-task-internal", hidden=True)(run_task_internal_command)

0 commit comments

Comments
 (0)