Skip to content

The roadmap's publish pass has never run: the credential the model carries, the units never supplied - #9501

Merged
briansrls merged 6 commits into
mainfrom
session/calm-ram-380-belt-cred
Aug 28, 2026
Merged

briansrls merged 6 commits into
mainfrom
session/calm-ram-380-belt-cred

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

The roadmap's publish pass has never once run, and it is not a git-state defect

I went to fix R3 — "green is missing an origin remote" — and measured srv1 instead of trusting the note. The note was wrong, and correcting it is most of this PR's value.

The belt is not stalled. It ticked at 18:13 today. RemoteUrlIn and LsRemoteHeads both succeed. Green is the retired slot (three stale Aug 1–2 worktrees); the live slot is /opt/gunbc/gunbc and it has a working origin. Nothing was ever blocked on a missing green remote.

belt-last-tick.json says exactly two things:

spawn_pass    refused   deployed tree is not the admitted fleet revision
teardown_pass refused   (same)
verify_pass   deferred  no attempt is ready
publish_pass  failed    one or more publication outcomes could not be recorded

and the publication receipt names the cause exactly:

github.Pulls.List against gunb-ai/gunbc was not attempted because no usable
credential was available: GITHUB_TOKEN is not set in the belt's environment

The first two are revision drift — that's #9489's subject. This PR is the second.

The model was right; the deployment was incomplete

belt_publication_credential does the correct thing and its own annotation explains why: the credential is carried as a CredentialSource and probed for presence, so a read without a usable token is not attempted rather than sent as an empty bearer and raised as a 401. That is a well-formed fail-closed refusal, typed, located, and recorded.

It refused on every tick because neither deployed unit ever supplied the variable. The graph carried a CredentialSource the units could not satisfy. A usable credential has been on the host the whole time — gh is authenticated as briansrls with repo scope, which is also what makes ls-remote work — it simply was not reachable as an env var.

Two units reach that resolver, not one

The belt tick reaches it through belt_publish_pass. The HTTP server reaches the same code through POST /publish/{node_id} → serve_publish_handler_response_for_instance → belt_publish_node_for_instance — the manual trigger serve_publish_note describes. Fixing only the belt would have left that route refusing for a reason its caller could not distinguish from the belt's.

So: one instance-scoped file, read by both units. One credential belonging to one instance; two files would be the second representation §2 forbids, and would drift on rotation.

The optional form is the load-bearing choice

EnvironmentFile had no optionality, so I added EnvironmentFileIfPresent as a variant rather than a required: Bool or a - smuggled into the path string — following this file's own precedent for Environment vs EnvironmentFile and OnBootSec vs OnCalendar, and §4's dissolution of idempotent: Bool into a variant.

Choosing the optional form is not laxity, and the annotation argues it: where the consumer adjudicates absence itself, the required form is strictly worse. A missing file would fail the unit, so the belt would never start and would write no receipt at all — a missing credential and a broken binary would render identically to every reader downstream. That is execution-provenance loss, bought in exchange for a strictness the consumer was already providing. The optional form preserves the typed, located, recorded refusal that already exists.

This is explicitly not the escape hatch gunbc.host_disk_reclaim_units refuses. That file would carry thresholds — host-local values overriding gates this repository owns. A file carrying a credential overrides no gate. The annotation states the distinction so the next reader picks by what the file carries rather than by which directive sounds stricter.

The apply must not write this file

Unlike gunbc-tree-sync.env, whose content is a runner path the deploy knows, a token is not derivable from anything in this graph and must not become one. The host owns the file; the graph owns only its name. A fresh instance whose file is absent gets a located refusal naming the path, rather than a belt that dies.

Evidence

  • the_optional_environment_file_renders_systemds_dash_prefix — a new witness asserting both arms over one path. The inequality is the load-bearing half: the failure worth catching is not a wrong string but the two arms collapsing, which would silently make every required file optional.
  • Both emission oracles updated. Their comments now say they deliberately diverge from the live bytes and why — srv1's installed units carry no EnvironmentFile, which is the defect. Leaving them reading "as deployed on srv1" would have been a false claim in the row whose whole job is to be true.
  • the_tree_sync_unit_matches_the_deployed_bytes_plus_the_exclusion_widening is untouched and still passes — the required form is still reachable and still renders without the prefix.

One out-of-band step remains, and it is not mine to take

Creating /etc/gunbc-roadmap-credentials.env (0600) containing the token is a host credential write. I have not done it. It is a no-op until the units that read it are deployed, and deploying is itself blocked on #9489. Say the word and I will, or provision it yourself.

What this does not claim

The publish pass is not verified working end to end. It cannot be until an apply installs these units, and #9489's own comment records that the restored deploy mode should not be dispatched until the .git rsync leg is replaced. What is verified is emission: the units now carry the directive, and the model can express the distinction it needs.

@gunbai-bot gunbai-bot Bot changed the title roadmap dispatch The roadmap's publish pass has never run: the credential the model carries, the units never supplied Aug 27, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 27, 2026 18:39
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Execution receipt, at head f37eb1f60f9 (BuildBuddy, linux/amd64):

claim_batch --entry dag/test/claim/systemd_unit_file_witness_test.dag --functions <27>
            --entry dag/test/claim/live_deploy_unit_emission_oracle_witness_test.dag --functions <4>

PASS the_optional_environment_file_renders_systemds_dash_prefix        <- new
PASS environment_and_environment_file_are_distinct_directives
PASS the_added_service_directives_render_their_own_systemd_names
PASS the_belt_service_unit_matches_the_deployed_bytes                  <- changed
PASS the_serve_unit_matches_the_deployed_bytes                         <- changed
PASS the_belt_timer_unit_matches_the_deployed_bytes
PASS the_tree_sync_unit_matches_the_deployed_bytes_plus_the_exclusion_widening
... 31 total
[resolve-summary] 2 resolve(s) in 50435ms wall; 31 witness(es) in 37ms wall

31/31, and the footer is quoted for the denominator rather than the verdict: an earlier dispatch of this same command passed --entry with no --functions, ran zero witnesses, and exited 0. A green exit code from claim_batch is not evidence that anything executed; the N witness(es) line is.

Three things the run establishes beyond "it renders":

  • The two arms do not collapse. the_optional_environment_file_renders_systemds_dash_prefix asserts required != optional over one path, not just two literals. The failure worth catching is not a wrong string — it is a renderer that emitted the same line for both, which would silently make every required file optional and start units whose configuration never loaded.
  • The required form is untouched. the_tree_sync_unit_matches_the_deployed_bytes_plus_the_exclusion_widening was not modified and still passes, so EnvironmentFile= still renders without the prefix. The new variant added a form; it did not redefine the existing one.
  • systemd_service_directive_line is the only match over SystemdServiceDirective — checked, since the compiler is blind to non-exhaustive matches and a missing arm here would not have surfaced as an error.

What this does not establish, stated because the PR title is about a pass that has never run: nothing here proves the publish pass works. It proves the units now carry the directive. The pass cannot be observed until an apply installs them, and #9489 records that the restored deploy mode should not be dispatched until the .git rsync leg is replaced.

— sent from calm-ram-380

@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

CI failure here is inherited from main, not from this diff, and I am not pushing a fix because there is nothing in this PR to fix.

required-witnesses-floor fails; required-witnesses-build is still pending.

What main is doing. main's own last two completed required runs are red. Reading the floor lane's log directly (run 33096015791) rather than the aggregator — which explicitly warns that a failed lane "does not establish whether its subject was evaluated" — the cause is product.fabric.contention:

contention.dag:23:19  name 'grant_duration_seconds' not found in module 'product.fabric.supply'
contention.dag:280:3  non-exhaustive match: missing variant(s) UnobservedGrantDuration
contention.dag:421:3  non-exhaustive match: missing variant(s) QuoteNotPriceableWithoutDuration
contention.dag:463:30 function 'grant_duration_seconds' not found in scope
contention.dag:469:43 (same)

#9397 renamed grant_duration_seconds -> grant_duration_bound_seconds and made GrantDuration a coproduct, and left contention.dag behind. This branch is based on origin/main and carries it verbatim — supply.dag declares only grant_duration_bound_seconds, contention.dag still imports and calls the old name at three sites.

Why I am confident it is that and not mine, with a control rather than a plausible story. The floor produced none of its four artifacts (required_floor_disposition.tsv, expected_red_roster_join.tsv, long_home_storage_agreement.tsv, required_floor_claim_cost.tsv). That is the discriminator: those are written by the fold, so their absence means the run refused during strict preparation, before any witness executed. A failing witness of mine would have produced them. And main's failing floor job carries a byte-identical annotation set — same four missing artifacts, same exit 1 — against a job whose cause I read from the log.

What that does and does not establish. Identical annotations establish the same failure shape, not by themselves the same cause. Joined with this branch demonstrably carrying the same five-error breakage, and a diff that touches only extdeps/systemd, live_deploy and two witness files, I am confident — but the conclusive log is not downloadable until the run completes, so I am stating this as a strong inference with its control named rather than as a read receipt.

#9488 is the repair for contention.dag, now out of draft and mergeable. This clears when that lands; no push from me will move it.

— sent from calm-ram-380

@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Upgrading the previous comment from inference to a read, since I flagged it as the former pending the log.

The run has now completed, so the floor lane log is downloadable. It names the identical breakage:

required-ci: declarations FAIL IMPORT-MEMBER-ABSENT dag/product/fabric/contention.dag:23:19:
  `product.fabric.contention` imports `grant_duration_seconds` from `product.fabric.supply`,
  which declares no such name

contention.dag:23:19  name 'grant_duration_seconds' not found in module 'product.fabric.supply'
contention.dag:280:3  non-exhaustive match: missing variant(s) UnobservedGrantDuration
contention.dag:421:3  non-exhaustive match: missing variant(s) QuoteNotPriceableWithoutDuration
contention.dag:463:30 function 'grant_duration_seconds' not found in scope
contention.dag:469:43 (same)

And the negative half, which is the part that actually clears this diff: grepping the full 1029-line log for systemd/unit_file, live_deploy, EnvironmentFile and credentials.env returns nothing. No file this PR touches appears anywhere in the failure.

required-witnesses-build passed (49m10s). Only the floor lane failed, on preparation, before any witness executed — which is why none of the four disposition artifacts were produced.

Confirmed rather than assumed. #9488 remains the repair.

Worth noting for whoever picks up #9488: the first line above is the declaration index catching this as IMPORT-MEMBER-ABSENT ahead of the compiler's own five diagnostics. That wall landed on 2026-08-25 and this is it doing exactly its job.

— sent from calm-ram-380

gunbc-ci-auto-heal and others added 3 commits August 27, 2026 22:38
…tead

Side-chat review ruled the previous cut grants far more than it names, and
the ruling is right.

WHY THE BINDING IS WITHDRAWN. EnvironmentFileIfPresent on both multipurpose
units delivers the credential to the whole process AND every child. The belt
spawns providers through `env -C <workdir> <program>` with neither -i nor
-u, and GNU env inherits unless told otherwise, so the token reaches git,
tmux and the dispatched provider session. Upstream makes it concrete: gh
consults GH_TOKEN then GITHUB_TOKEN and those OVERRIDE credentials stored by
`gh auth login` -- so an ambient publication token does not merely leak into
a worker, it silently REPLACES the identity that worker was meant to act as.

FILE-BACKED DELIVERY ALONE WOULD NOT HAVE FIXED IT, which is the tempting
next move and is why it is written down. Moving the token from a variable to
a file stops automatic inheritance and creates no authorization boundary:
both units run as the deployment service user and the worker is spawned
beneath that process, so a file readable by that principal is readable by
the worker. The transport changes; the set of principals that can read it
does not.

TWO CREDENTIALS, TWO CAPABILITIES, DECLARED AND UNBUILT. Publication needs
only to LIST pull requests (Pull requests: read). A worker needs to PUSH a
branch and OPEN a pull request (Pull requests: write plus Contents). They
are one token only by accident of both addressing GitHub. Neither is modeled
today -- the dispatch platform declares a git workspace, tmux, environment
binding and validation tools, and declares NO GitHub-authoring capability,
no worker identity, and no credential source proving a worker can push.

WHAT THIS PR NOW IS: the systemd optional-EnvironmentFile variant with its
pair witness, and the instance-scoped credential path as a NAME with no
consumer. Publication stays refused rather than restored by a grant nobody
bounded. Restoring it needs a publication-only consumer boundary -- an
uncredentialed belt handing a typed request to a minimal publisher the
worker principal cannot read -- shared by both ingress paths.

Stripping GH_TOKEN/GITHUB_TOKEN at the provider spawn is deliberately NOT
the interim: with no worker credential modeled or observed, removing them
could take away the only authentication a worker has and break its ability
to offer finished work -- trading a scope defect for a capability loss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two unit oracles asserted an EnvironmentFile line the previous commit
removed, and their annotations described a deliberate divergence-until-next-
apply that no longer exists. Both now agree with the live bytes again.

The round trip is recorded rather than silently reverted, because the
intermediate state was committed and a reader finding only the current text
would not know why the credential line is absent.

WHAT DID NOT CHANGE: publish_pass still refuses on every tick with
"GITHUB_TOKEN is not set in the belt's environment". That refusal is now
CORRECT rather than pending -- the belt is not supposed to hold that
credential -- and it closes when the publication-only boundary lands, not
when these units are next applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls merged commit c957309 into main Aug 28, 2026
0 of 3 checks passed
@briansrls
briansrls deleted the session/calm-ram-380-belt-cred branch August 28, 2026 00:56
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