# Ops Hub Activity-Core Widget Mapping Date: 2026-06-15 Workplan: `IHUB-WP-0022` ## Purpose `OPS_HUB_WIDGET_MAPPING` tells activity-core which Inter-Hub widget receives each ops evidence event. The value must be non-secret JSON. It may contain Inter-Hub widget UUIDs and logical references, but it must never contain `OPS_HUB_KEY` or any operator credential. Activity-core currently only checks that a mapping value is present before returning `inter_hub_sink_deferred`. This document defines the contract that the future Inter-Hub submission implementation should parse. ## Versioned Shape ```json { "version": "ops-hub.activity-core.widget-mapping.v1", "hub": { "slug": "ops-hub", "id": "" }, "policyScope": "ops-evidence", "defaultViewContext": "ops-inventory-probe", "events": { "ops-service-observed": { "family": "services", "aggregateWidgetRef": "ops:service:aggregate" }, "ops-endpoint-verified": { "family": "endpoints", "aggregateWidgetRef": "ops:endpoint:aggregate" }, "ops-access-path-checked": { "family": "accessPaths", "aggregateWidgetRef": "ops:access-path:aggregate" }, "ops-backup-verified": { "family": "backups", "aggregateWidgetRef": "ops:backup:aggregate" }, "ops-inventory-drift": { "family": "drift", "aggregateWidgetRef": "ops:drift:aggregate" } }, "widgets": { "aggregate": { "ops:service:aggregate": { "widgetId": "", "widgetType": "ops-service-card", "name": "Ops Service Evidence Intake" }, "ops:endpoint:aggregate": { "widgetId": "", "widgetType": "ops-endpoint-card", "name": "Ops Endpoint Evidence Intake" }, "ops:access-path:aggregate": { "widgetId": "", "widgetType": "ops-access-path-card", "name": "Ops Access Path Evidence Intake" }, "ops:backup:aggregate": { "widgetId": "", "widgetType": "ops-backup-card", "name": "Ops Backup Evidence Intake" }, "ops:drift:aggregate": { "widgetId": "", "widgetType": "ops-drift-card", "name": "Ops Inventory Drift Evidence Intake" } }, "services": { "state-hub": { "widgetRef": "ops:service:state-hub", "widgetId": "" } }, "endpoints": { "gitea:gitea-oci-registry": { "widgetRef": "ops:endpoint:gitea-registry", "widgetId": "" } }, "accessPaths": { "gitea:gitea-access-1": { "widgetRef": "ops:access-path:gitea-access-1", "widgetId": "" } }, "backups": { "gitea:database:gitea-db": { "widgetRef": "ops:backup:gitea-db", "widgetId": "" } }, "drift": { "gitea:gitea-oci-registry": { "widgetRef": "ops:drift:gitea-oci-registry", "widgetId": "" } } } } ``` ## Selector Rules Activity-core should choose a widget in this order: 1. If the evidence payload carries a `widget_ref` and that reference exists in the mapping, use it. 2. For `ops-service-observed`, use `services[""]`. 3. For `ops-endpoint-verified`, use `endpoints[":"]`. 4. For `ops-access-path-checked`, use `accessPaths[":"]`. 5. For `ops-backup-verified`, use `backups[":"]`. 6. For `ops-inventory-drift`, use `drift[":"]`. 7. If no entity-specific widget exists, use the event's aggregate widget. 8. If neither an entity-specific nor aggregate widget exists, skip Inter-Hub submission with a non-secret result that names the missing selector. ## Bootstrap Widget Names The initial aggregate widgets should be seeded before activity-core is pointed at Inter-Hub: | Widget ref | Widget type | Suggested name | |---|---|---| | `ops:service:aggregate` | `ops-service-card` | Ops Service Evidence Intake | | `ops:endpoint:aggregate` | `ops-endpoint-card` | Ops Endpoint Evidence Intake | | `ops:access-path:aggregate` | `ops-access-path-card` | Ops Access Path Evidence Intake | | `ops:backup:aggregate` | `ops-backup-card` | Ops Backup Evidence Intake | | `ops:drift:aggregate` | `ops-drift-card` | Ops Inventory Drift Evidence Intake | Per-entity widgets may be seeded later without changing the event contract. When a per-entity widget is added, update the mapping and keep the aggregate widget as the fallback. ## Compatibility Rules - `version` is required. Reject unknown major versions. - Consumers must tolerate additional fields. - Widget UUIDs may rotate, but `widgetRef` values should remain stable. - Removing a widget mapping is a breaking change for that selector unless the aggregate fallback remains present. - Mapping updates must be deployed before activity-core starts sending events that depend on the new selectors. - The mapping is non-secret and may be stored in a ConfigMap or environment variable. `OPS_HUB_KEY` must remain Secret-only. ## Minimum Valid Mapping For the first live smoke, an aggregate-only mapping is enough: ```json { "version": "ops-hub.activity-core.widget-mapping.v1", "hub": { "slug": "ops-hub", "id": "" }, "policyScope": "ops-evidence", "defaultViewContext": "ops-inventory-probe", "events": { "ops-service-observed": { "family": "services", "aggregateWidgetRef": "ops:service:aggregate" }, "ops-endpoint-verified": { "family": "endpoints", "aggregateWidgetRef": "ops:endpoint:aggregate" }, "ops-access-path-checked": { "family": "accessPaths", "aggregateWidgetRef": "ops:access-path:aggregate" }, "ops-backup-verified": { "family": "backups", "aggregateWidgetRef": "ops:backup:aggregate" }, "ops-inventory-drift": { "family": "drift", "aggregateWidgetRef": "ops:drift:aggregate" } }, "widgets": { "aggregate": { "ops:service:aggregate": { "widgetId": "" }, "ops:endpoint:aggregate": { "widgetId": "" }, "ops:access-path:aggregate": { "widgetId": "" }, "ops:backup:aggregate": { "widgetId": "" }, "ops:drift:aggregate": { "widgetId": "" } } } } ```