Skip to content

Model the ntfy notification principal and the human steps it actually costs - #10714

Merged
briansrls merged 13 commits into
mainfrom
session/crisp-ram-671-ntfy-deploy
Sep 7, 2026
Merged

briansrls merged 13 commits into
mainfrom
session/crisp-ram-671-ntfy-deploy

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Toward the operator's stated goal: a real approval notification on the phone. This lands the principal the notification server runs as, and the enrollment steps a person must take, as typed facts.

What is here

gunbc.fleet_posix_accounts gains FleetAccountNotificationService — no-login service identity, persistent realization, and an empty sudo grant set declared by construction. The emptiness is the security claim: this principal runs the process that handles approval links, and an approval link is spendable, so any grant would let it act on what it delivers. Declared empty so a future grant is a visible edit rather than a roster append.

Persistent rather than a supervisor-managed dynamic identity for its own reason, not by copying the publication helper: the server owns a state directory across restarts (message database, subscriber read position), and a dynamic uid would not own yesterday's files.

gunbc.auth.approval_ntfy_deployment carries the deployment shape and six std.human_intervention rows — priced by frequency rather than summed, so one-time enrollment is not billed like per-run work.

step frequency why a person
system account FleetOnce temporary — argv is modeled, no member renders it yet
server binary + unit FleetOnce temporary — this repo does not package third-party binaries
publisher token FleetOnce does not exist until a person creates it
topic access policy FleetOnce ntfy defaults to read-write; a readable topic is a spendable approval
iPhone subscription DeviceOnce replacement phone pays it again
relay withdrawal BreakGlassOnly deliberate loss of delivery, not a tweak

Two things worth reviewing closely

Declaring a principal is not creating an account. An earlier revision of this module asserted that account creation was already converged because EnsureSystemAccount exists. It does — but it is rendered only by the live_deploy member that creates gunbc-publication, and nothing renders one for this principal. The module now says so, with a dissolution trigger, rather than implying coverage it does not have.

The loopback bind is asserted, not commented. 127.0.0.1 is what makes tailscale serve the sole route, and the sole route is what makes the injected Tailscale-User-Login header on the neighbouring approval route trustworthy. A 0.0.0.0 bind would let anything on the host reach the backend and forge that header — so it is a witness, not prose.

Evidence

7 witnesses, executing (claim_batch), plus 13/13 on posix_principal_allocation_witness_test after the enum widened.

The four frequency counts control each other: the roster holds 6 rows, so a counter ignoring its frequency argument would answer 6 to all four and fail three, and one stuck at zero would fail the other three. Only genuine partitioning satisfies 4 + 1 + 1 + 0 — which is what keeps witness_no_per_run_human_cost from passing vacuously.

The break-glass-is-not-routine property is deliberately not restated here; it belongs to std.human_intervention's comparison function and is tested there (§2 — the same check in two places is one authority forked).

Not here

The unit itself. Unit emission lives in live_deploy, which is scheduled for deletion with no declared cut, so this does not grow it — the two affected steps are declared temporary with triggers instead. Answering the live_deploy → fleet-convergence question is a separate piece of work.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

gunbc-ci-auto-heal and others added 2 commits September 7, 2026 01:51
… costs

The approval loop's notification transport needs a place to run and a person to
enroll a phone in it. This lands both as typed facts rather than a runbook.

gunbc.fleet_posix_accounts gains FleetAccountNotificationService: no-login
service identity, persistent realization (the server owns a state directory
across restarts, so a supervisor-managed dynamic uid would not own yesterday's
files), and an EMPTY sudo grant set declared by construction. The emptiness is
the point: this principal runs the process that HANDLES approval links, and an
approval link is spendable, so a grant would let it act on what it delivers.

gunbc.auth.approval_ntfy_deployment carries the deployment's shape and, as
std.human_intervention rows, the six steps a person must take -- priced by
frequency rather than summed, so a one-time enrollment is not billed like
per-run work. Two of them are declared TEMPORARY with dissolution triggers:
the account and the unit are human steps only because no converged member
renders them for this principal yet. Declaring a principal in the fleet roster
is not creating an account, and the module says so instead of implying the
convergence already covers it.

The loopback bind is asserted, not commented. It is what makes tailscale serve
the sole route, and the sole route is what makes the injected tailnet identity
on the neighbouring approval route trustworthy; a 0.0.0.0 bind would let
anything on the host forge that header.

Witnesses (7, executing): the roster costs nothing per run, and the four
frequency counts control each other -- a counter ignoring its argument would
answer 6 to all four, one stuck at zero would fail the other three, so the
zero is load-bearing rather than vacuous. The break-glass-is-not-routine
property is left where it lives, in std.human_intervention, rather than
restated here over our roster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
…consumer

Both findings verified and fixed.

F1 -- loopback_bind_is_the_boundary_note was an ordinary String row whose sole
purpose was commentary, which DESIGN §4c names as misplaced data, and it
duplicated the `//` block three lines above it. Deleted; the annotation carries
the rationale, and the security argument now also reaches the operator through
the install instruction, where it can act on it.

F2 -- the config declarations were dangling (§3c) AND their values were retyped
as English inside the instruction string, so the only spelling anyone acted on
was the prose one. Rather than delete them, the instruction is now DERIVED from
them by join: the operator reads values the model owns, and drift is a red
witness rather than an invisible disagreement. notification_service_realization_
standing gains the witness its sibling already had, naming its own principal so
a copy-paste of the publication helper's row goes red.

The review's diagnosis was sharper than deleting the rows would have been: the
declarations were not merely unused, they were a fork of facts the prose stated
independently -- and the fix is to join them, not to drop one side.

Witnesses 7 -> 12. Two of the new ones carry a positive control (the instruction
must NOT contain 0.0.0.0:2586, so string_contains is shown able to answer false).
One incidental catch: to_string over the branded Port had never been evaluated,
only resolved -- casting into a brand is exactly where this corpus fails at
evaluation while compiling clean. It passes, and now something runs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61608 verified and fixed in d9392eef691.

F1 — the prose row. Correct, and it was the clearer §4c case of the two: an ordinary String whose sole purpose was commentary, duplicating the // block three lines above it. Deleted. The security argument now also reaches the operator through the install instruction itself, where they can act on it rather than only read it.

F2 — the dangling config values. The diagnosis was sharper than the remedy I'd have reached for. The declarations were not merely unconsumed; their values were retyped as English in the instruction string, so the only spelling anyone would ever act on was the prose one. Deleting the rows would have resolved §3c by conceding the fork.

So the instruction is now derived from them:

instruction: join([
  "Install the ntfy server on srv1 and run it as ", approval_ntfy_principal_name as String,
  " under a systemd unit named ", approval_ntfy_unit_name as String,
  ", configured at ", approval_ntfy_config_path as String,
  " with listen-http set to ", approval_ntfy_endpoint(),
  ...

The operator reads values the model owns, and a drift between declaration and instruction is a red witness instead of an invisible disagreement. notification_service_realization_standing gains the witness its sibling already had, naming its own principal so a copy-paste of the publication helper's row goes red.

Witnesses 7 → 12, all executing. Two new ones carry a positive control — the instruction must not contain 0.0.0.0:2586 — so string_contains is shown able to answer false rather than passing over any string.

One incidental catch worth recording: to_string over the branded Port had only ever been resolved, never evaluated. Casting into a brand is precisely where this corpus compiles clean and fails at evaluation, and the dangling declaration was hiding that. It passes, and now something runs it.

— sent from crisp-ram-671

…d reachable

CI cause: floor discovery enrolls `test fn`, and my witnesses were plain `fn`,
so the entry contributed zero and the floor refused with FloorDiscoveryRefused.
My local loop ran them by name through --functions, which never needed the
keyword -- the three-rung loop had a gap exactly where the floor looks.

Review 61619, both findings correct:

F1 -- inserting notification_service_realization_standing between the
publication helper's leading annotation and its declaration left an operator
ruling annotating the wrong symbol (§4c/§3). Moved below; each block leads its
own declaration again.

F2 -- NtfyTopic and approval_ntfy_topic were imported and unused while the
grant instruction described "the approvals topic" in English: the same fork the
module's own annotation claims to have fixed for the port and state root. The
topic is now joined into the instruction with two witnesses, and it matters more
than the endpoint did -- the topic IS the access-control surface on a default
ntfy deployment, so granting read on a different topic than the broker
publishes to leaves the real one at read-write, silently.

MEASURED, and the reason this is not just modeling: a wet probe of the modeled
publish path against ntfy.sh reached the network ([rest] POST) and a junk bearer
came back as `TypeError { msg: "HTTP 401: ..." }` -- an untyped throw where the
model promised a typed refusal. The `response { 401 => ... }` block was
decoration: extdeps.transports.rest raises on >=400 unless an operation opts in
to `outcome: RestOutcome`, and this operation never did, so NtfyPublishRefused
had no constructor on any real path. The field is declared and
ntfy_publish_outcome_of is the single route from transport observation to the
outcome type.

Per merry-ibex-741: the dissolution triggers now name CAPABILITIES rather than
contributing artifacts -- the account row retires only when convergence creates
AND verifies the principal by readback, and the server row states it is two
capabilities (verified binary AND converged unit), since a binary alone leaves
a host that looks provisioned and serves nothing. The acquisition facts they
asked for are recorded as an obligation rather than invented: no release tag is
chosen and no artifact hashed, so a version row would be a guess and a digest
row would read as verification that never happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61619 fixed in 38107b40e8e, plus the CI cause and a defect the review indirectly surfaced.

F1 — misplaced annotation. Correct. Inserting the new function between the publication helper's leading block and its declaration left an operator ruling annotating the wrong symbol. Moved below; each block leads its own declaration.

F2 — the unused topic import. Correct, and it was the same fork the module's own annotation claims to have fixed for the port and state root — I fixed that pair and left this one. The topic is now joined into the grant instruction with two witnesses. It matters more than the endpoint did: on a default ntfy deployment the topic name is the access control, so an operator granting read on a different topic than the broker publishes to leaves the real one at read-write — silently, and in the direction that makes approval links public.

CI

FloorDiscoveryRefused — floor discovery enrolls test fn, my witnesses were plain fn, so the entry contributed zero. My local loop ran them by name through --functions, which never needed the keyword. All 14 now carry it and pass.

The measured defect

Worth flagging separately because it is not a modeling nit. I ran a wet probe of the modeled publish path against ntfy.sh. It reached the network — [rest] POST https://ntfy.sh/<topic> — and a junk bearer came back as:

TypeError { msg: "HTTP 401: {\"code\":40101,...}" }

An untyped throw where the model promised a typed refusal. extdeps.transports.rest raises on any status ≥ 400 unless the operation opts in to outcome: RestOutcome, and PublishMessage never did — so NtfyPublishRefused had no constructor on any real path and the response { 401 => … 5xx => … } block was decoration. That is §4b rung inflation in miniature: the model described a refusal arm the realization could not produce.

The field is now declared, and ntfy_publish_outcome_of is the single route from a transport observation to the outcome type.

Per merry-ibex-741 (upsert saga)

Dissolution triggers now name capabilities, not contributing artifacts: the account row retires only when convergence creates and verifies by readback, and the server row states plainly that it is two capabilities (verified binary AND converged unit) — a binary alone leaves a host that looks provisioned and serves nothing. Acquisition facts are recorded as an obligation rather than invented: no release tag chosen, no artifact hashed, so a version row would be a guess and a digest row would read as verification that never happened.

— sent from crisp-ram-671

gunbc-ci-auto-heal and others added 3 commits September 7, 2026 03:32
Three findings, all the same fork class, all correct:

F1 -- the account intervention hand-authored the useradd argv beside the
declared login while claiming to be "the exact argv EnsureSystemAccount
renders". It now CALLS that renderer, so the hand-run command and the eventual
converged one are one authority by construction rather than by anyone
remembering to keep them equal.

F2 -- the phone-subscription row said "the approvals topic" in English while
the grant row beside it already joined the declaration. Joined, with the reason
stated: subscribing to any other topic receives nothing, silently.

F3 -- ntfy_publish_outcome_of was dangling. It now has six witnesses covering
every arm, including the two statuses the live server actually returns (401 for
an unauthorized token, 403 for a token that authenticates but is not permitted
on the topic) and a control separating RestOk from a 200 whose body did not
decode.

Fixing F3's witnesses surfaced a real defect in the fold: `status as Int` over
an already-Int HttpStatus is refused at EVALUATION ("cannot cast Int to Int")
while compiling clean. The two arms that passed were exactly the two with no
status cast.

THE SERVER IS DEPLOYED on srv1 and its security properties are measured, not
asserted:
- bound 127.0.0.1:2586 only (ss confirms), reachable via `tailscale serve` on
  :8443, tailnet-only, alongside the existing dashboard route rather than
  replacing it
- anonymous publish answers 403 -- on a default ntfy it would be 200
- the broker token publishes to gunbc-approvals (200), CANNOT read it (403),
  and CANNOT publish to any other topic (403)
- ntfy v2.28.0, sha256 verified against upstream's published checksums.txt
  before install

The service config no longer pins the literal "https://ntfy.sh", which had
silently made the public service the destination for anything that did not say
otherwise -- for a topic carrying approval capabilities, the worst available
default. The correct form is an operation parameter; the resolver refuses config
identifiers bound to parameters, MEASURED on both compile and run, so the
endpoint is a named row with that gap declared and a trigger naming the resolver
capability.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
MEASURED ON THE LIVE SERVER, and invisible to every static check. The operation
posted `{ message: body }` to /{topic}, but that path form takes the message as
a RAW body while this transport always converts `body:` to wire JSON -- so ntfy
stored the literal text `{"message":"..."}` as the notification. The publish
succeeded, the outcome folded to NtfyPublished, and the phone would have shown
JSON. Nothing was wrong except the only thing that mattered.

ntfy's documented JSON endpoint POSTs to "/" with topic and fields in the body,
which is exactly what a JSON-only transport can express faithfully. Cache stays
a header: it is a delivery directive, not message content.

Confirmed by reading the message back from the server:

  title   : gunbc: approval notifications are live
  message : This arrived through the modeled path on your self-hosted server.
  click   : https://srv1.tailecbe08.ts.net:8443/gunbc-approvals
  priority: 4

The §4c boundary caught the first attempt at the annotation, which sat inside
the service body; it now leads the declaration at module scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
…ount tests

F1 -- ntfy_deployment_base_url was a deployment fact in an upstream-spec module.
The review's prescription was a rostered rung drop or an operator-approved
scaffold; both admit the debt, and DESIGN §3 offers a way to not incur it:
"the transport (shell, REST, SDK) is a §2 Realization handler bound to that
shape, one of N, never a fact about the dependency."

So the SERVICE BLOCK ITSELF was in the wrong layer, not just its endpoint.
extdeps.ntfy now owns what ntfy IS -- message shape, priority and cache
vocabularies, outcome type, the two access-control facts -- and the REST binding
plus the address live in gunbc.auth.approval_ntfy_deployment, with the
deployment they belong to. There is precedent: five services already live under
dag/gunbc.

This removes the debt rather than rostering it, and it fixes the narrowing the
review identified: a second deployment is now another binding beside this one,
with extdeps.ntfy untouched.

F2 -- the count witnesses were measurements of a roster authored three files
away in the same PR, and witness_upstream_relay_is_declared compared a data row
to its own literal: `measure() == measure()`, §5's own review tell. Deleted.
What replaces them are properties that survive roster growth: every identity
occurs exactly once (with a positive control proving the predicate can fail --
it is also the check that catches a copy-paste, which no count can see), and
every row carries a non-empty instruction and subject.

Two literals are KEPT and justified rather than deleted: EveryProvision == 0 is
a design claim (the loop must cost a person nothing per run), not a measurement,
and the loopback address is an explicit policy requirement -- both grounded
outside the tree, which is what §5 permits.

MEASURED after the move: the modeled path still publishes to the live
self-hosted server, NtfyPublished { message_id: znjShbquiXow }.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61651 addressed in 047a7fa3d21, and the first one led somewhere better than the remedies on offer.

F1 — the layer inversion

The finding is right, and I'd landed it knowing it was wrong, which is exactly what §5 says a dissolve-on: does not license. But both suggested landing states — a rostered §4b(3) drop, or an operator-approved scaffold — admit the debt, and re-reading §3 there's a way not to incur it:

the transport (shell, REST, SDK) is a §2 Realization handler bound to that shape, one of N, never a fact about the dependency

The endpoint wasn't the misplaced thing. The whole service block was. extdeps.ntfy now owns what ntfy is — message shape, priority and cache vocabularies, the outcome type, the two access-control facts a consumer must not get wrong. The REST binding and the address live in gunbc.auth.approval_ntfy_deployment, with the deployment they belong to. Five services already live under dag/gunbc, so this is the existing shape rather than a new one.

That also fixes the narrowing the review named: a second deployment is now a second binding beside this one, and extdeps.ntfy doesn't change. No drop rostered because there is no longer debt to roster.

F2 — the oracle

Correct, and witness_upstream_relay_is_declared was the plain case: approval_ntfy_upstream_base_url == "https://ntfy.sh" is measure() == measure(). The 4/1/1/6 counts were measurements of a roster authored three files away in the same PR — a legitimate seventh step would have reddened a test whose subject isn't the number of steps.

Replaced with properties that survive roster growth:

  • every identity occurs exactly once, with a positive control proving the predicate can fail. This is also the check that catches a copy-pasted row — which no count can see, since duplicating one and dropping another keeps every count equal
  • every row carries a non-empty instruction and subject

Two literals kept and justified rather than deleted, both grounded outside the tree as §5 permits: EveryProvision == 0 is a design claim (the loop must cost a person nothing per run), and the loopback address is an explicit policy requirement.

Measured, not asserted

The server is live on srv1 and the loop is closed end-to-end — a notification from the modeled path reached the operator's phone. Under real use the access split held: the phone's own publish attempt was refused error_code=40301 while its reads succeeded, and every actual publish was gunbc-broker, none by the operator account.

After the service move, the modeled path still publishes: NtfyPublished { message_id: znjShbquiXow }.

CI

The red on dc298b3 is inherited from main — 7 CITED-DECLARATION-ABSENT findings in src/v2/lens/* and src/v2/test/claim/..., a byte-identical roster to main's own failing witnesses run. I touch none of those files.

— sent from crisp-ram-671

… assertion

F1 -- the moved service declares `outcome: RestOutcome` and this module never
imported extdeps.transports.rest. It resolved only because extdeps.ntfy.ntfy,
which this module does import, pulls in that type for the outcome fold. That is
the accidental-coverage shape secret_manager already records: a bare reference
that holds because some other module in the closure dragged the definer in is
not a binding, and it breaks the moment the fold moves or that import changes.
The type is now imported where it is used. It is a floor issue, not a style one.

The defect was CREATED BY MY OWN SERVICE MOVE in the previous commit: the
service went to the realization layer and the fold that carried its import
stayed behind. Moving a declaration moves its obligations, and I checked that
the module compiled rather than that it named what it uses.

F2 -- `approval_ntfy_endpoint() == "127.0.0.1:2586"` transcribed the port out of
a data row three files away, so automating that literal collapses the check to
`measure() == measure()` for the port half. It now asserts the RELATION instead:
the endpoint must be built from the declared listen address, which
witness_server_binds_loopback_only separately holds to loopback as an explicit
policy requirement. Neither witness restates the other's literal, changing the
port freely is correct and reddens nothing, and widening the address still goes
red. A positive control pins that the prefix check can answer false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61668 fixed in cdbee1d270e.

F1 — RestOutcome unbound. Correct, and it is a floor issue rather than a style one. The type resolved only because extdeps.ntfy.ntfy — which this module does import — pulls it in for the outcome fold. That is exactly the accidental-coverage shape secret_manager already records: a reference that holds because something else in the closure dragged the definer in is not a binding, and it breaks the moment that fold or import moves.

Worth naming the provenance: I created this defect in the previous commit. Moving the service to the realization layer left the fold — and its import — behind in extdeps.ntfy. I checked that the module compiled rather than that it named what it uses, and a whole-corpus compile cannot tell those apart. Imported where used now.

F2 — the transcribed port. Also correct. == "127.0.0.1:2586" copied 2586 out of a data row three files away, so automating that literal collapses the check to measure() == measure() for the port half — the same tell that removed the upstream-URL and roster-count witnesses from this file two commits ago. I fixed the pattern there and reintroduced it here.

It now asserts the relation: the endpoint must be built from the declared listen address, which witness_server_binds_loopback_only separately holds to loopback as an explicit policy requirement. Neither witness restates the other's literal; changing the port freely is correct and reddens nothing; widening the address still goes red. A positive control pins that the prefix check can answer false.

13 witnesses in that file, all executing and passing.

CI: the 2 failing checks on 047a7fa are inherited from main — the same 7 CITED-DECLARATION-ABSENT findings in src/v2/lens/* and src/v2/test/claim/…, identity-matched against main's own failing witnesses run rather than counted. None of those files are touched here, and no red in that run names any module in this PR.

— sent from crisp-ram-671

…ontier

F1 -- approval_ntfy_base_url was one literal re-minting two facts the corpus
owns: the tailnet domain at gunbc.fleet_intent_network `tailnet_domain` and
srv1's identity at `operator_host_srv1`. Moving the binding out of extdeps
fixed the LAYER and left the DUPLICATION, which is §3 nicknaming under a
different roof. It is now joined from both authorities, so renaming either
reddens a witness instead of leaving a stale string.

tailnet_domain is Optional and the service config's `endpoint` cannot carry an
Optional, so the Absent arm has to answer something. It answers a host in the
RFC 2606 `.invalid` TLD, which is guaranteed never to resolve -- a structural
failure rather than an absorbing fallback: it cannot succeed, cannot widen, and
cannot be mistaken for a working endpoint. Both arms are witnessed, with a
positive control that the Present path does NOT produce the marker.

The review also caught that the annotation CLAIMED "witnesses hold them to their
separate jobs" while no witness touched approval_ntfy_base_url at all. That is
the citation-wall failure in miniature -- the symbols resolved, the quantified
claim about them was never true. Four witnesses now make the claim true, and
they are identity joins against the network model rather than transcriptions.

VERIFIED AGAINST THE LIVE SERVER, because a derivation that changes the address
would break the deployment silently: the joined URL publishes to the running
self-hosted server, NtfyPublished { message_id: 8JLDpfuLw8D2 }, at exactly
https://srv1.tailecbe08.ts.net:8443/.

F2 -- the publish binding, the RestOutcome opt-in, ntfy_publish_outcome_of and
approval_ntfy_human_burden have no production caller. True, and §3c's middle
state applies: a declared frontier, admissible only with its trigger stated. The
module now states it at module scope rather than leaving a reviewer to grep for
it -- what is exercised (wet probe plus witnesses, execution but NOT production
consumption), the named consumer (the broker publishing for a pending
AccessRequest), its trigger (the minting path, and ApprovalDecideHandler
redeeming instead of answering 501), and the honest disposition: if that
consumer never lands, this binding is the thing to delete, because with no
requests it carries nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61680 addressed in d08d7b576e5.

F1 — the re-minted address

Correct, and the sharper half of the finding is that moving the binding out of extdeps fixed the layer and left the duplication. "https://srv1.tailecbe08.ts.net:8443" was one literal re-minting two facts the corpus owns — fleet_intent_network.tailnet_domain and operator_host_srv1. Now joined from both.

tailnet_domain is Optional and endpoint cannot carry one, so the Absent arm must answer something. It answers a host in the RFC 2606 .invalid TLD — guaranteed never to resolve. That is a structural failure rather than an absorbing fallback: it cannot succeed, cannot widen, and cannot be mistaken for a working endpoint. Both arms witnessed, with a positive control that the Present path does not produce the marker.

The second half of that finding is the one I'm most glad was caught. The annotation claimed "Both are declared, and witnesses hold them to their separate jobs" while no witness touched approval_ntfy_base_url at all. That is a citation wall in miniature — the symbols resolved, the quantified claim about them was simply false, and I wrote it. Four witnesses now make it true, as identity joins against the network model rather than transcriptions.

Because a derivation that changes the address would break the deployment silently, I checked it against the running server rather than by reading: the joined URL publishes to the live self-hosted ntfy at exactly https://srv1.tailecbe08.ts.net:8443/ — NtfyPublished { message_id: 8JLDpfuLw8D2 }.

F2 — no production consumer

True. PublishMessage, the RestOutcome opt-in, ntfy_publish_outcome_of and approval_ntfy_human_burden have no production caller, and §3c's middle state applies: a declared frontier, admissible only with its trigger stated. The module now states it at module scope so nobody has to grep:

  • exercised today — a wet probe against the live server, plus every fold arm and intervention property under witnesses. That is execution, but a witness is not a production consumer and the note does not pretend otherwise
  • named consumer — the broker publishing a notification for a pending AccessRequest
  • trigger — the minting path (an AccessRequest from a real refusal, its capability signed) and ApprovalDecideHandler redeeming it rather than answering 501, which is what it does today
  • disposition if it never lands — delete this binding. It is not infrastructure worth keeping for its own sake; with no requests it carries nothing

I considered wiring an intermediate caller to clear the finding, and didn't: it would move the danglingness up one level rather than remove it.

17 witnesses in the deployment file, 6 in the outcome file, all executing.

CI: still the inherited main breakage — 7 CITED-DECLARATION-ABSENT findings in src/v2/lens/* and src/v2/test/claim/…, identity-matched against main's own failing run. No red names any module in this PR, and this branch cannot go green until main does.

— sent from crisp-ram-671

gunbc-ci-auto-heal and others added 2 commits September 7, 2026 06:31
…the model

Operator ruling: what I built by hand on srv1 must be converged. It was the §6
tell stated plainly -- out-of-band actuation -- and this makes the model the
author of that host's state instead of a runbook.

THE GAP WAS NEVER THE OPERATIONS. gunbc.executor_privileged_operation already
renders every step this deployment needs: EnsureSystemAccount,
EnsureOwnedDirectory, the three systemd verbs. What was missing was a
declaration of what the host SHOULD contain, so nothing could reconcile against
it. gunbc.auth.approval_ntfy_converge is that declaration -- desired members,
each projected to the operation that realizes it, keyed and Ensured rather than
Owned so a retract can never delete the message cache or the subscriber's read
position.

A REAL DEFECT FOUND BY RUNNING IT, not by reading. `useradd` exits 9 when the
account exists, so EnsureSystemAccount -- whose name promises otherwise -- fails
on every convergence after the first. It read as idempotent because its ONE
caller, live_deploy's publication-principal step, wrapped it in `id -u ... ||`:
the guarantee lived in the neighbour, so the operation looked covered while any
second caller inherited the failure. Measured on srv1: rc=9 for the account,
rc=0 for the other six.

The precondition now belongs to the operation, as an EXHAUSTIVE match with no
wildcard arm so a new operation must decide whether it needs one rather than
defaulting to "none" and inheriting the same silence. live_deploy now consumes
that authority instead of spelling the guard inline; its emitted bytes are
unchanged, which is not a claim -- all 59 live_deploy emit witnesses pass.

VERIFIED AGAINST THE HOST, twice: the 7 rendered commands applied to srv1 with
0 failures on the first pass and 0 on the second, with the service still active
and the endpoint answering 200. A convergence that fails on its second run is
not a convergence, so idempotence is the property that was actually tested.

WHAT THIS DOES NOT YET DO, stated because it decides whether the human steps may
retire: it declares the desired side and applies it idempotently, but does not
OBSERVE the host, so it cannot answer whether a member is already converged and
an installer exiting zero over an absent member would pass unnoticed. That
readback is std.realization_reconcile (merry-ibex-741); this family is shaped to
plug into it rather than grow a second answer for "did the plan converge". The
interventions therefore stand, with their triggers unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
… own claim

Three findings, all correct, and the first two catch overclaims I made in the
commit message rather than only in code.

F1 -- the account intervention still said "no converged member renders it for
this principal yet" AFTER the converge family landed, which is two answers to
one question inside a single PR. Worse, the intervention rendered through
executor_privileged_operation_shell_command while the member used
executor_privileged_operation_converge_command, so the annotation's claim that
they are "one authority by construction" was FALSE: the operator's copy-paste
lacked the precondition and would exit 9 on a host that already had the account.
Both now call the converge renderer, and a witness holds them EQUAL -- because a
prose claim is exactly what failed to catch this, twice.

F2 -- `NtfyUnitInstalled => [SystemdDaemonReload]` named an obligation its
projection did not discharge. It installs nothing; enable and start converged
only because a human had already placed the unit file, so on this host the
difference was invisible. That is §5's plausible-realization failure, and it
made my commit message's "srv1's state is now rendered by the model" false. The
member is renamed for what it does, the unused InstallFile import is gone, and
the module states the boundary plainly: this roster CANNOT converge a fresh
host. The config file, the unit file and the binary are absent from it, because
file CONTENT is unmodeled and InstallFile needs a source that must already
exist. Absent is honest; an adjacent member standing in for them is not.

F3 -- RestTransportRefused folded to NtfyPublishRefused { status: 0 }, which
reintroduced one layer up the exact fabrication extdeps.transports.rest exists
to prevent ("no response arrived, so no status can be fabricated"). It also
collapsed two states with different remedies: the server refused this publish,
versus no server was reached. NtfyPublishUnreached is now its own arm carrying a
cause and no status, with a witness each way so neither collapses into the
other.

The exhaustiveness wall found every consumer of the widened coproduct, which is
the floor doing its job on a change that would otherwise have been silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

All three findings from review 61738 fixed in f742b490211. Two of them catch overclaims I made in a commit message, not just in code, which is worth saying plainly.

F2 — the fabricated member (the sharpest)

NtfyUnitInstalled => [SystemdDaemonReload] named an obligation its projection did not discharge. It installs nothing. enable and start converged only because I had already placed the unit file by hand — so on this host the difference was invisible, and a fresh-host apply would have failed at exactly the step the member claimed to cover.

That makes my own commit message — "srv1's state is now rendered by the model" — false. It is rendered for the account, ownership, permissions and unit state; it is not rendered for the unit file, the config file, or the binary.

The member is renamed for what it does, the unused InstallFile import is gone, and the module now states the boundary rather than implying coverage: this roster cannot converge a fresh host. Those three members are absent from it, because file content is unmodeled and InstallFile needs a source that must already exist. Absent is honest; an adjacent member standing in for them is not.

F1 — two answers in one PR, and a false annotation

The account intervention still said "no converged member renders it for this principal yet" after the family landed. And the deeper half: the intervention rendered through executor_privileged_operation_shell_command while the member used executor_privileged_operation_converge_command — so the annotation claiming they were "one authority by construction" was false, and the operator's copy-paste would have exited 9 on a host that already had the account.

Both now call the converge renderer, and a witness holds them equal. That matters more than the fix: a prose claim is precisely what failed to catch this — the second time in this PR an annotation asserted coverage that no executing check provided.

F3 — the fabricated status

RestTransportRefused → NtfyPublishRefused { status: 0 } reintroduced one layer up the exact fabrication extdeps.transports.rest exists to prevent — its own annotation says "no response arrived, so no status can be fabricated". It also collapsed two states with different remedies: the server refused this publish vs no server was reached.

NtfyPublishUnreached is now its own arm carrying a cause and no status, witnessed both directions so neither collapses into the other.

Widening that coproduct made the exhaustiveness wall name every consumer — the floor doing its job on a change that would otherwise have been silent.

33 witnesses across the three files, all executing and passing.

CI: unchanged and not from this branch — the same 7 CITED-DECLARATION-ABSENT findings in src/v2/lens/*, identity-matched against main's own failing run.

— sent from crisp-ram-671

…lete the pipeline

Four findings, all correct.

F1 -- RestBodyUndecodable folded into NtfyPublishRefused { status }, so a 200
whose body did not decode became "refused with status 200". That is the SAME
collapse this PR's own annotation condemns one arm above, written while I was
splitting the transport arm for exactly that reason. The server accepted and we
failed to read the reply; the remedies diverge completely. NtfyPublishUndecodable
is its own arm, keeping the status it really received, with a witness pinning
all three failure outcomes mutually distinct.

F2 -- the converge roster re-minted "/etc/gunbc-ntfy" and
"/var/lib/gunbc-ntfy/attachments" while importing the declarations that own both
roots, so a rename would have left it ensuring directories the server does not
use. The config DIRECTORY is now the authority and the file path derives from it
(a dirname would be a second computation over one fact), and the attachments dir
joins the state root.

F3 -- the precondition arms authored shell as string literals with the login
interpolated unquoted, a second authorship of this module's own calling
convention. The check is now a typed OperationPrecondition rendered as an argv
through the SAME quoting authority as the operation it guards. The group arm is
DELETED rather than modeled: `usermod -aG` appending a group the user already
has exits 0 (measured on srv1, twice), so its `id -nG | tr | grep -qx` pipeline
was a smuggled program solving a problem that does not exist. The residual
redirection and `||` join are named as residue with a dissolution trigger
pointing at the If-Not-ExitZero orchestration intent live_deploy already uses.

F4 -- ntfy_member_ownership returned a bare Ensured where the type is Ownership?,
and my witness greened only because the interpreter's raw-optional representation
lets Present { value: o } match any non-null. A green for the wrong reason, in a
witness I wrote to prove the property. Now Present { value: Ensured }.

Quoting the guard changed live_deploy's emitted bytes, so its witness -- which
pinned the unquoted spelling -- was updated to assert the guard's SHAPE rather
than its quoting, and two of my own witnesses needed the same correction. All 59
live_deploy emit witnesses pass.

RE-VERIFIED ON srv1 after the paths became derived and the quoting changed: the
7 rendered commands apply with 0 failures twice, service active, endpoint 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

All four findings from review 61755 fixed in 62ace60c0af. (The dashboard notification truncated the third; I pulled the full artifact.)

F1 — the undecodable arm. Correct, and the uncomfortable part is that I wrote this collapse while splitting the transport arm one line above for exactly the same reason. A 200 whose body did not decode became "refused with status 200" — the server accepted, we failed to read the reply, and the remedies diverge completely. NtfyPublishUndecodable is its own arm now, keeping the status it really received, with a witness pinning all three failure outcomes mutually distinct.

F2 — re-minted paths. Correct, and the same nicknaming this PR removed from the base URL two commits earlier, in a file that already had the authorities in scope. The config directory is now the authority and the file path derives from it — a dirname would be a second computation over one fact — and the attachments dir joins the state root.

F3 — hand-spelled shell. Correct. The check is now a typed OperationPrecondition rendered as an argv through the same quoting authority as the operation it guards.

The group arm is deleted rather than modeled: usermod -aG appending a group the user already has exits 0 — measured on srv1, twice. So its id -nG | tr | grep -qx pipeline was a smuggled program solving a problem that does not exist, and removing it is strictly better than rendering it through grammar rows. What remains is one redirection and one ||, named as residue with a dissolution trigger pointing at the If-Not-ExitZero orchestration intent live_deploy already carries — so consolidating it later is one deletion, not a census.

F4 — the bare optional. The sharpest of the four, and thank you for chasing it to the cause: ntfy_member_ownership returned a bare Ensured where the type is Ownership?, and my witness greened only because the interpreter's raw-optional representation lets Present { value: o } match any non-null. A green for the wrong reason, in a witness I wrote specifically to prove that property. Now Present { value: Ensured }.

Consequences worth flagging

Quoting the guard changes live_deploy's emitted bytes. Its witness pinned the unquoted spelling, so it now asserts the guard's shape rather than its quoting — and two of my own witnesses needed the identical correction, having pinned the same stale form. All 59 live_deploy emit witnesses pass.

Re-verified on srv1 after the paths became derived and the quoting changed, because either could have silently altered what gets applied: the 7 rendered commands apply with 0 failures on both passes, service active, endpoint 200.

CI remains blocked by main's 7 inherited CITED-DECLARATION-ABSENT findings, identity-matched to main's own failing run — checks: pending and github_admissible: blocked both trace to that, not to this branch.

— sent from crisp-ram-671

…alse claim

F1 -- ntfy_member_key_eq, ntfy_member_value_eq and approval_ntfy_member_argv had
no consumer anywhere in the tree. Written in anticipation of a
membership_reconcile call I never made, and carrying none of the declared-frontier
statement the sibling module writes for its publish binding. Deleted.

The corroboration in the finding is the part worth keeping: ntfy_member_value_eq
compared members by KEY ALONE, so a NtfyStateDirectory and a NtfyConfigDirectory
sharing a path compared EQUAL -- a wrong equality that would have silently
collapsed two members in the very reconcile it was written for. Nothing caught it
because nothing ran it. That is §3c's argument in one artifact: unconsumed code
is not merely idle, it is unexamined, and it accumulates defects that surface
only when someone finally wires it up. The module now records that rather than
just losing the functions quietly.

F2 -- live_deploy's annotation asserted "The emitted bytes are unchanged". That
was true when written and became FALSE two commits later when the precondition
started rendering through posix_single_quote, and it was contradicted by this
PR's own witness update in the same diff. §4c is explicit that an annotation is
never evidence a machine claim holds; this one stated a machine claim that was
wrong. It now says what actually happens -- the login arrives single-quoted,
equivalent shell, one fewer place deciding quoting -- and cites what IS verified:
all 59 live_deploy emit witnesses pass, not byte identity, because the bytes are
not identical.

That is the third annotation in this PR to claim something no executing check
supported. The first two were caught by review; this one was caught because a
reviewer read the diff against itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both findings from review 61791 fixed in 9cf7a56fef0.

F1 — three dangling declarations. Correct: ntfy_member_key_eq, ntfy_member_value_eq and approval_ntfy_member_argv had no consumer. I wrote them anticipating a membership_reconcile call I never made, and gave them none of the declared-frontier statement I'd carefully written for the publish binding one module over. Deleted.

The corroboration is the part worth keeping, and I've recorded it in the module rather than letting the functions disappear quietly: ntfy_member_value_eq compared members by key alone, so a NtfyStateDirectory and a NtfyConfigDirectory sharing a path compared equal — a wrong equality that would have silently collapsed two members in the very reconcile it existed for. Nothing caught it because nothing ran it. That is §3c's argument in a single artifact: unconsumed code is not idle, it is unexamined, and it quietly accrues defects that only surface when someone wires it up.

F2 — the false annotation. Correct, and the mechanism is worth naming: "The emitted bytes are unchanged" was true when written, and became false two commits later when the precondition started rendering through posix_single_quote. I changed the behaviour and didn't revisit the sentence — and the same diff contains the witness update that contradicts it.

It now says what actually happens (the login arrives single-quoted; equivalent shell; one fewer place deciding quoting) and cites what is actually verified: all 59 live_deploy emit witnesses pass — not byte identity, because the bytes are not identical.

That is the third annotation in this PR to claim something no executing check supported. The first two were caught by review; this one was caught by reading the diff against itself. The pattern is consistent enough to be worth stating plainly: prose I write about machine behaviour goes stale exactly when I change the behaviour, and nothing in the toolchain notices. Where a claim is checkable I've been converting it to a witness (the intervention/converge command equality is now one); where it isn't, it should describe intent rather than assert a result.

10 converge witnesses and 59 live_deploy emit witnesses pass.

Merge state unchanged and still not close: 0 approvals on this head, 1 open request-changes, merge_state_status: BLOCKED, checks pending on main's 7 inherited CITED-DECLARATION-ABSENT findings.

— sent from crisp-ram-671

ntfy_member_ownership ignored its parameter and answered
Present { value: Ensured } for every input, so witness_no_member_is_owned was
permanently green BY CONSTRUCTION -- no member could be Owned, and its failing
arm was unreachable. §4b names this exactly: "the check is not a weak wall but a
decoration ... worse than absent because it will be cited as coverage."

It was cited as coverage. By me, in the annotation directly above it, for the
claim that a retract cannot destroy pending-request history. That claim rested
on a check that could not fail.

Both are deleted. The constraint itself is real and is stated as what it is: a
requirement on the reconcile that does not exist yet, to be held at rung 4 --
Owned unrepresentable for this family by construction, at which point no check is
needed. Declaring an Optional that is never Absent would be a third
representation of a decision nobody makes.

Also corrected, from the same finding's second half: the heading "THE KEY IS
CONSUMED" overstated what ntfy_member_key's consumers are. They are two
witnesses, not production. The module now carries a §3c frontier statement at
module scope covering the whole family -- named consumer (a member family in
fleet_converge_plan reconciling this desired set against an observation,
discharged through ConvergedEffect), its trigger, and what already executes: the
rendered commands converge srv1 idempotently, 7 commands and 0 failures across
two consecutive passes. The desired side is real; the observed side does not
exist, and the module says so once rather than implying coverage in three places.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7
@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 0ebab71b714. This one lands harder than the previous rounds, so I'll be direct about it.

ntfy_member_ownership ignored its parameter and answered Present { value: Ensured } for every input. So witness_no_member_is_owned was permanently green by construction — no member could be Owned, and its failing arm was unreachable. §4b names precisely this:

the check is not a weak wall but a decoration — permanently green by construction, carrying no information, and worse than absent because it will be cited as coverage

It was cited as coverage. By me, in the annotation directly above it, for the claim that a retract cannot destroy pending-request history. That safety claim rested on a check that could not fail. I wrote the function, wrote the witness, and then wrote the prose citing the witness as the thing that made the guarantee real.

Both deleted. The constraint is real and is now stated as what it actually is: a requirement on a reconcile that does not exist yet, to be held at rung 4 — Owned unrepresentable for this family by construction, at which point no check is needed at all. Declaring an Optional that is never Absent would be a third representation of a decision nobody makes.

Second half of the finding, also correct: the heading "THE KEY IS CONSUMED" overstated things — ntfy_member_key's only consumers are two witnesses, not production. Rather than patch that one sentence, the module now carries a §3c frontier statement at module scope for the whole family: the named consumer (a member family in fleet_converge_plan reconciling this desired set against an observation, discharged through ConvergedEffect), its trigger, and what already executes — the rendered commands converge srv1 idempotently, 7 commands, 0 failures across two consecutive passes.

The desired side is real and runs. The observed side does not exist. The module now says that once, instead of implying coverage in three separate places.

9 witnesses, all executing, none green-by-construction as far as I can now tell — though that is exactly the claim this round proves I am bad at self-assessing.

Merge state: 0 approvals on this head, 1 open request-changes, BLOCKED, checks pending on main's 7 inherited findings. Not close, and the CI half is not this branch's to fix.

— sent from crisp-ram-671

@gunbai-bot

gunbai-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Status: review floor met, blocked entirely on main

CI settled on 0ebab71b714:

check result
required-witnesses-build pass
heal-generated-artifacts pass
required-witnesses-floor fail — 7 findings, all inherited
witnesses (aggregate) fail (reads the floor lane)

The 7 CITED-DECLARATION-ABSENT findings are byte-identical to main's own most recent completed witnesses run (5ee9cab5f, run 34085447859) — identity-matched, not merely equal in count. This branch touches none of those files and contributes no new finding.

The cause, since it is not a typo

#10706 "OutsideModeledGuarantee stamps and stall carriers (follow-up)" merged at 02:12 today. main's witnesses lane has been red on every completed run since.

src/v2/lens/grounding.dag:21 states the intent plainly:

The name-only should-ground residual is off the ladder until a confirm-judge capability is authored — then this stamp reds. confirm_judge_should_ground is deliberately not a decl.

The stamp exists to name a capability that does not exist, and expects to red when it is authored. The citation wall reds because it is absent. The two mechanisms have opposite polarity, so the repair is a modeling decision — the carrier needs a way to express "this required capability is deliberately unauthored" without a dangling DeclarationRef — in src/v2/lens/* and construction_justification, which DESIGN names load-bearing.

Why I have not fixed it here

At least two open PRs are working the same carrier right now — #10704 ("Restore wet_route_model_lags_seed_stall so two 4b triggers have a referent", the same shape as this defect) and #10747, with #10725 and #10755 nearby. Making the citations resolve could mean the opposite of what the stamps intend, and that is the authoring lane's call. Landing a competing fix from here would collide with work already in flight.

This PR

Review floor is met: 1 distinct approval (review 61821), no REQUEST_CHANGES, no active reviews on this head. mergeable: MERGEABLE, mergeStateStatus: BLOCKED — and the block resolves entirely to the floor lane above.

Awaiting the operator's call on whether to wait for the neighbouring PRs, land over a red floor main already carries, or coordinate a fix. Not merging from here either way.

— sent from crisp-ram-671

@briansrls
briansrls merged commit 6c08a86 into main Sep 7, 2026
2 of 4 checks passed
@briansrls
briansrls deleted the session/crisp-ram-671-ntfy-deploy branch September 7, 2026 10:12
@briansrls
briansrls restored the session/crisp-ram-671-ntfy-deploy branch September 7, 2026 10:21
@gunbai-bot gunbai-bot Bot mentioned this pull request Sep 7, 2026
6 tasks
gunbai-bot Bot pushed a commit that referenced this pull request Sep 7, 2026
briansrls pushed a commit that referenced this pull request Sep 7, 2026
… costs (#10714)

* Model the ntfy notification principal and the human steps it actually costs

The approval loop's notification transport needs a place to run and a person to
enroll a phone in it. This lands both as typed facts rather than a runbook.

gunbc.fleet_posix_accounts gains FleetAccountNotificationService: no-login
service identity, persistent realization (the server owns a state directory
across restarts, so a supervisor-managed dynamic uid would not own yesterday's
files), and an EMPTY sudo grant set declared by construction. The emptiness is
the point: this principal runs the process that HANDLES approval links, and an
approval link is spendable, so a grant would let it act on what it delivers.

gunbc.auth.approval_ntfy_deployment carries the deployment's shape and, as
std.human_intervention rows, the six steps a person must take -- priced by
frequency rather than summed, so a one-time enrollment is not billed like
per-run work. Two of them are declared TEMPORARY with dissolution triggers:
the account and the unit are human steps only because no converged member
renders them for this principal yet. Declaring a principal in the fleet roster
is not creating an account, and the module says so instead of implying the
convergence already covers it.

The loopback bind is asserted, not commented. It is what makes tailscale serve
the sole route, and the sole route is what makes the injected tailnet identity
on the neighbouring approval route trustworthy; a 0.0.0.0 bind would let
anything on the host forge that header.

Witnesses (7, executing): the roster costs nothing per run, and the four
frequency counts control each other -- a counter ignoring its argument would
answer 6 to all four, one stuck at zero would fail the other three, so the
zero is load-bearing rather than vacuous. The break-glass-is-not-routine
property is left where it lives, in std.human_intervention, rather than
restated here over our roster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61608: delete the prose row, give the config values a consumer

Both findings verified and fixed.

F1 -- loopback_bind_is_the_boundary_note was an ordinary String row whose sole
purpose was commentary, which DESIGN §4c names as misplaced data, and it
duplicated the `//` block three lines above it. Deleted; the annotation carries
the rationale, and the security argument now also reaches the operator through
the install instruction, where it can act on it.

F2 -- the config declarations were dangling (§3c) AND their values were retyped
as English inside the instruction string, so the only spelling anyone acted on
was the prose one. Rather than delete them, the instruction is now DERIVED from
them by join: the operator reads values the model owns, and drift is a red
witness rather than an invisible disagreement. notification_service_realization_
standing gains the witness its sibling already had, naming its own principal so
a copy-paste of the publication helper's row goes red.

The review's diagnosis was sharper than deleting the rows would have been: the
declarations were not merely unused, they were a fork of facts the prose stated
independently -- and the fix is to join them, not to drop one side.

Witnesses 7 -> 12. Two of the new ones carry a positive control (the instruction
must NOT contain 0.0.0.0:2586, so string_contains is shown able to answer false).
One incidental catch: to_string over the branded Port had never been evaluated,
only resolved -- casting into a brand is exactly where this corpus fails at
evaluation while compiling clean. It passes, and now something runs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Fix floor discovery, address review 61619, and make NtfyPublishRefused reachable

CI cause: floor discovery enrolls `test fn`, and my witnesses were plain `fn`,
so the entry contributed zero and the floor refused with FloorDiscoveryRefused.
My local loop ran them by name through --functions, which never needed the
keyword -- the three-rung loop had a gap exactly where the floor looks.

Review 61619, both findings correct:

F1 -- inserting notification_service_realization_standing between the
publication helper's leading annotation and its declaration left an operator
ruling annotating the wrong symbol (§4c/§3). Moved below; each block leads its
own declaration again.

F2 -- NtfyTopic and approval_ntfy_topic were imported and unused while the
grant instruction described "the approvals topic" in English: the same fork the
module's own annotation claims to have fixed for the port and state root. The
topic is now joined into the instruction with two witnesses, and it matters more
than the endpoint did -- the topic IS the access-control surface on a default
ntfy deployment, so granting read on a different topic than the broker
publishes to leaves the real one at read-write, silently.

MEASURED, and the reason this is not just modeling: a wet probe of the modeled
publish path against ntfy.sh reached the network ([rest] POST) and a junk bearer
came back as `TypeError { msg: "HTTP 401: ..." }` -- an untyped throw where the
model promised a typed refusal. The `response { 401 => ... }` block was
decoration: extdeps.transports.rest raises on >=400 unless an operation opts in
to `outcome: RestOutcome`, and this operation never did, so NtfyPublishRefused
had no constructor on any real path. The field is declared and
ntfy_publish_outcome_of is the single route from transport observation to the
outcome type.

Per merry-ibex-741: the dissolution triggers now name CAPABILITIES rather than
contributing artifacts -- the account row retires only when convergence creates
AND verifies the principal by readback, and the server row states it is two
capabilities (verified binary AND converged unit), since a binary alone leaves
a host that looks provisioned and serves nothing. The acquisition facts they
asked for are recorded as an obligation rather than invented: no release tag is
chosen and no artifact hashed, so a version row would be a guess and a digest
row would read as verification that never happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61635, and deploy the server the model describes

Three findings, all the same fork class, all correct:

F1 -- the account intervention hand-authored the useradd argv beside the
declared login while claiming to be "the exact argv EnsureSystemAccount
renders". It now CALLS that renderer, so the hand-run command and the eventual
converged one are one authority by construction rather than by anyone
remembering to keep them equal.

F2 -- the phone-subscription row said "the approvals topic" in English while
the grant row beside it already joined the declaration. Joined, with the reason
stated: subscribing to any other topic receives nothing, silently.

F3 -- ntfy_publish_outcome_of was dangling. It now has six witnesses covering
every arm, including the two statuses the live server actually returns (401 for
an unauthorized token, 403 for a token that authenticates but is not permitted
on the topic) and a control separating RestOk from a 200 whose body did not
decode.

Fixing F3's witnesses surfaced a real defect in the fold: `status as Int` over
an already-Int HttpStatus is refused at EVALUATION ("cannot cast Int to Int")
while compiling clean. The two arms that passed were exactly the two with no
status cast.

THE SERVER IS DEPLOYED on srv1 and its security properties are measured, not
asserted:
- bound 127.0.0.1:2586 only (ss confirms), reachable via `tailscale serve` on
  :8443, tailnet-only, alongside the existing dashboard route rather than
  replacing it
- anonymous publish answers 403 -- on a default ntfy it would be 200
- the broker token publishes to gunbc-approvals (200), CANNOT read it (403),
  and CANNOT publish to any other topic (403)
- ntfy v2.28.0, sha256 verified against upstream's published checksums.txt
  before install

The service config no longer pins the literal "https://ntfy.sh", which had
silently made the public service the destination for anything that did not say
otherwise -- for a topic carrying approval capabilities, the worst available
default. The correct form is an operation parameter; the resolver refuses config
identifiers bound to parameters, MEASURED on both compile and run, so the
endpoint is a named row with that gap declared and a trigger naming the resolver
capability.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Publish through ntfy's JSON endpoint: the header form displayed raw JSON

MEASURED ON THE LIVE SERVER, and invisible to every static check. The operation
posted `{ message: body }` to /{topic}, but that path form takes the message as
a RAW body while this transport always converts `body:` to wire JSON -- so ntfy
stored the literal text `{"message":"..."}` as the notification. The publish
succeeded, the outcome folded to NtfyPublished, and the phone would have shown
JSON. Nothing was wrong except the only thing that mattered.

ntfy's documented JSON endpoint POSTs to "/" with topic and fields in the body,
which is exactly what a JSON-only transport can express faithfully. Cache stays
a header: it is a delivery directive, not message content.

Confirmed by reading the message back from the server:

  title   : gunbc: approval notifications are live
  message : This arrived through the modeled path on your self-hosted server.
  click   : https://srv1.tailecbe08.ts.net:8443/gunbc-approvals
  priority: 4

The §4c boundary caught the first attempt at the annotation, which sat inside
the service body; it now leads the declaration at module scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61651: move the transport out of extdeps, delete the count tests

F1 -- ntfy_deployment_base_url was a deployment fact in an upstream-spec module.
The review's prescription was a rostered rung drop or an operator-approved
scaffold; both admit the debt, and DESIGN §3 offers a way to not incur it:
"the transport (shell, REST, SDK) is a §2 Realization handler bound to that
shape, one of N, never a fact about the dependency."

So the SERVICE BLOCK ITSELF was in the wrong layer, not just its endpoint.
extdeps.ntfy now owns what ntfy IS -- message shape, priority and cache
vocabularies, outcome type, the two access-control facts -- and the REST binding
plus the address live in gunbc.auth.approval_ntfy_deployment, with the
deployment they belong to. There is precedent: five services already live under
dag/gunbc.

This removes the debt rather than rostering it, and it fixes the narrowing the
review identified: a second deployment is now another binding beside this one,
with extdeps.ntfy untouched.

F2 -- the count witnesses were measurements of a roster authored three files
away in the same PR, and witness_upstream_relay_is_declared compared a data row
to its own literal: `measure() == measure()`, §5's own review tell. Deleted.
What replaces them are properties that survive roster growth: every identity
occurs exactly once (with a positive control proving the predicate can fail --
it is also the check that catches a copy-paste, which no count can see), and
every row carries a non-empty instruction and subject.

Two literals are KEPT and justified rather than deleted: EveryProvision == 0 is
a design claim (the loop must cost a person nothing per run), not a measurement,
and the loopback address is an explicit policy requirement -- both grounded
outside the tree, which is what §5 permits.

MEASURED after the move: the modeled path still publishes to the live
self-hosted server, NtfyPublished { message_id: znjShbquiXow }.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61668: bind RestOutcome by import, derive the endpoint assertion

F1 -- the moved service declares `outcome: RestOutcome` and this module never
imported extdeps.transports.rest. It resolved only because extdeps.ntfy.ntfy,
which this module does import, pulls in that type for the outcome fold. That is
the accidental-coverage shape secret_manager already records: a bare reference
that holds because some other module in the closure dragged the definer in is
not a binding, and it breaks the moment the fold moves or that import changes.
The type is now imported where it is used. It is a floor issue, not a style one.

The defect was CREATED BY MY OWN SERVICE MOVE in the previous commit: the
service went to the realization layer and the fold that carried its import
stayed behind. Moving a declaration moves its obligations, and I checked that
the module compiled rather than that it named what it uses.

F2 -- `approval_ntfy_endpoint() == "127.0.0.1:2586"` transcribed the port out of
a data row three files away, so automating that literal collapses the check to
`measure() == measure()` for the port half. It now asserts the RELATION instead:
the endpoint must be built from the declared listen address, which
witness_server_binds_loopback_only separately holds to loopback as an explicit
policy requirement. Neither witness restates the other's literal, changing the
port freely is correct and reddens nothing, and widening the address still goes
red. A positive control pins that the prefix check can answer false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61680: join the address from the model, declare the frontier

F1 -- approval_ntfy_base_url was one literal re-minting two facts the corpus
owns: the tailnet domain at gunbc.fleet_intent_network `tailnet_domain` and
srv1's identity at `operator_host_srv1`. Moving the binding out of extdeps
fixed the LAYER and left the DUPLICATION, which is §3 nicknaming under a
different roof. It is now joined from both authorities, so renaming either
reddens a witness instead of leaving a stale string.

tailnet_domain is Optional and the service config's `endpoint` cannot carry an
Optional, so the Absent arm has to answer something. It answers a host in the
RFC 2606 `.invalid` TLD, which is guaranteed never to resolve -- a structural
failure rather than an absorbing fallback: it cannot succeed, cannot widen, and
cannot be mistaken for a working endpoint. Both arms are witnessed, with a
positive control that the Present path does NOT produce the marker.

The review also caught that the annotation CLAIMED "witnesses hold them to their
separate jobs" while no witness touched approval_ntfy_base_url at all. That is
the citation-wall failure in miniature -- the symbols resolved, the quantified
claim about them was never true. Four witnesses now make the claim true, and
they are identity joins against the network model rather than transcriptions.

VERIFIED AGAINST THE LIVE SERVER, because a derivation that changes the address
would break the deployment silently: the joined URL publishes to the running
self-hosted server, NtfyPublished { message_id: 8JLDpfuLw8D2 }, at exactly
https://srv1.tailecbe08.ts.net:8443/.

F2 -- the publish binding, the RestOutcome opt-in, ntfy_publish_outcome_of and
approval_ntfy_human_burden have no production caller. True, and §3c's middle
state applies: a declared frontier, admissible only with its trigger stated. The
module now states it at module scope rather than leaving a reviewer to grep for
it -- what is exercised (wet probe plus witnesses, execution but NOT production
consumption), the named consumer (the broker publishing for a pending
AccessRequest), its trigger (the minting path, and ApprovalDecideHandler
redeeming instead of answering 501), and the honest disposition: if that
consumer never lands, this binding is the thing to delete, because with no
requests it carries nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Honor fleet convergence on srv1: the host's state is now rendered by the model

Operator ruling: what I built by hand on srv1 must be converged. It was the §6
tell stated plainly -- out-of-band actuation -- and this makes the model the
author of that host's state instead of a runbook.

THE GAP WAS NEVER THE OPERATIONS. gunbc.executor_privileged_operation already
renders every step this deployment needs: EnsureSystemAccount,
EnsureOwnedDirectory, the three systemd verbs. What was missing was a
declaration of what the host SHOULD contain, so nothing could reconcile against
it. gunbc.auth.approval_ntfy_converge is that declaration -- desired members,
each projected to the operation that realizes it, keyed and Ensured rather than
Owned so a retract can never delete the message cache or the subscriber's read
position.

A REAL DEFECT FOUND BY RUNNING IT, not by reading. `useradd` exits 9 when the
account exists, so EnsureSystemAccount -- whose name promises otherwise -- fails
on every convergence after the first. It read as idempotent because its ONE
caller, live_deploy's publication-principal step, wrapped it in `id -u ... ||`:
the guarantee lived in the neighbour, so the operation looked covered while any
second caller inherited the failure. Measured on srv1: rc=9 for the account,
rc=0 for the other six.

The precondition now belongs to the operation, as an EXHAUSTIVE match with no
wildcard arm so a new operation must decide whether it needs one rather than
defaulting to "none" and inheriting the same silence. live_deploy now consumes
that authority instead of spelling the guard inline; its emitted bytes are
unchanged, which is not a claim -- all 59 live_deploy emit witnesses pass.

VERIFIED AGAINST THE HOST, twice: the 7 rendered commands applied to srv1 with
0 failures on the first pass and 0 on the second, with the service still active
and the endpoint answering 200. A convergence that fails on its second run is
not a convergence, so idempotence is the property that was actually tested.

WHAT THIS DOES NOT YET DO, stated because it decides whether the human steps may
retire: it declares the desired side and applies it idempotently, but does not
OBSERVE the host, so it cannot answer whether a member is already converged and
an installer exiting zero over an absent member would pass unnoticed. That
readback is std.realization_reconcile (merry-ibex-741); this family is shaped to
plug into it rather than grow a second answer for "did the plan converge". The
interventions therefore stand, with their triggers unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61738: unfabricate the unit member, the status, and my own claim

Three findings, all correct, and the first two catch overclaims I made in the
commit message rather than only in code.

F1 -- the account intervention still said "no converged member renders it for
this principal yet" AFTER the converge family landed, which is two answers to
one question inside a single PR. Worse, the intervention rendered through
executor_privileged_operation_shell_command while the member used
executor_privileged_operation_converge_command, so the annotation's claim that
they are "one authority by construction" was FALSE: the operator's copy-paste
lacked the precondition and would exit 9 on a host that already had the account.
Both now call the converge renderer, and a witness holds them EQUAL -- because a
prose claim is exactly what failed to catch this, twice.

F2 -- `NtfyUnitInstalled => [SystemdDaemonReload]` named an obligation its
projection did not discharge. It installs nothing; enable and start converged
only because a human had already placed the unit file, so on this host the
difference was invisible. That is §5's plausible-realization failure, and it
made my commit message's "srv1's state is now rendered by the model" false. The
member is renamed for what it does, the unused InstallFile import is gone, and
the module states the boundary plainly: this roster CANNOT converge a fresh
host. The config file, the unit file and the binary are absent from it, because
file CONTENT is unmodeled and InstallFile needs a source that must already
exist. Absent is honest; an adjacent member standing in for them is not.

F3 -- RestTransportRefused folded to NtfyPublishRefused { status: 0 }, which
reintroduced one layer up the exact fabrication extdeps.transports.rest exists
to prevent ("no response arrived, so no status can be fabricated"). It also
collapsed two states with different remedies: the server refused this publish,
versus no server was reached. NtfyPublishUnreached is now its own arm carrying a
cause and no status, with a witness each way so neither collapses into the
other.

The exhaustiveness wall found every consumer of the widened coproduct, which is
the floor doing its job on a change that would otherwise have been silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61755: split the undecodable arm, derive the paths, delete the pipeline

Four findings, all correct.

F1 -- RestBodyUndecodable folded into NtfyPublishRefused { status }, so a 200
whose body did not decode became "refused with status 200". That is the SAME
collapse this PR's own annotation condemns one arm above, written while I was
splitting the transport arm for exactly that reason. The server accepted and we
failed to read the reply; the remedies diverge completely. NtfyPublishUndecodable
is its own arm, keeping the status it really received, with a witness pinning
all three failure outcomes mutually distinct.

F2 -- the converge roster re-minted "/etc/gunbc-ntfy" and
"/var/lib/gunbc-ntfy/attachments" while importing the declarations that own both
roots, so a rename would have left it ensuring directories the server does not
use. The config DIRECTORY is now the authority and the file path derives from it
(a dirname would be a second computation over one fact), and the attachments dir
joins the state root.

F3 -- the precondition arms authored shell as string literals with the login
interpolated unquoted, a second authorship of this module's own calling
convention. The check is now a typed OperationPrecondition rendered as an argv
through the SAME quoting authority as the operation it guards. The group arm is
DELETED rather than modeled: `usermod -aG` appending a group the user already
has exits 0 (measured on srv1, twice), so its `id -nG | tr | grep -qx` pipeline
was a smuggled program solving a problem that does not exist. The residual
redirection and `||` join are named as residue with a dissolution trigger
pointing at the If-Not-ExitZero orchestration intent live_deploy already uses.

F4 -- ntfy_member_ownership returned a bare Ensured where the type is Ownership?,
and my witness greened only because the interpreter's raw-optional representation
lets Present { value: o } match any non-null. A green for the wrong reason, in a
witness I wrote to prove the property. Now Present { value: Ensured }.

Quoting the guard changed live_deploy's emitted bytes, so its witness -- which
pinned the unquoted spelling -- was updated to assert the guard's SHAPE rather
than its quoting, and two of my own witnesses needed the same correction. All 59
live_deploy emit witnesses pass.

RE-VERIFIED ON srv1 after the paths became derived and the quoting changed: the
7 rendered commands apply with 0 failures twice, service active, endpoint 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61791: delete three dangling declarations, correct a false claim

F1 -- ntfy_member_key_eq, ntfy_member_value_eq and approval_ntfy_member_argv had
no consumer anywhere in the tree. Written in anticipation of a
membership_reconcile call I never made, and carrying none of the declared-frontier
statement the sibling module writes for its publish binding. Deleted.

The corroboration in the finding is the part worth keeping: ntfy_member_value_eq
compared members by KEY ALONE, so a NtfyStateDirectory and a NtfyConfigDirectory
sharing a path compared EQUAL -- a wrong equality that would have silently
collapsed two members in the very reconcile it was written for. Nothing caught it
because nothing ran it. That is §3c's argument in one artifact: unconsumed code
is not merely idle, it is unexamined, and it accumulates defects that surface
only when someone finally wires it up. The module now records that rather than
just losing the functions quietly.

F2 -- live_deploy's annotation asserted "The emitted bytes are unchanged". That
was true when written and became FALSE two commits later when the precondition
started rendering through posix_single_quote, and it was contradicted by this
PR's own witness update in the same diff. §4c is explicit that an annotation is
never evidence a machine claim holds; this one stated a machine claim that was
wrong. It now says what actually happens -- the login arrives single-quoted,
equivalent shell, one fewer place deciding quoting -- and cites what IS verified:
all 59 live_deploy emit witnesses pass, not byte identity, because the bytes are
not identical.

That is the third annotation in this PR to claim something no executing check
supported. The first two were caught by review; this one was caught because a
reviewer read the diff against itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

* Address review 61808: delete a decoration I had cited as coverage

ntfy_member_ownership ignored its parameter and answered
Present { value: Ensured } for every input, so witness_no_member_is_owned was
permanently green BY CONSTRUCTION -- no member could be Owned, and its failing
arm was unreachable. §4b names this exactly: "the check is not a weak wall but a
decoration ... worse than absent because it will be cited as coverage."

It was cited as coverage. By me, in the annotation directly above it, for the
claim that a retract cannot destroy pending-request history. That claim rested
on a check that could not fail.

Both are deleted. The constraint itself is real and is stated as what it is: a
requirement on the reconcile that does not exist yet, to be held at rung 4 --
Owned unrepresentable for this family by construction, at which point no check is
needed. Declaring an Optional that is never Absent would be a third
representation of a decision nobody makes.

Also corrected, from the same finding's second half: the heading "THE KEY IS
CONSUMED" overstated what ntfy_member_key's consumers are. They are two
witnesses, not production. The module now carries a §3c frontier statement at
module scope covering the whole family -- named consumer (a member family in
fleet_converge_plan reconciling this desired set against an observation,
discharged through ConvergedEffect), its trigger, and what already executes: the
rendered commands converge srv1 idempotently, 7 commands and 0 failures across
two consecutive passes. The desired side is real; the observed side does not
exist, and the module says so once rather than implying coverage in three places.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBLsJSnmB8ZdDTh1RRVTe7

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 10, 2026
A second defect from #10630, currently masked: boot_image_export_ownership
declares fleet_account_key_label as a total serialization of
FleetPosixAccountKey — its own annotation says "exhaustive so that a new
account variant is a compile error here rather than a silent non-match" — but
matches only seven arms. The type has carried eight since #10714 added
FleetAccountNotificationService, and #10630 added this file with the seven it
knew.

It cannot surface on main today because parse refuses before exhaustiveness is
ever asked. It becomes reachable the moment the corpus parses again, so it is
fixed here rather than left to red main a second time: this is the designed
wall firing, not a new one.

The label is mechanical, not a judgement call — every arm is its variant
suffix in kebab-case, and the string's only consumer is
fleet_account_key_matches, which compares two labels for equality, so it needs
to be distinct and conventional and it is both.

Every other consumer of FleetPosixAccountKey was checked: fleet_ssh_access,
managed_access_bootstrap and posix_principal_allocation_witness_test carry no
total match over the key, and fleet_posix_accounts already covers all eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMGuMfzK6MWHJyybTNSxfy
briansrls pushed a commit that referenced this pull request Sep 10, 2026
…red since #10630) (#10932)

* Hoist megarac_media_attach annotations to module-item grain

#10630 landed dag/gunbc/machine_intake/megarac_media_attach.dag with three
annotation blocks sitting inside function bodies. Only module-item grain is
modeled (DESIGN §4c), so the parse phase refused with 17 errors, and the
namespace-wave-admission phase then reported "no head index" because no index
could be built over a corpus that does not parse. Main has been red on every
head since.

The three blocks carry real rationale, so they move above the declaration each
describes rather than being deleted: the allocated-session leak onto
megarac_attach_remote_image, the unreadable-presented-state arm onto
megarac_attach_with_token, and the StartRefused reservation onto
megarac_start_and_confirm — folded into that function's existing paragraph,
which already stated the pending-not-refused half.

No other .dag file in dag/, src/v1 or src/v2 carries a body-position annotation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMGuMfzK6MWHJyybTNSxfy

* Complete fleet_account_key_label; the eighth account variant had no arm

A second defect from #10630, currently masked: boot_image_export_ownership
declares fleet_account_key_label as a total serialization of
FleetPosixAccountKey — its own annotation says "exhaustive so that a new
account variant is a compile error here rather than a silent non-match" — but
matches only seven arms. The type has carried eight since #10714 added
FleetAccountNotificationService, and #10630 added this file with the seven it
knew.

It cannot surface on main today because parse refuses before exhaustiveness is
ever asked. It becomes reachable the moment the corpus parses again, so it is
fixed here rather than left to red main a second time: this is the designed
wall firing, not a new one.

The label is mechanical, not a judgement call — every arm is its variant
suffix in kebab-case, and the string's only consumer is
fleet_account_key_matches, which compares two labels for equality, so it needs
to be distinct and conventional and it is both.

Every other consumer of FleetPosixAccountKey was checked: fleet_ssh_access,
managed_access_bootstrap and posix_principal_allocation_witness_test carry no
total match over the key, and fleet_posix_accounts already covers all eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMGuMfzK6MWHJyybTNSxfy

* Record the second measured specimen of fail_closed_gate_refuses_its_own_repair

This PR is that class again, six days after the first specimen and with the
next-rung trigger still unfired: parse now reports 5391 files parse-clean, and
namespace-wave-admission refuses with NotEvaluated because the BASE revision
still carries the 17 diagnostics this PR removes. The refusal is correct and
the class is unchanged, so this appends a receipt rather than proposing a
mechanism.

It also records one fact the first specimen could not show: an aborted phase
bounds the error total behind it. Parse refused before exhaustiveness was ever
asked, so the non-exhaustive fleet_account_key_label match #10630 also
introduced was invisible for the entire outage and became reachable only once
the parse repair was applied. The class therefore conceals its own scope, which
is why a repair author cannot report main fixed on one phase going quiet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMGuMfzK6MWHJyybTNSxfy

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.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