Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions gaps/open.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
# Open Gaps

## G-15 — Tier C enrichment MQTT transport wiring

- Domain: `gateway-api/v1`
- Status: Contract defined in `ori-gateway` (`internal/contracts/tier_c_enrichment.go`), DECISIONS.md entry recorded. MQTT topic constants and `app.go` handler are not yet wired. Runtime has no client for sending enrichment requests.
- Summary: The advisory Tier C enrichment flow requires topic constants in `contracts.go` and a subscription handler in `app.go` on the gateway side, and a corresponding MQTT client on the runtime side. Until wired, enriched Tier C operator messages are unavailable.
- Tracking: `ori-platform/ori-gateway#41`

## G-16 — `reasoning_log` export missing from gateway runtimeclient

- Domain: `gateway-api/v1`
- Status: Export type documented in spec and served by runtime. Gateway `runtimeclient/mqtt.go` implements `Health`, `SensorHistory`, `ActionLog`, `TierCDecisionLog` but has no `ReasoningLog` method.
- Summary: The reasoning audit export surface is unusable from the gateway until the runtimeclient method is added.
- Tracking: `ori-platform/ori-gateway#42`
No open v1 contract gaps are currently recorded. New implementation gaps should be added here with repo tracking links.
19 changes: 15 additions & 4 deletions gaps/resolved.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ freshness and uses it before routing non-explicit deterministic escalation signa
gateway reasoning. Gateway heartbeat publisher is fully implemented in `ori-gateway`
(`internal/heartbeat/publisher.go`).

Note: runtime-side MQTT subscription to `ori/gateway/health` is tracked in
`ori-platform/ori-runtime#144` — the posture tracking mechanism is correct but
currently fed only through the in-process EventBus rather than a live MQTT subscribe.

## G-14 — Runtime export surface for gateway/reporting

Resolved: runtime exposes bounded MQTT exports for `health`, `sensor_history`,
Expand All @@ -79,3 +75,18 @@ types, defaults, and validation rules for all five top-level sections: `gateway`
`provider`, `reporting`, `sim`, `fleet`. Separation between `provider` (Tier 3
reasoning) and `reporting.provider` (advisory/product) is made explicit. Tracked
by `ori-platform/ori-specs#5`.

## G-15 — Tier C enrichment MQTT transport wiring

Resolved: `ori-gateway` now defines Tier C enrichment topics, subscribes per
configured runtime device when `reporting.tier_c_enrichment.enabled=true`, and
publishes advisory-only responses on `ori/{device_id}/tier_c/enrichment/response`.
The gateway-side contract and handler are implemented in `internal/contracts`,
`internal/enrichment`, and `cmd/ori-gateway/app.go`. Runtime client integration
is a runtime-side follow-up, not a gateway contract gap.

## G-16 — `reasoning_log` export missing from gateway runtimeclient

Resolved: `ori-gateway` runtimeclient exposes `ReasoningLog` over the bounded
runtime export surface. Reporting/cloud consumers can request `reasoning_log`
without reading runtime SQLite directly.
165 changes: 107 additions & 58 deletions gateway-api/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,24 @@ gateway owns product/reporting behavior.

## Topics

| Message family | Direction | Topic |
|---|---|---|
| Reasoning | Runtime -> Gateway | `ori/{device_id}/reasoning/request` |
| Reasoning | Gateway -> Runtime | `ori/{device_id}/reasoning/response` |
| Gateway heartbeat | Gateway -> Runtime | `ori/gateway/health` |
| Runtime export | Gateway -> Runtime | `ori/{device_id}/export/request` |
| Runtime export | Runtime -> Gateway | `ori/{device_id}/export/response/{request_id}` |

Gateway subscribes to `ori/+/reasoning/request` to serve every runtime on the LAN.
For runtime data exports, the gateway publishes a request to the target device's
export request topic and subscribes to `ori/+/export/response/+` or the specific
response topic for the request.
| Message family | Direction | Topic |
| ---------------------- | ------------------ | ---------------------------------------------- |
| Reasoning | Runtime -> Gateway | `ori/{device_id}/reasoning/request` |
| Reasoning | Gateway -> Runtime | `ori/{device_id}/reasoning/response` |
| Gateway heartbeat | Gateway -> Runtime | `ori/gateway/health` |
| Runtime node heartbeat | Runtime -> Gateway | `ori/{device_id}/runtime/heartbeat` |
| Runtime export | Gateway -> Runtime | `ori/{device_id}/export/request` |
| Runtime export | Runtime -> Gateway | `ori/{device_id}/export/response/{request_id}` |

Gateway serves only configured runtime devices (`gateway.device_ids`). Reasoning
may use the `ori/+/reasoning/request` filter internally, but runtime heartbeat
and Tier C enrichment handlers subscribe per configured device. For runtime data
exports, the gateway publishes a request to the target device's export request
topic and subscribes to `ori/+/export/response/+` or the specific response topic
for the request.

The device owns the `ori/{device_id}/...` namespace. The gateway owns only its
health topic.
site-level health topic. Production broker ACLs must enforce the same boundary.

---

Expand All @@ -58,7 +61,7 @@ health topic.
"value": 0.0,
"unit": "string",
"timestamp": 0,
"history": [{"value": 0.0, "timestamp": 0}]
"history": [{ "value": 0.0, "timestamp": 0 }]
},
"action_tier_hint": "A|B|C|D",
"timeout_ms": 10000
Expand Down Expand Up @@ -124,25 +127,65 @@ message_type\ndevice_id\nrequest_id\nsigned_at_ms\ncanonical_json

Where:

| Field | Value | Notes |
| --- | --- | --- |
| `message_type` | `"gateway.heartbeat"` | Fixed string. |
| `device_id` | `""` | Empty string — heartbeat is site-scoped, not per-device. |
| `request_id` | `""` | Empty string — no request correlation. |
| `signed_at_ms` | string of the `signed_at_ms` integer | Should equal `timestamp_ms`. |
| `canonical_json` | JSON with keys sorted, no extra whitespace | Must reproduce the full heartbeat payload exactly. |
| Field | Value | Notes |
| ---------------- | ------------------------------------------ | -------------------------------------------------------- |
| `message_type` | `"gateway.heartbeat"` | Fixed string. |
| `device_id` | `""` | Empty string — heartbeat is site-scoped, not per-device. |
| `request_id` | `""` | Empty string — no request correlation. |
| `signed_at_ms` | string of the `signed_at_ms` integer | Should equal `timestamp_ms`. |
| `canonical_json` | JSON with keys sorted, no extra whitespace | Must reproduce the full heartbeat payload exactly. |

The shared secret is the same secret used for reasoning and export envelopes, named by `gateway.auth.shared_secret_env`. `device_id` is intentionally empty: a LAN-broadcast heartbeat is site-scoped via the shared secret, not per-device. This is a distinct trust model from reasoning and export envelopes, which bind to `device_id` for per-device isolation.

Replay protection uses `message_type + signed_at_ms + signature` as the cache key (TTL-bounded, in-memory on the runtime side). Clock skew is validated against `gateway.auth.max_clock_skew_ms`.

## Runtime Node Heartbeat Payload

Published by each runtime to `ori/{device_id}/runtime/heartbeat` when runtime
`gateway.enabled` and `gateway.node_heartbeat.enabled` are true. The gateway
subscribes per configured `gateway.device_ids` and stamps `gateway_seen_ms` when
it ingests the heartbeat.

```json
{
"device_id": "site-a-edge-01",
"status": "healthy|degraded|starting",
"last_seen_ms": 1717000000000,
"gateway_seen_ms": 0,
"active_triggers": ["trigger_name"],
"auth": {
"scheme": "hmac-sha256",
"signed_at_ms": 1717000000000,
"signature": "hmac-sha256:<hex-digest>"
}
}
```

`auth` is present when runtime `gateway.auth.enabled: true` and omitted when
false. Gateway deployments with auth enabled reject unsigned, stale, replayed, or
device-mismatched runtime heartbeats. Runtime heartbeats are infrastructure
liveness signals, not sensor events, and must use `retain=false`.

The HMAC signing input uses the regular device-bound gateway envelope:

```text
runtime.heartbeat
device_id
request_id
signed_at_ms
canonical_json
```

`request_id` is an empty string for runtime heartbeats because the heartbeat is
not a request/response exchange.

## Tier 3 Timeout and Retry Target Behavior

| Parameter | Target |
|---|---|
| response timeout | 10s default |
| retry | 1 retry |
| fallback | Runtime policy decides: non-explicit deterministic signals may fall back to local Tier 2 (`local_slm`); explicit `escalate_to: gateway` returns a gateway-unavailable result instead of silently downgrading. |
| Parameter | Target |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| response timeout | 10s default |
| retry | 1 retry |
| fallback | Runtime policy decides: non-explicit deterministic signals may fall back to local Tier 2 (`local_slm`); explicit `escalate_to: gateway` returns a gateway-unavailable result instead of silently downgrading. |

## Gateway Availability Check

Expand Down Expand Up @@ -180,16 +223,16 @@ Published by gateway to `ori/{device_id}/export/request`.

### Request Fields

| Field | Required | Notes |
|---|---:|---|
| `request_id` | yes | Correlates request and response. Must be non-empty. |
| `export_type` | yes | One of `health`, `sensor_history`, `action_log`, `reasoning_log`, `tier_c_decision_log`. |
| `device_id` | yes | Must match the target runtime device ID. |
| `since_ms` | export-specific | Unix milliseconds lower bound. Required for `sensor_history`. |
| `until_ms` | export-specific | Unix milliseconds upper bound. Required for `sensor_history`. |
| `limit` | no | Runtime caps page size. Clients should use `<= 1000`. |
| `page_token` | no | Empty string for first page. Subsequent value comes from response `next_page_token`. |
| `params` | no | Export-specific object. Defaults to `{}`. |
| Field | Required | Notes |
| ------------- | --------------: | ---------------------------------------------------------------------------------------- |
| `request_id` | yes | Correlates request and response. Must be non-empty. |
| `export_type` | yes | One of `health`, `sensor_history`, `action_log`, `reasoning_log`, `tier_c_decision_log`. |
| `device_id` | yes | Must match the target runtime device ID. |
| `since_ms` | export-specific | Unix milliseconds lower bound. Required for `sensor_history`. |
| `until_ms` | export-specific | Unix milliseconds upper bound. Required for `sensor_history`. |
| `limit` | no | Runtime caps page size. Clients should use `<= 1000`. |
| `page_token` | no | Empty string for first page. Subsequent value comes from response `next_page_token`. |
| `params` | no | Export-specific object. Defaults to `{}`. |

`until_ms` must be greater than or equal to `since_ms` when both are present.
Invalid or mismatched requests return an error response envelope instead of
Expand All @@ -214,6 +257,12 @@ Published by runtime to `ori/{device_id}/export/response/{request_id}`.
Every response, including validation failures, includes the envelope fields above.
When `error` is non-null, `items` is empty and `complete` is true.

When `gateway.auth.enabled: true`, export responses carry an `auth` block signed
with `message_type=export_response`. When runtime `gateway.encryption.enabled`
is `true`, sensitive export responses (`sensor_history`, `action_log`,
`reasoning_log`, and `tier_c_decision_log`) are AES-GCM encrypted before HMAC
signing. Health exports remain plaintext operational posture.

## Supported Export Types

### `health`
Expand All @@ -233,10 +282,10 @@ Returns bounded sensor history for one sensor.

Request `params`:

| Field | Required | Notes |
|---|---:|---|
| `sensor_id` | yes | Runtime sensor ID to export. |
| `bucket_ms` | no | Aggregation bucket width in milliseconds. `3600000` means hourly buckets. `0` or omitted returns runtime-selected source rows. |
| Field | Required | Notes |
| ----------- | -------: | ------------------------------------------------------------------------------------------------------------------------------ |
| `sensor_id` | yes | Runtime sensor ID to export. |
| `bucket_ms` | no | Aggregation bucket width in milliseconds. `3600000` means hourly buckets. `0` or omitted returns runtime-selected source rows. |

Response item shape for unbucketed rows:

Expand Down Expand Up @@ -283,27 +332,26 @@ Returns bounded action-log rows for the runtime device.

Optional request `params`:

| Field | Required | Notes |
|---|---:|---|
| `tier` | no | Optional `A`, `B`, `C`, or `D` filter. |
| Field | Required | Notes |
| ------ | -------: | -------------------------------------- |
| `tier` | no | Optional `A`, `B`, `C`, or `D` filter. |

Response items include action name, tier, execution status, approval status,
action taken, operator response, proposal ID, safe-default usage, device ID,
sensor ID, sensor type, trigger name, and timestamp.


### `reasoning_log`

Returns bounded reasoning audit rows for the runtime device.

Optional request `params`:

| Field | Required | Notes |
|---|---:|---|
| `tier_used` | no | Optional `rule`, `local_slm`, or `gateway` filter. |
| `action_tier` | no | Optional `A`, `B`, `C`, or `D` filter. |
| `reasoning_status` | no | Optional `complete`, `incomplete`, or `skipped` filter for post-action reasoning. |
| `correlation_id` | no | Optional event correlation ID filter. |
| Field | Required | Notes |
| ------------------ | -------: | --------------------------------------------------------------------------------- |
| `tier_used` | no | Optional `rule`, `local_slm`, or `gateway` filter. |
| `action_tier` | no | Optional `A`, `B`, `C`, or `D` filter. |
| `reasoning_status` | no | Optional `complete`, `incomplete`, or `skipped` filter for post-action reasoning. |
| `correlation_id` | no | Optional event correlation ID filter. |

Response items include device ID, skill name, trigger name, sensor ID, sensor
type, tier used, model, prompt text, reasoning text, confidence, proposed
Expand Down Expand Up @@ -343,19 +391,20 @@ outcome, proposal ID, and creation timestamp.

## Tier C Enrichment

> Status: Contract defined in `ori-gateway` (`internal/contracts/tier_c_enrichment.go`).
> MQTT transport wiring is pending (see gap G-15).
> Status: Implemented in `ori-gateway` (`internal/contracts/tier_c_enrichment.go`,
> `internal/enrichment/handler.go`, and `cmd/ori-gateway/app.go`). Runtime client
> wiring is a runtime-side follow-up; gateway transport and advisory contract are live.

The gateway may optionally enrich the operator-facing message for an existing Tier C
proposal. Enrichment is advisory: the runtime remains the sole authority for action
tier, proposed action, safe default, approval requirement, and execution. Enrichment
failure or timeout must not block or delay the approval workflow.

### Topics (pending transport wiring)
### Topics

| Message family | Direction | Topic |
|---|---|---|
| Enrichment request | Runtime -> Gateway | `ori/{device_id}/tier_c/enrichment/request` |
| Message family | Direction | Topic |
| ------------------- | ------------------ | -------------------------------------------- |
| Enrichment request | Runtime -> Gateway | `ori/{device_id}/tier_c/enrichment/request` |
| Enrichment response | Gateway -> Runtime | `ori/{device_id}/tier_c/enrichment/response` |

### Tier C Enrichment Request Payload
Expand Down Expand Up @@ -407,8 +456,8 @@ failure or timeout must not block or delay the approval workflow.

Both `request_id` and `proposal_id` must match the pending runtime proposal. On
enrichment failure, the gateway publishes an error response (non-null `error` field)
so the runtime does not wait. The runtime preserves the original operator message
when enrichment returns an error or times out.
so a runtime client does not wait. The runtime must preserve the original operator
message when enrichment returns an error or times out.

Enrichment responses must not contain fields that change action tier, action name,
safe default, approval requirement, or any other runtime authority field.
Expand Down
Loading
Loading