// 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). // // **Phase A ships only the `static-key` shape.** 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. The refreshable-grant machinery — central rotation, an // identity guard, a usage reading, refresh-token-stripped delivery — is Phase B, and its verbs are // named here as optional capabilities (Refresher, Identifier, UsageReader) so the seam exists // before the code does. The abstraction earns its keep by making the common vendor small, not the // rare one clever (ADR 0050). package adapters import ( "context" "fmt" "sort" "strings" "github.com/novox/mesh-control/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 } // Refresher is implemented only by a refreshable-grant adapter (ADR 0050, Phase B): the // lease / rotate / publish machinery a manager node runs. A static-key adapter does not implement // it, and a caller finds its absence by a type assertion. type Refresher interface { Refresh(ctx context.Context, licence string) error } // 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. For Phase A the only entry is anthropic as a // static-key vendor, which is what the lab bed exercises. // // TODO(Phase B, ADR 0050): anthropic's real credential is a subscription OAuth grant, so its entry // becomes RefreshableGrant and a refreshable-grant adapter is registered for it; the static-key // path for the same vendor's plain API keys moves to a separate `anthropic-api-key` vendor, the // early second static-key case ADR 0050 names. Static-key vendors are added here as one line each. var registry = map[string]Shape{ "anthropic": 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 default: // A vendor registered with a shape this build does not implement yet — the refreshable-grant // seam. Said plainly rather than answered with a static-key adapter that would silently // mishandle it. return nil, fmt.Errorf( "the vendor %q needs a %s adapter, which this build does not ship yet", 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 }