|
| 1 | +// Package offerkind is the single source of truth for what a ServiceOffer / |
| 2 | +// ServiceRequest "type" means: how it renders (storefront copy, bazaar and |
| 3 | +// OpenAPI discovery shapes), which price slot it uses, and — crucially — |
| 4 | +// which integrity checks apply to it. |
| 5 | +// |
| 6 | +// It replaces the type-collapse logic that was previously duplicated across |
| 7 | +// packages: internal/x402 (normalizeOfferType) and |
| 8 | +// internal/serviceoffercontroller (fallbackOfferType, openAPIPathsForOffer, |
| 9 | +// offerPriceRawAndUnit). Each of those re-implemented "given a spec.type, |
| 10 | +// what shape is this" with subtly different defaults. Centralizing here means |
| 11 | +// adding a 7th service type is a single table entry instead of an 8-file sweep. |
| 12 | +// |
| 13 | +// Design: this is a ZERO-DEPENDENCY leaf package (stdlib only). Call sites pass |
| 14 | +// the raw spec.type string (offer.Spec.Type), never a CRD struct, so both x402 |
| 15 | +// and the controller can import it with no risk of an import cycle. The data |
| 16 | +// table mirrors how internal/bounty/registry.go makes task types data-driven. |
| 17 | +// |
| 18 | +// Integrity is kept strictly separate from pricing/upstream/rendering: the |
| 19 | +// IntegrityProfile declares only authenticity/identity/scope obligations, not |
| 20 | +// "is the price valid" (that is a pricing concern, not an integrity one). |
| 21 | +package offerkind |
| 22 | + |
| 23 | +// PaymentClass is the payment-integrity obligation for a type. Payment |
| 24 | +// verification itself is uniform x402 "exact"; the only real axis is whether a |
| 25 | +// payment proof is required at all (it always is, today) — method (card vs |
| 26 | +// crypto) is handled separately by the verifier, not here. |
| 27 | +type PaymentClass string |
| 28 | + |
| 29 | +const ( |
| 30 | + PaymentX402Exact PaymentClass = "x402-exact" |
| 31 | + PaymentNone PaymentClass = "none" |
| 32 | +) |
| 33 | + |
| 34 | +// ContentClass is the data-authenticity obligation: how a buyer proves the |
| 35 | +// bytes it received are the bytes the seller committed to. |
| 36 | +type ContentClass string |
| 37 | + |
| 38 | +const ( |
| 39 | + ContentNone ContentClass = "none" |
| 40 | + ContentSignedVersionLog ContentClass = "signed-version-log" // dataset / fine-tuning: owner-signed secp256k1 hash-chain (internal/dataset) |
| 41 | + ContentBundleSHA256 ContentClass = "bundle-sha256" // skill: controller-validated bundle hash+size |
| 42 | +) |
| 43 | + |
| 44 | +// IdentityClass is the caller-identity / membership obligation. |
| 45 | +type IdentityClass string |
| 46 | + |
| 47 | +const ( |
| 48 | + IdentityNone IdentityClass = "none" |
| 49 | + IdentityGroupAuth IdentityClass = "groupauth" // membership-gated via internal/research/groupauth |
| 50 | +) |
| 51 | + |
| 52 | +// ScopeClass is the entitlement-scope obligation layered on top of membership. |
| 53 | +type ScopeClass string |
| 54 | + |
| 55 | +const ( |
| 56 | + ScopeNone ScopeClass = "none" |
| 57 | + ScopeVersionEntitlement ScopeClass = "version-entitlement" // dataset: token entitled only up to a paid version |
| 58 | +) |
| 59 | + |
| 60 | +// IntegrityProfile declares the integrity checks a service type requires. |
| 61 | +// Consumed by the verifier and controller (to enforce) and by the buy-side |
| 62 | +// (to know what to verify before trusting a response). |
| 63 | +type IntegrityProfile struct { |
| 64 | + Payment PaymentClass |
| 65 | + Content ContentClass |
| 66 | + Identity IdentityClass |
| 67 | + Scope ScopeClass |
| 68 | +} |
| 69 | + |
| 70 | +// Kind is the resolved capability + integrity descriptor for one spec.type. |
| 71 | +type Kind struct { |
| 72 | + // Type is the canonical spec.type string this entry represents ("" for the |
| 73 | + // unset default). Resolve(unknown) returns the http Kind, so its Type is |
| 74 | + // "http", not the unknown input. |
| 75 | + Type string |
| 76 | + |
| 77 | + // PaymentCopy collapses the type into the three storefront-copy branches |
| 78 | + // ("inference" | "agent" | "http"). Replaces x402.normalizeOfferType. |
| 79 | + PaymentCopy string |
| 80 | + // BazaarShape is the x402 bazaar discovery shape ("chat" | "generic"). |
| 81 | + BazaarShape string |
| 82 | + // OpenAPIShape is the controller's OpenAPI path shape |
| 83 | + // ("chat" | "multipart" | "generic"). |
| 84 | + OpenAPIShape string |
| 85 | + // CatalogType is the display/catalog label (fallbackOfferType): the type |
| 86 | + // string, or "http" when unset. |
| 87 | + CatalogType string |
| 88 | + |
| 89 | + // PriceUnits lists the price slot(s) a type conventionally uses, in |
| 90 | + // precedence order. Informational/validation; the live price reader still |
| 91 | + // keys off whichever Price.* field is populated. |
| 92 | + PriceUnits []string |
| 93 | + |
| 94 | + // SemanticInference mirrors monetizeapi.(*ServiceOffer).IsInference(): true |
| 95 | + // for "" and "inference". Drives model-reconciliation gating and the |
| 96 | + // OpenAPI empty-type edge (IsInference("")==true → chat shape). |
| 97 | + SemanticInference bool |
| 98 | + // ResolvesAgentRef: upstream comes from an Agent CR status, not spec. |
| 99 | + ResolvesAgentRef bool |
| 100 | + // RendersBundle: controller renders a skill bundle server. |
| 101 | + RendersBundle bool |
| 102 | + // OneShotPurchase: price is a total (e.g. perMB × size), not a rate. |
| 103 | + OneShotPurchase bool |
| 104 | + |
| 105 | + Integrity IntegrityProfile |
| 106 | +} |
| 107 | + |
| 108 | +// paymentOnly is the integrity profile for inference/http/agent: an x402 |
| 109 | +// payment proof, nothing else. |
| 110 | +var paymentOnly = IntegrityProfile{ |
| 111 | + Payment: PaymentX402Exact, |
| 112 | + Content: ContentNone, |
| 113 | + Identity: IdentityNone, |
| 114 | + Scope: ScopeNone, |
| 115 | +} |
| 116 | + |
| 117 | +// kinds is the table. Keys are spec.type strings; "" is the unset default and |
| 118 | +// is deliberately distinct from "inference" because the legacy code treats the |
| 119 | +// empty type inconsistently — http-presentational (normalizeOfferType, |
| 120 | +// fallbackOfferType) yet inference-semantic (IsInference, openAPIPathsForOffer). |
| 121 | +// Encoding both faithfully keeps this refactor behavior-preserving. |
| 122 | +var kinds = map[string]Kind{ |
| 123 | + "": { |
| 124 | + Type: "", PaymentCopy: "http", BazaarShape: "generic", OpenAPIShape: "chat", |
| 125 | + CatalogType: "http", PriceUnits: []string{"perRequest", "perMTok"}, |
| 126 | + SemanticInference: true, Integrity: paymentOnly, |
| 127 | + }, |
| 128 | + "inference": { |
| 129 | + Type: "inference", PaymentCopy: "inference", BazaarShape: "chat", OpenAPIShape: "chat", |
| 130 | + CatalogType: "inference", PriceUnits: []string{"perRequest", "perMTok"}, |
| 131 | + SemanticInference: true, Integrity: paymentOnly, |
| 132 | + }, |
| 133 | + "http": { |
| 134 | + Type: "http", PaymentCopy: "http", BazaarShape: "generic", OpenAPIShape: "generic", |
| 135 | + CatalogType: "http", PriceUnits: []string{"perRequest"}, |
| 136 | + Integrity: paymentOnly, |
| 137 | + }, |
| 138 | + "agent": { |
| 139 | + Type: "agent", PaymentCopy: "agent", BazaarShape: "chat", OpenAPIShape: "chat", |
| 140 | + CatalogType: "agent", PriceUnits: []string{"perRequest", "perMTok"}, |
| 141 | + ResolvesAgentRef: true, Integrity: paymentOnly, |
| 142 | + }, |
| 143 | + "dataset": { |
| 144 | + Type: "dataset", PaymentCopy: "http", BazaarShape: "generic", OpenAPIShape: "generic", |
| 145 | + CatalogType: "dataset", PriceUnits: []string{"perMB"}, |
| 146 | + OneShotPurchase: true, |
| 147 | + Integrity: IntegrityProfile{ |
| 148 | + Payment: PaymentX402Exact, |
| 149 | + Content: ContentSignedVersionLog, |
| 150 | + Identity: IdentityGroupAuth, |
| 151 | + Scope: ScopeVersionEntitlement, |
| 152 | + }, |
| 153 | + }, |
| 154 | + "fine-tuning": { |
| 155 | + Type: "fine-tuning", PaymentCopy: "http", BazaarShape: "generic", OpenAPIShape: "multipart", |
| 156 | + CatalogType: "fine-tuning", PriceUnits: []string{"perHour"}, |
| 157 | + Integrity: IntegrityProfile{ |
| 158 | + Payment: PaymentX402Exact, |
| 159 | + Content: ContentSignedVersionLog, // reuses the dataset signed-log primitives |
| 160 | + }, |
| 161 | + }, |
| 162 | + "skill": { |
| 163 | + Type: "skill", PaymentCopy: "http", BazaarShape: "generic", OpenAPIShape: "generic", |
| 164 | + CatalogType: "skill", PriceUnits: []string{"perRequest"}, |
| 165 | + RendersBundle: true, |
| 166 | + Integrity: IntegrityProfile{ |
| 167 | + Payment: PaymentX402Exact, |
| 168 | + Content: ContentBundleSHA256, |
| 169 | + }, |
| 170 | + }, |
| 171 | +} |
| 172 | + |
| 173 | +// Resolve returns the Kind for a spec.type string. An unrecognized non-empty |
| 174 | +// type falls back to the http Kind (payment-only, generic shapes) — matching |
| 175 | +// the legacy normalizeOfferType / openAPIPathsForOffer defaults for unknown |
| 176 | +// types. The empty string resolves to its own dedicated entry. |
| 177 | +func Resolve(t string) Kind { |
| 178 | + if k, ok := kinds[t]; ok { |
| 179 | + return k |
| 180 | + } |
| 181 | + return kinds["http"] |
| 182 | +} |
| 183 | + |
| 184 | +// Types returns the canonical service-type strings the registry knows |
| 185 | +// (excluding the "" default), for drift checks against the CRD enum. |
| 186 | +func Types() []string { |
| 187 | + out := make([]string, 0, len(kinds)) |
| 188 | + for k := range kinds { |
| 189 | + if k == "" { |
| 190 | + continue |
| 191 | + } |
| 192 | + out = append(out, k) |
| 193 | + } |
| 194 | + return out |
| 195 | +} |
0 commit comments