The Authority Spine
How ICN makes the powers of an assembled runtime provable: where authority came from, what bounds it, how it is withdrawn, and what the runtime will honestly say about itself.
Truth status. This note describes a pattern implemented for gateway session authority only (issues #2436, #2437). Every other domain named in §4 is analysis: the extension is proposed, not built. Nothing here asserts production operation, pilot readiness, or institutional adoption.
1. The problem this answers
A recurring failure shape across the codebase: a capability is implemented in a
crate, unit-tested against a miniature composition, and then not installed
in the assembled runtime — while documentation describes the library capability
as though it were a runtime guarantee. Verified instances at the time of writing
included RPC token revocation (machinery complete, constructed as None in the
daemon), the gateway session lifetime (token_expiry_hours parsed, tested, and
never applied), credit-policy enforcement on gateway-owned ledgers, ledger
inbound sync, invariant gates, and hybrid blob storage.
The common cause is not carelessness. It is that composition is not a first-class value: optional capabilities are exposed as setter seams, the composition root takes the minimal default at each seam, and nothing compares "built and tested" against "installed here".
2. The invariants
Four properties, enforced together, are what turn a credential into accountable authority rather than a bearer secret.
| Invariant | Statement | Where enforced |
|---|---|---|
| Attenuation | issued ⊆ issuer ∩ flow_allowed ∩ requested — every term a ceiling, never a grant |
session_authority::attenuate_scopes |
| Expiration | The configured lifetime bounds the gateway session credentials actually issued and accepted; client responses report that same lifetime, without hidden verification leeway. The RPC surface is not bounded — see below | TokenLifetimePolicy, AuthManager::with_token_ttl, SessionAuthority::verify (acceptance bound), auth/invite/session responses |
| Revocation | An issued credential can be individually withdrawn and is revalidated before each protected operation on every surface that accepted it | RevocationAuthority, HTTP middleware, WebSocket operations, RPC verification |
| Truth | The runtime reports which of the above it actually installed, and a profile that requires a guarantee refuses to serve without it | AuthorityCapabilities, AuthorityProfile::validate |
Two design rules make these hold under failure:
- Fail closed at the boundary, not per-caller. Issuance handlers and
jwt_authresolve the sameSessionAuthorityfromapp_data; the runtime no longer registers a separate bare issuer. A route cannot opt out by forgetting to check, and a misassembly cannot silently restore signature-only verification. - Unreadable state is not authorization. A revocation lookup that errors denies the request. "We could not determine whether this was revoked" must never resolve to "not revoked".
3. What a deployment profile means
A profile is a requirement, not a description. PortableEvaluator may run
volatile revocation because the deployment is disposable — and says so in its
capability report. Institutional requires durable revocation and refuses to
assemble without it, naming the capability, the reason, the refused fallback,
and the fix.
Which deployments actually get which profile today, since a profile
document that does not say this is exactly the kind of unbacked capability claim
this note argues against: the profile is inferred from whether a revocation
store was supplied, not declared by operator configuration. The supply is
conditional, not guaranteed — revocation_store is an Option on the gateway
handles (supervisor/lifecycle.rs) that init_gateway consumes with if let Some(..). A daemon startup that completes the component set opens
<store_path>/auth-revocation and supplies it, so a fully started daemon —
including the portable evaluator appliance — runs Institutional with durable
revocation.
The consequence to state plainly, because the inference is silent: Institutional
is not a property of being a daemon. Any path that does not reach that
assignment leaves the handle None and yields PortableEvaluator — embedded and
test callers that construct a GatewayServer without a store, and equally a
daemon run that never gets that far, such as one whose keystore does not unlock.
In that case a daemon downgrades its own authority guarantee without an
operator asking for it, and the only thing that says so is the capability report.
Making the profile an operator-declared configuration value — so that a
deployment which asked for Institutional fails instead of quietly becoming
disposable — is a follow-up.
Precisely what "refuses" means today: the gateway does not come up, and the daemon logs the error and continues running without a gateway. The supervisor waits for the gateway's initialization-and-bind acknowledgement and marks the gateway actor active only after that acknowledgement; failed startup is reported inactive. This is fail-closed — no request is ever served under an unmet guarantee — but it is not a process abort. Making an unmet authority profile fail the whole daemon is a deliberate follow-up decision. An institution that cannot make a withdrawal survive a restart does not have revocation, and the software should not claim otherwise on its behalf.
Current lifetime and continuing-authorization semantics
- The canonical unconfigured session lifetime is 24 hours across
AuthManager,TokenLifetimePolicy, embedded gateways, and daemonGatewayConfig. Explicit configuration replaces that value; authority assembly rejects a mismatch between the issuer and the reported policy. /auth/verify, invite join, and QR-session responses derive their reported lifetime from the installed authority. Verification uses the credential's exactexpboundary; the JWT library's default expiry leeway is disabled.- The configured lifetime bounds acceptance, not just issuance. Every
gateway mint already carries exactly the configured lifetime, but a co-issuer
holding the signing secret —
icnctl auth token --local-mintis the supported one — signs whatever expiry it chooses.SessionAuthority::verifytherefore refuses a credential whoseexp - iatexceeds the configured lifetime (loudly, naming the bound — not clamped, which would silently shorten a session the holder was told was longer), and independently refuses any credential whose expiry lies more than one configured lifetime from now, so a fabricated or forward-datediatcannot buy extra validity. The local mint takes--expiry-hours, validated through the sameTokenLifetimePolicythe gateway applies to its own configuration, for deployments configured shorter than the canonical default. - The acceptance bound covers the gateway surface only. The daemon derives
the RPC signing key from the same
gateway.jwt_secret(supervisor/init_rpc.rs), andRpcTokenClaimsdoes not reject unknown fields, so a gateway-issued credential is structurally verifiable on the RPC surface.icn-rpc'sverify_tokenchecks signature,exp, and revocation — it applies no configured-lifetime bound, and its own issuance lifetime is a hardcoded 24 hours that never readstoken_expiry_hours. Verified by direct test on this branch: on a deployment configured for one-hour sessions, a 24-hour co-issued credential is refused bySessionAuthority::verifyand accepted byRpcAuthManager::verify_token. Consequence an operator must know: loweringtoken_expiry_hoursshortens gateway sessions immediately but does not shorten already-issued credentials on the RPC surface; revocation, which is shared, remains the instrument that reaches both. Closing this requires bounding RPC acceptance and issuance together — bounding acceptance alone would refuse the RPC manager's own freshly-minted tokens — which is a separate change with its own credential-invalidation migration, tracked in #2445. - HTTP bearer routes revalidate on every request. WebSockets retain the credential, revalidate after asynchronous subscription setup, and revalidate before every protected event and every backfill operation/event. A revoked or expired socket is stopped before protected delivery. An idle socket may remain connected until its next protected operation; it retains no protected access during that idle period.
- Gateway and RPC revocation caches are positive-only. A miss consults the shared durable store, so a revocation written by either surface is visible to the other without restart. Store and cache errors deny verification.
- This means verification of a non-revoked credential performs one point read from the revocation store. No negative cache is installed: no demonstrated bottleneck currently justifies a stale-revocation window.
4. Extending the pattern (analysis — not implemented)
Authority is not only about people. A compute worker, a storage provider, a model endpoint, and a federation peer each hold powers over cooperative infrastructure, and each currently follows the same failure shape the session work just corrected. The same lifecycle applies:
advertised → discovered → attested → institutionally authorized
→ allocated → invoked → metered → receipt-producing
→ revocable → degraded → unavailable/withdrawn
The load-bearing distinction is between three things that are routinely conflated:
- Technical capacity — the machine can do it.
- Institutional legitimacy — someone with standing authorized it.
- Current availability — it works right now.
A capability report must never let (1) or (3) stand in for (2). Concretely:
- A machine may advertise compute without being authorized to execute protected workloads.
- A storage node may be reachable without being authorized to retain member evidence.
- A model may be callable without being approved for a given data class.
- A federation peer may authenticate a message without holding authority to alter institutional state. (This is a live gap: inbound institutional-state application is currently ambient — see #2441.)
- A ledger implementation may exist without the runtime enforcing the institution's credit policy — see #2438, the same "installed?" question in the economic domain.
For each such domain, the extension is the same three moves this work made: give the subsystem one explicit composition value; make the profile's required guarantees a startup invariant; and derive the capability report from the constructed object rather than a hand-maintained list.
5. What this pattern does NOT decide
It enforces technical ceilings on authority. It does not decide who is legitimate. Which DIDs may approve a session, which scopes a role carries, how long an institution wants credentials to live, who may revoke whose authority, and what recourse a member has — these are institutional questions that belong to charter and governance policy above this layer, and several of them are unanswered today:
| Question | Status |
|---|---|
| Who may approve a session on another member's behalf? | Open — currently any credential holder for the cooperative, attenuated to their own scopes |
| Who may revoke, and on whose authority? | Open — the revocation mechanism exists; the institutional act does not |
| What evidence should a revocation leave? | Open — no revocation receipt yet; belongs with the ADR-0026 ladder |
| How is authority recovered when an administrator disappears? | Open — operator succession is unaddressed |
| How does a member contest a revocation? | Open — challenge paths are weaker than institutional action paths |
Recording these as open is deliberate. A system whose authority cannot be revoked, contested, or understood by its members does not meet ICN's purpose, and naming the gap is more useful than a mechanism that implies it is closed.
6. References
icn/crates/icn-gateway/src/session_authority.rs— the composition boundaryicn/crates/icn-gateway/tests/session_authority_enforcement.rs— enforcement proof through the real middleware- Issues #2436 (attenuation), #2437 (revocation + lifetime), #2421 (production route assembly not test-constructible — the remaining gap between "this boundary is tested" and "every mounted route is tested")