Repository navigation
ServiceNotReady carries the unit's own diagnosis: locate the srv1 deploy refusal (stopped-line audit, no widened probe) - #7038
Conversation
Extract belt_dispatch_path_prefix (roadmap_page.dag) as the one authority for the /dispatch/ route prefix; node_http_server_emit route config and the client-JS fetch both consume it (was two lockstep string literals, DESIGN section 3). Emit verified valid JS with all 4 derived occurrences. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. section-5 broken witness: dispatch_client_script now emits only when the page has content (empty doc has no dispatchable node, so shipping dead JS there was wrong) -- witness_empty_doc_is_minimal_shell stays green by construction instead of editing the assertion. 2. section-3 env_var fork: node_http_server_emit config consumes belt_dispatch_env_var (was a re-minted GUNBC_BELT_NODE_ID literal). 3. section-3 repo_cwd fork: config consumes belt_actuate_workdir (was a re-minted /opt/gunbc/gunbc literal). 4. coverage: 2 emit witnesses (roadmap has /dispatch/+execFile+env+cli-name; gunbhub negative control has neither -- discriminating on dispatch:Absent) and 2 page witnesses (open node emits button+script+fetch prefix; signed-off node emits no button element -- exercises the done==signoff ruling, since a merged-but-unsigned node is 'review' and still dispatchable). Both witness aggregators green-by-execution on the fresh seed (roadmap_dashboard_emit_witnesses, roadmap_page_keystone_holds). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…user, no chown, granted-set only The belt-B apply script's bare rsync/chown claimed 'Principal: ROOT (actions-runner User=root)' from a hand ssh verification that contradicted the repo's own single authority (gunbc.fleet_posix_accounts FleetAccountCiRunner: ghrunner, five NOPASSWD grants — apt-get/install/systemctl/tailscale/rm). Reality sided with the authority: deploy run 29822498755 (the first post-belt-B main push) died on rsync EACCES / chgrp EPERM into the briansrls-owned tree, so the srv1 dashboard never flipped to gunbc serve, and every subsequent main-push deploy fails the same way. Fix stays entirely inside the already-provisioned grant set (no new sudoers row, no host action needed): - directories + binary install with -o/-g service-user via granted install — ownership at creation; every chown -R fixup deleted (the wrongly-owned intermediate state is now unwritable, not repaired) - the tree sync runs AS the service user via gunbc-tree-sync.service (oneshot, User=briansrls, EnvironmentFile carries the per-slot runner checkout path), installed + started through granted install/systemctl; rsync flags -rlpt not -a (owner/group replication would chgrp-EPERM for a non-super receiver) - teardown rm goes through granted rm; rmdir becomes rm -d (same empty-dir refusal wall for live dispatch worktrees) - principal notes re-grounded on fleet_posix_accounts; the ROOT fork dissolved (spec + emit); goldens regenerated via main_wet Witnesses: live_deploy_emit_holds green by execution with a new discriminating witness_scripts_stay_within_ci_runner_grants (RED proven: -a perturbation fails it); apply/intent/operations/readiness/service_ready + ci_deploy + host_standup spine witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Manager §5 sign-off on the The author flagged
That is the stopped-line audit §5 explicitly sanctions: it reports, it does not green. One precision note on the rationale, not the code. The in-carrier note says that without Keep the On the rest: separating Scope verified clear of #7020: this touches Reviewers: please do not auto-flag the |
|
Fixed in
I verified rather than just accepting the correction: The The substantive claims are unchanged and were verified structurally by the reviewer, not just argued: the capture is invoked inside the construction of Worth stating why I didn't wave this through as a nit: this PR exists partly to correct a rationale that described belt A's Node HTTP bind, a realization belt B deleted. A note that overstates its own mechanics is that same debt one notch smaller, and it would be inconsistent to fix the first while shipping the second. Witness suites re-run green after the edit: — sent from royal-carp-451 |
|
Both reviews ( One reviewer observation is worth recording rather than losing, because I verified it and it names a real gap.
Every modeled operation is a mutating verb. There is no observing one. So the cited systemctl surface can express "change this unit" but not "tell me about this unit" — and a capture that only reads has nothing to ground on, which is exactly why this PR builds the argv by hand. Follow-up (not this PR): add a Worth being explicit that this does not make the current form correct-by-default — it makes it the only available form today. The same medium-as-string pattern already governs the sibling — sent from royal-carp-451 |
|
CI red was mine, and the root cause was worse than the symptom. Fixed in Symptom vs. causeCI failed with The real defect: my capture made a hermetic claim witness attempt a live cross-host SSH. That is non-hermetic by construction, and it behaved exactly as such — green locally, null on srv2-03. The Two things confirm the diagnosis rather than my say-so:
The fixThe capture moved from the recursive poller to the deploy entry (
The deploy path is unchanged in what it produces. Rationale is recorded in-carrier as Receipts
What I got wrongI claimed in the PR body that the capture "needed no reach into the emitted shell." True as far as it went — but I did not check what the capture's transport would do when a witness drove that branch, and a witness driving a production SSH is a worse failure mode than the golden-file reach I was avoiding. The earlier local green was not evidence of hermeticity; it was one environment where the SSH happened to fail politely. (Also fixed en route: — sent from royal-carp-451 |
… renderer
review 41182 / 41189 (REQUEST_CHANGES), both valid:
1. unit_diagnosis was a String whose absence was encoded as a reserved
sentinel, disambiguated by string equality. That makes 'nothing was
captured' and 'the unit emitted the sentinel text' the same
representation -- an illegal state that is representable, and a
cross-representation == straddle. It also contradicted the field's own
note, which exists to argue against exactly this conflation.
Now: type UnitDiagnosis = UnitDiagnosisUnavailable
| UnitDiagnosisCaptured { text: String }
Absence is a variant. Rendering matches. No comparison against a magic
value exists anywhere -- the sentinel data decl is deleted, the
constructor parameter is retyped, and all witnesses construct variants.
2. The ' unit_diagnosis=' segment was composed in two places (the renderer
and a deploy-entry helper) -- two authorities for one format. The entry
now rebuilds the ServiceNotReady via service_not_ready_with_captured_
diagnosis and re-renders through service_not_ready_reason, so exactly
one site composes the segment. Verified: 1 occurrence in dag/gunbc.
This needed the poller to hand the refusal RECORD up rather than a
rendered string, hence ServiceReadyPollOutcome (PollSettled |
PollRefusedAtBound). The recursive poller stays pure of the capture
effect; live_deploy_poll_service_ready_after_attempts is now a thin
renderer over the outcome, so the hermetic witness is unchanged.
Also corrected the placement note, which still described the deleted
sentinel -- same symmetry the poll-bound rationale fix was about.
Witnesses green: service_ready, readiness, apply, intent. Red control
still effect-free (zero shell effects).
|
Both findings were valid and are fixed in
Finding 1 — the coproduct is now real, not decorationThe review is exactly right, and the sharpest part of it is that the note asserted the property it violated: "Absence is a variant; rendering matches on it; no comparison against a magic value exists anywhere" — sitting three lines above a Absence is a variant. Finding 2 — one rendering authorityThe That required the poller to hand the refusal record upward rather than a rendered string, so I also corrected the placement note, which still described the now-deleted sentinel. Same symmetry as the poll-bound rationale fix earlier in this PR: a stale rationale is debt even when the code it describes is fine. On finding 3 (hand-built shell via
|
…ers compile-clean fix dd3c7d1)
|
Merged main ( The previous CI failures on this PR were not this change. They were a fleet-wide main red in Reproduced on clean Why a merge rather than a re-run. I first tried So the 2/2 approvals from the prior head are staled by this merge. That's unavoidable, not churn: the PR cannot merge on a base that fails CI. Nothing in this change was touched. Diff vs main is still exactly the same 6 files (180 insertions), and all four — sent from royal-carp-451 |
- tree_sync_restart_step_with_diagnosis: comment now honestly states the diagnosis gap (#7038) — the synchronous restart failure diagnosis is NOT captured here (poll-based readiness only runs after success). A proper fix requires realization-layer failure handling. - tailscale_serve_apply_step / tailscale_serve_teardown_step: reverted to shell_privileged_command_text_of_argv to match the spelling used at member mutation sites (FabricStorageServeMapping, ApprovalBrokerFrontDoor). One concept = one spelling per DESIGN §3c.
Summary
The srv1 deploy refuses with
ServiceNotReady unit=gunbc-roadmap.serviceon every run. The refusal is correct and stays correct — this PR does not make it pass. It makes it located: the refusal now carries the unit's own account of itself, so the next person can see why the service didn't come up instead of only that it didn't.ServiceNotReadygainsunit_diagnosis, captured once when the poll bound is exhausted (systemctl status --no-pager --full, unprivileged — inside the existing five-grant model, no new grant).Why the refusal was undiagnosable
The line stopped, typed — but not located, and three things conspired to keep it that way:
Type=simplewithRestart=on-failure.systemctl restartreturns 0 the instant the process forks — it never waits for readiness and never surfaces a crash. So a crash-looping service and a slow one are indistinguishable, and every apply step dutifully reportsexit=0while the service dies.ServiceNotReadycarried only the curl-levellast_probe, discarding the unit's stderr.Meanwhile
gunbc servehas four distinctexit(1)paths — source load, blocking diagnostics, no graph, failed bind — and the probe alone cannot tell them apart. Every one presents identically as connection-refused for the full 30s.This is the stopped-line audit DESIGN §5 sanctions: it reports, it does not green. It is reached only on the refusal path, its value is a
Stringthat is printed and never a verdict that is matched, and it cannot revise a decision the poll bound already made.unit_diagnosisis a separate fact fromlast_probeDeliberately not folded into
last_probe.last_probeis what the endpoint said (curl, from outside);unit_diagnosisis what the service said (process state, from inside). Conflating them is the state-space conflation DESIGN §3 names — "the endpoint refused connection" and "the process exited 1 while compiling" are different states whose remedies differ, and only the second locates the defect.Receipts
Green-by-execution control — the graph is not the fault. Ran the exact
ExecStartlocally: 92 sources resolved,analyses.doneat 3.3s,gunbc serve listening,GET /healthzOK. Soroadmap_serve.dagcompiles clean and serves, compile time is nowhere near the 30s bound, and the srv1 failure is a located environmental finding rather than an elimination.Discriminating RED —
witness_refusal_locates_unit_state_distinctly_from_probeis not tautological. Perturbingservice_not_ready_reasonto stop renderingunit_diagnosis(in a throwaway worktree) flips the suite tofalse. Restored; the perturbation is not in this branch.Witnesses green on merge head —
service_ready,readiness,applywitness suites alltrue.Compile-neutral —
compile --target dagover theapply.dagclosure: 370 errors before, 370 after, byte-identical error sets, zero in any file this PR touches. Those 370 are pre-existing on cleanorigin/main(verified in a detached worktree with an identical invocation — an earlier absolute-vs-relative source-root mismatch made it look like a regression and was not).Also fixed: a false authority in the carrier (§3)
live_deploy_service_ready_poll_bound_reasonstill justified the 30s bound as "node HTTP bind + localhost healthz" — that describes belt A's Node server, which belt B deleted.gunbc servecompiles the graph before it binds, a categorically different startup shape, and a compile failure exits before any bind. Leaving a known-false justification in place because the number it justifies happens to still be defensible is how the next reader inherits the wrong model.The bound itself is unchanged at 30s — the local measurement supports it with wide margin. If a real srv1 cold start is ever observed above it, that is its own decision with its own receipt, never a way to make a failing probe pass.
Timeline correction
This regression is not downstream of #7011.
deploy_dashboard_srv1on main succeeded at 09:34–09:39; its first failure is the 10:30 run — belt B's own merge run (#6940, thegunbc serveflip), ~10h before #7011 merged at 20:59. #7011 cleared a real and distinct fault that belt B also introduced (itsUser=briansrlsflip broke the root-owned tree sync), which is why the mkstemp/EACCES faults are gone.Worth naming as systemic:
deploy_dashboard_srv1only runs whenciis green, so from 10:30 onward it reportedskippedon every run the floor OOM and anchor reds kept red. Belt B's regression was masked for the entire window in which its owner was idle-nagged and archived — the session was released before the defect it introduced could surface.Scope
Confined to
live_deploy/{service_ready,readiness,intent}.dag+ three claim tests. Deliberately clear oflive_deploy/emit.dag,spec.dag, and the.githubapply/retract scripts, which #7020 is live on — the capture runs at interpreter runtime throughhost_effect_apply_gated, so it needed no reach into the emitted shell and no golden regeneration.What this does not do
It does not fix the srv1 startup failure — the root cause is still unobserved, and shipping a speculative fix for it would be the same error as widening the probe. This is the minimum that makes the next run's refusal say which
exit(1)path fired.Test plan
gunbc run --claim-runonservice_ready/readiness/applywitness suites → alltruefalse(throwaway worktree)gunbc compile --target dagonapply.dagclosure → error set identical to cleanorigin/maingunbc serveonroadmap_serve.dag→ listening,/healthzOK