Design filing from the icebox fleet (operator-directed), first of four cross-linked: commons carries the type, claustrum/insula/astrocyte adopt it. The design was consulted across the seats that own the adjacent schemas; the sharpest finding leads because it is the one that gets discovered in production if unaddressed.
The finding that leads: provider is two axes, and a shared type with one field ships a silent three-way ambiguity
astrocyte's shipped schema (PR #4 review range) already carries both, verbatim:
provider TEXT NOT NULL, -- CodexBar provider name (QTA's sweep unit, e.g. "claude")
api_provider TEXT, -- canonical models.dev slug (e.g. "anthropic"); NULL when the
-- source reports no counterpart — fall back to `provider`.
These are different namespaces: the capacity source's sweep unit vs the catalog slug the spend side joins on. A shared {provider, account_ref} with one field forces every consumer to guess which namespace it holds — claustrum would populate it from credential config (a third namespace: opencode provider ids), insula from its usage source, astrocyte keys on the CodexBar name. Three modules, one field, three meanings, every value a plausible provider string. Proposal: carry both and name them — a source-scoped provider (mandatory) plus catalog_provider: Option<String> (canonical models.dev slug, explicitly named, nullable with documented fallback — the kimi consumer sub is a live account with no catalog counterpart). Which one anchors the cross-module join key is a disposition we'd rather you own; astrocyte's api_provider precedent suggests the canonical slug where present.
The type (three layers, policy excluded)
// Account: the stable join key
AccountIdentity { provider, catalog_provider: Option<..>, account_ref, account_ref_origin }
// account_ref MUST derive from rotation-invariant material (account id/email hash),
// never from credential content — or rotating accounts change identity on every
// refresh and every history join dies. Live precedent: openai-auth's stable short ids.
account_ref_origin: Declared | Inferred { basis: String } | Unlabeled
// Provenance as a field, not a convention. Declared-by-custodian is a fact; inferred-from-
// response-shape is a hypothesis; astrocyte today smuggles this as an "unlabeled:<provider>"
// string prefix a consumer must parse. Inferred carries its basis so a later custodian can
// upgrade the row to Declared with an audit trail.
// CredentialSurface: 1..N per account — custody rules attach here, NOT to the account
CredentialSurface { kind: OauthRotating | OauthStatic | ApiKey | WebCookie,
purpose: Traffic | QuotaBilling | Both }
The surface split and the purpose field are both forced by live evidence: minimax is one account with api_key-for-traffic + cookie-for-quota; xai is one account with oauth-for-traffic + cookie-for-billing, and the surfaces are empirically non-substitutable (the OAuth token cannot reach the billing endpoints — insula#1). Same kinds, opposite roles; only a declared purpose disambiguates. Single-surface providers are the degenerate 1..1 case. kind decides custody hazard: api_key/web_cookie/oauth_static accounts are freely multi-importable (no rotation, no dual-custody collision); oauth_rotating is where the one-refresher rule binds (claustrum filing).
Excluded by name so nobody schema-tizes it: plugin policy state (sticky routing, account selection, drain order) is not a credential surface and stays outside the type.
Venue
Recommendation: a tiny cortexkit-account-identity crate — identity should be near-frozen while cortexkit-provider-usage moves with insula's wire (0.3.0→0.6.0 in a week); dependency direction wants provider-usage depending on identity, not custody-only consumers pulling usage vocabulary; and the seven-repo path-dep surface makes silent-drift blast radius a real input (check-wire-crate-versions.sh's own header). Survivable fallback: an identity module inside provider-usage with a hard no-imports-from-usage rule. Venue is reversible; the namespace finding is not — if only one thing survives review, make it the two-axis provider.
Consumers on our side, filed in parallel: claustrum (N-account store + custody), insula (per-account consumption contract), astrocyte (convention adoption — its schema already models everything here except the origin enum). Credit where due: the two-namespace finding, the origin enum, and the venue reasoning are the astrocyte seat's; the purpose field and stability rule came out of the opencode-side operator setup that has been running multi-account in production for weeks.
Cross-references (the full design round, filed together):
Design filing from the icebox fleet (operator-directed), first of four cross-linked: commons carries the type, claustrum/insula/astrocyte adopt it. The design was consulted across the seats that own the adjacent schemas; the sharpest finding leads because it is the one that gets discovered in production if unaddressed.
The finding that leads:
provideris two axes, and a shared type with one field ships a silent three-way ambiguityastrocyte's shipped schema (PR #4 review range) already carries both, verbatim:
These are different namespaces: the capacity source's sweep unit vs the catalog slug the spend side joins on. A shared
{provider, account_ref}with one field forces every consumer to guess which namespace it holds — claustrum would populate it from credential config (a third namespace: opencode provider ids), insula from its usage source, astrocyte keys on the CodexBar name. Three modules, one field, three meanings, every value a plausible provider string. Proposal: carry both and name them — a source-scopedprovider(mandatory) pluscatalog_provider: Option<String>(canonical models.dev slug, explicitly named, nullable with documented fallback — the kimi consumer sub is a live account with no catalog counterpart). Which one anchors the cross-module join key is a disposition we'd rather you own; astrocyte'sapi_providerprecedent suggests the canonical slug where present.The type (three layers, policy excluded)
The surface split and the
purposefield are both forced by live evidence: minimax is one account with api_key-for-traffic + cookie-for-quota; xai is one account with oauth-for-traffic + cookie-for-billing, and the surfaces are empirically non-substitutable (the OAuth token cannot reach the billing endpoints — insula#1). Same kinds, opposite roles; only a declared purpose disambiguates. Single-surface providers are the degenerate 1..1 case.kinddecides custody hazard: api_key/web_cookie/oauth_static accounts are freely multi-importable (no rotation, no dual-custody collision);oauth_rotatingis where the one-refresher rule binds (claustrum filing).Excluded by name so nobody schema-tizes it: plugin policy state (sticky routing, account selection, drain order) is not a credential surface and stays outside the type.
Venue
Recommendation: a tiny
cortexkit-account-identitycrate — identity should be near-frozen whilecortexkit-provider-usagemoves with insula's wire (0.3.0→0.6.0 in a week); dependency direction wants provider-usage depending on identity, not custody-only consumers pulling usage vocabulary; and the seven-repo path-dep surface makes silent-drift blast radius a real input (check-wire-crate-versions.sh's own header). Survivable fallback: anidentitymodule inside provider-usage with a hard no-imports-from-usage rule. Venue is reversible; the namespace finding is not — if only one thing survives review, make it the two-axis provider.Consumers on our side, filed in parallel: claustrum (N-account store + custody), insula (per-account consumption contract), astrocyte (convention adoption — its schema already models everything here except the origin enum). Credit where due: the two-namespace finding, the origin enum, and the venue reasoning are the astrocyte seat's; the purpose field and stability rule came out of the opencode-side operator setup that has been running multi-account in production for weeks.
Cross-references (the full design round, filed together):