Skip to content

Shared account-identity type: multi-account addressing for every credential kind — and the provider field is two namespaces wearing one name #13

Description

@iceteaSA

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):

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions