Skip to content

Fold phase 1: the registry slice is conferred, not configured - #442

Open
emooreatx wants to merge 2 commits into
mainfrom
fold/registry-slice-role-gate
Open

Fold phase 1: the registry slice is conferred, not configured#442
emooreatx wants to merge 2 commits into
mainfrom
fold/registry-slice-role-gate

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Phase 1 of the CIRISRegistry fold — the role gate. Composes nothing yet, and says so.

Full scope in the new FSD/REGISTRY_SLICE_ROLE_GATE.md.

The ordering constraint

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

Convert first and you turn three working registries into three blessed nodes that cannot do registry work. This gate is what makes "can serve registry capabilities" something the node evaluates rather than something the operator asserts.

Why the boolean was the wrong shape

The slice is selected today by cfg.slices.registry. 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, and the same should be true of the authority slice it runs.

The good news: no charter amendment

This was the open question. 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 — verbs a canonical node holds the moment it's blessed.

That matters because the scopes live in the signed bytes, so amending the charter is an m-of-n re-scrub by the holder roster, not an edit. This needs none.

What lands

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?". 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, and nothing special-cased it.

The check lives inside compose_registry rather than at the call site, because an authority check belongs with the thing it authorises. cfg.slices.registry survives as an operator opt-out only: it can keep a blessed node from serving, and can never make an unblessed node serve.

It cannot join Capabilities, which is evaluated before the Engine is open — a pre-corpus structural gate, which is exactly 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.

The trap this PR is shaped around

compose_registry was todo!(), unreachable only because the config bool defaults 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 charter verbs. That is a production outage dressed as a one-line change.

So the body is honest instead. Conferred nodes log that they're blessed and that the surfaces aren't composed yet; unblessed nodes log the withholding as the ordinary steady state it is. Ok(None) is not an error — a node that was never blessed must still finish booting and serve everything else.

Tests

Three, pinning that regression directly:

  • an unblessed node resolves to Ok(None) — a steady state, not a fault
  • withholding is not a boot failure (matches!(.., Ok(None)))
  • asking about a stranger's key confers nothing

379 lib tests green (376 pre-existing + 3).

Not in this PR

Phases 2–4, scoped in §6 of the FSD: the persist-native rewrite of builds / verify / revocation / integrity / transparency; Portal's UI arriving as client cards rather than a re-hosted RPC surface; and the public GenesisBundle broadcast that retires /v1/steward-key.

On that last one — steward-key retires rather than being carried because there is no working contract to preserve. 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 serves, so it fails to deserialize outright. The live response also declares signature_mode: "HYBRID_REQUIRED" while carrying no signature field at all. The GenesisBundle is self-authenticating — it carries its own hybrid authorizations from two accord holders over the charter — so it satisfies CIRISRegistry#133 by construction rather than by patch.

Upstream

CIRISRegistry#76 is done (CIRISRegistry#136, CI green): registry-core now resolves on this repo's exact triple — persist v32.3.0 / edge v17.4.1 / verify v13.3.1 — which is what makes it composable here at all. It absorbed 31 majors of persist drift with zero library changes; all six edits were test code.

Refs CIRISRegistry#62, CIRISRegistry#76, CIRISRegistry#133, #441

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Phase 1 of the registry fold — the role gate. Composes nothing yet, and says so.

## Why

The ordering constraint on the fold is that no server converts to a canonical
node until this repo can serve registry capabilities UNDER THE GRANTED ROLE.
Convert first and you turn three working registries into three blessed nodes
that cannot do registry work.

Today the slice is selected by `cfg.slices.registry` — a boolean. 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, and the same must
be true of the authority slice it runs.

## The verbs already exist — no charter amendment

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` — verbs a canonical node holds the moment it is blessed.

That matters because the scopes live in the signed bytes, so amending the
charter is an m-of-n re-scrub by the holder roster, not an edit. This change
needs none.

## What lands

`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 rather than at the call site, because an
authority check belongs with the thing it authorises. `cfg.slices.registry`
survives as an operator opt-out only: it can keep a blessed node from serving,
and can never make an unblessed node serve.

It cannot join `Capabilities`, which is evaluated BEFORE the Engine is open (a
pre-corpus structural gate — cf. DEFAULT_LENS_STORE_MIN_GIB, a baked constant
precisely because there is no corpus to read config from yet). A delegation-graph
walk needs the corpus, so this gate is necessarily post-corpus.

## The trap this change is shaped around

`compose_registry` was `todo!()`, unreachable only because the config bool
defaults 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 charter verbs.

So the body is honest instead: conferred nodes log that they are blessed and
that the surfaces are not composed yet; unblessed nodes log the withholding as
the ordinary steady state it is. Ok(None) is not an error — a node that was
never blessed must still finish booting and serve everything else.

Three tests pin it, including that regression directly: an unblessed node
resolves to Ok(None), withholding is not a boot failure, and asking about a
stranger's key confers nothing.

## Not in this change

The surfaces (phases 2-4): the persist-native rewrite of builds / verify /
revocation / integrity / transparency, Portal's UI arriving as client cards, and
the public GenesisBundle broadcast that retires /v1/steward-key. Scoped in
FSD/REGISTRY_SLICE_ROLE_GATE.md §6, including why steward-key retires rather
than being carried (three mutually incompatible schemas, none of which verify's
actual HTTP client can parse).

Upstream: CIRISRegistry#76 is DONE — registry-core now resolves on this repo's
exact triple (persist v32.3.0 / edge v17.4.1 / verify v13.3.1), which is what
made it composable here at all.

Refs CIRISRegistry#62, CIRISRegistry#76, CIRISRegistry#133, CIRISServer#441
Phase 2a. The federation-facing read of the GenesisBundle persist bakes: the
humanity-accord charter, its A1/B1/C1 holder roster, the infra:* scopes that
charter confers, and the serve-node grants issued under it.

## What this retires

CIRISRegistry published its own key as its own trust root at GET
/v1/steward-key. That endpoint is retired rather than repaired
(CIRISRegistry#133), and the reasons shape this module:

  - it carried NO signature at all while declaring
    `signature_mode: "HYBRID_REQUIRED"` — trust-root material arriving
    unauthenticated on the wire;
  - it asserted `hardware_class: HSM_PROD` under `self_attested: true`, a
    producer claim rather than evidence (CC 4.2.2.1);
  - and three mutually incompatible schemas for it exist across the fleet, no
    two of which agree. Verify's actual HTTP client expects single-steward
    `{classical{}, pqc{}, …}` whose non-Option fields are absent from what
    registry serves, so it fails to deserialize outright. Nothing downstream
    was successfully consuming it.

The replacement is not "the same thing, signed" — it is a different SHAPE of
claim. A node no longer publishes a root it asserts; it serves the root it was
conferred by, and that artifact carries its own proof.

## The invariant: authority lives inside `bundle`, nowhere else

The bundle is self-authenticating. Its `authorizations` are hybrid Ed25519 +
ML-DSA-65 signatures from accord holders over the charter, and
verify_bundle_quorum re-derives authority from the READER's own records rather
than from anything the bundle says about itself — a forged bundle carrying
attacker "holders" proves nothing (CIRISPersist#377).

Everything outside `bundle` — bundle_fingerprint, charter_root_key_id,
served_by — is unsigned convenience metadata: this node's unverified claim
about itself and about bytes it is relaying.

So there is deliberately NO response_signature. Signing the wrapper would prove
only that the relaying node said it, which is exactly what /v1/steward-key
proved and exactly what was worthless — and it would invite consumers to check
the envelope instead of the artifact. The field is named `served_by` rather
than anything that reads like an attestation, for the same reason. A test pins
this: response_signature / signature_mode / hardware_class must never appear on
the outer envelope.

## Public by design

Unlike trust_root_api's import/list/delete verbs — loopback-gated, because
choosing a node's trust root is the operator's own act — this is a federation
read and is mounted WITHOUT a loopback layer. A peer bootstrapping into the mesh
has to be able to fetch the root and check it against its own roster.

Everything in the bundle is public material: public keys, signatures, an
already-announced transport hint, and YubiKey PIV attestation certificates.
Scanned before exposing it; no private material, no seeds, no key material.

## Tests

Three, on the properties rather than the status code: the broadcast serves the
baked bundle through the same accessor everything else uses (not a second path
that could drift); the charter still confers infra:attest + infra:serve, pinned
HERE as well as at the consumer, because if it ever stops the registry slice's
role gate silently becomes unsatisfiable; and the outer envelope claims no
authority.

382 lib tests green. fmt + clippy -D warnings clean.

Refs CIRISRegistry#133, CIRISRegistry#62, CIRISServer#442
@emooreatx
emooreatx force-pushed the fold/registry-slice-role-gate branch from 7747164 to b5beb15 Compare August 19, 2026 23:28
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