From 45dbe81d57cae8bbe8f16dc31fbce13cae5639b5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 19 May 2026 02:40:21 +0200 Subject: [PATCH] docs: align v2 bootstrap api contract --- Web/Controller/Api/V2/OpenApi.hs | 309 +++++++++++++++++- .../functional/interaction-reporting-v1.md | 16 +- docs/new-hub-quickstart.md | 183 +++++++---- .../IHUB-WP-0019-vsm-hub-bootstrap-api.md | 15 +- 4 files changed, 453 insertions(+), 70 deletions(-) diff --git a/Web/Controller/Api/V2/OpenApi.hs b/Web/Controller/Api/V2/OpenApi.hs index 99aa6fa..2d14841 100644 --- a/Web/Controller/Api/V2/OpenApi.hs +++ b/Web/Controller/Api/V2/OpenApi.hs @@ -10,6 +10,7 @@ import IHP.Prelude import IHP.ControllerPrelude import Data.Aeson (object, (.=), Array, toJSON) import qualified Data.Aeson as A +import qualified Data.Aeson.Key as K import qualified Data.Vector as V import qualified Data.Text as T import qualified Data.Text.Encoding as TE @@ -94,11 +95,19 @@ buildOpenApiSpec = do , "Hub" .= hubSchema , "CreateHubRequest" .= createHubRequestSchema , "HubCapabilityManifest" .= manifestSchema + , "CreateHubCapabilityManifestRequest" .= createManifestRequestSchema + , "UpdateHubCapabilityManifestRequest" .= updateManifestRequestSchema , "ApiConsumer" .= apiConsumerSchema + , "CreateApiConsumerRequest" .= createApiConsumerRequestSchema , "ApiKey" .= apiKeySchema + , "CreateApiKeyRequest" .= createApiKeyRequestSchema + , "ApiKeyCreatedResponse" .= apiKeyCreatedResponseSchema , "Widget" .= widgetSchema + , "CreateWidgetRequest" .= createWidgetRequestSchema , "InteractionEvent" .= interactionEventSchema + , "CreateInteractionEventRequest" .= createInteractionEventRequestSchema , "Annotation" .= annotationSchema + , "CreateAnnotationRequest" .= createAnnotationRequestSchema , "RequirementCandidate" .= rcSchema , "DecisionRecord" .= drSchema , "DeploymentRecord" .= depSchema @@ -106,6 +115,12 @@ buildOpenApiSpec = do , "OutcomeCorrelation" .= outcomeCorrelationSchema , "PatternPerformanceRecord" .= patternPerformanceSchema , "InstitutionalKnowledgeEntry" .= institutionalKnowledgeSchema + , "HubRegistryEntry" .= hubRegistryEntrySchema + , "HubManifestSummary" .= hubManifestSummarySchema + , "WidgetPattern" .= widgetPatternSchema + , "WidgetPatternDetail" .= widgetPatternDetailSchema + , "WidgetPatternVersion" .= widgetPatternVersionSchema + , "PatternAdoptionResponse" .= patternAdoptionResponseSchema ] , "securitySchemes" .= object [ "BearerAuth" .= object @@ -134,10 +149,20 @@ buildPaths = object ] , "/hub-capability-manifests/{id}" .= object [ "get" .= showOp "HubCapabilityManifest" - , "patch" .= writeOpWithSummary "Update HubCapabilityManifest" "HubCapabilityManifest" "UpdateHubCapabilityManifestRequest" + , "patch" .= writeOpWithStatusAndParams + "Update HubCapabilityManifest" + "HubCapabilityManifest" + "UpdateHubCapabilityManifestRequest" + True + "200" + [pathParam "id"] ] , "/hub-capability-manifests/{id}/activate" .= object - [ "post" .= writeOpWithSummary "Activate HubCapabilityManifest" "HubCapabilityManifest" "ActivateHubCapabilityManifestRequest" + [ "post" .= postNoBodyOpWithStatusAndParams + "Activate HubCapabilityManifest" + "HubCapabilityManifest" + "200" + [pathParam "id"] ] , "/api-consumers" .= object [ "get" .= listOp "ApiConsumer" [] @@ -145,7 +170,13 @@ buildPaths = object ] , "/api-consumers/{id}" .= getShowPath "ApiConsumer" , "/api-consumers/{id}/api-keys" .= object - [ "post" .= writeOpWithSummary "Create ApiKey" "ApiKey" "CreateApiKeyRequest" + [ "post" .= writeOpWithResponseStatusAndParams + "Create ApiKey" + "ApiKeyCreatedResponse" + "CreateApiKeyRequest" + False + "201" + [pathParam "id"] ] , "/widgets" .= object [ "get" .= listOp "Widget" [] @@ -181,11 +212,15 @@ buildPaths = object , "/token" .= tokenPath -- Phase 10 — Hub Registry and Widget Marketplace , "/hub-registry" .= getListPath "HubRegistryEntry" - , "/hub-registry/{hubId}" .= getShowPath "HubRegistryEntry" + , "/hub-registry/{hubId}" .= getShowPathWithParam "HubRegistryEntry" "hubId" , "/widget-patterns" .= getListPath "WidgetPattern" - , "/widget-patterns/{id}" .= getShowPath "WidgetPattern" + , "/widget-patterns/{id}" .= getShowPath "WidgetPatternDetail" , "/widget-patterns/{id}/adopt" .= object - [ "post" .= writeOp "PatternAdoption" "AdoptPatternRequest" + [ "post" .= postNoBodyOpWithStatusAndParams + "Adopt WidgetPattern" + "PatternAdoptionResponse" + "200" + [pathParam "id"] ] ] @@ -197,6 +232,10 @@ getShowPath :: Text -> Value getShowPath schemaName = object [ "get" .= showOp schemaName ] +getShowPathWithParam :: Text -> Text -> Value +getShowPathWithParam schemaName paramName = object + [ "get" .= showOpWithParam schemaName paramName ] + listOp :: Text -> [(Text, Text, Text)] -> Value listOp schemaName extraParams = object [ "summary" .= ("List " <> schemaName) @@ -230,10 +269,13 @@ listOp schemaName extraParams = object ] showOp :: Text -> Value -showOp schemaName = object +showOp schemaName = showOpWithParam schemaName "id" + +showOpWithParam :: Text -> Text -> Value +showOpWithParam schemaName paramName = object [ "summary" .= ("Get " <> schemaName) , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] - , "parameters" .= [object ["name" .= ("id" :: Text), "in" .= ("path" :: Text), "required" .= True, "schema" .= object ["type" .= ("string" :: Text), "format" .= ("uuid" :: Text)]]] + , "parameters" .= [pathParam paramName] , "responses" .= object [ "200" .= object [ "description" .= ("OK" :: Text) @@ -251,23 +293,65 @@ writeOp :: Text -> Text -> Value writeOp schemaName reqSchema = writeOpWithSummary ("Create " <> schemaName) schemaName reqSchema writeOpWithSummary :: Text -> Text -> Text -> Value -writeOpWithSummary summaryText schemaName _reqSchema = object +writeOpWithSummary summaryText schemaName reqSchema = + writeOpWithStatusAndParams summaryText schemaName reqSchema True "201" [] + +writeOpWithStatusAndParams :: Text -> Text -> Text -> Bool -> Text -> [Value] -> Value +writeOpWithStatusAndParams = writeOpWithResponseStatusAndParams + +writeOpWithResponseStatusAndParams :: Text -> Text -> Text -> Bool -> Text -> [Value] -> Value +writeOpWithResponseStatusAndParams summaryText responseSchema reqSchema bodyRequired successStatus params = object [ "summary" .= summaryText , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] + , "parameters" .= params , "requestBody" .= object - [ "required" .= True + [ "required" .= bodyRequired , "content" .= object [ "application/json" .= object - ["schema" .= object ["$ref" .= ("#/components/schemas/" <> schemaName)]] + ["schema" .= object ["$ref" .= ("#/components/schemas/" <> reqSchema)]] ] ] , "responses" .= object - [ "201" .= object ["description" .= ("Created" :: Text)] + [ K.fromText successStatus .= object + [ "description" .= ("OK" :: Text) + , "content" .= object + [ "application/json" .= object + ["schema" .= object ["$ref" .= ("#/components/schemas/" <> responseSchema)]] + ] + ] + , "400" .= object ["description" .= ("Invalid request" :: Text)] , "401" .= object ["description" .= ("Unauthorized" :: Text)] , "422" .= object ["description" .= ("Validation error" :: Text)] ] ] +postNoBodyOpWithStatusAndParams :: Text -> Text -> Text -> [Value] -> Value +postNoBodyOpWithStatusAndParams summaryText responseSchema successStatus params = object + [ "summary" .= summaryText + , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] + , "parameters" .= params + , "responses" .= object + [ K.fromText successStatus .= object + [ "description" .= ("OK" :: Text) + , "content" .= object + [ "application/json" .= object + ["schema" .= object ["$ref" .= ("#/components/schemas/" <> responseSchema)]] + ] + ] + , "400" .= object ["description" .= ("Invalid request" :: Text)] + , "401" .= object ["description" .= ("Unauthorized" :: Text)] + , "422" .= object ["description" .= ("Validation error" :: Text)] + ] + ] + +pathParam :: Text -> Value +pathParam name = object + [ "name" .= name + , "in" .= ("path" :: Text) + , "required" .= True + , "schema" .= uuidProp + ] + publicListPath :: Text -> Value publicListPath schemaName = object [ "get" .= object @@ -360,6 +444,22 @@ widgetSchema = object ] ] +createWidgetRequestSchema :: Value +createWidgetRequestSchema = object + [ "type" .= ("object" :: Text) + , "required" .= (["hubId", "name", "widgetType"] :: [Text]) + , "properties" .= object + [ "hubId" .= uuidProp + , "name" .= strProp + , "widgetType" .= object ["$ref" .= ("#/components/schemas/WidgetType" :: Text)] + , "capabilityRef" .= strProp + , "viewContext" .= strProp + , "policyScope" .= object ["$ref" .= ("#/components/schemas/PolicyScope" :: Text)] + , "status" .= object ["type" .= ("string" :: Text), "enum" .= ["active" :: Text, "deprecated", "draft"]] + , "adapterSpecId" .= uuidProp + ] + ] + manifestSchema :: Value manifestSchema = object [ "type" .= ("object" :: Text) @@ -380,6 +480,32 @@ manifestSchema = object ] ] +createManifestRequestSchema :: Value +createManifestRequestSchema = object + [ "type" .= ("object" :: Text) + , "required" .= (["hubId"] :: [Text]) + , "properties" .= manifestRequestProperties True + ] + +updateManifestRequestSchema :: Value +updateManifestRequestSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= manifestRequestProperties False + ] + +manifestRequestProperties :: Bool -> Value +manifestRequestProperties includeHubId = + object $ + (if includeHubId then ["hubId" .= uuidProp] else []) + ++ [ "manifestVersion" .= strProp + , "declaredWidgetTypes" .= arrayOfRef "WidgetType" + , "declaredEventTypes" .= arrayOfRef "EventType" + , "declaredAnnotationCategories" .= arrayOfRef "AnnotationCategory" + , "declaredPolicyScopes" .= arrayOfRef "PolicyScope" + , "capabilityDescription" .= strProp + , "contact" .= strProp + ] + apiConsumerSchema :: Value apiConsumerSchema = object [ "type" .= ("object" :: Text) @@ -397,6 +523,19 @@ apiConsumerSchema = object ] ] +createApiConsumerRequestSchema :: Value +createApiConsumerRequestSchema = object + [ "type" .= ("object" :: Text) + , "required" .= (["name"] :: [Text]) + , "properties" .= object + [ "name" .= strProp + , "description" .= strProp + , "hubCapabilityManifestId" .= uuidProp + , "rateLimitPerMinute" .= object ["type" .= ("integer" :: Text), "minimum" .= (1 :: Int), "default" .= (60 :: Int)] + , "quotaPerDay" .= object ["type" .= ("integer" :: Text), "minimum" .= (1 :: Int), "default" .= (10000 :: Int)] + ] + ] + apiKeySchema :: Value apiKeySchema = object [ "type" .= ("object" :: Text) @@ -413,6 +552,27 @@ apiKeySchema = object ] ] +createApiKeyRequestSchema :: Value +createApiKeyRequestSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "scopes" .= strProp + ] + ] + +apiKeyCreatedResponseSchema :: Value +apiKeyCreatedResponseSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "apiKey" .= object ["$ref" .= ("#/components/schemas/ApiKey" :: Text)] + , "fullKey" .= object + [ "type" .= ("string" :: Text) + , "description" .= ("Static API key secret. Returned only in this creation response; it is stored hashed and cannot be recovered later." :: Text) + ] + , "displayOnce" .= boolProp + ] + ] + interactionEventSchema :: Value interactionEventSchema = object [ "type" .= ("object" :: Text) @@ -428,6 +588,18 @@ interactionEventSchema = object ] ] +createInteractionEventRequestSchema :: Value +createInteractionEventRequestSchema = object + [ "type" .= ("object" :: Text) + , "required" .= (["widgetId", "eventType"] :: [Text]) + , "properties" .= object + [ "widgetId" .= uuidProp + , "eventType" .= object ["$ref" .= ("#/components/schemas/EventType" :: Text)] + , "viewContext" .= strProp + , "metadata" .= objectProp + ] + ] + annotationSchema :: Value annotationSchema = object [ "type" .= ("object" :: Text) @@ -445,6 +617,17 @@ annotationSchema = object ] ] +createAnnotationRequestSchema :: Value +createAnnotationRequestSchema = object + [ "type" .= ("object" :: Text) + , "required" .= (["widgetId", "category", "body"] :: [Text]) + , "properties" .= object + [ "widgetId" .= uuidProp + , "category" .= object ["$ref" .= ("#/components/schemas/AnnotationCategory" :: Text)] + , "body" .= strProp + ] + ] + rcSchema :: Value rcSchema = object [ "type" .= ("object" :: Text) @@ -546,12 +729,114 @@ institutionalKnowledgeSchema = object ] ] +hubRegistryEntrySchema :: Value +hubRegistryEntrySchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "id" .= uuidProp + , "name" .= strProp + , "slug" .= strProp + , "domain" .= strProp + , "hubKind" .= strProp + , "hubFamily" .= strProp + , "vsmFunction" .= strProp + , "vsmSystem" .= strProp + , "gaafStatus" .= object ["type" .= ("string" :: Text), "enum" .= ["compliant" :: Text, "draft_only", "no_manifest"]] + , "manifest" .= object ["$ref" .= ("#/components/schemas/HubManifestSummary" :: Text)] + , "healthScore" .= intProp + , "healthAt" .= dtProp + ] + ] + +hubManifestSummarySchema :: Value +hubManifestSummarySchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "id" .= uuidProp + , "manifestVersion" .= strProp + , "status" .= strProp + , "declaredWidgetTypes" .= arrayOfRef "WidgetType" + , "declaredEventTypes" .= arrayOfRef "EventType" + , "declaredAnnotationCategories" .= arrayOfRef "AnnotationCategory" + , "declaredPolicyScopes" .= arrayOfRef "PolicyScope" + , "activatedAt" .= dtProp + ] + ] + +widgetPatternSchema :: Value +widgetPatternSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "id" .= uuidProp + , "hubId" .= uuidProp + , "name" .= strProp + , "description" .= strProp + , "widgetType" .= object ["$ref" .= ("#/components/schemas/WidgetType" :: Text)] + , "isCrossHub" .= boolProp + , "isPublished" .= boolProp + , "createdAt" .= dtProp + , "updatedAt" .= dtProp + ] + ] + +widgetPatternDetailSchema :: Value +widgetPatternDetailSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "pattern" .= object ["$ref" .= ("#/components/schemas/WidgetPattern" :: Text)] + , "versions" .= object + [ "type" .= ("array" :: Text) + , "items" .= object ["$ref" .= ("#/components/schemas/WidgetPatternVersion" :: Text)] + ] + , "adopterCount" .= intProp + ] + ] + +widgetPatternVersionSchema :: Value +widgetPatternVersionSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "id" .= uuidProp + , "versionNumber" .= intProp + , "definition" .= objectProp + , "changelog" .= strProp + , "publishedAt" .= dtProp + ] + ] + +patternAdoptionResponseSchema :: Value +patternAdoptionResponseSchema = object + [ "type" .= ("object" :: Text) + , "properties" .= object + [ "adopted" .= boolProp + , "adoptionId" .= uuidProp + ] + ] + uuidProp :: Value uuidProp = object ["type" .= ("string" :: Text), "format" .= ("uuid" :: Text)] strProp :: Value strProp = object ["type" .= ("string" :: Text)] +intProp :: Value +intProp = object ["type" .= ("integer" :: Text)] + +boolProp :: Value +boolProp = object ["type" .= ("boolean" :: Text)] + +objectProp :: Value +objectProp = object + [ "type" .= ("object" :: Text) + , "additionalProperties" .= True + ] + +arrayOfRef :: Text -> Value +arrayOfRef schemaName = object + [ "type" .= ("array" :: Text) + , "items" .= object ["$ref" .= ("#/components/schemas/" <> schemaName)] + ] + dtProp :: Value dtProp = object ["type" .= ("string" :: Text), "format" .= ("date-time" :: Text)] diff --git a/contracts/functional/interaction-reporting-v1.md b/contracts/functional/interaction-reporting-v1.md index caa44e2..64141a2 100644 --- a/contracts/functional/interaction-reporting-v1.md +++ b/contracts/functional/interaction-reporting-v1.md @@ -126,14 +126,28 @@ Domain hubs may register additional event types via `HubCapabilityManifest`. ## Phase 9 Extension: `/api/v2/` (IHUB-WP-0010) -The v2 API supersedes per-hub Bearer tokens with OAuth 2.0 client credentials. +The v2 API supports authenticated Bearer access with static API keys and, where +configured, OAuth 2.0 client credentials. **OpenAPI spec:** `/api/v2/openapi.json` (live-generated; `widget_type`, `event_type`, and `category` fields carry `enum` arrays from the type registries) **New endpoints in v2:** - `POST /api/v2/token` — OAuth 2.0 client credentials token exchange +- `GET /api/v2/hubs` / `POST /api/v2/hubs` — list or create hubs, including + first-class VSM hub metadata +- `GET /api/v2/hub-capability-manifests` / `POST /api/v2/hub-capability-manifests` + — list or create hub capability manifests +- `PATCH /api/v2/hub-capability-manifests/{id}` — update draft manifest + vocabulary and metadata +- `POST /api/v2/hub-capability-manifests/{id}/activate` — activate a draft + manifest and register its declared vocabulary +- `GET /api/v2/api-consumers` / `POST /api/v2/api-consumers` — list or create + API consumers +- `POST /api/v2/api-consumers/{id}/api-keys` — create a static API key; the + raw `fullKey` is returned exactly once - `GET /api/v2/widgets` — paginated widget listing +- `POST /api/v2/widgets` — create a widget - `GET /api/v2/interaction-events` — paginated event listing - `POST /api/v2/interaction-events` — submit event (registry-validated) - `GET /api/v2/annotations` — paginated annotation listing diff --git a/docs/new-hub-quickstart.md b/docs/new-hub-quickstart.md index fa78f68..7f701ca 100644 --- a/docs/new-hub-quickstart.md +++ b/docs/new-hub-quickstart.md @@ -3,7 +3,9 @@ **Audience:** A developer starting a new domain hub (dev-hub, ops-hub, fin-hub, etc.) that will live in its own repository and use inter-hub as the governance substrate. -**Current state:** inter-hub v0.2.0-alpha.1 is running at `http://192.168.178.135:8080`. +**Current state:** inter-hub v0.2.0-alpha.1 exposes its supported integration +surface under `/api/v2`. The examples below use `$IHUB_BASE`; point it at the +environment you are bootstrapping against. --- @@ -30,76 +32,142 @@ or type-safe access to inter-hub's data model. ## Pattern A — API Consumer Hub -### 1. Create an API consumer in inter-hub +### 1. Start with an operator API key -Go to `http://192.168.178.135:8080/ApiConsumers` → New. - -Fill in: -- **Name:** `dev-hub` (or your hub name) -- **Contact:** your team email -- **Description:** what this hub does - -After creating the consumer, go to **API Keys → New** and generate a key -for this consumer. Copy the key — it is shown only once. - -### 2. Register your hub +Every write call below requires `Authorization: Bearer `. Use an existing +operator/admin API key for the first bootstrap call. New hub-specific keys can +then be created through the API and should replace the operator key for normal +runtime traffic. ```bash -curl -X POST http://192.168.178.135:8080/api/v2/hubs \ - -H "Authorization: Bearer " \ +export IHUB_BASE="http://127.0.0.1:8000" +export IHUB_OPERATOR_KEY="" +``` + +### 2. Register the VSM Operations hub + +```bash +curl -s -X POST "$IHUB_BASE/api/v2/hubs" \ + -H "Authorization: Bearer $IHUB_OPERATOR_KEY" \ -H "Content-Type: application/json" \ -d '{ - "name": "Dev Hub", - "slug": "dev-hub", - "domain": "dev.example.com", - "hubKind": "domain" + "name": "Operations Hub", + "slug": "ops-hub", + "domain": "operations", + "hubKind": "domain", + "hubFamily": "vsm", + "vsmFunction": "operations", + "vsmSystem": "1" }' ``` Save the returned `id` — this is your `hubId` for all subsequent calls. -### 3. Register your type vocabulary - -Before creating widgets with domain-specific types, register them via the -inter-hub UI at `/HubCapabilityManifests` → New → select your hub. - -In the manifest editor, declare your types: -```json -["dev-pipeline-run", "dev-pr-review", "dev-build-status"] -``` - -Click **Activate**. See `docs/domain-hub-extension-guide.md` for the full -naming rules and conflict-resolution workflow. - -### 4. Register widgets +### 3. Register and activate the ops-hub manifest ```bash -curl -X POST http://192.168.178.135:8080/api/v2/widgets \ - -H "Authorization: Bearer " \ +curl -s -X POST "$IHUB_BASE/api/v2/hub-capability-manifests" \ + -H "Authorization: Bearer $IHUB_OPERATOR_KEY" \ -H "Content-Type: application/json" \ -d '{ - "name": "Pipeline Status Panel", - "widgetType": "dev-pipeline-run", - "hubId": "", - "viewContext": "pipeline-dashboard" + "hubId": "", + "manifestVersion": "1.0", + "declaredWidgetTypes": ["ops-endpoint-card"], + "declaredEventTypes": ["ops-endpoint-verified"], + "declaredAnnotationCategories": ["ops-risk"], + "declaredPolicyScopes": ["ops-internal"], + "capabilityDescription": "Operations inventory and endpoint verification", + "contact": "ops@example.com" }' ``` -### 5. Record interaction events +Then activate the returned manifest: ```bash -curl -X POST http://192.168.178.135:8080/api/v2/interaction-events \ - -H "Authorization: Bearer " \ +curl -s -X POST "$IHUB_BASE/api/v2/hub-capability-manifests//activate" \ + -H "Authorization: Bearer $IHUB_OPERATOR_KEY" +``` + +Activation registers the declared vocabulary. Domain-owned widget types, +event types, annotation categories, and policy scopes must be declared here +before use. + +### 4. Create an ops-hub API consumer and key + +```bash +curl -s -X POST "$IHUB_BASE/api/v2/api-consumers" \ + -H "Authorization: Bearer $IHUB_OPERATOR_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "ops-hub", + "description": "Operations hub runtime client", + "hubCapabilityManifestId": "", + "rateLimitPerMinute": 120, + "quotaPerDay": 50000 + }' +``` + +Create the static key for the returned consumer: + +```bash +curl -s -X POST "$IHUB_BASE/api/v2/api-consumers//api-keys" \ + -H "Authorization: Bearer $IHUB_OPERATOR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"scopes": "ops:write"}' +``` + +The response contains `fullKey` exactly once. Store it in the hub runtime +secret store and use it for all following calls: + +```bash +export OPS_HUB_KEY="" +``` + +### 5. Register widgets + +```bash +curl -s -X POST "$IHUB_BASE/api/v2/widgets" \ + -H "Authorization: Bearer $OPS_HUB_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "CoulombCore Gitea Registry", + "widgetType": "ops-endpoint-card", + "hubId": "", + "viewContext": "operations-inventory", + "policyScope": "ops-internal" + }' +``` + +### 6. Record interaction events + +```bash +curl -s -X POST "$IHUB_BASE/api/v2/interaction-events" \ + -H "Authorization: Bearer $OPS_HUB_KEY" \ -H "Content-Type: application/json" \ -d '{ "widgetId": "", - "eventType": "clicked", - "userId": "", - "payload": {"button": "retry", "pipeline": "main-ci"} + "eventType": "ops-endpoint-verified", + "viewContext": "registry-readiness", + "metadata": { + "service": "gitea", + "endpoint": "https://gitea.coulomb.social/v2/", + "result": "auth-challenge-ok" + } }' ``` -### 6. What you get for free +### 7. Verify the bootstrap + +```bash +curl -s "$IHUB_BASE/api/v2/interaction-events?widgetId=&eventType=ops-endpoint-verified" \ + -H "Authorization: Bearer $OPS_HUB_KEY" +``` + +The event should appear with the submitted `metadata`. If the API returns +`event_type_not_in_manifest`, check that the API consumer is bound to the +active ops-hub manifest and that the event type was declared before activation. + +### 8. What you get for free Once events are flowing, the inter-hub framework automatically provides: - Annotation collection on any widget @@ -109,8 +177,8 @@ Once events are flowing, the inter-hub framework automatically provides: - Outcome signals and regression detection - Widget marketplace discovery -Your hub only needs to POST events. Everything downstream is managed by -inter-hub. +Your hub only needs to register its vocabulary, seed meaningful widgets, and +POST events. Everything downstream is managed by inter-hub. --- @@ -153,16 +221,16 @@ import IHP.Prelude import Network.HTTP.Simple postEvent :: Text -> Text -> Text -> Value -> IO () -postEvent apiKey widgetId eventType payload = do +postEvent apiKey widgetId eventType metadata = do let req = setRequestMethod "POST" $ setRequestHeader "Authorization" ["Bearer " <> cs apiKey] $ setRequestHeader "Content-Type" ["application/json"] $ setRequestBodyJSON (object [ "widgetId" .= widgetId , "eventType" .= eventType - , "payload" .= payload + , "metadata" .= metadata ]) - $ parseRequest_ "http://192.168.178.135:8080/api/v2/interaction-events" + $ parseRequest_ "http://127.0.0.1:8000/api/v2/interaction-events" void $ httpLBS req ``` @@ -239,6 +307,7 @@ every domain hub: - `WidgetEnvelope` helpers — consistent widget wrapping across hubs - `InterHubClient` — typed API client with retry and auth built in - `HubCapabilityManifest` bootstrap — auto-activate manifest on startup + (planned; use the API recipe above today) - Shared `defaultLayout` with inter-hub navigation integration Until `hub-core` exists, copy the client helper above and the 3-step @@ -248,10 +317,11 @@ registration pattern into your new hub. It is ~50 lines of boilerplate. ## Checklist for a New Hub -- [ ] Create ApiConsumer + ApiKey in inter-hub UI +- [ ] Start with an existing operator API key +- [ ] Create ApiConsumer + ApiKey through `/api/v2/api-consumers` - [ ] Record your hub ID and API key in the new hub's `.env` -- [ ] Register HubCapabilityManifest with domain type vocabulary -- [ ] Activate the manifest (validates no naming conflicts) +- [ ] Register HubCapabilityManifest with domain type vocabulary through `/api/v2/hub-capability-manifests` +- [ ] Activate the manifest through `/api/v2/hub-capability-manifests//activate` - [ ] Create at least one Widget per meaningful UI surface - [ ] Instrument interactions with POST to `/api/v2/interaction-events` - [ ] Verify events appear in inter-hub at `/InteractionEvents` @@ -266,8 +336,9 @@ registration pattern into your new hub. It is ~50 lines of boilerplate. | Resource | Location | |----------|----------| -| API reference (OpenAPI) | `http://192.168.178.135:8080/api/v2/openapi.json` | -| Type registry browser | `http://192.168.178.135:8080/TypeRegistries/WidgetTypes` | +| API reference (OpenAPI) | `$IHUB_BASE/api/v2/openapi.json` | +| Swagger UI | `$IHUB_BASE/api/v2/docs` | +| Type registry browser | `$IHUB_BASE/TypeRegistries/WidgetTypes` | | Domain hub extension guide | `docs/domain-hub-extension-guide.md` | | IHP data and queries | `docs/ihp-data-and-queries.md` | | IHP controllers and views | `docs/ihp-controllers-views-forms.md` | diff --git a/workplans/IHUB-WP-0019-vsm-hub-bootstrap-api.md b/workplans/IHUB-WP-0019-vsm-hub-bootstrap-api.md index de234b1..d432283 100644 --- a/workplans/IHUB-WP-0019-vsm-hub-bootstrap-api.md +++ b/workplans/IHUB-WP-0019-vsm-hub-bootstrap-api.md @@ -244,7 +244,7 @@ shell does not have `IHP_LIB`/the IHP dev environment loaded. ```task id: IHUB-WP-0019-T06 -status: todo +status: done priority: medium state_hub_task_id: "84c92e05-3e0f-490a-a48f-e2d9ddace764" ``` @@ -260,6 +260,19 @@ Update OpenAPI and docs to match the real API: Done when: a new hub implementer can follow docs without discovering missing API endpoints at runtime. +Implementation note (2026-05-19): updated the generated OpenAPI contract to +use distinct request schemas for hub, manifest, API consumer/key, widget, +interaction-event, and annotation writes. The spec now represents manifest +activation and widget-pattern adoption as no-body actions, documents the +one-time `ApiKeyCreatedResponse.fullKey`, adds missing hub registry and widget +pattern response schemas, and fixes path parameter naming for `hubId`. Updated +`docs/new-hub-quickstart.md` to show the supported `ops-hub` bootstrap path +through `/api/v2`, including VSM metadata, manifest activation, consumer/key +creation, widget seeding, and `metadata` event submission. Updated the +functional contract endpoint list. `git diff --check` passed; +`scripts/compile-check` remains blocked in this shell because `IHP_LIB` is not +set. + --- ### T07 — Add an ops-hub bootstrap smoke test