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
3 changes: 3 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"MD013": false
}
27 changes: 27 additions & 0 deletions gaps/resolved.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,30 @@ is a runtime-side follow-up, not a gateway contract gap.
Resolved: `ori-gateway` runtimeclient exposes `ReasoningLog` over the bounded
runtime export surface. Reporting/cloud consumers can request `reasoning_log`
without reading runtime SQLite directly.

## G-18 — Runtime/gateway MQTT hardening contract

Resolved: `gateway-api/v1.md`, `runtime-config/v1.md`, and `gateway-config/v1.md`
document runtime-gateway HMAC envelopes, verify-only previous secrets, AES-GCM
sensitive export encryption, runtime node heartbeat auth, gateway heartbeat auth,
and production broker posture attestation.

## G-19 — SMS webhook public-ingress hardening

Resolved: `runtime-config/v1.md` documents raw-body HMAC verification, nonce
replay protection, source CIDR requirements, no query-token fallback, and sender
allowlisting for runtime SMS webhook ingress. `gateway-config/v1.md` documents the
gateway SMS webhook signing bridge for providers that cannot sign webhooks.

## G-20 — Site health and weekly report delivery contracts

Resolved: `gateway-api/v1.md` and `gateway-config/v1.md` document the gateway
site-health HTTP projection, runtime posture enrichment, weekly report delivery
channels (`log`, `file`, `cloud`), customer-safe report payload constraints, and
ori-cloud identity ownership rules for report persistence.

## G-21 — Runtime public integration boundary

Resolved: `runtime-config/v1.md` documents the typed `ori.integration` rule
evaluation boundary and `py.typed` package marker used by product/demo consumers
such as `ori-energy`.
78 changes: 73 additions & 5 deletions gateway-api/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,19 @@ Published on `ori/gateway/health` every 30 seconds by default.
{
"status": "starting|healthy|degraded",
"uptime_s": 12.5,
"provider": "echo|llama_cpp|claude|other",
"provider": "echo|llama_cpp|claude|gemini|other",
"sim_available": false,
"timestamp_ms": 0,
"webhook_bridge": {
"enabled": false,
"ready": false,
"loopback_only": true,
"source_cidrs_configured": false,
"provider_cidr_count": 0,
"runtime_target_configured": false,
"body_limit_bytes": 65536,
"request_timeout_ms": 3000
},
"auth": {
"scheme": "hmac-sha256",
"signed_at_ms": 0,
Expand All @@ -115,6 +125,10 @@ Published on `ori/gateway/health` every 30 seconds by default.

`uptime_s` is a floating-point number of seconds to match runtime health semantics.

`webhook_bridge` is optional and present when the gateway reports SMS webhook
bridge posture. It deliberately excludes URLs, env var names, CIDR values,
tokens, HMAC secrets, and provider-specific headers.

The `auth` object is present when `gateway.auth.enabled: true` and omitted (`auth` field absent) when `false`. Runtimes with auth enabled reject heartbeats that omit or fail the `auth` block. Runtimes with auth disabled accept unsigned heartbeats (see `gateway.auth` in `runtime-config/v1.md`).

### Heartbeat Auth Envelope
Expand Down Expand Up @@ -471,9 +485,21 @@ safe default, approval requirement, or any other runtime authority field.
> export surfaces and delivers them via configured reporting provider.

The gateway generates periodic site-level summaries using bounded runtime exports.
Report content includes action log summary, sensor history aggregates, and Tier C
decision context. Report delivery (channel, frequency, format) is configured in
`gateway.yaml` under `reporting.weekly_report`.
Report content includes action log summary, sensor history aggregates, Tier C
decision context, and safe runtime posture summaries. Report delivery (channel,
frequency, format) is configured in `gateway.yaml` under
`reporting.weekly_report.delivery`.

Implemented delivery channels:

- `log`: always active; writes operational metadata to gateway logs.
- `file`: optional local JSON artifact using the customer-safe report payload.
- `cloud`: optional HTTPS POST to the ori-cloud ingest endpoint using the same
customer-safe payload and `Authorization: Bearer <api-key>`.

Cloud delivery authentication identifies the gateway/site to ori-cloud.
`customer_name` and `site_name` are display labels, not persistence authority;
ori-cloud must key report persistence by authenticated gateway/site identity.

### Invariants

Expand All @@ -482,5 +508,47 @@ decision context. Report delivery (channel, frequency, format) is configured in
- Report generation must not block reasoning request handling.
- Report failure must not affect Tier C approval handling, Tier D safety, or
runtime fallback behaviour.
- Cloud reporting credentials (`reporting.gemini.*`) are gateway-scoped. They
- Cloud reporting credentials (`reporting.gemini.*`,
`reporting.weekly_report.delivery.cloud.auth_env`) are gateway-scoped. They
must not appear in runtime config or be passed through reasoning envelopes.
- Customer-safe report payloads must exclude runtime device IDs, raw metadata,
lockout risk maps, MQTT URLs, credentials, filesystem paths, and phone numbers.

## Site Health HTTP Projection

> Status: Implemented in `ori-gateway` (`internal/site`, `cmd/ori-gateway/app.go`).

The gateway may expose a local diagnostic HTTP endpoint for site-level health.
This is not a runtime MQTT topic and is not pulled directly by ori-cloud by
default. Future cloud sync should push the same safe projection from the gateway
to ori-cloud rather than requiring ori-cloud to reach into a LAN/loopback address.

- Default listen address: `127.0.0.1:8765`
- Endpoint: `GET /health`
- Default state: disabled
- Response body: JSON `SiteHealth` projection

The projection includes expected runtime device IDs, heartbeat freshness, stale
state, active trigger counts, gateway operational posture, and runtime security
posture fetched via the runtime health export surface. It must not expose
secrets, tokens, MQTT URLs, filesystem paths, phone numbers, provider CIDRs,
raw active trigger names, or remote-command lockout risk maps.

Per-node runtime posture may include:

- broker hardening posture (`broker_hardening`)
- state-store encryption posture (`encryption`)
- alert outbox posture (`alert_outbox`)

`LockoutRiskLevels` is intentionally excluded because its map keys are sender
identities such as phone numbers.

## Contract Fixtures

> Status: Implemented in `ori-gateway` under `internal/contracts/testdata`.

Gateway contract fixtures are byte-stable golden JSON files. Tests unmarshal and
marshal request, response, error response, gateway heartbeat, runtime heartbeat,
and enrichment fixtures back to the exact same canonical bytes. Equivalent public
fixtures must be mirrored or referenced from `ori-sdk` when SDK contract coverage
is updated.
139 changes: 124 additions & 15 deletions gateway-config/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,26 @@ 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. |
| Key | Type | Default | Required | Notes |
| ---------------------------- | -------- | ----------------------- | -------- | ----- |
| `enabled` | `bool` | `false` | No | Signs gateway health and verifies runtime node heartbeats. |
| `shared_secret_env` | `string` | `GATEWAY_SHARED_SECRET` | No | Env var containing the current shared HMAC secret. |
| `previous_shared_secret_env` | `string` | `""` | No | Verify-only previous HMAC secret during rotation. |

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.

### `gateway.encryption`

Decrypts AES-GCM encrypted sensitive runtime export responses (`sensor_history`,
`action_log`, `reasoning_log`, and `tier_c_decision_log`). Requires
`gateway.auth.enabled: true`. Health exports remain plaintext operational posture.

| Key | Type | Default | Required | Notes |
| --------- | ------ | ------- | -------- | ----- |
| `enabled` | `bool` | `false` | No | Enables decryption for sensitive runtime exports. |

---

## `provider`
Expand Down Expand Up @@ -96,30 +107,63 @@ Required when `reporting.provider: gemini` and any reporting feature is enabled.
| ------------- | -------- | ---------------------------------------------------------------------------------------- |
| `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`. |
| `base_url` | `string` | Optional HTTPS endpoint override. Blank uses the Gemini default endpoint. |

### `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`. |
| `time` | `string` | — | 24-hour `HH:MM` format, e.g. `08:00`. |
| `timezone` | `string` | — | IANA timezone string, e.g. `Africa/Lagos`. |
| `device_id` | `string` | — | Runtime device ID whose exports feed the report. |
| `sensor_ids` | `string[]` | — | Runtime sensor IDs to include in report history. |
| `customer_name` | `string` | `""` | Optional display label, not persistence authority. |
| `site_name` | `string` | `""` | Optional display label and local file slug source. |

When `enabled: true`, `reporting.provider`, `day`, `time`, and `timezone` must all
be set. The gateway generates reports by consuming bounded runtime exports (via
the MQTT export surface defined in `gateway-api/v1.md`). It does not read runtime
SQLite directly.

#### `reporting.weekly_report.delivery`

Weekly report delivery channels. `log` delivery is always active and has no
config. File and cloud delivery are optional. Delivery failure is advisory and
does not stop future report schedules.

##### `reporting.weekly_report.delivery.file`

| Key | Type | Default | Notes |
| --------- | -------- | ------- | ----- |
| `enabled` | `bool` | `false` | Write a local customer-safe JSON artifact. |
| `path` | `string` | `""` | Absolute existing directory. Gateway never creates it. |

Files are named `weekly-{site_slug}-{YYYY-MM-DD}.json`.

##### `reporting.weekly_report.delivery.cloud`

| Key | Type | Default | Notes |
| ---------- | -------- | ------- | ----- |
| `enabled` | `bool` | `false` | POST the customer-safe report payload to ori-cloud. |
| `endpoint` | `string` | `""` | Absolute HTTPS ingest URL. Must not contain embedded credentials. |
| `auth_env` | `string` | `""` | Environment variable name holding the ori-cloud API key. Required when enabled. |

Cloud delivery uses `Authorization: Bearer <api-key>`. The API key must never
appear in errors or logs. Non-2xx responses are surfaced as errors without
logging response bodies. ori-cloud must key persistence by authenticated
gateway/site identity, not by `customer_name` or `site_name`.

### `reporting.tier_c_enrichment`

Advisory enrichment of Tier C operator messages.

| 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. |
| Key | Type | Default | Notes |
| --------- | ------ | ------- | ----- |
| `enabled` | `bool` | `false` | Enable advisory Tier C enrichment requests. |

Enrichment responses must not change action tier, proposed action, safe default,
approval requirement, or any other runtime authority field. Enrichment failure
Expand All @@ -129,6 +173,44 @@ runtime client wiring is a runtime-side follow-up.

---

## `webhook_bridge`

Optional provider-ingress signing bridge for SMS providers that cannot sign raw
webhook bodies. Production topology is provider -> gateway bridge -> runtime
localhost webhook. The bridge validates source CIDRs, signs the raw body with
HMAC-SHA256, forwards only approved headers, and applies bounded body/time limits.
Non-loopback binds require `provider_source_cidrs`.

| Key | Type | Default | Notes |
| ---------------------- | ---------- | ------- | ----- |
| `enabled` | `bool` | `false` | Enable bridge server. |
| `listen_addr` | `string` | `127.0.0.1:8090` | Bind address. |
| `path` | `string` | `/webhooks/sms/africastalking` | Provider-facing path. |
| `target_url` | `string` | Runtime localhost URL | Runtime SMS webhook URL. |
| `provider_source_cidrs` | `string[]` | `[]` | Required for non-loopback; rejects catch-all CIDRs. |
| `runtime_token_env` | `string` | `ORI_SMS_WEBHOOK_TOKEN` | Env var for runtime webhook bearer token. |
| `hmac_secret_env` | `string` | `ORI_SMS_WEBHOOK_HMAC_SECRET` | Env var for raw-body HMAC signing secret. |
| `request_timeout_ms` | `int` | `3000` | Forwarding timeout. |
| `max_body_bytes` | `int` | `65536` | Max provider request body. |

---

## `site_health`

Optional local HTTP projection of site health. Intended for local diagnostics and
future gateway-initiated cloud sync, not for ori-cloud to pull directly from LAN.

| Key | Type | Default | Notes |
| ------------- | -------- | ------- | ----- |
| `enabled` | `bool` | `false` | Start `GET /health` server. |
| `listen_addr` | `string` | `127.0.0.1:8765` | Defaults to loopback. |

The JSON projection excludes secrets, URLs, filesystem paths, phone numbers, raw
trigger names, and remote-command lockout risk maps. Runtime posture enrichment
is fetched asynchronously via the runtime health export surface.

---

## `sim`

SIM modem availability probe. Used by the heartbeat publisher to report
Expand All @@ -146,8 +228,8 @@ SIM modem availability probe. Used by the heartbeat publisher to report
> Status: Stub — deferred post-v1.

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.
The gateway constructs the client but disabled fleet performs no HTTP, DNS, or
auth work. Full ori-cloud fleet sync is deferred.

| Key | Type | Default | Notes |
| ----------- | -------- | ------- | -------------------------------------------------------- |
Expand Down Expand Up @@ -190,16 +272,43 @@ reporting:
day: monday
time: "08:00"
timezone: Africa/Lagos
device_id: "ori-runtime-dev-01"
sensor_ids: ["current-main"]
customer_name: ""
site_name: ""
delivery:
file:
enabled: false
path: ""
cloud:
enabled: false
endpoint: ""
auth_env: "ORI_CLOUD_API_KEY"
tier_c_enrichment:
enabled: false

sim:
enabled: false
modem_path: "/dev/ttyUSB0"

webhook_bridge:
enabled: false
listen_addr: "127.0.0.1:8090"
path: "/webhooks/sms/africastalking"
target_url: "http://127.0.0.1:8080/webhooks/sms/africastalking"
provider_source_cidrs: []
runtime_token_env: "ORI_SMS_WEBHOOK_TOKEN"
hmac_secret_env: "ORI_SMS_WEBHOOK_HMAC_SECRET"
request_timeout_ms: 3000
max_body_bytes: 65536

fleet:
enabled: false
cloud_url: ""

site_health:
enabled: false
listen_addr: "127.0.0.1:8765"
```

---
Expand Down
Loading
Loading