HOMELAB-1674: feat(harness): HarnessDriver seam + claudecode renderer/driver (zero dispatcher changes) #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1674-harness"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
The code half of the LD1.6 seam proof (design §4.3
HarnessDriver, §4.4 renderer table, §5.1's onboarding test; scouthomelab-1674/scout-findings-2026-09-30.md). It is the additive third of the three-artifact seam statement — one driver impl, one renderer, one image variant (image = Track 2,infra-core/agent-sandbox/claude-code-image/):HarnessDriverimplinternal/harness/claudecode/driver.gointernal/harness/claudecode/render.goinfra-core/agent-sandbox/claude-code-image/Nothing calls this yet. The dispatcher that selects a driver is LD2.1 (design §5.3 step 1/5; ticket HOMELAB-1674's own Verify line: "the full claim path lands in Epic 2"). There are zero edits to the dispatcher, bus, meters or cockpit — they do not exist in this repo yet — and zero edits to any existing file: the diff is additive (
go.mod+internal/harness/**).What follows the design exactly
Kind(),Prepare(pkg ContextPackage, sb SandboxEnvSpec, fs SandboxEnvFS) error,SpawnCmd(sb) (argv, env),ParseEvents(r io.Reader, emit func(SandboxEnvEvent)) error,Status(s SandboxEnvState) DerivedState,Artifacts(sb) ([]Artifact, error),Capabilities() Caps— withvar _ harness.HarnessDriver = (*Driver)(nil)as a compile-time proof.SandboxEnvSpec{RoleProfile, HarnessKind, ContextPackageRef, RepoSliceSpec, SandboxEnvLimits, EvidenceContract}.--append-system-prompt-filepreamble; S2 →CLAUDE.md+ first user message; S3 → verbatim first user message; S4 →CLAUDE.mdrules + pinned files + hash-check step; S5 →CLAUDE.mdlore block (by address); S6 → harness-agnostic sparse-checkout list; S7 → output/evidence contract +out/convention; S8 →settings.jsonenvblock.Validate()recomputes every pin and the content address and returns*StaleError{Section, Pinned, Recomputed}naming the section that moved (fail-loud on stale). A test pins the count at 8 (a ninth section is a contract amendment, not a refactor).SandboxEnvEvent,Caps,DerivedState,ContextPackage) carries no harness name; onlyclaudecode/*does.Adaptations and provisional choices (please review these)
/home/aaron/workspace/lab-directoras an "active app checkout, remote aaron/lab-director". That path is a clone of the dormant LDv1 app (Homelab/lab-director, private,live, last commit 2026-05-02) whoseoriginURL still readsaaron/lab-director— a stale-redirect relic now resolving to the contract repo (already documented as a hazard in.rpiv/artifacts/landing/ld0-1-repo-setup.md§6). The ticket and the scout both nameaaron/lab-directoras the home forinternal/harness/, so this branch is based onaaron/lab-director@main(f2e16ae) and the PR targetsmain. If the intent was instead "code goes into the LDv1 app underbackend/internal/harness", say so and this lands as a 5-minute re-target — the package is importable as-is.aaron/lab-directorhad nogo.mod; this PR addsmodule forgejo.aaron.reynoza.org/aaron/lab-director,go 1.24, stdlib only (matches the convention visible in the app'sforgejo.aaron.reynoza.org/aaron/lab-director/backend). LD2.1's dispatcher extends this module.Seal()(compiler-side pinning) lives here so the compiler, fixtures and tests share one hashing implementation; LD2.1 owns the compiler itself.Validate()is the consumer-side check.ctx-homelab-1654-builder-1-a91f3c, but §2.4 requires cross-worker memoization ("a second worker on the same ticket reuses the package unless inputs moved") — a per-sandbox-env address can never hit that cache. Address =ctx-<sha256 of sorted section hashes>, a pure function of section content (provenance likecompiled_atis not rendered and does not move it).SpawnCmdcannot carry the brief (the §4.3 signature takes no package), so argv emitsPromptPlaceholder = "{{first_user_message}}"andSubstitutePrompt(argv, brief)is the documented one-liner sandbox-init calls; the verbatim brief is rendered at.claude/context/S3-brief.md.--verboseis mandatory for-p --output-format stream-json(verified:Error: When using --print, --output-format=stream-json requires --verbose) — the driver adds it, and only for stream-json.--barefixes the context channel:claude --helpsays--bareskips "…CLAUDE.md auto-discovery" and names the explicit channels, so the spawn passes--append-system-prompt-file <S1>and--add-dir <state root>(the documented CLAUDE.md mechanism) — S1 inCLAUDE.mdwould otherwise never load.localhostOmniRoute endpoint (design §2.2 S8 + risk #10: never the mgmt-VM hot standby); refuse S8 withoutapi_key_helper(the token arrives as a file — provisioning contracts §1); refuse any credential-lookingenvkey/value (contract §11).Steer=false(design §4.3: steer is never harness-native),ACP=false(no ACP surface),Hooks=truewith a note that--bareskips settings/plugin hooks — i.e. advertised but inactive in our spawn mode.SandboxEnvEvent/DriverEvent/Caps/DerivedStateare markedLD1: provisionalin code (// LD1: provisional — …), with the owner named: meters owns the canonical event schema and the compiler owns the package shape (design §5.1).Test evidence
Coverage of the brief's requirements: golden tree from a fixture package (
testdata/golden/**, 9 files); a stale-hash package →Validateerror namingintent(testdata/stale-package.json); purity (render twice byte-identical; a recompiled package at a different time renders identically and the tree contains no provenance);Preparebyte-equality throughMemFS; spawn argv pinned as a golden list;ParseEventsover verbatimclaude 2.1.284stdout captured on the mgmt VM (testdata/stream-json-2.1.284.jsonl) plus a tool_use/result fixture; loud failure on dialect drift, quiet forward-compatibility for unknown event types; derived-state table; artifact hashing; capability honesty assertions.Fixtures are regenerated with
go test ./internal/harness/claudecode -update— never hand-edited.Open items (explicitly not done here)
execute_commandreaches only the runtime sidecar, so the proof run (Track 4) needs the single-container combine — an image decision, not a code one./v1/messagesremains unverified (Track 1); this PR deliberately does not claim it works.--bare; if we want them, the spawn mode is the thing to revisit (recorded inCaps.Notes).mcp.jsonis rendered (the MCP config is the dispatcher/image's, design §5.1 sandbox-init) —SpawnCmdreferences--mcp-configonly when configured.aaron/lab-directorhas no workflow yet, so there is no CI gate on this PR; the test commands above are the evidence. A tiny go-test workflow is a candidate follow-up (out of scope here).Landing review — LD1.6 renderer + driver (HOMELAB-1674,
2180fbc)Reviewer: fresh independent context (
pi -p --no-session, reviewer ≠ author) + landing worker mechanical checks. Tests run by the reviewer, not taken from the PR body:go test ./... -count=1ok;go vet ./...clean; 42 cases; coverage 79.0% / 91.0%. Diff--name-status= all added, 22 files +4410, no edits to existing files. Interface fidelity to design §4.3 = verbatim method set +var _ harness.HarnessDriver = (*Driver)(nil). Golden fixtures are byte-compared bidirectionally against the rendered tree; the stale fixture fails with*StaleError{Section:"intent"}; purity asserted across renders and recompiles. Spawn argv pinned;--verboseonly forstream-json. Review quality: high.Verdict: FIX-FIRST (not merged). Three confirmed defects in the fail-loud guards, all in
render.go/context.go, all cheap. Each was reproduced by probe (module copy under/tmp, worktree left clean).F1 —
Validatetrusts the package's ownrequiredflags (context.goValidate, vsSeal/requiredByDefault)Probe: take the valid fixture, set
section_pins.brief.required=false, empty the S3 payload and its pin, re-derivepackage_idviaContentAddress()→Validatereturns nil andRenderemits a 9-file tree whose brief is"# Assignment — HOMELAB-1674 (builder-core) …## Goal\n\n\n\n". The presence gate therefore lives only inSeal(producer side); the consumer-side check the dispatcher will call accepts a context-crippled package. Fix (3 lines, cannot reject a legitimate package): re-derive requiredness fromrequiredByDefaultinValidateinstead of readingPin.Required.F2 — guard/renderer precedence mismatch, loopback endpoint reachable (
render.gocheckEnvironmentvsrenderSettings)checkEnvironmentvalidatesEndpoints["omniroute"]first;renderSettingsrendersEnv["ANTHROPIC_BASE_URL"]if present. Probe: in-cluster endpoint +env.ANTHROPIC_BASE_URL=http://127.0.0.1:20128→ guard passes,settings.jsonrenders the mgmt-VM hot standby URL (design §2.2 S8 / risk #10 — the rule the guard exists for). Fix: validate the value that is actually rendered.F3 — de-risk rules unenforced (
render.gocheckEnvironment;driver.goSpawnCmd)/v1-suffixed base URL renders verbatim (→/v1/v1/messages); de-risk §7/Recommended config says no/v1suffix. Anomniroute/…-prefixed--modelpasses through unvalidated (de-risk §6: that form 401s with a misleadinginvalid_api_key). Both are dispatch-time traps the guard function is the right place to refuse.Non-blocking notes
identity.model=omniroute/deepseek-flashinfixture-package.jsonwhile--model/ANTHROPIC_MODEL=claude-opus-5-5; nothing cross-checks them (fixture-only, but it illustrates F3's trap).staleness.ttl/hard_invalidatorsare parsed but never enforced (noValidateAt); fine if LD2.1 owns TTL enforcement — worth a comment.--add-dirre-enabling CLAUDE.md ingestion under--bareis doc-derived (claude --help), not yet run-proven; the PR already lists the end-to-end proof as Track 1/4, so this is a note, not a defect.internal/harness/**+ newgo.modin the contract repo) accepted: the alternative checkout is the dormant LDv1 clone; no CI workflow exists to break. Re-target to the app repo remains a 5-minute option for LD2.1.Fix worker — all three blockers addressed (
5d04c02, same branch, not merged)Pre-fix replication used a detached worktree at
2180fbc(/tmp/prefix-1674, removed after use) plus the new probes; both runs are reproducible with the probes in the tree.F1 —
Validaterequired-set (internal/harness/context.go). Presence is now judged againstrequiredByDefault ∪ Pin.Required(a pin may add requiredness, never remove it).Validate(): <nil>,Render(): err=<nil> files=9(empty Goal)harness: required context package section "brief" is empty(Validate and Render)TestValidateEnforcesTheDefaultRequiredSet,TestRenderRefusesDemotedRequiredSectionF2 — endpoint/env agreement (
checkEnvironment). The guard now validates the value that is actually rendered (blank override = absent, matchingrenderSettings), and when both are declared they must agree on the destination. Rule: the rendered base URL must be cluster-reachable from the sandbox —*.svc.cluster.local(contract §1 shape); loopback/RFC1918/mgmt addresses are refused; any other host needs an explicit entry inendpointHostAllowlist, which ships empty. Same rule applied toConfig.ExtraEnv.Render(): <nil>(hot standby insettings.json); after:S8 ANTHROPIC_BASE_URL "http://127.0.0.1:20128" is a loopback address (unreachable from inside the cluster); …use the in-cluster Service DNS name (design §2.2 S8, risk #10)TestRenderRefusesUnreachableModelEndpoint(6 cases),TestModelEndpointRuleF3 — de-risk traps. Trailing
/v1andomniroute/-prefixed ids are refused with a message naming the correct form, everywhere the id can enter: S8ANTHROPIC_*MODEL, S1identity.model,Config.Model,Config.ExtraEnv. Fixtureidentity.modelcorrected to the bare served id (goldens regenerated via-update; diff = model id + package id only).Render(): <nil>(verbatim/v1),New(): driver=<built> err=<nil>; after: messages point atANTHROPIC_BASE_URL=…:20128(no/v1suffix) and the bare served id.TestRenderRefusesDeriskTraps,TestNewRejectsBadConfig(prefixed model,loopback ExtraEnv base url,prefixed ExtraEnv model)Gate:
go test ./... -count=1 -racegreen — 42 → 53 cases (39 tests + 14 subtests),go vet ./...clean,gofmtclean, coverage 79.1% / 90.6%. This repo has no CI workflow (no.forgejo/.github, no workflow files): tests are the gate.Open (not in scope):
staleness.ttl/hard_invalidatorsstill unenforced (LD2.1 owns TTL);fixture identity.modelvsConfig.Modelstill not cross-checked at spawn.Landing re-review — fix delta (
2180fbc..5d04c02) — CLEAN, MERGEDMerge:
e42ccf3onmain(contains5d04c02; head confirmed= origin/plane/HOMELAB-1674-harness). Diff is confined tointernal/harness/**, additive, nothing calls it yet → auto-merge class. No CI exists in this repo, so the local gate is the gate.Independent reproduction (module copied to
/tmp, each SHA checked out separately; worktree left clean):required:false, pin hash omitted,package_idre-derived): pre-fixValidate()=<nil>andRenderemitted the empty-Goal tree → post-fix both refuse with*MissingSectionError{Section:"brief"}. Note: omitting the pin hash is the escape hatch; keeping a matching hash was already caught pre-fix by the "empty but pinned" branch, so that spelling is not evidence of the fix.localhost/10.10.10.5/192.168.1.10base URL vs. in-clusterendpoints.omniroute: pre-fix 4/5 probes passed with the hot standby rendered intosettings.json→ post-fix all refuse naming the value and the remedy. Bonus: a blank override renderedANTHROPIC_BASE_URL:""pre-fix; now falls back to the declared endpoint./v1,/v1/,/v1/messagesandomniroute/-prefixed ids (S8ANTHROPIC_*MODEL, S1identity.model,Config.Model,Config.ExtraEnv): pre-fix all allowed → post-fix all refuse with the correct form named.Legit shapes unaffected: fixture as-is, explicit in-cluster base URL, bare model id,
New()with legal config — all still pass (I re-ran these as controls; the new tests assert the same).Gate (re-run by me, not taken from the PR):
go test ./... -count=1and-racegreen (39 tests + 14 subtests = 53 cases, reconciling the earlier "42");go vet ./...clean;gofmt -lclean; coverage 79.1% / 90.6%. Goldens byte-compare; the only golden deltas are the correctedidentity.modeland the content address it propagates. Merged tree re-tested green from a fresh extract ofmain.Non-blocking notes for the operator:
env.ANTHROPIC_BASE_URLmust now be a*.svc.cluster.localhost — shorter cluster spellings (omniroute:20128) and bare ClusterIPs are refused, and publichttps://omniroute.aaron.reynoza.orgmoved from allowed to refused. Per design §2.2 S8/contract §1 this is the intended tightening, withendpointHostAllowlist(ships empty) as the reviewable hatch — flagged so LD2.1's compiler builds the FQDN./v1/messagescase reports the doubled path as…/v1/messages/v1/messages. Both accurate, neither misleading.staleness.ttl/hard_invalidatorsunenforced (LD2.1),fixture identity.modelvsConfig.Modelnot cross-checked at spawn,--add-dirCLAUDE.md ingestion doc-derived not run-proven.