Skip to content

Registry fold, phase 1+2a (PR #442): the registry slice is conferred, not configured — and the portable trust root is broadcast #537

Description

@emooreatx

Tracking issue for PR #442 — the first two increments of folding ciris-registry-core into this repo (CIRISRegistry#62). The PR is green on all eight checks and rebased onto the edge-18.2 main; this records why it is shaped the way it is, so the decisions survive the PR body.

The ordering constraint the whole fold hangs on

No server converts to a canonical node until CIRISServer can serve registry capabilities under the granted role.

Convert registry-us / registry-eu into ciris-canonical-2 / -3 first, and you turn two working registries into two blessed nodes that cannot do registry work. So "can serve registry capabilities" has to be something the node evaluates, not something an operator asserts. That is what phase 1 makes true.

Phase 1 — the role gate (ffa3b5f)

Before: the slice was selected by cfg.slices.registry, a boolean defaulting to false. An operator setting a boolean is exactly the self-assertion the accord-scrub model exists to remove. A node is canonical because the trust root signed off; the authority slice it runs must be conferred the same way.

After: registry_slice_conferred() walks capability_roots_to_trusted_root(dir, me, me, INFRA_ATTEST_SCOPE). Both key ids are ours deliberately — the question is "do I hold this capability, from a root I myself accept?" — and the second half is the operator's un-trust lever: delete the trust:accepts row and the walk returns None, the slice goes dark on its own, nothing special-cased it. The check lives inside compose_registry, because an authority check belongs with the thing it authorises. cfg.slices.registry survives as an opt-out only: it can keep a blessed node from serving; it can never make an unblessed node serve.

No charter amendment was needed — the verbs already exist. This was the open question and the answer is favourable: the baked genesis-charter declares [infra:attest, infra:serve, infra:store, infra:transport], and genesis-grant:ciris-canonical-1-d7bdeu223k carries all four. Registry's work is attestation-shaped, so it rides infra:attest + infra:serve, which a canonical node holds the moment it is blessed. That matters because the scopes live in the signed bytes — amending the charter is an m-of-n re-scrub by the holder roster, not an edit.

The gate could not join Capabilities. Capabilities::detect runs before the Engine opens — it is a pre-corpus structural gate, which is why DEFAULT_LENS_STORE_MIN_GIB is a baked constant rather than a config:* object. A delegation-graph walk needs the corpus, so this gate is necessarily post-corpus, at slice-composition time.

The trap the PR is shaped around. compose_registry was a todo!(), unreachable only because the bool defaulted to false. Swapping the boolean for the grant check without first giving the function a non-panicking body would panic at boot on exactly the nodes that are blessed — canonical-1 holds all four verbs. A production outage dressed as a one-line change. So the body is honest instead: blessed nodes log that the surfaces are not composed yet; unblessed nodes log the withholding as the steady state it is; Ok(None) is not an error. Three tests pin it, including matches!(.., Ok(None)) on that regression directly.

Phase 2a — the portable trust root is broadcast (b5beb15)

GET /v1/trust-root/bundle serves the GenesisBundle persist bakes — charter, roster, scopes, serve-node grants. Public, mounted without a loopback layer (unlike trust_root_api's import/list/delete verbs, which are the operator's own act): a peer bootstrapping into the mesh has to be able to fetch the root and check it against its own roster. The bundle was scanned before exposure — public keys, signatures, an already-announced transport hint, YubiKey PIV certs; nothing withheld-worthy.

Why this retires /v1/steward-key rather than repairing it (CIRISRegistry#133). Registry published its own key as its own trust root, and the live state was worse than the issue said: three mutually incompatible schemas exist and no two agree — verify's actual HTTP client expects single-steward {classical{}, pqc{}, …} whose non-Option fields are absent from what registry served, so it failed to deserialize outright; the spec-conformant parser next to it was never reached by the HTTP path. The live response also declared signature_mode: "HYBRID_REQUIRED" while carrying no signature at all, and asserted hardware_class: HSM_PROD under self_attested: true. There was no working contract to preserve.

The invariant: authority lives inside bundle and nowhere else. The bundle is self-authenticating — verify_bundle_quorum re-derives authority from the reader's records, so a forged bundle carrying attacker "holders" proves nothing (CIRISPersist#377). Everything outside bundle is unsigned convenience metadata. There is deliberately no response_signature: signing the wrapper would prove only that the relaying node said it — precisely what /v1/steward-key proved, and precisely what was worthless — and it would invite consumers to check the envelope instead of the artifact. A test pins that response_signature / signature_mode / hardware_class never appear on the outer envelope. Registry v3.0.0 (CIRISRegistry#136) serves the identical schema at both paths, so the fold changes nothing a consumer sees.

What #442 does not do, and what comes next

Refs: #442, #25, #290, #441, CIRISRegistry#62, CIRISRegistry#133, CIRISRegistry#136, CIRISRegistry#138, CIRISPersist#377.

Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com

🤖 Generated with Claude Code

https://claude.ai/code/session_014iEaqmXHrbpy7P1rvXR6BS

Activity

  1. emooreatx commented on Sep 23, 2026

    @emooreatx
    ContributorAuthor

    Ruling to carry through the fold (Eric, 2026-09-22): infrastructure does not vote. Voting for infrastructure is done only by the accord; the canonical node's owner handles its configuration. Recording it here with the texts and the artifacts that currently say otherwise.

    The CC already says this. CC 3.4.7.1: node (infrastructure) = "steward + infra:* reach only, never agency." CC 3 (cc3 §"infrastructure must not have agency"): "Infrastructure is the right enforcer precisely because it has no agency of its own to exercise," and "Authority stays quorum-bound. A co-located steward role does NOT let a single node issue a federation-scope attestation." CC 3.2's ciris-canonical worked example lists founders as registry_steward_us/eu/apac — steward keys, human-rooted — not node keys. The infrastructure community is a roster/audience object for the service installs; its admission quorum is the accord's, not the nodes'. Config: CC 3.4.5 config:{scope} is self-or-owner — the node reports it, its owner sets it.

    The code already behaves this way. accord.rs::canonical_op_quorum_m resolves Structural ops through kill_switch_quorum_m → family_quorum_m, which reads humanity-accord's entrenched quorum:M/N. Operational is the 1 knob (#441). quorum.rs::canonical_community / verify_canonical_quorum — the node-founder quorum — have zero callers in src/. On the live canonical (read-only): federation_families = humanity-accord (A1/B1/C1, quorum:2/3); federation_communities = 0 rows.

    What contradicts the ruling and needs re-cutting:

    1. FSD/REGISTRY_FOLD_DERISK.md §1 — "entrenched 2-of-3 founder-quorum over the three ciris-canonical per-node keys. A node gains a vote, never a verdict." Nodes gain no vote. The trust-root swap is steward-key → accord quorum, with the ciris-canonical community as the conferred roster the accord admits into.
    2. src/quorum.rs — its stated purpose (a per-node founder quorum) and verify_canonical_quorum; either delete or re-document as verifying an accord signature set over the community record.
    3. add-canonical is 1-of-3 while the baked founding record is 2-of-3 — decide the Operational quorum before canonical-2/-3 are admitted #441 — the Operational/Structural quorum question is over the accord holders; a "canonical roster" that votes is not a thing.
    4. CC-native build manifests: drain the manifest Contribution, walk infra:attest delegation to the human, drop the steward key #25 §3 — the build-authority trust config hangs off the ciris-canonical community's stewards, not off node founders.

    What is still true and still missing: the ciris-canonical infrastructure community row itself (CC 3.2, Commons-tier per CC 4.4.3.2.1) — created by accord co-scrub, founders = the accord-conferred steward keys, members = the canonical installs with role member. It is the audience for manifests-as-blobs (CC 3.1.9.1 agent_files + holds_bytes) and the re-root target for consumers. Creating it does not give any node a vote.

  2. added
    state:needs-decisionBlocked on a maintainer/product/policy call — the question is in a comment.
    size:LMore than three days, or a new subsystem.
    enhancementNew feature or request
    on Sep 25, 2026
  3. emooreatx commented on Sep 25, 2026

    @emooreatx
    ContributorAuthor

    Status check against origin/main (046e1b3), 2026-09-25. Labelled state:needs-decision.

    PR #442 is still open (last updated 2026-09-04, based on the edge 18.2 main; main is now edge v31). None of it is on main: compose_registry is still todo!() (src/compose.rs:5161), and there is no registry_slice_conferred and no GET /v1/trust-root/bundle. The todo!() is still reachable if cfg.slices.registry is set true. That is the trap phase 1 was written to remove.

    The 2026-09-22 ruling also means src/quorum.rs and FSD/REGISTRY_FOLD_DERISK.md §1 need re-cutting.

    Question for the maintainer: should PR #442 be rebased onto v31 and landed with the ruling applied? Or should it be closed and phase 1 (the role gate and the non-panicking body) and phase 2a be re-cut fresh?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:trust-rootGenesis, accord, canonical blessing, haltenhancementNew feature or requestsize:LMore than three days, or a new subsystem.state:needs-decisionBlocked on a maintainer/product/policy call — the question is in a comment.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions