Skip to content

Harden Mercurial ancestry and head validation - #7392

Merged
briansrls merged 7 commits into
mainfrom
agent/scm-mercurial-r0-hardening
Jul 29, 2026
Merged

briansrls merged 7 commits into
mainfrom
agent/scm-mercurial-r0-hardening

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow up the merged Mercurial R0 model with construction-safe zero/one/two-parent ancestry, fail-closed cycle and duplicate-parent detection for changelog and filelog revisions, and validation that observed open named-branch heads equal the heads derived from the captured graph. Keep R0 conditional advance unavailable until an executing transport supplies a typed receipt.

This addresses the substantive Mercurial findings from review 44144 and the cross-PR model review without introducing a common SCM carrier.

Test plan

  • All 33 witnesses in dag/test/claim/mercurial_upstream_model_witness_test.dag: PASS.
  • PATH=/tmp/gunbc-mercurial-source.2qwAwR/mercurial-7.2.3:$PATH target/debug/claim_batch --wet --source-root dag --source-root src/v2 --entry dag/test/manual/mercurial_upstream_model_execution_test.dag --functions witness_mercurial_cli_fixture_projects_and_independently_reads_back: PASS.
  • git diff --check: PASS.

@gunbai-bot gunbai-bot Bot changed the title business Harden Mercurial ancestry and head validation Jul 28, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 28, 2026 23:54
@briansrls
briansrls merged commit fa3d4fc into main Jul 29, 2026
5 checks passed
@briansrls
briansrls deleted the agent/scm-mercurial-r0-hardening branch July 29, 2026 02:45
briansrls added a commit that referenced this pull request Jul 29, 2026
…ry missing function as "no main" (#7401)

The generated-artifact heal ran a binary and a source tree from DIFFERENT
revisions. `build` compiles the default pull_request checkout — the merge ref —
while `heal` checks out `github.head_ref`, the branch head, because it pushes the
regeneration back. Nothing paired them. A generated artifact's identity is
(authority source x compiler), and the job silently combined two revisions of it.

Latent until a seed change adds a Rust->.dag call. #7348 landed the scoped-run
observation seed, whose Rust calls
gunbc.observation_seed_render.scoped_run_begin_write_measured; every open PR whose
branch head predated it went red at once (#7392, #7394, #7395 — identical failure,
none of them at fault), while main stayed green because ci_regen_heal_if skips
this job on push. The regression was invisible where it landed and blamed three
PRs that did not cause it.

Reproduced by execution: ONE binary built from the merge ref, run against a
pre-#7348 tree reproduces the exact CI failure; run against the same tree with the
advance merged in it exits 0 with zero drift.

The guard compares the tree against `github.sha` — the revision the build job
actually compiled — not against `origin/main`. Per operator ruling 2026-07-29,
`origin/main` names several different things (this runner's, a developer's, the
real source of truth) and can race an advance mid-run; the binary's own provenance
has no such ambiguity and is known exactly within the same workflow run. An
unresolvable provenance refuses rather than assuming agreement.

It is deliberately narrow: only SEED (src/v1) commits can introduce a Rust->.dag
expectation, so a .dag-only advance cannot skew a compiled binary and does not red
the PR. At the incident's branch point the set is two commits, one of which IS
445bac3 (#7348) — the guard names the responsible commit in its own refusal; at
current main and post-merge it is empty and silent. Preventing a stale branch from
MERGING is deliberately out of scope: that needs a source-of-truth model the repo
does not have yet (SCM lane, after v1 deletion). Landed as a declared Scaffold —
validation where construction (heal building from the tree it heals) was too
expensive today — with that named dissolution trigger.

Second, independent defect, and the reason this cost hours to find: both
run_in_context and run_in_context_with_args threw away the name they had just
failed to look up and reported InterpError::NoMainFunction, so EVERY missing named
function surfaced as "no main function found". main_wet was never missing.
NoSuchFunction { name } already existed; both sites now use it, and since those
were NoMainFunction's only two constructors the variant is deleted rather than
left unconstructible. Its one consumer, run_claim_failure_receipt, had to match
both variants precisely because of this bug and now matches the single honest arm.
Proven by a discriminating run: the same failure now reads
"no such function: scoped_run_begin_write_measured".

Verified: whole-tree compile 0 blocking errors; generated_artifact_drift_test 8/8
including witness_committed_is_fixed_point (ci.yml regenerated from the model, not
hand-edited) and its RED control; observation_seed_scoped_run_witness_test 10/10;
cargo test --test scoped_run_observation 7/7; cargo fmt --all --check clean;
emitted guard bash -n clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 2, 2026
… emission

The heal capability died with `ci.yml` in the 2026-08-15 floor cut
(gunbc.rung_drop floor_cut_heal). Everything the job DOES survived that cut
unconsumed -- gunbc.heal_push_plan decides per artifact whether the Actions job
credential may advance its path, gunbc.ci_spec renders the git config / drift
census / staging / commit / push script from that decision, and
gunbc.ci_heal_credential models the checkout step whose persisted credential is
the push authority. What did not survive was a WORKFLOW emitting a job that
binds them, so the whole model was a check with no consumer.

This lands that consumer as a job of gunbc.witness_floor_workflow, the
repository's only CI workflow authority, rather than as a restored second
workflow file: witnesses.yml is already a registered generated artifact, so no
GeneratedArtifact variant or registry row is needed and DESIGN's "CI is one
emission" is preserved.

FOUR ARMS OF THE HISTORICAL JOB ARE ABSENT BY CONSTRUCTION, NOT BY DELETION.
The skew guard, its automatic merge remedy, the `checkout --ours` conflict arm
and the ~200-path hand-maintained `:(exclude)` roster all existed to manage one
defect: the old job's binary came from a job that checked out the MERGE ref
while heal checked out the BRANCH HEAD, so the two could name different
revisions of src/v1 (incident 2026-07-29; #7392, #7394, #7395). This job builds
its own `gunbc` from the SAME branch-head checkout it then regenerates, in one
job, so binary and tree are one revision and there is no second revision to skew
against. DESIGN 4b: the class moves from rung 2 to rung 4, and the arms dissolve
with the climb. Two of them would also be defects to restore -- taking the ours
side is what the generated-artifact merge driver now refuses to do, and the
roster was a second authority for a fact gunbc.generated_artifact owns. The
auto-push-versus-author-commit split is derived from that registry instead.

THE PUSHED HEAD IS NOT REVALIDATED AND THE JOB SAYS SO. An Actions-credential
push starts no workflow run, so after a heal the PR's checks describe the prior
head. tools.ci_heal_dispatch targets `ci.yml`, which does not exist, and
witnesses.yml declares no expected_healed_sha input -- dispatching without it
would fabricate a revalidation rather than perform one. The job instead prints
SupersededByHealedHead naming both revisions and exits nonzero. The gap is
declared in the emission and its trigger names the capability (a dispatch input
the run binds github.sha and its checkout against), not an artifact.

The job carries no lane and no `needs` edge of the required `witnesses`
aggregate: heal REMEDIATES, the build lane's generated-artifact phase
ADJUDICATES. Making a remediator a gate would let its own nonzero arm block the
branch it had just repaired.

Also repaired: ci_heal_credential's ci_heal_job_ref and ci_heal_workflow_ref
were DeclarationRefs to the deleted gunbc.ci_workflow, so the grant deciding
whether the heal push is authorized was computed against a workflow and a job
with no existence. They now name the emission that carries the job.

floor_cut_heal is NOT retired here. Its trigger demands the capability observed
writing and pushing on a real divergence; an emission that typechecks is not
that observation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVcFN21urwK3LttBSF6EWK
briansrls pushed a commit that referenced this pull request Sep 2, 2026
… emission (#10118)

* MAIN-H: restore the generated-artifact heal job as a modeled workflow emission

The heal capability died with `ci.yml` in the 2026-08-15 floor cut
(gunbc.rung_drop floor_cut_heal). Everything the job DOES survived that cut
unconsumed -- gunbc.heal_push_plan decides per artifact whether the Actions job
credential may advance its path, gunbc.ci_spec renders the git config / drift
census / staging / commit / push script from that decision, and
gunbc.ci_heal_credential models the checkout step whose persisted credential is
the push authority. What did not survive was a WORKFLOW emitting a job that
binds them, so the whole model was a check with no consumer.

This lands that consumer as a job of gunbc.witness_floor_workflow, the
repository's only CI workflow authority, rather than as a restored second
workflow file: witnesses.yml is already a registered generated artifact, so no
GeneratedArtifact variant or registry row is needed and DESIGN's "CI is one
emission" is preserved.

FOUR ARMS OF THE HISTORICAL JOB ARE ABSENT BY CONSTRUCTION, NOT BY DELETION.
The skew guard, its automatic merge remedy, the `checkout --ours` conflict arm
and the ~200-path hand-maintained `:(exclude)` roster all existed to manage one
defect: the old job's binary came from a job that checked out the MERGE ref
while heal checked out the BRANCH HEAD, so the two could name different
revisions of src/v1 (incident 2026-07-29; #7392, #7394, #7395). This job builds
its own `gunbc` from the SAME branch-head checkout it then regenerates, in one
job, so binary and tree are one revision and there is no second revision to skew
against. DESIGN 4b: the class moves from rung 2 to rung 4, and the arms dissolve
with the climb. Two of them would also be defects to restore -- taking the ours
side is what the generated-artifact merge driver now refuses to do, and the
roster was a second authority for a fact gunbc.generated_artifact owns. The
auto-push-versus-author-commit split is derived from that registry instead.

THE PUSHED HEAD IS NOT REVALIDATED AND THE JOB SAYS SO. An Actions-credential
push starts no workflow run, so after a heal the PR's checks describe the prior
head. tools.ci_heal_dispatch targets `ci.yml`, which does not exist, and
witnesses.yml declares no expected_healed_sha input -- dispatching without it
would fabricate a revalidation rather than perform one. The job instead prints
SupersededByHealedHead naming both revisions and exits nonzero. The gap is
declared in the emission and its trigger names the capability (a dispatch input
the run binds github.sha and its checkout against), not an artifact.

The job carries no lane and no `needs` edge of the required `witnesses`
aggregate: heal REMEDIATES, the build lane's generated-artifact phase
ADJUDICATES. Making a remediator a gate would let its own nonzero arm block the
branch it had just repaired.

Also repaired: ci_heal_credential's ci_heal_job_ref and ci_heal_workflow_ref
were DeclarationRefs to the deleted gunbc.ci_workflow, so the grant deciding
whether the heal push is authorized was computed against a workflow and a job
with no existence. They now name the emission that carries the job.

floor_cut_heal is NOT retired here. Its trigger demands the capability observed
writing and pushing on a real divergence; an emission that typechecks is not
that observation.

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

* Fix two defects the floor's parse phase and the citation-debt roster caught

BOTH ARE MINE AND BOTH WERE INVISIBLE TO THE INSTRUMENT I HAD BEEN READING.

(1) SOURCE ANNOTATION AT THE WRONG GRAIN. The comment explaining why the heal
regen step is capability_neutral sat INSIDE the WitnessFloorBoundStep record
literal. DESIGN 4c models annotations at module-item grain only, so the floor's
parse phase refused with nine located diagnostics and zero witnesses ran. The
reasoning is preserved verbatim, hoisted above
heal_generated_artifacts_bound_steps where it is authorable.

WHY IT REACHED CI: `gunbc run ... main_wet` returned 0 over the whole tree and I
read that as "the tree typechecks". It is not that check -- the annotation-grain
rule executes in compile-clean and in the floor's parse phase, neither of which
main_wet runs. A green from an instrument that does not cover the class is not
evidence about the class.

(2) SPENT CITATION-DEBT ROWS. Repointing ci_heal_job_ref and ci_heal_workflow_ref
at gunbc.witness_floor_workflow made their PRE_EXISTING_CITATION_DEBT rows in
declaration_index.rs stale: the citations no longer refuse, and that roster only
shrinks, so the rows are deleted. Found by running v1_src_dag_parse, which
reports it directly -- not by CI, which had not reached it yet. It would have
been the next red.

The second one is the reason to go looking for a covering instrument after a
failure instead of repairing only the line the failure named.

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

* Drop the heal job's `actions` permission: its only consumer is not built

ci_heal_job_permissions carried actions: PermWrite for exactly one consumer --
github.Workflows.CreateDispatch, which the heal job invoked to revalidate the
head it had just pushed (#7544). That dispatch is deliberately not restored in
this cut, so the job was emitting `actions: write` for a capability it does not
exercise: a standing control-plane grant held in anticipation of a follow-up that
may never land. The credential surface should describe what the job DOES.

Now `contents: write` / `actions: none`. Where the grant comes back, when it
does: with the dispatch consumer, as that consumer's own carrier, at the point it
becomes reachable -- rather than pre-granted here where the follow-up would land
against a permission nobody re-derived.

VERIFIED NOT TO MOVE THE PLAN'S VERDICT, which is the thing this edit could have
changed silently: the auto-push versus author-commit split is still exactly the
three workflow projections refused and 33 paths staged, and the refusal cause is
still ActionsJobCredentialScopeUnavailable -- that split turns on the `workflows`
axis, not on `actions`.

Also merges current main (8e025f8; the design-ledgers split into
design-failure-modes.md and design-rung-drops.md), and re-derives the projection
against it.

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

* EVIDENCE PROBE: hand-edit a generated projection without regenerating it

DELIBERATE DRIFT, and the only commit on this branch that is meant to be undone
by a machine rather than by me. gunbc.rung_drop floor_cut_heal retires on one
thing: the heal capability OBSERVED writing regenerated bytes back and pushing
them, on a real divergence. An emission that typechecks and a green no-drift run
establish neither.

THE SUBJECT IS CHOSEN, NOT CONVENIENT. docs/plans/input-envelope-roadmap.md is
auto-push-eligible, verified against the emitted script itself rather than
assumed: it carries a `git add` line and does NOT appear in the
AUTHOR_COMMIT_DRIFT population. The three workflow projections would have been
the wrong subject -- they deliberately select AuthorCommitRequired, so drifting
one exercises the bundle-and-refuse arm while LOOKING like a heal run and would
have told me nothing about whether this job pushes.

PRE-STATE, recorded here because the receipt is worthless without the base it is
a claim about: the clean file is 3555 bytes, sha256
4da64e1495ef627c31fa21f3e31e2746ba28c5be447c9f6565d9a004f2c23ead.

EXPECTED, written before the run so the result cannot be read to fit: the
generated-artifact phase in the build lane goes RED on this commit (it
adjudicates, and this tree is genuinely not at its fixed point); the heal job
regenerates, stages exactly this path, commits as gunbc-ci-auto-heal, pushes to
this branch, prints HealProduced with both heads and CHANGED_ARTIFACTS naming
this file alone, then prints SupersededByHealedHead and EXITS NONZERO because
nothing has revalidated the head it just created.

If instead it prints HealNoChange, or HealAuthorCommitRequired, or exits zero,
the capability is not what this PR claims and the rung-drop row stays exactly
where it is.

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

* chore: regenerate drifted generated artifacts (ci auto-heal)

* Retire floor_cut_heal on an observed repair, and let the ledger say so

TWO CHANGES, COUPLED: the retirement, and the rendering repair without which
the retirement is invisible at the surface DESIGN sends readers to.

THE RETIREMENT, ON EXECUTION AND NOT ON THE EMISSION. floor_cut_heal's trigger
is a capability: a required-run consumer that, on a branch head whose committed
generated artifacts diverge from their authorities, writes the regenerated bytes
back and pushes them under a credential the repository already models --
observed doing so on a real divergence, not merely emitted.

  PROBE  330c74f hand-edited
         docs/plans/input-envelope-roadmap.md, a generated projection,
         3555 -> 3754 bytes, and did not regenerate it. Pre-drift sha256
         4da64e1495ef627c31fa21f3e31e2746ba28c5be447c9f6565d9a004f2c23ead,
         recorded BEFORE the drift. Subject chosen against the emitted script
         rather than assumed: it carries a `git add` line and is absent from the
         AUTHOR_COMMIT_DRIFT population, so it exercises the PUSH arm. The three
         workflow projections would have exercised bundle-and-refuse while
         looking like a heal run.
  RUN    33683175090 job 100433326005:
         HealProduced prior_head=330c74f07 healed_head=bc70468754
         changed_artifacts=docs/plans/input-envelope-roadmap.md -- exactly one
         path, no blast radius across the other 32 auto-push rows -- then
         SupersededByHealedHead and exit 1.
  PUSH   branch head moved to bc70468,
         authored gunbc-ci-auto-heal, 1 file changed, 2 deletions.
  IDENTITY, two ways: the healed file is byte-identical to the pre-drift digest,
         and a clean source-built regeneration ON the healed head changed zero
         files. src/ and dag/ are identical between that head and the tree the
         binary was built from, so there is no compiler/tree pairing assumed.

The predictions were written into the probe commit before the run, so the result
is not a story fitted to an outcome. Restored rung: 2. NOT restored and not
claimed: revalidation of the head heal creates, which keeps its own capability
trigger. floor_cut does not move -- its conjunction is derived by
standing_rung_drops(), so it follows this row without being edited.

THE LEDGER COULD NOT EXPRESS A RETIREMENT, which is why it is repaired here
rather than deferred. gunbc.design_ledgers read subject/declared/authored and
never read `standing`, so all four retired rows -- including three that predate
this PR -- rendered under headings spelled identically to the live ones, and
trigger_fired was rendered nowhere. A reader counting outstanding safety
regressions in the file DESIGN points them to counted retired ones among them.
Rendering only: no model state moves, and standing_rung_drops remains the
authority anything joins against.

ACCEPTANCE TEST, a membership join and not a count, since a count can agree
while the sets differ: subjects rendered without the retired marker in
docs/design-rung-drops.md == subjects whose rows carry standing: Standing in
gunbc.rung_drop. PASS at identity grain, 18 standing and 4 retired, both sets
equal, LC_ALL=C pinned on both sides after locale collation alone produced a
false difference. SHOWN TO DISCRIMINATE rather than asserted: un-marking one
retired heading makes it go red with one extra subject.

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

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant