// Package adapters is the per-vendor lifecycle a licence needs, selected by the licence's vendor. // // novox/hq ADR 0050: model access is one consumer-facing, vendor-blind provision, and *a vendor is // an adapter* keyed by `licence.vendor`. A consumer names `model-access` and never a vendor; which // vendor answers, and the lifecycle that vendor's credential needs, lives here — exactly as // `public-dns` is one neutral interface answered by registrar-scoped providers (ADR 0044). // // **Two shapes.** A `static-key` vendor implements almost nothing: the credential is an // operator-supplied value, sealed to each holder by the generic anonymous box (ADR 0024) and // delivered unchanged. A `refreshable-grant` vendor carries the bounded carve-out — a manager node // holds the refresh token encrypted at rest, access tokens are still sealed per holder, and the // refresh token is never in a holder's delivery. This package holds the generic half of both. The // vendor-specific half of a refresh — the actual OAuth call against a vendor's endpoint — is a // VendorRefresher plugged in from outside (novox/hq ADR 0050, Phase C); this build ships none, and // a refresh on a vendor with nothing plugged in is refused in as many words rather than pretended. // The abstraction earns its keep by making the common vendor small, not the rare one clever. package adapters import ( "context" "fmt" "sort" "strings" "sync" "github.com/novox/mesh-controller/internal/secrets" ) // Shape is how a vendor's credential behaves, and the switch the ADR 0050 carve-out turns on. type Shape string const ( // StaticKey is an operator-supplied value with nothing to rotate. The full "the mesh cannot // read what it stores" guarantee holds: accept seals it and discards the plaintext. StaticKey Shape = "static-key" // RefreshableGrant is an OAuth-style grant a manager node refreshes centrally (ADR 0050, // Phase B). It carries the one bounded relaxation of that guarantee. No adapter of this shape // ships in Phase A. RefreshableGrant Shape = "refreshable-grant" ) // Adapter is a vendor's lifecycle, selected by a licence's vendor. // // Only these verbs are required, and they are all a static-key vendor needs: what shape it is, how // a supplied value is sealed, and what a consumer is delivered. The optional capabilities below are // found by type assertion, so a static-key adapter simply does not implement them. type Adapter interface { // Vendor is the name a licence's `vendor` field carries to select this adapter. Vendor() string // Shape is static-key or refreshable-grant. Shape() Shape // Accept takes an operator-supplied credential and seals it to one holder — the `accept` verb // ADR 0024 defines. For a static key this is the generic seal, unchanged. Accept(value, consumerKey, providerKey string) (secrets.Sealed, error) // Deliver is the credential value a consumer receives. For a sealed static key it is the sealed // blob unchanged: the mesh never unseals it, only the holding node's private key can. A // refreshable-grant adapter (Phase B) strips the refresh token here. Deliver(sealed string) string } // RefreshInput is what producing a new access token needs, and all a refresh is given. // // It carries the refresh token **only as its sealed blob** — the caller (the control plane) never // holds the refresh token in the clear, because it cannot open the box. Opening it, and the vendor // call that follows, happen where the manager node's private key is (ADR 0050, Phase C). type RefreshInput struct { Licence string // Manager is the node that holds the refresh token readably — the one place the box opens. Manager string // Sealed is the refresh token as an anonymous sealed box to the manager's key. Opaque to the // control plane; openable only by the manager node's private half. Sealed string // ManagerKey is the manager's public sealing key the token was sealed to. ManagerKey string } // RefreshResult is what a refresh produced: a new access token to seal per holder, and — only if the // vendor rotated it — the refresh token re-sealed to the manager, ready to replace the stored blob. type RefreshResult struct { // AccessToken is the new access token, in the clear. The mesh seals it per holder and discards // it, exactly as it does an accepted key. It is never the refresh token. AccessToken string // NewSealed is the refresh token re-sealed to the manager node, present only when the vendor // rotated the refresh token too. Empty leaves the stored blob untouched. Already sealed, so the // control plane stores it without ever seeing the refresh token in the clear. NewSealed string // NewManagerKey is the key NewSealed was sealed to, carried with it. NewManagerKey string } // Refresher is implemented only by a refreshable-grant adapter (ADR 0050): the vendor-neutral half // of the lease / rotate / publish machinery. A static-key adapter does not implement it, and a // caller finds its absence by a type assertion — which is exactly what gates the carve-out to // refreshable-grant vendors. type Refresher interface { Refresh(ctx context.Context, in RefreshInput) (RefreshResult, error) } // VendorRefresher is the vendor-specific half, plugged in from outside (ADR 0050, Phase C): given a // refresh, it opens the at-rest envelope with the manager node's key, calls the vendor's endpoint, // and returns the new access token (and a re-sealed refresh token if the vendor rotated it). It is // the one component that reads a refresh token in the clear, and in production it runs where the // manager node's private key is. This build registers none; Anthropic's OAuth refresh is Phase C. type VendorRefresher interface { Refresh(ctx context.Context, in RefreshInput) (RefreshResult, error) } // refreshers is what has been plugged in, keyed by vendor. Empty in this build. var refreshers struct { sync.RWMutex byVendor map[string]VendorRefresher } // RegisterRefresher plugs a vendor's real refresh in (ADR 0050, Phase C), or a fake in a test. The // generic refreshable-grant adapter for that vendor then delegates to it. Registering nothing leaves // the seam empty, and a refresh on that vendor is refused with a message that names Phase C rather // than failing obscurely. func RegisterRefresher(vendor string, r VendorRefresher) { refreshers.Lock() defer refreshers.Unlock() if refreshers.byVendor == nil { refreshers.byVendor = map[string]VendorRefresher{} } refreshers.byVendor[vendor] = r } func refresherFor(vendor string) VendorRefresher { refreshers.RLock() defer refreshers.RUnlock() return refreshers.byVendor[vendor] } // Identifier is the mis-binding guard (ADR 0050, Phase B): a vendor whose credential carries an // account identity worth checking implements it. A static-key adapter does not. type Identifier interface { Identity(credential string) (string, error) } // UsageReader is a vendor's usage reading (ADR 0050 / ADR 0054, Phase B), mapped to the common // normalised grain. A static-key adapter does not implement it. type UsageReader interface { Usage(ctx context.Context, licence string) ([]UsageRow, error) } // UsageRow is the vendor-neutral usage grain ADR 0054 fixes: the metric is vendor-defined and no // common unit is forced. Defined here as the Phase-B seam; nothing in Phase A produces one. type UsageRow struct { Licence string Consumer string Period string Metric string Value float64 // Raw is the vendor's own response, kept so a reading can be re-derived if the normalisation is // later found wrong. Raw []byte } // registry maps a vendor name to the shape its adapter has. // // The single place a vendor is taught to the mesh. anthropic's real credential is a subscription // OAuth grant, so it is the first `refreshable-grant` vendor; `anthropic-api-key` is the same // company's plain API keys, the early second `static-key` case ADR 0050 names — it exercises the // whole path with the carve-out switched off. Static-key vendors are added here as one line each. // // TODO(Phase C, ADR 0050): register anthropic's real VendorRefresher — the OAuth refresh against the // vendor's endpoint — with RegisterRefresher. Until then a refresh on it is refused, naming Phase C. var registry = map[string]Shape{ "anthropic": RefreshableGrant, "anthropic-api-key": StaticKey, "openai": StaticKey, } // For returns the adapter for a vendor, refusing an unknown one clearly. // // The refusal names what this mesh does know, because a vendor typo and a vendor this build has no // adapter for are the same symptom to whoever hits it, and the list is the difference between fixing // the name and filing a bug. func For(vendor string) (Adapter, error) { shape, known := registry[vendor] if !known { return nil, fmt.Errorf( "this mesh has no adapter for the vendor %q; it knows: %s", vendor, strings.Join(vendors(), ", ")) } switch shape { case StaticKey: return staticKey{vendor: vendor}, nil case RefreshableGrant: // The generic refreshable-grant adapter, carrying whatever VendorRefresher has been plugged // in for this vendor — nil in this build, which a refresh reports rather than hides. return refreshableGrant{vendor: vendor, refresher: refresherFor(vendor)}, nil default: // A shape this build has no adapter for at all. Said plainly rather than answered with an // adapter that would silently mishandle it. return nil, fmt.Errorf( "the vendor %q has shape %q, which this build has no adapter for", vendor, shape) } } // vendors is every vendor this mesh has an adapter for, sorted — what a refusal lists. func vendors() []string { out := make([]string, 0, len(registry)) for v := range registry { out = append(out, v) } sort.Strings(out) return out } // staticKey is the adapter for a vendor whose credential is a single operator-supplied value. // // It holds no vendor-specific logic, and that is the point: accept is the generic seal, deliver is // the value, and refresh / identity / usage are not implemented at all. It is one type shared by // every static-key vendor rather than one per vendor, because the sealing is vendor-independent. type staticKey struct{ vendor string } func (s staticKey) Vendor() string { return s.vendor } func (s staticKey) Shape() Shape { return StaticKey } // Accept is the generic anonymous-box seal (ADR 0024): a static key needs nothing vendor-specific. func (s staticKey) Accept(value, consumerKey, providerKey string) (secrets.Sealed, error) { return secrets.Accept(value, consumerKey, providerKey) } // Deliver hands the consumer the credential value. For a sealed static key that is the sealed blob // unchanged — there is no vendor step between the store and the node, and the mesh never sees the // plaintext. func (s staticKey) Deliver(sealed string) string { return sealed } // refreshableGrant is the adapter for a vendor whose credential is an OAuth-style grant a manager // node refreshes centrally (novox/hq ADR 0050). It is vendor-neutral: the generic half — the shape // that gates the carve-out, the per-holder seal of an access token, delivery that carries only an // access token, and the dispatch of a refresh to a plugged-in VendorRefresher — lives here; the // vendor's actual OAuth call is the injected refresher. // // **What makes this the carve-out and not a second static key.** The refresh token never touches // this adapter. It lives in the licences context's own refresh_grant store, keyed by licence, sealed // to the manager node (secrets.Seal) and delivered to the manager holder alone. What Accept seals and // Deliver hands out to a CONSUMER is the ACCESS token, per holder, exactly as a static key's value is // — so "a consumer is never delivered the refresh token" is structural here: a consumer's row never // holds it, because the refresh token is a different holder's credential entirely. type refreshableGrant struct { vendor string refresher VendorRefresher } func (r refreshableGrant) Vendor() string { return r.vendor } func (r refreshableGrant) Shape() Shape { return RefreshableGrant } // Accept seals an access token to one holder — the generic anonymous-box seal, the same as a static // key. The refresh token is not accepted here: it is adopted onto the manager node and kept in the // at-rest store (ADR 0050, Phase C), never sealed per holder. func (r refreshableGrant) Accept(value, consumerKey, providerKey string) (secrets.Sealed, error) { return secrets.Accept(value, consumerKey, providerKey) } // Deliver hands the consumer its sealed ACCESS token, unchanged. There is no refresh token to strip // because a holder's row never held one — the stripping is in the shape of the store, not a step // here. func (r refreshableGrant) Deliver(sealed string) string { return sealed } // Refresh is the vendor-neutral half of the lease/rotate/publish machinery: it dispatches to the // vendor's plugged-in refresher and returns what that produced. The lease, the per-holder reseal and // the publish are the licences context's (ADR 0050, Phase B); the OAuth call is the vendor's // (Phase C). With nothing plugged in, it refuses in a way that names why. func (r refreshableGrant) Refresh(ctx context.Context, in RefreshInput) (RefreshResult, error) { if r.refresher == nil { return RefreshResult{}, fmt.Errorf( "the vendor %q is refreshable-grant but has no refresher plugged in, so its grant "+ "cannot be refreshed in this build (novox/hq ADR 0050, Phase C)", r.vendor) } return r.refresher.Refresh(ctx, in) }