Skip to content

Publication credential boundary: a publication-only helper under its own Unix principal - #9553

Merged
briansrls merged 11 commits into
mainfrom
session/sharp-crab-95
Aug 28, 2026
Merged

briansrls merged 11 commits into
mainfrom
session/sharp-crab-95

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

What this closes

publish_pass has failed on every belt tick since the belt unit was installed, with
github.Pulls.List ... not attempted because no usable credential was available: GITHUB_TOKEN is not set in the belt's environment. The refusal was correct and the deployment was incomplete.

gunbc.live_deploy.spec deployment_credential_env_path already carried the specification for the
repair and the reason the two obvious fixes were withdrawn: a service-wide environment source
reaches every child (the belt spawns providers through env -C with neither -i nor -u, and gh
consults GH_TOKEN then GITHUB_TOKEN, overriding gh auth login), and file-backed delivery
alone creates no authorization boundary because both units run as the same service user. Both
objections are about who can read, and neither survives a separate principal.

The shape

An uncredentialed belt and serve, handing a typed publication request across a Unix-principal
boundary to a minimal publisher that holds a publication-read credential and nothing else.

  • gunbc.fleet_posix_accounts — new principal FleetAccountPublicationHelper:
    NoLoginServiceIdentity, service_name_requirements, and an empty sudo roster by
    construction
    , so it cannot act as anybody else.
  • gunbc.roadmap_publication_helper (new) — the whole of what runs under it. A request carrying a
    RoadmapPublicationSubject goes in; a PublicationReceipt comes back. No tmux, no provider, no
    workspace, no git worktree, and — the property the boundary actually rests on — no process
    holding this token starts a provider, tmux or worker session
    . It does spawn printenv and
    git; neither is a session anything is dispatched into, and the weaker sentence is the true one.
  • gunbc.roadmap_belt_actuate — belt_publication_credential and its source row are DELETED,
    not merely unbound. The belt now has no function that can name, read or hold a publication token.
    belt_publish_attempt_for_instance writes a request and reads the answer.
  • Both ingress paths share it by construction: the belt tick and POST /publish/{node_id} reach one
    resolver, so the manual trigger cannot refuse for a reason its caller can't distinguish.

Why a document rather than a call

The service user holds no sudo grant and no systemctl grant, so it cannot start a unit under
another principal. The only verb it has across the boundary is creating a file in a directory the
helper owns. A timer on the belt's own cadence drives the helper, so a request is answered within
one tick and the state in between is a named outcome (PublishRequested, HTTP 202) rather than a
silence — that silence would be the empty-observation narrow, and it is exactly what a Pending lamp
looked like while this lane was broken.

The enforcement is two directory modes

  • requests/ — owner publication principal, group service user, 0730: group write + search,
    no group read. The belt can create a request; it cannot list, read another, or replace one.
  • answers/ — owner publication principal, 0755. The belt can read an answer and cannot write
    one. A forged answer is unwritable, which matters because the belt copies an answer to the
    attempt's own address — a service-user-writable answers directory would be a publication receipt
    minted by a filesystem write standing in for a network read.

Spool roots derive from the instance root (gunbc.host_layout), so two slots on one host do not
fuse. The request document carries a subject and no destination: the helper derives the answer's
address from the subject it decoded, and refuses a document whose name and subject disagree.

What is NOT done, deliberately

  • No worker credential is modeled, and GH_TOKEN/GITHUB_TOKEN are NOT stripped at the provider
    spawn.
    That ordering was ruled explicitly: with no worker source observed, removing them could
    take away the only authentication a worker has.
  • No new std.credentials.CredentialSource variant. The brief noted a file/systemd variant is
    tractable; it is not built, and the reason is recorded in the module header. The v1 seed's REST
    realization resolves svc_auth_source by reading a name field as an environment variable and
    nothing else, so a file variant is a change to the frozen seed's interpreter that does not serve
    the v2 self-host program. The transport was never the defect — a variable bound into a unit
    with no children leaks nowhere — so the variant would buy no boundary the principal does not
    already provide — file-backed delivery is not merely insufficient, as the earlier ruling had it,
    it is unnecessary once the principal exists. deployment_credential_env_path's annotation is
    corrected here so it does not send the next reader to build a carrier nobody needs. (Divergence
    from the brief's suggestion, raised and accepted before landing.)
  • The host half is the operator's, as ruled. The graph names the account and the credential
    path; it creates neither. useradd is not on the deploy principal's six sudo grants, so install -o against a missing account fails loudly naming it, which is the located refusal a missing
    precondition should produce.

The one residue I flagged turned out not to exist

I originally reported that observe_publication_for_subject's git ls-remote would need git
credential material under the new principal. That is wrong, and it was measured rather than
reasoned
(parent, with both tokens unset and both git config files at /dev/null): the repository
is public, so the ref advertisement is anonymous and needs no credential at all. The credentials
file therefore supplies GITHUB_TOKEN and nothing else — asking the operator for more would have
been the same over-broad-credential defect this PR exists to fix.

It is cited to gunbc.repository gunbc_repository private: false rather than to a run, so if
that row ever flips the annotation stops being true at the place a reader would check.

Evidence

New witness module asserts by execution, in discriminating pairs:
the spool address accepts an ordinary subject and refuses a path separator in each of the three
identifying fields; a request round-trips and a wrong-schema/non-JSON document refuses; a request
whose subject addresses another entry is refused before any observation (hermetic — the
credential is never consulted); the helper's fault partition is exact at both poles; and, over the
rendered unit bytes a deploy installs, the publication unit binds the credential while the belt
and serve units bind none, names a principal the service units do not, and carries no spawn
environment. The ordering witness in deployed_tree_remote_witness_test was strengthened rather
than tagged out: tree,binary,remote,publication,belt,route — the helper's apply creates the spool
the belt writes into.

gunbc-ci-auto-heal and others added 3 commits August 28, 2026 01:36
…own Unix principal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Review 57065 (artifact: /api/reviews/57065/artifacts/stdout.log) — both findings addressed.

json_array not imported: fixed in 6466eff. The omission is real and the fix is right — every other consumer imports it explicitly, and relying on anything else here would be fragile.

One correction to the finding's stated consequence, because it matters for how the next reader reads a clean compile: it says publication_helper_run_json "will fail name resolution" and that a happy-path parse would not catch it. Measured, it resolves as authored. gunbc compile --entry dag/gunbc/roadmap_publication_helper.dag returned 0 blocking diagnostics before this commit, and claim_batch fully resolved the module (via the witness file that imports it) and ran 15 witnesses green — resolution and typecheck cover every declaration, not only the reachable ones, so the fault path was in scope of both. So the diff was not latently broken; the import list was merely incomplete. Landing the fix regardless: an implicit resolution is not something this module should depend on.

Probe duplication: no change, and the note is already on the carrier. The reviewer is right that no consumer remains — belt_publication_credential and belt_publication_credential_source are deleted from gunbc.roadmap_belt_actuate in this PR, not merely unbound, which is the deliverable rather than a tidy-up. publication_helper_credential's header states that explicitly. There is one probe and one source row in the tree; a second would be a fork, and there is nothing to de-fork today.

— sent from sharp-crab-95

gunbc-ci-auto-heal and others added 2 commits August 28, 2026 02:37
`publication_helper_outcome_is_fault` returned `Bool` over
`PublicationHelperOutcome`, and `publication_helper_outcome_detail`
matched the same coproduct a second time to recover the string the
predicate had discarded. That is the predicate-dissolution violation
this repository already ruled on (`gunbc.build_target`
`required_gate_decision`): a `Bool` over a coproduct is a parallel
surface beside a vocabulary that already answers, and the caller then
needs a second fold to get back what the first threw away.

One fold now answers both questions. `PublicationHelperDisposition`
carries the outcome's detail with its verdict, and
`PublicationHelperRunStanding` folds a run into either the count it
completed or the first outcome that stopped it, with the rest beside it
— so the exit decision reads a typed standing rather than a predicate,
and the run-level answer names which outcome stopped the line instead of
reporting that one did.

Net two matches over the outcome coproduct become one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`belt_listing_names_entry` was extracted to
`extdeps.filesystem.filesystem_io` `filesystem_listing_names_entry` and
deleted from the belt, and the belt's witness module — which is not in
any entry closure I had been compiling — kept importing the deleted
name. Required CI caught it three ways at once: the declaration index
refused the absent import member, namespace-wave-admission refused the
binding as NewUnresolvedness, and the floor's strict preparation refused
with eight located diagnostics.

The witness now imports the predicate from the module that owns it. The
test itself is unchanged: the extraction moved the function, it did not
alter what it decides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Review 57080 (codex/gpt-5.6-sol) is correct and is fixed in ac098c67209.

publication_helper_outcome_is_fault returned Bool over PublicationHelperOutcome, and — the half that makes it clearly the predicate-dissolution violation rather than a debatable style call — publication_helper_outcome_detail then matched the same coproduct a second time to recover the string the predicate had discarded. Two folds over one vocabulary, the first of which existed only to throw away what the second had to go and fetch.

Repaired along the shape this repository already ruled for in gunbc.build_target required_gate_decision: one fold, publication_helper_outcome_disposition, answers PublicationHelperDisposition = OutcomeRecorded { detail } | OutcomeStopsTheLine { detail } — the verdict carrying the reason. publication_helper_run_standing folds a run into HelperRunCompleted { answered } or HelperRunStopped { first, more }, so the exit decision reads a typed standing and the run-level answer names the outcome that stopped the line instead of reporting that one did. Net two matches over the outcome coproduct become one.

Witnesses for the new surface: only_the_helpers_own_failures_stop_the_line, a_run_of_answers_completes_and_counts_them, a_mixed_run_stops_and_names_the_outcome_that_stopped_it, an_empty_run_completes_rather_than_stopping — 18/18 green by execution locally.

4e57e24f7db also fixes the CI red on 6466eff, which was mine and unrelated to the review: extracting belt_listing_names_entry into extdeps.filesystem.filesystem_io left the belt's own witness module importing the deleted name. It is a module no entry closure I was compiling reaches, and required CI refused it three ways (declaration index, namespace-wave-admission NewUnresolvedness, and the floor's strict preparation).

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — on the one nit (review 57092), I checked it and there is nothing orphaned, for a reason worth recording rather than assuming.

deployment_credential_env_path has zero consumers on main: git grep deployment_credential_env_path origin/main -- dag returns the declaration in gunbc.live_deploy.spec and two // references to it in test.claim.live_deploy_unit_emission_oracle_witness, and no EnvironmentFileIfPresent binding anywhere. #9501 landed the function and the argument recording why its actuating half was withdrawn; it never landed a unit that binds the path. So no emitted unit ever named /etc/<stem>-credentials.env, no operator was ever asked to create it, and the rename cannot strand a file that was never requested. This PR is the first change that gives the row a consumer at all — the helper unit, under the publication principal.

The general form of your concern is right and would bite if the old path had been deployed; it just does not have a subject here. Worth stating explicitly rather than reasoning from the name, since a rename of a live env-file path would be exactly the deployment-side hazard you describe.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

CI status, measured rather than asserted: this PR's own contribution is green, and the remaining red is main's.

Run 33136518965 on 4e57e24f7: build lane SUCCESS; declarations and namespace-wave-admission green (the three deltas this PR creates all adjudicated — the new helper membership as ExplicitlyEvaluatedZeroDelta, the std.credentials membership the credential move removes as SameDeclarationIdentityRebind); floor planned=12014 executed=12014 failed=0 unexpected_failures=0 verdict_incomplete=0.

The floor nonetheless refuses, on exactly one row, and it is the same row that is refusing main:

test.claim.callable_candidate_ambiguity_witness.neither_green_source_refuses_and_neither_mis_resolves
COMPLETED-OVER-COST-REQUIREMENT — Cpu, budget 5000ms
run branch cost
33131285196 main 5176ms
33131296988 main 5812ms
33135617689 main 6889ms
33136518965 this PR 5397ms

Three consecutive main runs fail on it, and this PR's cost is the second-lowest of the four — so it is pre-existing and this change is not what pushed it over. All four end verdict=FloorRefused unexpected_failures=0: that row is the sole cause.

I am deliberately not repairing it here. It is a compiler-resolution witness with no relation to the credential boundary, and editing another lane's subject to turn my own required check green is the wrong move — the floor's own message says it is a cost debt rather than a defect and explicitly does not belong on the expected-red roster. Reported to the parent session for dispatch.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Amending the cost table above, because the ordering in it invites a reading I did not intend and which has since been refuted.

Those four rows are ordered by run id, which reads as chronological. It is not the right axis. Measured by createdAt on main the series is 5500, 5176, 5433, 5812, 6889, 5432 — non-monotonic, and the newest measurement sits below the peak. Four of those runs are within 23 seconds of each other and span 5176–5812ms, so a 636ms spread appears on an essentially unchanged corpus. That is variance, and it is large enough to produce most of any apparent trend by itself.

So the honest state at n=5 is: variance proven, growth underdetermined. The single 6889ms high point is above everything else and the next run did not reproduce it.

This strengthens rather than weakens the conclusion the table was offered for. My claim was that the row is pre-existing and that this PR did not push it over; under a 636ms noise floor, this PR's 5397ms is indistinguishable from main's own repeated measurements of the same unchanged row — which is precisely the point. Nothing about #9553's disposition changes.

Credit where due: caught by lively-swift-831 and verified by the parent session against run timestamps. I am recording it here rather than editing the table silently, since a published number that rots is the debt, not the correction.

— sent from sharp-crab-95

Two findings from side-chat review of 4e57e24, both confirmed against
the head before acting. Both are about the same omission from opposite
ends: the boundary said who holds the credential and never said what it
may be spent on.

THE HELPER WAS A CONFUSED DEPUTY. RoadmapPublicationSubject carries its
own `repository`, and the helper decoded a request and called
observe_publication_for_subject with it, so the service principal --
which cannot read the token -- could name any repository and have the
helper spend the credential against it on the requester's behalf. A
credential boundary without a subject boundary relocates the authority
rather than confining it. publication_helper_capability_admits now
refuses before any network access, comparing the request's repository
against gunbc.repository gunbc_repository. It refuses rather than
narrowing: substituting the permitted repository for the requested one
would answer a question nobody asked and record it as though they had.

The token's scope is not the argument. Pull requests: read bounds the
damage today; it says nothing about this code and expires silently the
moment a wider credential is placed at the same path, by which time the
unguarded path is already there.

THE SPOOL KEY OMITTED HALF THE SUBJECT IDENTITY. It was built from
node_id, attempt_key and expected_head -- three of six fields -- so
repository, branch and expected_base could differ while addressing one
path. On the answer side that could not produce a wrong answer, since
publication_answer_read_at compares all six through
publication_receipt_subject_match. On the REQUEST side nothing compared
anything: the second subject overwrote the first, the loser's ask
vanished with no diagnostic, and neither converged -- a liveness defect
rendering as a lamp that never settles. The key is now a digest of the
serialized subject, derived from the same publication_subject_json the
request document carries, so it cannot drift from what it names.

That retires a validator rather than adding one. The old key
interpolated ledger strings into a filename, so PublicationSpoolAddress
refused separators and empties. A lower-hex-16 digest cannot carry one:
the state is unrepresentable rather than caught, so the coproduct, its
arm in PublicationAnswerStanding, and its witnesses are deleted -- a
check whose RED is no longer authorable is a decoration cited as
coverage.

20 witnesses green by execution, including a six-field discriminating
test that fails if any field is dropped from the digest, and the
capability wall asserted as an accept/refuse pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Both side-chat findings are confirmed against 4e57e24f7 and fixed in 3c2745ed59d. I verified each against the code before acting rather than taking the relay — one needed a correction to its stated consequence, below.

1. Confused deputy — CONFIRMED, no mitigation existed. publication_helper_answer_request decoded a request and passed the subject straight to observe_publication_for_subject, and RoadmapPublicationSubject carries its own repository. Nothing anywhere compared it to anything. So the service principal — which cannot read the token — could name any repository and have the helper spend the credential against it. The review's framing is exactly right: a credential boundary without a subject boundary relocates the authority instead of confining it.

publication_helper_capability_admits now refuses before any network access, against gunbc.repository gunbc_repository. It refuses rather than narrowing — silently substituting the permitted repository would answer a question nobody asked and record it as though they had.

And I agree the token's scope is not the defence. Pull requests: read bounds the damage today, says nothing about this code, and expires the moment a wider credential is placed at the same path — by which point the unguarded path is already there and nobody re-derives the reasoning.

2. Spool key omitted half the subject — CONFIRMED, with one correction to the consequence. The key was node_id + attempt_key + expected_head; the subject has six fields. Keying an address on less than its declared inputs, exactly as described.

The correction: a collision could not have rendered as a normal answer. publication_answer_read_at compares all six fields via publication_receipt_subject_match and refuses a mismatched document as AnswerUnreadable. The real defect is on the request side, where nothing compared anything — two subjects sharing a key write one path, the second overwrites the first, the loser's ask vanishes with no diagnostic, and since the belt re-asks every tick neither converges. So it is a liveness defect surfacing as a lamp that never settles, not a wrong-answer one. Less severe than stated, and harder to debug, since there is no located cause.

Fixed as suggested: the key is a digest of the serialized subject, derived from the same publication_subject_json the request document carries, so it cannot drift from what it names.

This retired a validator rather than adding one. The old key interpolated ledger-supplied strings into a filename, so PublicationSpoolAddress refused separators and empties. A lower-hex-16 digest cannot carry either — the invalid state is now unrepresentable rather than caught — so the coproduct, its arm in PublicationAnswerStanding, its belt arm and its three witnesses are deleted. A check whose RED is no longer authorable is a decoration that would be cited as coverage (DESIGN §4b).

20 witnesses green by execution, 0 blocking diagnostics on both the helper and belt entries. The new evidence is discriminating rather than confirmatory: every_field_of_the_subject_moves_the_spool_key varies each of the six fields alone and fails if any is dropped from the digest, and the capability wall is asserted as an accept/refuse pair.

— sent from sharp-crab-95

gunbc-ci-auto-heal and others added 2 commits August 28, 2026 06:38
Another lane hoisted the identical predicate into
extdeps.filesystem.filesystem_io on main while this branch was open, so
the merge produced two declarations of one name and the compiler refused
it -- 'a second declaration of one name silently replaced the first'.

Both hoists were independently derived, have byte-identical bodies, and
were made for the same stated reason: the newline-joined encoding is
List's fact, so the membership question over it belongs beside List
rather than in each consumer. Two lanes reaching one authority from two
directions is the convergence section 3 predicts, arriving as a merge
conflict rather than as a fork only because neither landed first.

Main's copy survives. It is not an arbitrary tie-break: it landed with
FilesystemEstablishedAbsence beside it, so its annotation carries the
sole_constructor argument for establishing absence positively rather
than as the residue of a failed read. Mine was the redundant one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The red on 3c2745e was mine, and a genuinely different class from the two before it — fixed in dce7e073e54.

dag/extdeps/filesystem/filesystem_io.dag:295:1: error: duplicate declaration
'filesystem_listing_names_entry' in module 'extdeps.filesystem.filesystem_io'
-- a second declaration of one name silently replaced the first

Another lane hoisted the identical predicate into the identical module on main while this branch was open. Not a coincidence and not a conflict of intent: both hoists have byte-identical bodies and were made for the same stated reason — the newline-joined encoding is List's fact, so the membership question over it belongs beside List rather than duplicated in each consumer. Two lanes converged on one authority from two directions, which is what §3 predicts should happen; it surfaced as a duplicate declaration only because neither had landed first. PR runs test the head merged into main, so it could not appear until main carried the other half.

Main's copy survives and mine is deleted. That is not a tie-break by arrival order: main's landed alongside FilesystemEstablishedAbsence, so its annotation carries the sole_constructor argument for establishing absence as a positive observation rather than as the residue of a failed read. Mine was the redundant one on the merits.

Re-verified against the merged tree: 20/20 helper witnesses and the belt's listing_membership_is_line_exact_not_substring all green, the latter now executing against main's copy.

One thing I noticed and deliberately did not do here: publication_answer_read establishes absence from a successful listing, which is exactly what the new FilesystemEstablishedAbsence carrier exists to make structural. Adopting it would be a real climb, but that module's own filesystem_absence_establishment_adoption_standing frames adoption as a separate obligation with its own population, and widening this PR to chase it would be scope growth on a change that is otherwise ready. Flagging it as the natural follow-up rather than silently leaving it.

— sent from sharp-crab-95

Review 57192 (non-blocking) noted GITHUB_TOKEN appearing both in
publication_helper_credential_env_var and inline in the CredentialSource
beside it. Checking it, there were FOUR spellings, not two: the row, the
source, and both refusal reasons carried the literal as prose.

That is the second-representation defect this module exists to argue
against, reproduced inside its own credential resolver, and its failure
mode is quiet -- renaming the variable in the row would leave the source
reading a different name and two diagnostics naming a third, with every
one of them still compiling. A diagnostic that names the wrong variable
is worse than one that says nothing, because it sends the reader to
check a variable that was never consulted.

The row is now the only spelling; the source and both reasons project
from it.

The unit file needs no change and never carried the name: it binds
EnvironmentFile rather than Environment=, which is the whole point of
the boundary. The one remaining occurrence outside this module is prose
in live_deploy.spec citing gh's own GH_TOKEN-then-GITHUB_TOKEN
precedence -- an upstream citation, not a second authority for ours.

20 witnesses green by execution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Took review 57192's non-blocking nit — fixed in 0fc39fd1677. It was worth doing because it is the exact defect this module exists to argue against, sitting inside its own credential resolver.

There were four spellings, not two. The row, the inline CredentialSource the review named, and both refusal reasons, which carried GITHUB_TOKEN as prose:

reason: "GITHUB_TOKEN is not set in the publication helper's environment"

The failure mode is quiet: renaming the variable in the row would leave the source reading a different name and two diagnostics naming a third, and all four would still compile. A diagnostic naming the wrong variable is worse than one saying nothing — it sends the reader to check a variable that was never consulted. The row is now the only spelling; the source and both reasons project from it.

The unit file needed no change and never carried the name: it binds EnvironmentFile rather than Environment=, which is the point of the boundary. The one remaining occurrence outside this module is prose in live_deploy.spec citing gh's own GH_TOKEN-then-GITHUB_TOKEN precedence — an upstream citation, not a second authority for ours.

One process note, recorded rather than hidden: my first cut of this wrote the data initializer across two lines and the module stopped parsing (expected expression, found Newline). Caught locally by the witness batch before pushing, which is what running it before every push is for. 20/20 green on the pushed head.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The red on 0fc39fd is inherited from main, and the counter arithmetic settles it exactly.

counter main 3a8344b5c this PR 0fc39fd16 delta
planned 12949 12969 +20
passed 12615 12635 +20
failed 47 47 0
unexpected_failures 47 47 0
interrupted_before_verdict 44 44 0
stale_quarantine 1 1 0
known_red_held 33 33 0
route_gap_held 207 207 0

Twenty is exactly this PR's witness count. It adds twenty planned rows and twenty passing rows, and every failure-side counter is unchanged. The failing set is identical in both runs — the same 26 identities, none of them mine: ci_budget_tree_witness, doc_reachability_witness, external_model_scope_witness, guarantee_floor_class_probe_witness, guarantee_probe_corpus_witness_test, language_source_scaffold_index_test, legacy_test_behavior_disposition_acceptance_test, operation_argv_corpus_witness, quarantine_probe_disposition_witness_test, sole_constructor_completeness_audit_probe. All corpus-wide claims; no publication, belt or deploy witness among them.

This is a different failure from the intermittent cost gate discussed earlier in this PR, and it is not intermittent. callable_candidate_ambiguity is no longer the refusal at all. The two over-cost rows are now transport_script_wall_compile_red (18634ms and 17797ms against a 10000ms Wall budget), and they are not what fails the run — 47 unexpected failures are, with 44 rows never reaching a verdict. Main has been red this way for four consecutive runs from 04:51Z to 05:32Z.

Escalated to the parent session. I am not touching any of those 26 witnesses: they belong to other lanes, and editing them to turn my own required check green is precisely the move that converts someone else's debt into my merge convenience.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment on two points. The conclusion is unchanged and now rests on stronger evidence, but the claim I published was wrong and the reason is worth stating.

1. The failing set is 47 identities, not 26. I extracted with a regex anchored on //test/claim/…, which silently dropped the entire v2.* namespace — 11 further families including v2.test.self_host.*, v2.test.claim.enforcement.*, v2.lens.mandatory_tag.*, v2.test.lens_doc_reachability.*. Re-extracted on the FAIL marker itself rather than on a path shape:

grep -ao "FAIL [A-Za-z0-9_.]*" <log> | sed "s/^FAIL //" | sort -u

main 3a8344b5c: 47 · this PR 0fc39fd16: 47 · diff of the two sorted sets: byte-identical.

The tell was in my own comment and I walked past it. I reported failed=47 and then listed 26 identities. A failure counter and an identity count that disagree by 21 is a statement about the instrument, not about the corpus, and reconciling them is the check I skipped. Credit to calm-ram-380 for catching it. The stronger result is that the two sets are now confirmed identical, which is a sharper claim than the one I made.

2. Keeping the cost rows out of it. I noted transport_script_wall_compile_red at 18634ms/17797ms as "not what fails the run", which understates the separation: over_cost_line_diagnostic does not fail a run at all. That is an independent cost-shape question, not a weaker part of this refusal, and it should not appear in the same account.

What did not change: the counter delta — +20 planned, +20 passed, zero on every failure-side counter — was computed from the floor's own summary line and never depended on the identity extraction. Against a now-confirmed-identical 47-identity inherited failure set, it stands.

Root cause, from calm-ram-380, and it makes the 935-row jump a cause rather than a correlation: #9106 deleted the floor's DeclinedLiveTree arm, so ~900 previously-declined witnesses now execute; the 47 are corpus-wide ReadsLiveTree claims that answer false against the live tree, which is exactly why every one belongs to another lane. It declared rung drops in gunbc.guarantee_rung_drop while the floor gates on v2.workflow.floor_expected_red, with nothing relating the two — DESIGN's authority-substitution class, at 47 rows. The fix is open as #9591, verified at identity grain to cover 47/47. When it merges this PR becomes mergeable unchanged.

— sent from sharp-crab-95

Five conflicts, all one event: main adopted FilesystemEstablishedAbsence
in belt_publication_receipt_source -- the follow-up this branch flagged
on the PR and deliberately did not take, because that module's own
adoption-standing row frames it as a separate obligation.

Main's side wins every hunk, and not as a tie-break. Its
belt_publication_receipt_source decides presence through
filesystem_entry_presence and filesystem_file_observation, so
ReceiptSourceAbsent can be projected only from a carrier whose sole mint
consumes a successful listing -- absence established by construction
rather than by a predicate the caller is trusted to reach for. That
deletes the belt's need for filesystem_listing_names_entry entirely, so
the import this branch added is gone and the witness that covered it is
main's to delete: the predicate is still exercised where it lives, and a
second copy of that coverage beside the module that owns it is the
duplication both sides were removing.

None of this branch's own changes are in the conflicted hunks. The
PublishRequested arm, the capability wall, the digest spool key and the
uncredentialed belt all merged clean.

Verified on the merged tree: 0 blocking diagnostics on both the belt and
helper entries, 20/20 helper witnesses green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The red on c57e91e is main's, and this time it is a hard break: main's stage0 seed does not compile.

src/v1/stage0/src/cli_run.rs on origin/main declares record_shared_artifact_fill_wall twice — lines 2593 and 2601 — as a verbatim duplicated block, doc comment and body identical:

fn record_shared_artifact_fill_wall(nanos: u128) {
    SHARED_ARTIFACT_FILL_WALL_NANOS.with(|c| c.set(c.get().saturating_add(nanos)));
}

error[E0428]: the name 'record_shared_artifact_fill_wall' is defined multiple times. The v1-compiler crate fails to build, cargo exits 101, and the build lane dies before any phase runs — which is why all three jobs are red rather than one.

Attribution at commit grain rather than by inference:

commit occurrences
deb0db89bf7 (#9555 — the main this branch merged) 0 — clean
e910a393574 (#9612 — current main) 2 — broken

So it arrived with #9612. PR runs test head-merged-into-main, which is why it appears here: this branch does not touch cli_run.rs at all, and git diff origin/main -- src/v1/stage0/src/cli_run.rs on my side is purely the lines main added after my merge base.

What this PR's own head measures clean. Before pushing c57e91e06 I verified on the merged tree: 0 blocking diagnostics on both the belt and helper entries, 20/20 helper witnesses green. Review 57340 approved that head. The only unmet requirement is checks.

I am not fixing it here. Deleting a duplicated block in the frozen v1 seed from this branch would be editing another lane's subject under the wind-down freeze, and the repair has to land on main regardless or every branch keeps inheriting it. Reported upward for whoever owns #9612. Holding rather than re-pushing — a re-push would only spend runner capacity re-measuring main's break.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my attribution above. The diagnosis holds; the ownership claim in it was wrong, and that is the half that mattered.

I wrote that the duplicate "arrived with #9612" and that "whoever owns #9612 should push the one-block deletion." Measured per commit, the block did not arrive whole:

commit occurrences
deb0db89bf7 0 my merge base, below both
a231ccb4539 1 #9609 — introduced the trio legitimately
e910a393574 2 #9612 — current main

So #9609 landed it correctly, #9612 branched before that and introduced it again, and the squash-merge resolved the overlap by keeping both sides. Neither PR authored a duplicate. Nobody wrote this — which means my sentence assigning the repair to #9612's author named an owner who does not exist, and under a wind-down freeze that is the kind of error that sends someone to the wrong place.

The tell was in my own comment. I wrote "verbatim duplicated block, doc comment and body identical" and reported it as description. Byte-identical blocks including doc comments are not what a second intended definition looks like — that identity is the signature of a kept-both-sides merge resolution. I had the discriminating fact in my own output and read it as colour rather than interrogating it. Same shape as the 26-vs-47 miss earlier in this PR: the evidence that would have corrected me was already on the page.

Credit to calm-ram-380 for the measurement; I re-derived it against the three commits before publishing this.

The fix is #9632, already up and approved. Its merge ref carries one of each symbol and its build step passes where every open PR's fails — the discriminating evidence, rather than an absence of the error. Also flagged: #9632 then failed at a later step, the toolchain-home interference probe, which is the shared-runner contention class rather than a code defect, so a green main may still want a re-run rather than a patch.

Nothing changes for this PR. Still holding, still not re-pushing.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment again — the last paragraph of it was wrong, and it is the paragraph that told a reader what to expect.

I wrote that #9632's later failure was "the shared-runner contention class rather than a code defect, so a green main may still want a re-run rather than a patch." That is refuted. Main is broken two ways, not one:

  1. cli_run.rs — the E0428 duplicates already described.
  2. dag/gunbc/guarantee_rung_drop.dag — an unclosed record literal. wall_deadline_shared_fill_attribution_stall ends on its next_rung_trigger field and is followed directly by data heterogeneous_child_list_stall with no closing }. I verified this by reading the structure rather than by counting braces, since a naive brace count over a file full of string literals is not a reliable oracle.

The step that failed — "Discriminate private homes from a hostile legacy writer" — runs gunbc run over the .dag corpus, so break (2) explains it directly. No contention needed.

Why it looked environmental, which is the part worth recording. #9632 was the first run in the fleet to get past Build the witness fold, and therefore the first to reach that step at all — every other PR dies before it. So the observation had no comparison class, and I relayed it as a transient anyway. A first observation with nothing to compare against is not evidence of a transient; it is one sample.

That is the third time on this PR that I have published a claim whose refutation was available in the material I already had: the 26-vs-47 identity count that disagreed with my own failure counter, the byte-identical duplicate blocks that were the signature of a kept-both-sides merge, and now a single observation reported as a class. Different subjects, one habit — accepting a characterization without asking what its denominator was. Recording it here rather than quietly fixing it, since the PR is the durable artifact.

#9632 is closed in favour of #9634, which carries both repairs. So the remedy is #9634 landing, not a contention re-run.

Nothing changes for this PR. Holding, not pushing.

— sent from sharp-crab-95

Main is compiling again (179d3f2), so this takes it and follows the
reorganization that landed alongside: every gunbc roadmap module moved
into dag/gunbc/roadmap/, and this branch's new module is one of them.

Three parts, only the first of which git could do:

The prose_row_frontier roster conflicted because main rewrote every
roadmap path while this branch added a row at the old one. Main's list
wins and roadmap_publication_helper.dag is re-added at its new path, in
alphabetical position rather than the position the earlier row happened
to occupy.

The module file itself is moved with git mv. Module NAMES did not change
in main's relocation -- roadmap_publish.dag under the new directory still
declares `module gunbc.roadmap_publish` -- so no import in this branch
needed editing, and the witness still imports
gunbc.roadmap_publication_helper unchanged.

The publication unit's ExecStart named the old path as a string, so no
compiler or resolver could see it go stale. A relocation that git records
as a rename leaves exactly this class behind: a path spelled inside a
literal that a move does not follow. Unfixed it would have deployed a
systemd unit whose --entry names a file that is not there.

Also formats src/v1/stage0/src/cli_run.rs, which arrived from main
failing cargo fmt --all --check. Not this branch's code and not this
branch's subject, but the pre-commit hook refuses the tree until it is
formatted, so it is a prerequisite for landing rather than scope growth.
Formatting only: six lines rewrapped, no semantics.

Verified on the merged tree: 20/20 helper witnesses green, and the
footer reports 20 discovered rather than 0 -- checked because a corpus
that cannot parse reports PASS=0 FAIL=0, so a pass count alone would not
distinguish green from unmeasured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The red on 010e3e8 is main's, and this time the argument is a disjointness one rather than a counter delta.

17 diagnostics, across 12 files. This branch changes 11 files. The intersection is empty.

declarations, 5 findings — 4× CITED-MODULE-ABSENT where test.claim.annotation_carrier cites test.fixture.frontier, which no module declares, plus 1× CITATION-DEBT-ROW-STALE for a spent exemption row in declaration_index.rs. Both files are byte-identical between this branch and origin/main (git diff origin/main -- <both> → 0 lines), and main carries all four citations and the stale row.

floor refused at strict preparation, 12 diagnostics in extdeps/bmc/pid_control_program.dag, gunbc/source_integration_landing_spine.dag, test/claim/fabric/fabric_terminal_contract_witness_test.dag, and six training_*_witness_test.dag modules. Every one of those files is also byte-identical to main. No witness ever ran — the floor refused during preparation, so there are no witness-level failures to attribute at all.

This branch's 11 files are the publication helper, the belt, the deploy spec/emit, host layout, posix accounts, the prose-row roster, and three witness modules. None appears in any diagnostic.

Worth stating explicitly because the situation changed an hour ago: main now compiles, so a red here could have been mine, which was not true earlier tonight. I checked rather than assumed, and the parent session had independently flagged both declarations (5 findings) and floor refused as pre-existing main-side state. Not authoring a repair for either — they belong to other lanes, and under the wind-down freeze fixing them from here is neither needed for this PR to go green nor mine to do.

One consistency note I am deliberately not acting on: main's reorganization moved roadmap_belt_actuate_witness_test.dag into dag/test/claim/roadmap/, and this branch's roadmap_publication_helper_witness_test.dag is still at dag/test/claim/. Nothing breaks — witness discovery is by module, not path — so moving it is cosmetic and the freeze says do not widen. Flagging it so the asymmetry is a recorded choice rather than an oversight.

— sent from sharp-crab-95

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Completing the picture: the build lane also failed, and it is also main's — reproduced line-for-line against main's own run.

this PR 010e3e825 main 3a458d9c9
regen FAIL generated surface drift: v1_rt.rs identical
generated-artifact rostered=29 adjudicated=29 matches=23 drifted=4 absent=2 identical
lane summary phases_run=5 failed=2 identical

Same four drifted projections (.gitignore, .gitattributes, .githooks/pre-push, .githooks/pre-commit) and the same two absent (stage0_crate_layout_generated.dag, stage0_crate_partition_generated.dag). Main's last three build lanes are all failure.

So both lanes are red on main independently of this PR. Combined attribution across both: 17 diagnostics over 12 files; this branch changes 11 files; the intersection is empty, and every diagnosed file diffs zero lines against main.

One reason I am not "just regenerating" the generated artifacts, since that is the repair that looks cheapest and is the one worth being careful about. Four are drifted and the gate prints their regeneration recipe. Two are absent — and DESIGN records this exact case: a drift gate makes whatever it adjudicates binding, so an absence that was inert before the gate becomes a line-stop after it, and the cheapest way to clear it is to regenerate. That can resurrect a file someone deleted deliberately, with the gate then permanently enforcing the reverse of a decision already taken. The stale half may well be the registry row rather than the missing file. Whoever owns it should establish those two were ever meant to exist before producing them — which is not a call to make from this branch under the freeze.

Reported to the parent session with that caveat attached. Holding: not repairing, not re-pushing.

— sent from sharp-crab-95

@briansrls
briansrls merged commit bb0451b into main Aug 28, 2026
0 of 3 checks passed
@briansrls
briansrls deleted the session/sharp-crab-95 branch August 28, 2026 23:16
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