generated from coulomb/repo-seed
Implement ops-hub evidence intake contracts
This commit is contained in:
220
docs/contracts/ops-hub-activity-core-event-payloads.md
Normal file
220
docs/contracts/ops-hub-activity-core-event-payloads.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Ops Hub Activity-Core Event Payloads
|
||||
|
||||
Date: 2026-06-15
|
||||
|
||||
Workplan: `IHUB-WP-0022`
|
||||
|
||||
## Inter-Hub Request Shape
|
||||
|
||||
Activity-core should submit ops evidence through:
|
||||
|
||||
```text
|
||||
POST /api/v2/interaction-events
|
||||
Authorization: Bearer ${OPS_HUB_KEY}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Each request body must use the Inter-Hub v2 interaction-event shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<widget-uuid-from-OPS_HUB_WIDGET_MAPPING>",
|
||||
"eventType": "ops-endpoint-verified",
|
||||
"viewContext": "ops-inventory-probe",
|
||||
"metadata": {
|
||||
"type": "ops-endpoint-verified",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Inter-Hub sets `occurredAt` on receipt. Activity-core must send the actual
|
||||
probe timestamp as `metadata.attributes.observed_at`.
|
||||
|
||||
## Shared Rules
|
||||
|
||||
- `widgetId` must be a UUID for an existing ops-hub widget.
|
||||
- `eventType` must exist in Inter-Hub's event type registry.
|
||||
- If the API consumer is bound to an active ops-hub manifest, `eventType` must
|
||||
be declared by that manifest.
|
||||
- `viewContext` should be `ops-inventory-probe` unless a more specific context
|
||||
is useful, such as `ops-inventory-probe/endpoints`.
|
||||
- `metadata.type` must match the Inter-Hub `eventType`.
|
||||
- `metadata.version` must match the activity-core event definition version.
|
||||
- `metadata.publisher` must be `activity-core`.
|
||||
- `metadata.attributes.idempotency_key` is required, even though Inter-Hub does
|
||||
not currently enforce idempotency.
|
||||
- Duplicate tolerance is required on the reader side until Inter-Hub provides a
|
||||
unique idempotency constraint.
|
||||
- Payloads must not include secrets, authorization headers, cookies, token-like
|
||||
values, private key material, raw response bodies, command output, or
|
||||
unredacted URL query strings.
|
||||
|
||||
## Status Vocabulary
|
||||
|
||||
Use the activity-core status vocabulary:
|
||||
|
||||
- `ok`
|
||||
- `degraded`
|
||||
- `down`
|
||||
- `skipped`
|
||||
|
||||
Use `reason` for compact machine-readable explanations, for example:
|
||||
|
||||
- `expected_status_mismatch`
|
||||
- `expected_signal_missing`
|
||||
- `unsupported_access_path_type`
|
||||
- `backup_probe_not_implemented`
|
||||
- `missing_endpoint`
|
||||
|
||||
## Example: Service Observed
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<service-widget-uuid>",
|
||||
"eventType": "ops-service-observed",
|
||||
"viewContext": "ops-inventory-probe/services",
|
||||
"metadata": {
|
||||
"type": "ops-service-observed",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {
|
||||
"activity_core_run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"idempotency_key": "12345678-aaaa-bbbb-cccc-123456789abc:state-hub:ops-service-observed",
|
||||
"service_id": "state-hub",
|
||||
"service_name": "State Hub",
|
||||
"environment": "local",
|
||||
"lifecycle_state": "observed",
|
||||
"observed_status": "ok",
|
||||
"observed_at": "2026-06-05T10:15:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Endpoint Verified
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<endpoint-widget-uuid>",
|
||||
"eventType": "ops-endpoint-verified",
|
||||
"viewContext": "ops-inventory-probe/endpoints",
|
||||
"metadata": {
|
||||
"type": "ops-endpoint-verified",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {
|
||||
"activity_core_run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"idempotency_key": "12345678-aaaa-bbbb-cccc-123456789abc:gitea:gitea-oci-registry:ops-endpoint-verified",
|
||||
"service_id": "gitea",
|
||||
"endpoint_id": "gitea-oci-registry",
|
||||
"endpoint_type": "https",
|
||||
"endpoint_url": "https://gitea.coulomb.social/v2/",
|
||||
"expected_status": 401,
|
||||
"status_code": 401,
|
||||
"matched_expected_status": true,
|
||||
"matched_expected_signal": true,
|
||||
"observed_status": "ok",
|
||||
"observed_at": "2026-06-05T10:15:01Z",
|
||||
"widget_ref": "ops:endpoint:gitea-registry"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Access Path Checked
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<access-path-widget-uuid>",
|
||||
"eventType": "ops-access-path-checked",
|
||||
"viewContext": "ops-inventory-probe/access-paths",
|
||||
"metadata": {
|
||||
"type": "ops-access-path-checked",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {
|
||||
"activity_core_run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"idempotency_key": "12345678-aaaa-bbbb-cccc-123456789abc:gitea:gitea-access-1:ops-access-path-checked",
|
||||
"service_id": "gitea",
|
||||
"access_path_id": "gitea-access-1",
|
||||
"access_path_type": "k8s",
|
||||
"declared_status": "unknown",
|
||||
"observed_status": "skipped",
|
||||
"reason": "unsupported_access_path_type",
|
||||
"observed_at": "2026-06-05T10:15:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Backup Verified
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<backup-widget-uuid>",
|
||||
"eventType": "ops-backup-verified",
|
||||
"viewContext": "ops-inventory-probe/backups",
|
||||
"metadata": {
|
||||
"type": "ops-backup-verified",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {
|
||||
"activity_core_run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"idempotency_key": "12345678-aaaa-bbbb-cccc-123456789abc:gitea:database:gitea-db:ops-backup-verified",
|
||||
"service_id": "gitea",
|
||||
"backing_store_ref": "database:gitea-db",
|
||||
"backup_evidence_ref": "state-hub:progress:<progress-id>",
|
||||
"restore_verified": false,
|
||||
"observed_status": "skipped",
|
||||
"reason": "backup_probe_not_implemented",
|
||||
"observed_at": "2026-06-05T10:15:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Inventory Drift
|
||||
|
||||
```json
|
||||
{
|
||||
"widgetId": "<drift-widget-uuid>",
|
||||
"eventType": "ops-inventory-drift",
|
||||
"viewContext": "ops-inventory-probe/drift",
|
||||
"metadata": {
|
||||
"type": "ops-inventory-drift",
|
||||
"version": "1.0",
|
||||
"publisher": "activity-core",
|
||||
"attributes": {
|
||||
"activity_core_run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"idempotency_key": "12345678-aaaa-bbbb-cccc-123456789abc:gitea:gitea-oci-registry:ops-inventory-drift",
|
||||
"service_id": "gitea",
|
||||
"inventory_object_id": "gitea-oci-registry",
|
||||
"drift_kind": "status_mismatch",
|
||||
"declared_summary": "expected_status=401",
|
||||
"observed_summary": "status_code=200",
|
||||
"observed_status": "degraded",
|
||||
"reason": "expected_status_mismatch",
|
||||
"observed_at": "2026-06-05T10:15:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Expected API Errors
|
||||
|
||||
Activity-core should treat these as configuration or rollout errors:
|
||||
|
||||
| Error | Meaning | Recovery |
|
||||
|---|---|---|
|
||||
| `401` | Missing or invalid `OPS_HUB_KEY` | Check Secret provisioning; do not log the key. |
|
||||
| `422` with `unregistered_event_type` | Event type not in Inter-Hub registry | Activate the ops-hub manifest vocabulary. |
|
||||
| `422` with `event_type_not_in_manifest` | Runtime consumer manifest does not declare the event | Bind the consumer to the active manifest or activate a corrected manifest. |
|
||||
| `422` with `Widget not found` | Mapping points at a missing widget | Refresh `OPS_HUB_WIDGET_MAPPING`. |
|
||||
| `422` with `unregistered_policy_scope` during widget seed | Policy scope is absent | Declare and activate `ops-evidence`. |
|
||||
|
||||
For the first activity-core slice, a failed Inter-Hub submission should not
|
||||
fail the whole probe if State Hub fallback posting succeeds. It should return a
|
||||
compact sink result naming the non-secret failure class.
|
||||
206
docs/contracts/ops-hub-activity-core-mapping.md
Normal file
206
docs/contracts/ops-hub-activity-core-mapping.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 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": "<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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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>" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user