Skip to content

fix(runner): code_sha is refused when unmeasured, never the all-zero placeholder - #208

Merged
cipher813 merged 1 commit into
mainfrom
fix/code-sha-is-forty-zeros-i10454
Sep 11, 2026
Merged

fix(runner): code_sha is refused when unmeasured, never the all-zero placeholder#208
cipher813 merged 1 commit into
mainfrom
fix/code-sha-is-forty-zeros-i10454

Conversation

@cipher813

@cipher813 cipher813 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What / why

Closes: alpha-engine-config-I10454.

Every v2 manifest a dispatched box wrote carried code_sha as forty zeros. crucible.runner._code_sha() fell back to the all-zero placeholder whenever git rev-parse HEAD failed — a wheel install on a spot box has no git checkout — and that value satisfied code_sha's own ^[0-9a-f]{40}$ pattern, identical to a real commit sha. crucible/AGENTS.md calls code_sha "half of explain's answer to 'why did it do that'" — that half was silently absent on every box-produced manifest, and nothing refused it.

Where code_sha was meant to come from, and why the box can't derive it: the box installs a wheel, not a checkout — there is no .git to read. The commit that produced the wheel IS the sha releases/{sha}/ is addressed by (the same sha the box already resolves CRUCIBLE_RELEASE_SHA from). So code_sha has to be carried IN the release, the same path release_sha already travels — not derived on the box, and not a second value to compute. The companion PR (nous-ergon-ops-PR1203) exports CRUCIBLE_CODE_SHA from that same already-resolved $SHA.

The fix here (crucible side):

  • crucible.runner.resolve_code_sha replaces _code_sha(). $CRUCIBLE_CODE_SHA wins when set and is validated as a real 40-hex sha (not the placeholder); otherwise git rev-parse HEAD off the tree this module ships from (the laptop/CI path, where the env var is normally unset). Either source producing anything else raises CodeShaError.
  • Resolved once, at the very top of run_job — before the trading day, before any work — mirroring resolve_run_mode's existing shape exactly. This is deliberate: code_sha is required in every manifest the runner writes, including the _minimal_failed_manifest fallback-of-fallbacks, so a raise from inside the write path would collide with "manifest or it did not happen" (rule 1) — the write path can never be the place this fails. Raising before run_job does anything means the process never reaches a job that would need a manifest, the same way an undeclared run_mode already refuses.
  • RunManifestV2 gains _code_sha_is_not_the_placeholder, a model_validator (not a tightened pattern — pydantic-core's regex engine has no look-around: SchemaError: look-around, including look-ahead and look-behind, is not supported, measured against pydantic-core 2.46.5). Mirrored into the published schema's allOf by _run_manifest_v2_json_schema_extra, same shape as the existing status/reason cross-field rule, so a consumer with no Python import gets the same refusal.
  • release_sha is untouched. It is real on every manifest measured today; this issue is about code_sha specifically (the issue body explicitly separates the two questions).

Found and NOT fixed here (filed separately, see below): crucible/deploy.py and crucible/promote.py carry the identical all-zero-sha-as-fallback shape (_UNKNOWN_SHA = "0" * 40" in deploy.py; code_sha or "0" * 40 in promote.py, feeding ChampionPointer). Neither is in this PR's owned files (runner.py, manifest.py, release.py, explain.py), and touching them risks colliding with the sibling session working track_f.py/gate.py/config.py. Filed as alpha-engine-config-I10506 — see the tracker note below.

crucible explain: already reads code_sha straight off the manifest (explain.py's Lineage.to_dict/render) — no code change needed there. It was already correctly wired; it just displayed zeros because the producer wrote zeros. It now displays the real value automatically.

Proof of red

Reproduced against the PRE-FIX code (temporarily reverted crucible/models.py, crucible/runner.py, crucible/schemas/run_manifest.v2.json via git checkout --, then restored via git apply of the saved diff — not part of this diff):

def test_git_unavailable_writes_all_zero_code_sha(tmp_path, monkeypatch):
    monkeypatch.delenv("CRUCIBLE_CODE_SHA", raising=False)
    monkeypatch.setenv("PATH", str(tmp_path))  # a directory with no `git` in it
    store = LocalStore(tmp_path / "store")
    run_job("smoke", lambda ctx: None, store=store, trading_day=TRADING_DAY)
    doc = json.loads(store.get_bytes(manifest_key("smoke", TRADING_DAY.isoformat())))
    validate(doc)  # PRE-FIX: passes even though code_sha is the placeholder
    assert doc["code_sha"] == "0" * 40, doc["code_sha"]

Pre-fix: 1 passed — the manifest validated clean with code_sha == "0"*40. Post-fix (this diff), the identical scenario: 1 failed, crucible.runner.CodeShaError: $CRUCIBLE_CODE_SHA is unset and \git rev-parse HEAD` could not run ([Errno 2] No such file or directory: 'git')` — no manifest written at all.

The committed proof lives in two new test classes, both written and seen failing before the fix:

  • tests/test_runner.py::TestCodeShaIsMeasuredNotPlaceholder (5 tests: env override used, malformed/placeholder env refused, git-unavailable refused with no manifest written, git used when env absent, failed-run path still carries the real value)
  • tests/test_manifest_schema.py::TestCodeShaRefusesTheAllZeroPlaceholder (4 tests: model refuses it, published schema file alone refuses it with no Python import, a real-looking-but-mostly-zero sha still validates, release_sha is unaffected)

Test plan

  • uv run --frozen ruff check . — clean
  • uv run --frozen ruff format --check . — clean
  • uv run --frozen pytest -q --ignore=tests/acceptancefull blocking suite green, run BEFORE opening this PR
  • tests/acceptance unaffected (still RED by design, unrelated to this change)

Cross-repo

Paired PR: nous-ergon-ops-PR1203 (branch fix/export-crucible-code-sha-i10454) exports CRUCIBLE_CODE_SHA from the crucible-v2 dispatcher's already-resolved release sha. Neither PR depends on the other's merge to pass its own CI — this PR's resolve_code_sha already handles the "no env var, no git" case by raising, which is correct even before the box exports the variable (a box without the env var will simply refuse to run until the companion PR is live, which is the intended fail-loud behavior rather than a silent zero).

Out-of-scope finding filed

alpha-engine-config-I10506crucible/deploy.py and crucible/promote.py carry the same all-zero-sha placeholder fallback class this issue fixes in runner.py; both are outside this PR's owned files.

Prepared by: Claude Opus 5 (1M context) via Claude Code

…placeholder

alpha-engine-config-I10454: every v2 manifest a dispatched box wrote carried
code_sha as forty zeros. `_code_sha()` fell back to it whenever `git
rev-parse HEAD` failed (a wheel install has no git checkout), and the value
validated against code_sha's `^[0-9a-f]{40}$` pattern the same as a real
commit sha -- half of `explain`'s answer to "why did it do that" was
silently absent on every box-produced manifest.

- crucible.runner.resolve_code_sha replaces _code_sha(): $CRUCIBLE_CODE_SHA
  wins when set (the box's dispatcher will export this from the same
  releases/current sha it already reads CRUCIBLE_RELEASE_SHA from -- see
  the paired nous-ergon-ops PR), else `git rev-parse HEAD` off the tree
  this module ships from (the laptop/CI path). Either producing something
  other than a real sha raises CodeShaError, resolved once at the top of
  run_job -- before the trading day, before any work -- mirroring
  resolve_run_mode's existing shape, so the refusal happens where a raise
  cannot collide with "manifest or it did not happen": the process never
  reaches a job that would need one.
- RunManifestV2 gains _code_sha_is_not_the_placeholder (a model_validator,
  not a pattern -- pydantic-core's regex engine has no look-around),
  mirrored into the published schema's allOf by
  _run_manifest_v2_json_schema_extra, same shape as the status/reason
  cross-field rule. Both the model and the plain-jsonschema file refuse
  the placeholder on their own.
- release_sha is untouched: it is real today on every manifest measured,
  and this issue is about code_sha specifically.

Proof of red: a standalone reproduction (kept out of the diff, evidence in
the PR body) showed the pre-fix runner writing code_sha="0"*40 on a
simulated no-git box and the manifest validating clean; re-run against this
fix, the same scenario now raises CodeShaError before any manifest is
written. tests/test_manifest_schema.py::TestCodeShaRefusesTheAllZeroPlaceholder
and tests/test_runner.py::TestCodeShaIsMeasuredNotPlaceholder are the
committed proof, both new and both failing against the pre-fix code.

Full suite green (uv run --frozen pytest -q --ignore=tests/acceptance),
ruff clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WARhC81oatq9czVeyaM66L
@cipher813

Copy link
Copy Markdown
Contributor Author

HOLD — this PR must not merge until the crucible-v2 stack is applied. Marking it draft with gate:dependency rather than leaving it mergeable.

The ordering hazard is real and this PR's own body names it honestly: resolve_code_sha raises when neither $CRUCIBLE_CODE_SHA nor a git checkout yields a real sha. That is the correct fail-loud behaviour and it is exactly why the order matters.

nous-ergon-ops-PR1203 — the box-side half that exports CRUCIBLE_CODE_SHA — merged to nous-ergon-ops main at f1237b7c. A merge is not an apply. The running box still boots from the previously-applied template, which does not export that variable. So if this PR merges now, deploy.yml flips releases/current to a wheel whose every job refuses to start on a box that cannot supply the value — and the next thing to run on that box is Saturday 2026-09-12's graded weekly arc, whose live_saturdays_first_attempt_ok clause grades the FIRST attempt and cannot be re-run for credit.

Blocked-by: the crucible-v2 CloudFormation stack applied at or after nous-ergon-ops f1237b7c

Clearing it, in order:

  1. Operator applies the stack: bash /Users/brianmcmahon/Development/nous-ergon-ops/scripts/apply_crucible_v2_stack.sh (one line, no arguments — it derives all 1,233 characters of --parameter-overrides and refuses a checkout whose template is behind origin/main).
  2. Confirm Successfully created/updated stack - crucible-v2.
  3. Then gh pr ready 208, merge, and let deploy.yml smoke and flip.
  4. Re-rehearse the full nine-stage arc against the new release before Saturday: bash nous-ergon-ops/scripts/dispatch_crucible_v2_job.sh weekly 2026-07-31 replay. That date is outside both the five graded replay Saturdays and the five-trading-day absence horizon, so a failed rehearsal cannot take a green clause red or fire a page.
  5. If the rehearsal fails, crucible release.pin <previous sha> restores the known-good pointer in about two seconds.

This is the same class as alpha-engine-config-I10499, filed today: a merged CFN change is inert until applied, and only crucible-v2 has a detector that says so. Here the detector is this comment plus the gate label — which is weaker than a check, and is why I10499 exists.

@cipher813 cipher813 added the gate:dependency Blocked by work outside this PR; cleared by gate_dependency_sweep.py when the named blocker closes label Sep 11, 2026
@cipher813
cipher813 marked this pull request as draft September 11, 2026 18:02
@cipher813
cipher813 marked this pull request as ready for review September 11, 2026 21:11
@cipher813
cipher813 merged commit 1e2a913 into main Sep 11, 2026
8 checks passed
@cipher813
cipher813 deleted the fix/code-sha-is-forty-zeros-i10454 branch September 11, 2026 21:11
@cipher813 cipher813 added agent-merged Merged by an agent under an explicit in-session instruction or a standing exception and removed gate:dependency Blocked by work outside this PR; cleared by gate_dependency_sweep.py when the named blocker closes labels Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-merged Merged by an agent under an explicit in-session instruction or a standing exception

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant