You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 insidecompose_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.
The surfaces.compose_registry still composes nothing. Phases 2b–4 are the persist-native rewrite of the residual with live consumers — builds, verify manifests, revocation, integrity, transparency — dropping sqlx and tonic rather than porting them (12,153 + 14,400 LOC of registry-core's 21,689 touch one or the other), and Portal's UI arriving as client cards so its RPC surface stops existing rather than being re-hosted. Scoped in the PR's FSD/REGISTRY_SLICE_ROLE_GATE.md §6.
Tracking issue for PR #442 — the first two increments of folding
ciris-registry-coreinto 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
Convert registry-us / registry-eu into
ciris-canonical-2/-3first, 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 tofalse. 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()walkscapability_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 thetrust:acceptsrow and the walk returnsNone, the slice goes dark on its own, nothing special-cased it. The check lives insidecompose_registry, because an authority check belongs with the thing it authorises.cfg.slices.registrysurvives 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-charterdeclares[infra:attest, infra:serve, infra:store, infra:transport], andgenesis-grant:ciris-canonical-1-d7bdeu223kcarries all four. Registry's work is attestation-shaped, so it ridesinfra: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::detectruns before the Engine opens — it is a pre-corpus structural gate, which is whyDEFAULT_LENS_STORE_MIN_GIBis a baked constant rather than aconfig:*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_registrywas atodo!(), 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, includingmatches!(.., Ok(None))on that regression directly.Phase 2a — the portable trust root is broadcast (
b5beb15)GET /v1/trust-root/bundleserves theGenesisBundlepersist bakes — charter, roster, scopes, serve-node grants. Public, mounted without a loopback layer (unliketrust_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-keyrather 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-Optionfields 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 declaredsignature_mode: "HYBRID_REQUIRED"while carrying no signature at all, and assertedhardware_class: HSM_PRODunderself_attested: true. There was no working contract to preserve.The invariant: authority lives inside
bundleand nowhere else. The bundle is self-authenticating —verify_bundle_quorumre-derives authority from the reader's records, so a forged bundle carrying attacker "holders" proves nothing (CIRISPersist#377). Everything outsidebundleis unsigned convenience metadata. There is deliberately noresponse_signature: signing the wrapper would prove only that the relaying node said it — precisely what/v1/steward-keyproved, and precisely what was worthless — and it would invite consumers to check the envelope instead of the artifact. A test pins thatresponse_signature/signature_mode/hardware_classnever 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
compose_registrystill composes nothing. Phases 2b–4 are the persist-native rewrite of the residual with live consumers — builds, verify manifests, revocation, integrity, transparency — dropping sqlx and tonic rather than porting them (12,153 + 14,400 LOC of registry-core's 21,689 touch one or the other), and Portal's UI arriving as client cards so its RPC surface stops existing rather than being re-hosted. Scoped in the PR'sFSD/REGISTRY_SLICE_ROLE_GATE.md§6.provenance:build_manifest:*at confidence 1.0 and reads the directory instead, but nothing emits the Contributions yet — the producer and the CI-key bless ceremony (Serve /v1/accord/ci-key/{propose,cosign} — the CI-bless (approve CI runner) serve leg; verify 10.3.0 consumer half already bundled (CIRISVerify#185) #290) both shipped; the drain has not (CIRISRegistry#138 has the order).add-canonicalfrom the Trust Root card. Which still wants the quorum decision in 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 op is 1-of-3 today while the baked founding record is 2-of-3, and these two admissions double the canonical set.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