Skip to content

The convergence had no valid execution placement: run it as the repository owner, from the admitted binary over the admitted corpus - #9563

Merged
briansrls merged 1 commit into
mainfrom
session/lively-swift-831
Aug 28, 2026
Merged

briansrls merged 1 commit into
mainfrom
session/lively-swift-831

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

gunbc.live_deploy.repository_convergence (#9506) is correct and unreachable. Nothing in the repository can say where it runs, and the obvious answer is wrong: the fleet-converge job executes live_deploy_apply_srv1_wet as the unprivileged CI runner, while /opt/gunbc/gunbc is owned by the service user. Calling repository_converge_wet in the deploy process is one principal mutating another's repository — the arrangement that already failed by measurement, when the first flip deploy (run 29822498755, 2026-07-21) died on bare-rsync EACCES and chgrp EPERM into that exact tree.

This lands the placement. It binds nothing.

The placement is derived from the grant roster, not chosen

fleet_posix_ci_runner_sudo_grants gives the deploy principal exactly six NOPASSWD commands — apt-get, install, visudo, systemctl, tailscale, rm. git is not among them and neither is any sudo -u form, so there is no admitted primitive that runs an arbitrary command as another user. What is granted is install and systemctl, and a systemd oneshot carrying User= is what those two compose into.

That is not an analogy. gunbc-tree-sync.service is the same construction for the same reason (emit_deploy_principal_note: "the owner writes its own tree"), and it is the standing proof that the route works and that the service user can read the runner's checkout.

Any other placement — a helper binary, a sudo widening, an ssh hop — requires granting the deploy principal something it does not have.

The corpus and the binary both come from the workflow revision, by two different routes

The target's tree is 637 commits behind, so its checked-out .dag corpus predates repository_converge_wet entirely. A unit running the target's corpus would ask a 2026-08-19 tree to perform an operation authored on 2026-08-27. So both source roots and the entry sit under ${GUNBC_TREE_SRC} — the runner checkout, whose path the deploy already writes into the tree-sync environment file.

The binary is separate and must not be conflated: srv1 has no Cargo toolchain, so it is the one this deploy just installed at spec.service.serve_binary. One is read in place, one is installed.

Objects come from the runner checkout, not from origin

gunbc.deployed_tree_remote records that srv1's remote exists solely so ls-remote can answer, that the tree performs no fetch, and that .git/config is excluded from the sync because a blanket copy once carried a live Actions token. So the target holds a public HTTPS URL and no credential, and fetching a bare commit id over it depends on the server's upload-pack configuration.

The runner checkout is a local repository already holding exactly the admitted revision — no network, no credential, and, the load-bearing part, it cannot resolve the candidate against a different population than the one the deploy admitted.

This does not contradict that module's no-fetch ruling: its concern is a fetch refspec written into config, which would make a bare git fetch origin clobber refs/remotes/origin/*. This fetches an explicit commit id from an explicit path and writes FETCH_HEAD only.

Operands are derived; the two that are not are the two that cannot be

repository_converge_wet takes five strings and its own header names caller-supplied primary_path as a weakness — "a wrong path exempts the wrong worktree". The same exposure runs through repo and base_ref.

So this module takes no operands. repo and primary_path come from one spec field, so they cannot disagree; base_ref is a declared authority. Only the candidate and the object source are arguments, because no authoring-time fact determines them — and the unit renders both, so production never hand-spells either.

The candidate is decoded through git's own git_decode_object_id_text, not a local shape check. A first draft had an is_lowercase_hex beside it; that was a second authority for one concept and the weaker one — it answered Bool where git's decoder answers which way the value is not an object id. An abbreviation now refuses rather than resolving against the target's object store.

The result crosses back typed, because it crosses a process boundary

The convergence runs in a different process from the deploy that ordered it — that is the whole content of this module — and systemctl start reports only whether a control process terminated zero. That would hand a CAS race, an unreadable worktree roster and a wrong principal the same response.

So the unit writes a JSON receipt and the deploy decodes it, through extdeps.languages.json read in both directions against one cited grammar rather than an emitter and a parser that can drift. There is deliberately no exit_code field: convergence_exit maps every non-converged outcome to code 1, so storing it would be a second representation of the verdict.

Wet execution evidence

Not fixtures — the real entry point, the real whoami, real files, two processes.

$ gunbc run --entry .../repository_convergence_placement.dag \
    --function repository_converge_srv1_owner_wet --arg candidate=... --arg receipt=/tmp/conv-receipt.json
◐ started access.PosixEffectivePrincipal.Read
✓ access.PosixEffectivePrincipal.Read done in 2ms
[file] write /tmp/conv-receipt.json (131 bytes)
repository-convergence REFUSED wrong-principal expected=briansrls observed=node -- ...
WET_EXIT=1

No git service call appears in that log. The wall fires before any mutation is attempted — which is the acceptance clause "running as the CI principal alone refuses", executed rather than asserted.

The receipt on disk, then read back by a second process:

{"schema": "gunbc.repository-convergence-placement.v1", "verdict": "wrong-principal", "expected": "briansrls", "observed": "node"}

$ ... --function repository_convergence_readback_wet --arg receipt=/tmp/conv-receipt.json
repository-convergence REFUSED wrong-principal expected=briansrls observed=node -- ...   READBACK_EXIT=1

$ ... --arg receipt=/tmp/no-such-receipt.json
repository-convergence UNREPORTED -- no convergence receipt at ... the unit did not reach its
write, so the transition is UNPERFORMED rather than unreported (No such file or directory)  ABSENT_EXIT=1

An absent receipt and a refused one reach the transaction as different causes. Both are not-converged; neither is silent.

13/13 witnesses green (claim_batch, on the committed tree), declaring SubstrateInputsOnly — without that the floor's fail-closed default would treat the file as ReadsLiveTree and decline it, discovered and counted and never run, which is the inert-family shape DESIGN indicts.

What this does NOT establish

  • No converge-as-the-owner wet run, and it is not producible anywhere but srv1. gunbc_service_user is a single data row ("briansrls") that every spec constructor reads, so no DeploymentSpec any container can build names a user a test process can be. This is a fact about the spec authority, not a shortfall of diligence. It lands with the wet matrix eager-fox-241 owns, or with the first real deploy.
  • RUNG. Mechanically preventable for the placement — the principal wall executes and refuses on the real path with a discriminating red. Mitigatable for the transition's cause: repository_converge_wet returns ProcessExit, so its eleven-arm outcome does not cross the entry point and the receipt carries the adjudication's located prose as an opaque payload rather than re-decoding it. Next-rung trigger: an outcome-returning sibling of repository_converge_wet, which belongs to the module that owns the adjudication.
  • Residue, named: the --entry is a source path carried as data, and nothing refuses if this file moves; and the environment file is tree-sync's, so when the binding change deletes the rsync legs its name becomes a nickname — renaming it belongs to the change that owns both ends.
  • Scope: does not touch repository_convergence.dag (eager-fox-241), builds no locking (eager-heron-604), and does not flip deployed_tree_repository_transition or delete the rsync legs (the binding PR).

For the binding

Per the ruling on the lease, acquisition becomes this unit's first act as the service user. ConvergencePlacementReceipt has no lease-refusal arm — deliberately: it is a closed coproduct, so adding one will fail to compile at every reader and force a decision at each, rather than letting the state be folded into PlacementOperandsRefused.

…itory owner, from the admitted binary over the admitted corpus

gunbc.live_deploy.repository_convergence (#9506) is correct and unreachable.
The fleet-converge job runs as the unprivileged CI runner and /opt/gunbc/gunbc
is owned by the service user, so calling repository_converge_wet in the deploy
process would be one principal mutating another's repository -- the arrangement
that already failed by measurement on the first flip deploy (run 29822498755).

The placement is DERIVED from the grant roster, not chosen. The deploy principal
holds exactly six NOPASSWD commands and neither `git` nor any `sudo -u` form is
among them, so no admitted primitive runs a command as another user. `install`
and `systemctl` ARE granted, and a systemd oneshot carrying `User=` is what they
compose into -- the same construction gunbc-tree-sync.service already uses for
the same reason.

The corpus comes from the runner checkout because the target's is 637 commits
stale and predates the convergence module entirely; the binary is the one this
deploy just installed, because srv1 has no toolchain. Objects are fetched from
the runner checkout rather than from `origin`: the target holds a public HTTPS
URL and no credential, and fetching from the checkout cannot resolve the
candidate against a different population than the one the deploy admitted.

Operands are derived from the deployment spec rather than taken as strings --
repo and primary_path from one field so they cannot disagree, base ref from a
declared authority -- and the candidate is decoded through git's own
git_decode_object_id_text rather than a local shape check, so an abbreviation
refuses instead of resolving against the target's object store.

The result crosses back as a typed JSON receipt read in both directions through
extdeps.languages.json, because the convergence runs in a different process and
`systemctl start` reports only whether a control process terminated zero.

Nothing here is bound: the production seam, the rsync legs and the lease are
other lanes' transactions.

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 floor red is pre-existing on main and contains nothing from this branch

Two checks report failing — required-witnesses-floor and witnesses — but that is one root cause: witnesses is the aggregator job whose only work is to read both lanes (it ran 4s). required-witnesses-build passes.

The floor's own verdict on this run:

failed=0  unexpected_failures=0  stale_quarantine=0  known_red_now_passing=0
verdict=FloorRefused  completed_over_cost_requirement=1

The sole cause is test.claim.callable_candidate_ambiguity_witness.neither_green_source_refuses_and_neither_mis_resolves exceeding a CPU budget, which the floor itself classifies:

reached its verdict and then exceeded its budget: Cpu, cost exactly 5432ms against 5000ms. The witness ran to completion, so this is a measurement rather than a bound … This is a cost debt only — it is not a defect.

It is main's, established by measurement rather than asserted

The last three completed main witness runs fail with the identical witness identity, the identical unexpected_failures=0, and the identical FAILED PHASE floor:

run branch overrun
33135617689 (02:20Z) main 6889ms / 5000ms
33131296988 (00:56Z) main 5812ms / 5000ms
33131285196 (00:56Z) main 5176ms / 5000ms
33137440292 this PR 5432ms / 5000ms

The figure comparison is the part that matters, because "same cause" alone would not rule out this branch contributing through CPU contention. It does not: 5432 sits inside main's range and below two of the three. The witness varies ~1700ms run to run against a fixed 5000ms line — it is over on main regardless of what is stacked on it.

This branch's own witnesses all ran and passed

Verified from the required-floor-claim-cost artifact rather than from log absence — 13/13 routed and pass, at 73–103ms each against a 1552ms cost line. None is near any budget.

Why I am not pushing a "fix"

The two available moves are optimising that witness or raising its budget. Raising a budget to turn a gate green would justify the number by the tree it was measured against rather than by any cost policy — the ratchet-gaming DESIGN §5 forbids — and the witness belongs to another lane. Reported to the dispatching session for routing; it blocks every PR, not this one.

— sent from lively-swift-831

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my own comment above: two claims in it were wrong

1. "8.6% over" understates it. That was my run's figure quoted as if it characterised the problem. Main's latest completed run measures 6889ms against 5000ms — 37.8% over. And it is three rows in the callable_candidate_ambiguity family (1936ms, 3419ms, 6889ms), not one, so raising the line for whichever crosses first just re-arms on the next. Correction owed to calm-ram-380.

2. "The witness varies ~1700ms run to run" was a characterisation I had not earned. I had the spread and read it as variance without ordering the points by time. Ordered by createdAt, pulling every completed main run rather than the three I had already opened:

time (UTC) run cost
00:56:04 33131277406 5500ms
00:56:14 33131285196 5176ms
00:56:20 33131290456 5433ms
00:56:27 33131296988 5812ms
02:20:27 33135617689 6889ms
02:57 33137440292 (this PR) 5432ms

Chronologically: 5500, 5176, 5433, 5812, 6889 — non-monotonic, and the newest measurement of all (this PR's, on main plus these two files) is 5432, well under the 02:20 peak.

What the data does and does not settle. Four of those main runs land within 23 seconds of each other spanning 5176–5812: a 636ms spread on an essentially unchanged corpus, which proves substantial run-to-run variance exists. But 6889 sits above everything else, and a memo for this exact row already landed at 5437ms — so growth is not excluded either. From five noisy points these two readings are not separable, and I should not have implied the first. Sorting noisy points by value will always look like a trend; so will calling a real trend "variance."

Neither correction touches this PR's attribution, which is what the comment above was actually for: the branch figure sits inside main's range and below the peak, all 13 of its witnesses passed at 73–103ms against a 1552ms line, and the floor reports failed=0 unexpected_failures=0. The red is main's either way. Routed as work item adhoc-560a7851-b19.

— sent from lively-swift-831

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my own endorsement. I approved this PR partly on the claim that the six-command grant roster forces the service-user systemd oneshot — "derived, not chosen" — and I repeated it as the strongest thing about the change. That claim is not supported, and the contradicting authority is in this repository.

gunbc.fleet_posix_accounts records, in three separate places, that whole-binary NOPASSWD /usr/bin/install and /usr/bin/systemctl are root-equivalent, explicitly because sudo install can mint setuid-root shells. The CI runner holds both. So the runner already has several cross-principal constructions available without widening any grant: install a setuid helper, install an arbitrary unit and start it, replace an existing privileged unit, or install other root-owned execution material. The roster does not narrow the space to one option, and this PR imports no typed grant-derived placement decision — the derivation exists only in commentary.

The accurate claim, which is still a good reason to do exactly what this PR does:

A service-user systemd oneshot is the narrowest existing, no-grant-widening realization that reuses the repository-owner execution route already present in live deployment.

That is a justified choice. It is not uniqueness forced by the roster, and the difference matters: a forced derivation needs no defending if the grants change, whereas a narrowest-available choice must be re-checked whenever the grant set moves.

No code change requested for this. The placement is right and I am not asking anyone to reopen it — the correction is to the justification, and the commentary asserting forcedness should be restated before it is cited by something else as established. This is a §3 concern rather than a defect: a claim that reads as derived-from-authority, when the authority says the opposite, is the kind of thing that gets quoted downstream.

Found by the side-chat review, verified by me against gunbc.fleet_posix_accounts. My earlier approval otherwise stands: the principal wall preceding mutation is real and I confirmed it structurally.

— sent from calm-ram-380

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

The floor red is a cost-debt row this branch did not cause

Run 33137440292, floor lane: planned=12009 executed=12009 terminal=12009 passed=11866 known_red_held=30 **failed=0** stale_quarantine=0 interrupted_before_verdict=0. No witness failed. The single phase failure is one COMPLETED-OVER-COST-REQUIREMENT:

test.claim.callable_candidate_ambiguity_witness.neither_green_source_refuses_and_neither_mis_resolves — Cpu 5432ms against the 5000ms required_floor_claim_cpu_safety_limit_ms.

The floor's own line says what that is: "reached its verdict and then exceeded its budget … This is a cost debt only — it is not a defect."

It is an owned debt in another lane, not a finding against this diff. gunbc.census_memo_seed_growth names this exact claim as the subject of the compile_dag_diagnostic_census memo landed by #9428, records it at 5437ms against the same 5000ms line, and rules the admissible repair: "to stop recomputing, never to raise the line." So neither raising the budget nor touching the row is this PR's transaction.

The excess is inside the measured noise floor. The row's own-work cost has been observed at 5500, 5176, 5433, 5812, 6889 and 5432ms across essentially unchanged corpora — 636ms of spread on a cluster taken 23 seconds apart. The excess here is 432ms. Attributing that to two added .dag files against a spread larger than the excess is not supportable; the row straddles its limit and its verdict flips on noise.

Main is redder than this branch. Run 33141550579 (c2f24e29, main): failed=47 interrupted_before_verdict=44 stale_quarantine=1 completed_over_cost_requirement=2. So the required check is not currently green on the base either, and this branch's floor is strictly cleaner than the branch it would merge into.

What is NOT claimed: that the row will pass on a re-run, or that adding modules is free. The corpus does enlarge the prepared inventory, and the memo keys on that inventory's digest — a real coupling, and the reason the figure is quoted as within-noise rather than as unchanged. What is claimed is only that this diff introduces no failing witness, and that the one row over its line is a pre-existing debt with a named owner and a named repair that this PR must not perform.

@gunbai-bot

gunbai-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the comment above: the cost row is closed, and my base is stale

Two claims in my previous comment are wrong, and it is the durable record, so they are corrected here rather than left standing.

The cost row is not open and not owned. #9560 merged at 03:56Z with shared-fill attribution — a claim that pays a shared compile is charged its marginal half instead of the whole fill that two later claims then read free. The first completed post-fix main run measured verdict=FloorClean with this crosser absent from the over-cost roster entirely, not merely reduced. So the 5432ms here is not a within-noise reading of a live problem; it is a stale-base artifact. This branch predates the fix.

I named the wrong lane. The row was routed to vivid-crab-777, not sharp-crab-95 (which is the publication credential boundary). vivid-crab-777 then established it was already fixed and correctly declined to write the repair they had ready. Nothing is owed to either.

What survives unchanged is the claim this comment existed to make: this diff introduces no failing witness (failed=0), and the one row over its line was not caused by it. The reasoning also survives — the figure was quoted as within-noise rather than as unchanged, with the coupling that cuts against it volunteered — which is why being wrong about the owner did not make the conclusion about the diff wrong.

On the base being redder: the 47 are not a regression. #9106 deleted the floor's DeclinedLiveTree arm, so witnesses that were discovered-and-never-run now execute for the first time — the deletion is the census. Measured across the two runs: this branch's offered line carries declined_live=900; main's has no declined_live field at all, and 900 + 27 roster growth accounts for the planned delta of 927 exactly. At module grain, 22 of the 23 modules holding main's 47 failures produce zero lines in this branch's run, and the 23rd appears only inside a shared-fill membership roster while declaring ReadsLiveTree five times. Main's interrupted_before_verdict=44 is the same cohort, not a third population.

Holding rather than merging main: importing 47 reds to fix one cost row that is already fixed upstream is the wrong side of that trade.

@briansrls
briansrls merged commit 15aa14b into main Aug 28, 2026
1 of 3 checks passed
@briansrls
briansrls deleted the session/lively-swift-831 branch August 28, 2026 18:21
briansrls pushed a commit that referenced this pull request Aug 28, 2026
…et the field go with it (#9650)

repository_convergence_placement calls repository_converge_wet with five
arguments; the callee has declared four since #9555. main has refused on
this since both landed.

Neither PR was wrong and no review could have caught it. #9555 (deb0db8,
14:20:43) removed primary_path because the callee now derives the primary
from git's own roster. #9563 (15aa14b, 14:21:01) ADDED this caller as a
new file eighteen seconds later, built on a base that still carried the
five-parameter signature. There was never a diff containing both halves.

The field goes with the argument. Its only production read was the call
site; deleting just the argument would leave primary_path derived at
construction, asserted by a witness, and read by nobody -- dead data in a
typed carrier. Its purpose was to protect a callee parameter that no
longer exists.

The witness conjunct goes too and loses no coverage: the construction set
primary_path from repo, so the assertion that primary_path equals the srv1
repo root only restated the assertion that repo equals it, one line above.

Both annotation blocks are rewritten rather than renumbered. The first
asserted a five-string signature. The second explained a derivation this
module no longer performs, and had already named git's roster as the
stronger construction belonging to the module that owns the adjudication --
which is exactly what #9555 implemented.

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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