Files
inter-hub/docs/contracts/ops-hub-activity-core-mapping.md

6.2 KiB

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

{
  "version": "ops-hub.activity-core.widget-mapping.v1",
  "hub": {
    "slug": "ops-hub",
    "id": "<ops-hub-uuid>"
  },
  "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": "<uuid>",
        "widgetType": "ops-service-card",
        "name": "Ops Service Evidence Intake"
      },
      "ops:endpoint:aggregate": {
        "widgetId": "<uuid>",
        "widgetType": "ops-endpoint-card",
        "name": "Ops Endpoint Evidence Intake"
      },
      "ops:access-path:aggregate": {
        "widgetId": "<uuid>",
        "widgetType": "ops-access-path-card",
        "name": "Ops Access Path Evidence Intake"
      },
      "ops:backup:aggregate": {
        "widgetId": "<uuid>",
        "widgetType": "ops-backup-card",
        "name": "Ops Backup Evidence Intake"
      },
      "ops:drift:aggregate": {
        "widgetId": "<uuid>",
        "widgetType": "ops-drift-card",
        "name": "Ops Inventory Drift Evidence Intake"
      }
    },
    "services": {
      "state-hub": {
        "widgetRef": "ops:service:state-hub",
        "widgetId": "<uuid>"
      }
    },
    "endpoints": {
      "gitea:gitea-oci-registry": {
        "widgetRef": "ops:endpoint:gitea-registry",
        "widgetId": "<uuid>"
      }
    },
    "accessPaths": {
      "gitea:gitea-access-1": {
        "widgetRef": "ops:access-path:gitea-access-1",
        "widgetId": "<uuid>"
      }
    },
    "backups": {
      "gitea:database:gitea-db": {
        "widgetRef": "ops:backup:gitea-db",
        "widgetId": "<uuid>"
      }
    },
    "drift": {
      "gitea:gitea-oci-registry": {
        "widgetRef": "ops:drift:gitea-oci-registry",
        "widgetId": "<uuid>"
      }
    }
  }
}

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["<service_id>"].
  3. For ops-endpoint-verified, use endpoints["<service_id>:<endpoint_id>"].
  4. For ops-access-path-checked, use accessPaths["<service_id>:<access_path_id>"].
  5. For ops-backup-verified, use backups["<service_id>:<backing_store_ref>"].
  6. For ops-inventory-drift, use drift["<service_id>:<inventory_object_id>"].
  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:

{
  "version": "ops-hub.activity-core.widget-mapping.v1",
  "hub": {
    "slug": "ops-hub",
    "id": "<ops-hub-uuid>"
  },
  "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": "<uuid>" },
      "ops:endpoint:aggregate": { "widgetId": "<uuid>" },
      "ops:access-path:aggregate": { "widgetId": "<uuid>" },
      "ops:backup:aggregate": { "widgetId": "<uuid>" },
      "ops:drift:aggregate": { "widgetId": "<uuid>" }
    }
  }
}