diff --git a/gaps/open.md b/gaps/open.md index 3e4507d..16b4262 100644 --- a/gaps/open.md +++ b/gaps/open.md @@ -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. diff --git a/gaps/resolved.md b/gaps/resolved.md index 80b6033..00b0dca 100644 --- a/gaps/resolved.md +++ b/gaps/resolved.md @@ -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`, @@ -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. diff --git a/gateway-api/v1.md b/gateway-api/v1.md index 4d244b1..a3744a7 100644 --- a/gateway-api/v1.md +++ b/gateway-api/v1.md @@ -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. --- @@ -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 @@ -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:" + } +} +``` + +`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 @@ -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 @@ -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` @@ -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: @@ -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 @@ -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 @@ -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. diff --git a/gateway-config/v1.md b/gateway-config/v1.md index e525585..b0759df 100644 --- a/gateway-config/v1.md +++ b/gateway-config/v1.md @@ -14,10 +14,26 @@ validation rules. The MQTT contract between runtime and gateway is in Core LAN transport settings. -| Key | Type | Default | Required | Notes | -|---|---|---|---|---| -| `broker_url` | `string` | — | Yes | MQTT broker URL, e.g. `tcp://localhost:1883` or `mqtts://broker:8883`. | -| `heartbeat_interval_s` | `int` | `30` | No | Must be positive. Gateway publishes `ori/gateway/health` on this interval. | +| Key | Type | Default | Required | Notes | +| ---------------------- | ---------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `broker_url` | `string` | — | Yes | MQTT broker URL, e.g. `tcp://localhost:1883` or `mqtts://broker:8883`. | +| `device_ids` | `string[]` | — | Yes | Runtime device IDs this gateway will serve. Must contain at least one entry. IDs must not contain MQTT separators/wildcards or auth delimiters. | +| `heartbeat_interval_s` | `int` | `30` | No | Must be positive. Gateway publishes `ori/gateway/health` on this interval. | + +### `gateway.auth` + +Authenticates runtime-gateway MQTT envelopes with the shared HMAC secret used by +both runtime and gateway. The value in YAML is an environment variable **name**, +not the secret itself. + +| Key | Type | Default | Required | Notes | +| ------------------- | -------- | ----------------------- | -------- | ------------------------------------------------------------------------------------ | +| `enabled` | `bool` | `false` | No | When true, gateway signs `ori/gateway/health` and verifies runtime node heartbeats. | +| `shared_secret_env` | `string` | `GATEWAY_SHARED_SECRET` | No | Environment variable containing the shared HMAC secret. Must not contain whitespace. | + +When enabled, malformed, stale, replayed, retained, or unsigned runtime node +heartbeats are ignored. Reasoning/export/enrichment MQTT handling uses the same +runtime-gateway message-authentication family where implemented. --- @@ -28,30 +44,30 @@ Tier 3 reasoning provider. This section is intentionally separate from `reporting.provider` drives advisory/product features like weekly reports and Tier C enrichment. Credentials from one must never flow into the other. -| Key | Type | Default | Required | Notes | -|---|---|---|---|---| -| `name` | `string` | — | Yes | `echo \| llama_cpp \| cloud_llm` | -| `timeout_ms` | `int` | `10000` | No | Must be positive. Per-request provider timeout for Tier 3 reasoning. | +| Key | Type | Default | Required | Notes | +| ------------ | -------- | ------- | -------- | -------------------------------------------------------------------- | +| `name` | `string` | — | Yes | `echo \| llama_cpp \| cloud_llm` | +| `timeout_ms` | `int` | `10000` | No | Must be positive. Per-request provider timeout for Tier 3 reasoning. | ### `provider.llama_cpp` Required when `provider.name: llama_cpp`. -| Key | Type | Notes | -|---|---|---| -| `url` | `string` | Required. HTTP completion endpoint, e.g. `http://localhost:8080/completion`. | +| Key | Type | Notes | +| ------- | -------- | ------------------------------------------------------------------------------- | +| `url` | `string` | Required. HTTP completion endpoint, e.g. `http://localhost:8080/completion`. | | `model` | `string` | Fallback model name used when `/props` is unreachable or returns no model name. | ### `provider.cloud_llm` Required when `provider.name: cloud_llm`. -| Key | Type | Required | Notes | -|---|---|---|---| -| `vendor` | `string` | Yes | `claude \| openai \| gemini \| deepseek \| openai_compatible` | -| `api_key_env` | `string` | Yes | Name of an environment variable holding the API key. Must not contain whitespace. | -| `model` | `string` | Yes | Model identifier, e.g. `claude-sonnet-4-5`. | -| `base_url` | `string` | When `vendor: openai_compatible` | Custom base URL for OpenAI-compatible endpoints. | +| Key | Type | Required | Notes | +| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- | +| `vendor` | `string` | Yes | `claude \| gemini \| openai \| deepseek \| openai_compatible` | +| `api_key_env` | `string` | Yes | Name of an environment variable holding the API key. Must not contain whitespace. | +| `model` | `string` | Yes | Model identifier, e.g. `claude-sonnet-4-5`. | +| `base_url` | `string` | No | Optional endpoint override. Required only when `vendor: openai_compatible`. Vendor defaults are used when blank. | Cloud provider API keys are gateway-scoped environment variables. They must not appear in runtime config (`ori.yaml`) or be carried inside MQTT reasoning envelopes. @@ -68,29 +84,29 @@ set whenever any reporting feature is enabled. reporting path must not affect Tier 3 reasoning, Tier C approval handling, Tier D safety, or runtime fallback behaviour. -| Key | Type | Default | Required | Notes | -|---|---|---|---|---| -| `provider` | `string` | `""` | When any feature enabled | `gemini` is the only supported value in v1. | +| Key | Type | Default | Required | Notes | +| ---------- | -------- | ------- | ------------------------ | ------------------------------------------- | +| `provider` | `string` | `""` | When any feature enabled | `gemini` is the only supported value in v1. | ### `reporting.gemini` Required when `reporting.provider: gemini` and any reporting feature is enabled. -| Key | Type | Notes | -|---|---|---| +| Key | Type | Notes | +| ------------- | -------- | ---------------------------------------------------------------------------------------- | | `api_key_env` | `string` | Name of an environment variable holding the Gemini API key. Must not contain whitespace. | -| `model` | `string` | Gemini model identifier, e.g. `gemini-2.5-flash`. | +| `model` | `string` | Gemini model identifier, e.g. `gemini-2.5-flash`. | ### `reporting.weekly_report` Periodic site-level summary generation. -| Key | Type | Default | Notes | -|---|---|---|---| -| `enabled` | `bool` | `false` | Enable weekly report generation. | -| `day` | `string` | — | Weekday name: `monday` through `sunday` (case-insensitive). Required when enabled. | -| `time` | `string` | — | 24-hour `HH:MM` format, e.g. `08:00`. Required when enabled. | -| `timezone` | `string` | — | IANA timezone string, e.g. `Africa/Lagos`. Required when enabled. Validated against the Go timezone database. | +| Key | Type | Default | Notes | +| ---------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------- | +| `enabled` | `bool` | `false` | Enable weekly report generation. | +| `day` | `string` | — | Weekday name: `monday` through `sunday` (case-insensitive). Required when enabled. | +| `time` | `string` | — | 24-hour `HH:MM` format, e.g. `08:00`. Required when enabled. | +| `timezone` | `string` | — | IANA timezone string, e.g. `Africa/Lagos`. Required when enabled. Validated against the Go timezone database. | When `enabled: true`, `reporting.provider`, `day`, `time`, and `timezone` must all be set. The gateway generates reports by consuming bounded runtime exports (via @@ -101,14 +117,15 @@ SQLite directly. Advisory enrichment of Tier C operator messages. -| Key | Type | Default | Notes | -|---|---|---|---| +| Key | Type | Default | Notes | +| --------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | `bool` | `false` | Enable Tier C enrichment requests. When enabled, the gateway accepts enrichment requests from the runtime, enriches the operator message with advisory LLM context, and returns an advisory-only response. | Enrichment responses must not change action tier, proposed action, safe default, approval requirement, or any other runtime authority field. Enrichment failure -must not delay or block the Tier C approval workflow. See `gateway-api/v1.md` for -the enrichment MQTT contract (gap G-15: transport wiring is pending). +must not delay or block the Tier C approval workflow. See `gateway-api/v1.md` +for the enrichment MQTT contract. Gateway-side transport wiring is implemented; +runtime client wiring is a runtime-side follow-up. --- @@ -117,10 +134,10 @@ the enrichment MQTT contract (gap G-15: transport wiring is pending). SIM modem availability probe. Used by the heartbeat publisher to report `sim_available` in the `ori/gateway/health` payload. -| Key | Type | Default | Notes | -|---|---|---|---| -| `enabled` | `bool` | `false` | Enable SIM modem probe. | -| `modem_path` | `string` | — | Path to the modem device, e.g. `/dev/ttyUSB0`. Required when `enabled: true`. | +| Key | Type | Default | Notes | +| ------------ | -------- | ------- | ----------------------------------------------------------------------------- | +| `enabled` | `bool` | `false` | Enable SIM modem probe. | +| `modem_path` | `string` | — | Path to the modem device, e.g. `/dev/ttyUSB0`. Required when `enabled: true`. | --- @@ -132,10 +149,10 @@ Fleet coordination client. Currently an inert stub in `internal/fleet/fleet.go`. Not wired in `app.go`. Config keys are validated but the module performs no network operations until wiring is complete. -| Key | Type | Default | Notes | -|---|---|---|---| -| `enabled` | `bool` | `false` | Enable fleet coordination. | -| `cloud_url` | `string` | — | Fleet cloud endpoint URL. Required when `enabled: true`. | +| Key | Type | Default | Notes | +| ----------- | -------- | ------- | -------------------------------------------------------- | +| `enabled` | `bool` | `false` | Enable fleet coordination. | +| `cloud_url` | `string` | — | Fleet cloud endpoint URL. Required when `enabled: true`. | --- @@ -144,19 +161,24 @@ network operations until wiring is complete. ```yaml gateway: broker_url: "tcp://localhost:1883" + device_ids: + - "ori-edge-site-a-01" heartbeat_interval_s: 30 + auth: + enabled: false + shared_secret_env: "GATEWAY_SHARED_SECRET" provider: - name: "echo" # echo | llama_cpp | cloud_llm + name: "echo" # echo | llama_cpp | cloud_llm timeout_ms: 10000 llama_cpp: url: "http://localhost:8080/completion" - model: "llama-3.2-3b-instruct.gguf" # fallback if /props is unreachable + model: "llama-3.2-3b-instruct.gguf" # fallback if /props is unreachable cloud_llm: - vendor: "claude" # claude | openai | gemini | deepseek | openai_compatible - api_key_env: "ANTHROPIC_API_KEY" - model: "claude-sonnet-4-5" - base_url: "" # required only for openai_compatible + vendor: "gemini" # claude | gemini | openai | deepseek | openai_compatible + api_key_env: "GEMINI_API_KEY" + model: "gemini-2.5-flash" + base_url: "" # optional override; required only for openai_compatible reporting: provider: "gemini" diff --git a/runtime-config/v1.md b/runtime-config/v1.md index 6d04225..7c22049 100644 --- a/runtime-config/v1.md +++ b/runtime-config/v1.md @@ -103,6 +103,19 @@ This document captures cross-repo config keys that other Ori repos must align wi When any `protocol=coap` sensors are configured, host allowlist enforcement remains mandatory. +## `actions.alert_outbox` + +| Key | Type | Default | Notes | +|---|---|---|---| +| `retry_interval_minutes` | `float` | `0.5` | Must be `>0`. Background retry cadence for queued alerts. | +| `max_non_tier_d_attempts` | `int` | `10` | Must be `>=1`. Non-Tier-D alerts expire after this many failed attempts. | +| `tier_d_critical_warning_threshold` | `int` | `3` | Must be `>=1`. Tier D alerts never abandon; repeated failure emits critical warnings. | +| `batch_size` | `int` | `50` | `1..1000`. Max queued alerts processed per retry tick. | + +Alert outbox health is exposed in runtime health snapshots so gateway/site health +can surface degraded notification delivery. Notification failure must never affect +physical action execution status. + ## `reasoning.capability_posture` | Key | Type | @@ -127,13 +140,27 @@ When any `protocol=coap` sensors are configured, host allowlist enforcement rema | Key | Type | Notes | |---|---|---| | `enabled` | `bool` | Enables runtime MQTT integration with the LAN gateway. | -| `broker_url` | `string` | MQTT broker URL, e.g. `mqtt://localhost:1883`. Required when gateway reasoning/export integration is enabled. | +| `broker_url` | `string` | MQTT broker URL, e.g. `mqtt://localhost:1883` or `mqtts://broker:8883`. Required when gateway integration is enabled. | | `reasoning.enabled` | `bool` | Enables Tier 3 request/response over `ori/{device_id}/reasoning/*`. | | `reasoning.timeout_ms` | `int` (`>=100`) | Per-phase MQTT timeout for connect and response wait. | Cloud provider configuration is intentionally absent from runtime config. Cloud reasoning providers are gateway backends. +### `gateway.tls` + +| Key | Type | Notes | +|---|---|---| +| `enabled` | `bool` | Enable TLS for runtime-gateway MQTT clients. `mqtts://` also implies TLS. | +| `ca_certfile` | `string` | Optional CA bundle path for private broker CAs. | +| `certfile` | `string` | Optional client certificate path. Required when `keyfile` is set. | +| `keyfile` | `string` | Optional client private key path. | +| `keyfile_password_env` | `string` | Optional environment variable name holding key password. | + +`gateway.tls.insecure_skip_verify` is not supported. Use `ca_certfile` for +self-signed or private broker certificates. TLS protects the transport; HMAC +`gateway.auth` remains the message-authentication boundary. + ### `gateway.auth` | Key | Type | Default | Notes | @@ -153,6 +180,16 @@ When `gateway.auth.enabled: false`, the runtime logs a startup WARNING. An unsig Sensitive export types (`sensor_history`, `action_log`, `reasoning_log`, `tier_c_decision_log`) are encrypted before HMAC signing when enabled. Health exports remain plaintext. +### `gateway.node_heartbeat` + +| Key | Type | Default | Notes | +|---|---|---|---| +| `enabled` | `bool` | `true` | Publishes runtime liveness to `ori/{device_id}/runtime/heartbeat` when `gateway.enabled` is true. | +| `interval_seconds` | `float` | `30` | Must be `>=1`. | + +Runtime node heartbeat payloads are signed when `gateway.auth.enabled` is true, +use `retain=false`, and never route through the sensor EventBus. + ## `reasoning.context_enricher` > Status: Implemented — 2026-06-08 diff --git a/runtime-health/v1.md b/runtime-health/v1.md index b6bd2e5..46aa2a2 100644 --- a/runtime-health/v1.md +++ b/runtime-health/v1.md @@ -58,6 +58,8 @@ Failure: | `sensors` | `array` | Sensor connectivity and staleness snapshot | | `last_alert_timestamps` | `object` | Alert timestamp maps by channel and trigger | | `device_policy` | `object` | Policy state snapshot including `enabled` and version/expiry fields | +| `alert_outbox` | `object` | Queued alert retry posture and config | +| `remote_command_lockout` | `object` | Advisory sender risk state when available | ### `capability_posture` object @@ -86,6 +88,32 @@ Failure: | `last_seen_ms` | `int \| null` | | `stale` | `bool` | +### `alert_outbox` object + +| Field | Type | +|---|---| +| `enabled` | `bool` | +| `pending_count` | `int` | +| `oldest_queued_age_ms` | `int \| null` | +| `retry_interval_minutes` | `float` | +| `max_non_tier_d_attempts` | `int` | +| `tier_d_critical_warning_threshold` | `int` | +| `batch_size` | `int` | +| `error` | `string \| null` | + +### `remote_command_lockout` object + +Advisory-only risk state for remote-command abuse. `locked_out` remains false in +v1 until recovery-safe lockout semantics are defined. + +| Field | Type | +|---|---| +| `enabled` | `bool` | +| `risk_window_ms` | `int` | +| `stale_after_ms` | `int` | +| `incident_sender_limit` | `int` | +| `senders` | `object` | + ## Error Codes Known response error codes: