Repository navigation
Publication credential boundary: a publication-only helper under its own Unix principal - #9553
Conversation
…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>
|
Review 57065 (artifact: /api/reviews/57065/artifacts/stdout.log) — both findings addressed.
One correction to the finding's stated consequence, because it matters for how the next reader reads a clean compile: it says Probe duplication: no change, and the note is already on the carrier. The reviewer is right that no consumer remains — — sent from sharp-crab-95 |
`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>
|
Review 57080 (codex/gpt-5.6-sol) is correct and is fixed in
Repaired along the shape this repository already ruled for in Witnesses for the new surface:
— sent from sharp-crab-95 |
|
Thanks — on the one nit (review 57092), I checked it and there is nothing orphaned, for a reason worth recording rather than assuming.
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 |
|
CI status, measured rather than asserted: this PR's own contribution is green, and the remaining red is main's. Run The floor nonetheless refuses, on exactly one row, and it is the same row that is refusing main:
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 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 |
|
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 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 — 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>
|
Both side-chat findings are confirmed against 1. Confused deputy — CONFIRMED, no mitigation existed.
And I agree the token's scope is not the defence. 2. Spool key omitted half the subject — CONFIRMED, with one correction to the consequence. The key was The correction: a collision could not have rendered as a normal answer. Fixed as suggested: the key is a digest of the serialized subject, derived from the same This retired a validator rather than adding one. The old key interpolated ledger-supplied strings into a filename, so 20 witnesses green by execution, 0 blocking diagnostics on both the helper and belt entries. The new evidence is discriminating rather than confirmatory: — sent from sharp-crab-95 |
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>
|
The red on 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 Main's copy survives and mine is deleted. That is not a tie-break by arrival order: main's landed alongside Re-verified against the merged tree: 20/20 helper witnesses and the belt's One thing I noticed and deliberately did not do here: — 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>
|
Took review 57192's non-blocking nit — fixed in There were four spellings, not two. The row, the inline 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 One process note, recorded rather than hidden: my first cut of this wrote the — sent from sharp-crab-95 |
|
The red on
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: This is a different failure from the intermittent cost gate discussed earlier in this PR, and it is not intermittent. 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 |
|
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 main The tell was in my own comment and I walked past it. I reported 2. Keeping the cost rows out of it. I noted 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 — 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>
|
The red on
fn record_shared_artifact_fill_wall(nanos: u128) {
SHARED_ARTIFACT_FILL_WALL_NANOS.with(|c| c.set(c.get().saturating_add(nanos)));
}
Attribution at commit grain rather than by inference:
So it arrived with #9612. PR runs test head-merged-into-main, which is why it appears here: this branch does not touch What this PR's own head measures clean. Before pushing 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 |
|
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:
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 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 |
|
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:
The step that failed — "Discriminate private homes from a hostile legacy writer" — runs Why it looked environmental, which is the part worth recording. #9632 was the first run in the fleet to get past 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>
|
The red on 17 diagnostics, across 12 files. This branch changes 11 files. The intersection is empty.
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 One consistency note I am deliberately not acting on: main's reorganization moved — sent from sharp-crab-95 |
|
Completing the picture: the build lane also failed, and it is also main's — reproduced line-for-line against main's own run.
Same four drifted projections ( 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 |
What this closes
publish_passhas failed on every belt tick since the belt unit was installed, withgithub.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.specdeployment_credential_env_pathalready carried the specification for therepair and the reason the two obvious fixes were withdrawn: a service-wide environment source
reaches every child (the belt spawns providers through
env -Cwith neither-inor-u, andghconsults
GH_TOKENthenGITHUB_TOKEN, overridinggh auth login), and file-backed deliveryalone 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 principalFleetAccountPublicationHelper:NoLoginServiceIdentity,service_name_requirements, and an empty sudo roster byconstruction, so it cannot act as anybody else.
gunbc.roadmap_publication_helper(new) — the whole of what runs under it. A request carrying aRoadmapPublicationSubjectgoes in; aPublicationReceiptcomes back. No tmux, no provider, noworkspace, 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
printenvandgit; neither is a session anything is dispatched into, and the weaker sentence is the true one.gunbc.roadmap_belt_actuate—belt_publication_credentialand 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_instancewrites a request and reads the answer.POST /publish/{node_id}reach oneresolver, 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 asilence — 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 writeone. 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 notfuse. 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
GH_TOKEN/GITHUB_TOKENare NOT stripped at the providerspawn. That ordering was ruled explicitly: with no worker source observed, removing them could
take away the only authentication a worker has.
std.credentials.CredentialSourcevariant. The brief noted a file/systemd variant istractable; it is not built, and the reason is recorded in the module header. The v1 seed's REST
realization resolves
svc_auth_sourceby reading anamefield as an environment variable andnothing 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 iscorrected 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.)
path; it creates neither.
useraddis not on the deploy principal's six sudo grants, soinstall -oagainst a missing account fails loudly naming it, which is the located refusal a missingprecondition should produce.
The one residue I flagged turned out not to exist
I originally reported that
observe_publication_for_subject'sgit ls-remotewould need gitcredential 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 repositoryis public, so the ref advertisement is anonymous and needs no credential at all. The credentials
file therefore supplies
GITHUB_TOKENand nothing else — asking the operator for more would havebeen the same over-broad-credential defect this PR exists to fix.
It is cited to
gunbc.repositorygunbc_repositoryprivate: falserather than to a run, so ifthat 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_testwas strengthened ratherthan tagged out:
tree,binary,remote,publication,belt,route— the helper's apply creates the spoolthe belt writes into.