Implement private Q2 signal contract and durable reference receiver
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
parent
31884baf4e
commit
cecef79f31
14 changed files with 655 additions and 20 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -3,3 +3,10 @@
|
|||
.claude/*
|
||||
!.claude/rules/
|
||||
!.claude/rules/*.md
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
|
|
|||
58
AGENTS.md
58
AGENTS.md
|
|
@ -121,7 +121,56 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||
|
||||
---
|
||||
|
||||
{CREDENTIAL_ROUTING}
|
||||
## Credential and access routing
|
||||
|
||||
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
|
||||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=railiance-platform` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
|
||||
every repo's agent instructions because it is high-frequency, high-risk, and easy to
|
||||
get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
|
||||
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
|
|
@ -197,3 +246,10 @@ To create a new workplan:
|
|||
1. Write the file following the format above
|
||||
2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or
|
||||
State Hub API is unavailable.
|
||||
|
||||
## Local implementation workflow — 2026-09-06
|
||||
|
||||
Python 3.11+ standard library; no install step. Run
|
||||
`python3 -m unittest discover -s tests -v`. See docs/signal-contract.md for the
|
||||
private CLI workflow. No live monitoring deployment target exists. RTEL-WP-0002
|
||||
owns runtime/recipient acceptance; local inbox evidence is not notification proof.
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -3,8 +3,10 @@
|
|||
Observability for Railiance providing monitoring, metrics, alerting etc to
|
||||
enable the self-organizing control loop.
|
||||
|
||||
**Status: seeded.** `INTENT.md` is written; there is no implementation yet. The
|
||||
live cluster currently has no monitoring namespace — this is greenfield.
|
||||
**Status: private reference implementation.** A versioned signal contract,
|
||||
SQLite receipt/inbox CLI and platform adapter are implemented and tested.
|
||||
No monitoring runtime or notification schedule is installed by this work.
|
||||
See [the contract and workflow](docs/signal-contract.md).
|
||||
|
||||
## Why this repo exists
|
||||
|
||||
|
|
@ -23,9 +25,11 @@ carries and surfaces their output.
|
|||
3. [`AGENTS.md`](AGENTS.md) — session protocol
|
||||
4. `workplans/`
|
||||
|
||||
## Open question
|
||||
## Placement and verification
|
||||
|
||||
Stack-dimension placement (S3 platform capability, or a Quality-dimension
|
||||
concern outside the stack) is **undecided** and belongs to `railiance-master`.
|
||||
See `INTENT.md` → "Open Placement Question". It is called out explicitly so this
|
||||
repo does not drift into the unplaced state `railiance-forge` ended up in.
|
||||
Quality dimension, Q2 Observability, as recorded in INTENT/SCOPE. Monitoring
|
||||
workload packaging remains outside this repo.
|
||||
|
||||
Run `python3 -m unittest discover -s tests -v` (Python 3.11+, no dependencies).
|
||||
Live acceptance is tracked by RTEL-WP-0002-T04; local receipts are not external
|
||||
operator delivery proof.
|
||||
|
|
|
|||
6
SCOPE.md
6
SCOPE.md
|
|
@ -68,8 +68,10 @@ output.
|
|||
|
||||
## Current State
|
||||
|
||||
- Status: **seeded** — `INTENT.md` written 2026-08-11, no implementation yet
|
||||
- The live cluster has no monitoring namespace; this is greenfield
|
||||
- Status: **private reference implementation** — contract, durable local inbox,
|
||||
platform adapter and missing-emission checks implemented September 6
|
||||
- No deployed monitoring runtime or external delivery acceptance established;
|
||||
live work is tracked in RTEL-WP-0002-T04
|
||||
- Placement: **Quality dimension, Q2 Observability** — canon defines Q2 as
|
||||
"telemetry and monitoring: metrics, logs, traces". Ratification sits with
|
||||
`railiance-master`; the question is answered, not open
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@
|
|||
|
||||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | RTELE-WP-0001 | proposed | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTELE-WP-0001-T01 | todo | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTELE-WP-0001-T02 | todo | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTELE-WP-0001-T03 | todo | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| workplan | RTEL-WP-0002 | active | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||
| workplan | RTELE-WP-0001 | finished | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTEL-WP-0002-T01 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||
| task | RTEL-WP-0002-T02 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||
| task | RTEL-WP-0002-T03 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||
| task | RTEL-WP-0002-T04 | wait | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||
| task | RTELE-WP-0001-T01 | done | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTELE-WP-0001-T02 | done | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
| task | RTELE-WP-0001-T03 | done | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||
|
|
|
|||
34
contracts/platform-assurance.json
Normal file
34
contracts/platform-assurance.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"schema": "railiance-telemetry.stream.v1",
|
||||
"stream": "railiance-platform.service-assurance",
|
||||
"producer": "railiance-platform",
|
||||
"recipient": "railiance-platform-operator",
|
||||
"signals": [
|
||||
"apps-pg.ready",
|
||||
"apps-pg.backup",
|
||||
"apps-pg.wal",
|
||||
"apps-pg.restore",
|
||||
"apps-pg.headroom",
|
||||
"platform-pg.ready",
|
||||
"platform-pg.backup",
|
||||
"platform-pg.wal",
|
||||
"platform-pg.restore",
|
||||
"platform-pg.headroom",
|
||||
"platform-pg-2.ready",
|
||||
"platform-pg-2.backup",
|
||||
"platform-pg-2.wal",
|
||||
"platform-pg-2.restore",
|
||||
"platform-pg-2.headroom",
|
||||
"openbao.seal",
|
||||
"openbao.snapshot",
|
||||
"openbao.restore",
|
||||
"offsite.upload",
|
||||
"offsite.restore",
|
||||
"eso.ready",
|
||||
"eso.refresh",
|
||||
"forgejo-db.restore"
|
||||
],
|
||||
"max_event_age_seconds": 900,
|
||||
"heartbeat_seconds": 900,
|
||||
"retention_days": 30
|
||||
}
|
||||
72
docs/signal-contract.md
Normal file
72
docs/signal-contract.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Private signal contract v1
|
||||
|
||||
Q2 owns carriage and visibility; producers own signal meaning. This initial
|
||||
contract and CLI receiver are a local reference implementation, not a deployed
|
||||
monitoring package or authenticated network service.
|
||||
|
||||
The stream contract pins producer, stream, exact signal names, intended local
|
||||
recipient, freshness budget, heartbeat budget and retention. See
|
||||
`contracts/platform-assurance.json`. Its 15-minute budgets and 30-day retention
|
||||
are proposed defaults pending runtime/operator acceptance. The recipient
|
||||
`railiance-platform-operator` names an intended role, not a confirmed person or
|
||||
external messaging address. No notification is sent by these tools.
|
||||
|
||||
An event has exactly `schema` (`railiance-telemetry.signal.v1`), canonical UUID
|
||||
`id`, `stream`, `producer`, timezone-aware `observed_at`, and `states`. Every
|
||||
registered signal is present, with one of healthy/stale/missing/unavailable/
|
||||
failed. There are no values, logs, URLs or free-text details. Duplicate JSON
|
||||
keys, unknown fields, payloads over 32 KiB, wrong producer/stream/signal names,
|
||||
future/expired events and out-of-order events are rejected. Local process/file
|
||||
access is the trust boundary; producer names are not authentication. Do not
|
||||
expose this CLI through an unauthenticated HTTP wrapper.
|
||||
|
||||
Acceptance commits an event and an operator notice in one SQLite transaction.
|
||||
The returned receipt says `local-inbox-only`. Reusing an ID with identical data
|
||||
is idempotent and does not advance liveness; changed data under that ID fails.
|
||||
Equal timestamps under new IDs also fail. Retry the same event file, not a newly
|
||||
translated event. Contract changes require an explicit database migration.
|
||||
|
||||
`check` detects both never-seen and expired emissions, based on original event
|
||||
time. It suppresses repeated missing-emission notices until a new event arrives.
|
||||
An independent accepted scheduler must run it. If that scheduler or this receiver
|
||||
stops, these tools cannot report their own absence: runtime acceptance must add
|
||||
an external receiver heartbeat check. Inbox reads never acknowledge implicitly.
|
||||
`ack` records explicit local operator acknowledgment; it proves no external
|
||||
notification. Run in a private directory (`umask 077`), one database per stream.
|
||||
|
||||
Retention is an explicit `prune` operation: retain at least the contract's days,
|
||||
all unacknowledged notices/events and the latest replay/absence anchors. Pending
|
||||
acknowledgments can extend retention indefinitely. At 10,000 events ingestion
|
||||
fails rather than silently dropping evidence. Monitor capacity and prune through
|
||||
the approved owner; no background expiry is installed. This is operational
|
||||
storage, not immutable audit custody or independently backed-up evidence.
|
||||
|
||||
## Local workflow
|
||||
|
||||
Python 3.11+ standard library only. From the repository root:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
umask 077
|
||||
mkdir -p /tmp/rtel-private
|
||||
python3 scripts/platform_event.py --contract contracts/platform-assurance.json /path/to/fresh-platform-report.json > /tmp/rtel-private/event.json
|
||||
python3 scripts/receiver.py --contract contracts/platform-assurance.json --database /tmp/rtel-private/receiver.db ingest /tmp/rtel-private/event.json
|
||||
python3 scripts/receiver.py --contract contracts/platform-assurance.json --database /tmp/rtel-private/receiver.db check
|
||||
python3 scripts/receiver.py --contract contracts/platform-assurance.json --database /tmp/rtel-private/receiver.db inbox
|
||||
```
|
||||
|
||||
The platform adapter consumes the existing assurance evaluator report, preserves
|
||||
`evaluated_at` as producer observation time, and carries each existing state.
|
||||
Backup/restore age remains S3's classification; Q2 does not re-age those receipts.
|
||||
The adapter does not change the source report's unmonitored/unsupported status.
|
||||
Its signal inventory is pinned to platform source at initial review; changes
|
||||
require coordinated contract review and tests.
|
||||
|
||||
## Required live acceptance
|
||||
|
||||
RTEL-WP-0002-T04 tracks runtime/package owner selection, private authenticated
|
||||
execution, durable storage/backup, confirmed operator recipient, accepted cadence
|
||||
and retention, independent receiver-watchdog delivery, and controlled failure /
|
||||
producer-absence delivery acknowledged by that operator. Request scope already
|
||||
covers reference implementation; no live scheduler or messaging authorization
|
||||
is inferred. RPF-WP-0036-T04 stays open until those receipts exist.
|
||||
15
history/2026-09-06-local-receiver-proof.json
Normal file
15
history/2026-09-06-local-receiver-proof.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"schema": "railiance-telemetry.local-acceptance.v1",
|
||||
"recorded_at": "2026-09-06T17:06:47.655166+00:00",
|
||||
"scope": "controlled local fixture; separate CLI processes; no production notification",
|
||||
"acceptance": {
|
||||
"status": "accepted",
|
||||
"id": "f5dc8e8c-6462-4224-8be6-df440cb7d322",
|
||||
"delivery": "local-inbox-only"
|
||||
},
|
||||
"recipient": "test-operator",
|
||||
"failure_visible": true,
|
||||
"missing_emission_visible": true,
|
||||
"watchdog_invocation": "manual test, not installed cadence",
|
||||
"temporary_storage_removed": true
|
||||
}
|
||||
22
history/2026-09-06-signal-contract-implementation.md
Normal file
22
history/2026-09-06-signal-contract-implementation.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# First receiving contract — 2026-09-06
|
||||
|
||||
Implemented under RTEL-WP-0002 in response to RPF-WP-0036-T04. The former
|
||||
bootstrap plan is finished; its live residual is RTEL-WP-0002-T04.
|
||||
|
||||
Delivered strict metadata-only v1 stream/event contracts, a standard-library
|
||||
SQLite receipt/inbox CLI, producer absence checks, explicit acknowledgment and
|
||||
retention, and an adapter preserving platform assurance classifications and
|
||||
evaluation time. Nine tests pass. A separate-process CLI fixture proves failure
|
||||
and missing-emission visibility after restart; see the adjacent JSON receipt.
|
||||
The proof uses a test recipient and temporary storage, not production delivery.
|
||||
|
||||
README now agrees with INTENT/SCOPE on Q2 placement and describes the implemented
|
||||
workflow. Historical plan identity/UUIDs were preserved. Existing classification
|
||||
says infotech while AGENTS/workplan metadata uses financials; no domain identity
|
||||
migration was inferred from this implementation request.
|
||||
|
||||
Remaining live task: select package/runtime owner and private authenticated
|
||||
execution; accept recipient, cadence, retention/storage/backup; install scheduler
|
||||
and independent receiver watchdog; prove controlled failure and absence delivery
|
||||
acknowledged by the actual operator. No public endpoint, deployment, outgoing
|
||||
notification, credential retrieval or monitoring package was introduced.
|
||||
44
scripts/platform_event.py
Normal file
44
scripts/platform_event.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Translate S3-owned state classifications without redefining their meaning."""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from receiver import read_json, instant, STATES
|
||||
|
||||
|
||||
def translate(report, contract):
|
||||
if (set(report) != {'schema', 'cluster_uid', 'evaluated_at', 'signals', 'transport',
|
||||
'guarantees', 'threshold_status', 'healthy'}
|
||||
or report['schema'] != 'railiance-platform.assurance-signal.v1'
|
||||
or report['cluster_uid'] != 'a553c742-0115-43d4-99a4-a5ca56fe0786'
|
||||
or report['transport'] != 'unmonitored'
|
||||
or report['guarantees'] != 'unsupported'
|
||||
or report['threshold_status'] != 'local-diagnostic-only'
|
||||
or set(report['signals']) != set(contract['signals'])):
|
||||
raise ValueError('unexpected platform report')
|
||||
instant(report['evaluated_at'])
|
||||
states = {}
|
||||
for name, value in report['signals'].items():
|
||||
if (set(value) != {'state', 'owner'} or value['state'] not in STATES
|
||||
or value['owner'] not in ('railiance-platform', 'rapp-postgres')):
|
||||
raise ValueError('unexpected signal')
|
||||
states[name] = value['state']
|
||||
if type(report['healthy']) is not bool or report['healthy'] != all(s == 'healthy' for s in states.values()):
|
||||
raise ValueError('inconsistent summary')
|
||||
return dict(schema='railiance-telemetry.signal.v1', id=str(uuid.uuid4()),
|
||||
stream=contract['stream'], producer=contract['producer'],
|
||||
observed_at=report['evaluated_at'], states=states)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--contract', required=True, type=Path)
|
||||
p.add_argument('report', type=Path)
|
||||
a = p.parse_args()
|
||||
try:
|
||||
print(json.dumps(translate(read_json(a.report), read_json(a.contract))))
|
||||
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
||||
print(json.dumps({'status': 'rejected', 'error': 'invalid-platform-report'}))
|
||||
sys.exit(2)
|
||||
189
scripts/receiver.py
Normal file
189
scripts/receiver.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Private reference receiver. No listener, credentials or notification sending."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
STATES = {'healthy', 'stale', 'missing', 'unavailable', 'failed'}
|
||||
MAX_BYTES = 32768
|
||||
|
||||
|
||||
def instant(value):
|
||||
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
if parsed.tzinfo is None:
|
||||
raise ValueError('timezone required')
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def strict_json(raw):
|
||||
def pairs(items):
|
||||
result = {}
|
||||
for key, value in items:
|
||||
if key in result:
|
||||
raise ValueError('duplicate field')
|
||||
result[key] = value
|
||||
return result
|
||||
if len(raw) > MAX_BYTES:
|
||||
raise ValueError('payload too large')
|
||||
return json.loads(raw, object_pairs_hook=pairs)
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with path.open('rb') as stream:
|
||||
return strict_json(stream.read(MAX_BYTES + 1))
|
||||
|
||||
|
||||
def contract_check(c):
|
||||
if set(c) != {'schema', 'stream', 'producer', 'recipient', 'signals',
|
||||
'max_event_age_seconds', 'heartbeat_seconds', 'retention_days'}:
|
||||
raise ValueError('contract fields')
|
||||
if c['schema'] != 'railiance-telemetry.stream.v1':
|
||||
raise ValueError('contract version')
|
||||
for key in ('stream', 'producer', 'recipient'):
|
||||
if not isinstance(c[key], str) or not c[key] or len(c[key]) > 100:
|
||||
raise ValueError('identity missing')
|
||||
if (not isinstance(c['signals'], list) or not c['signals']
|
||||
or not all(isinstance(s, str) and 0 < len(s) <= 100 for s in c['signals'])
|
||||
or len(c['signals']) != len(set(c['signals']))):
|
||||
raise ValueError('signal inventory')
|
||||
for key in ('max_event_age_seconds', 'heartbeat_seconds', 'retention_days'):
|
||||
if type(c[key]) is not int or not 0 < c[key] <= 31536000:
|
||||
raise ValueError('invalid budget')
|
||||
|
||||
|
||||
class Receiver:
|
||||
def __init__(self, database, contract):
|
||||
contract_check(contract)
|
||||
self.contract = contract
|
||||
self.db = sqlite3.connect(database, timeout=10)
|
||||
self.db.executescript('''
|
||||
CREATE TABLE IF NOT EXISTS binding (id INTEGER PRIMARY KEY, digest TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY, digest TEXT NOT NULL, emitted REAL NOT NULL,
|
||||
received REAL NOT NULL, states TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS notices (
|
||||
id INTEGER PRIMARY KEY, kind TEXT NOT NULL, created REAL NOT NULL,
|
||||
event_id TEXT, acknowledged REAL);
|
||||
''')
|
||||
digest = hashlib.sha256(json.dumps(contract, sort_keys=True).encode()).hexdigest()
|
||||
with self.db:
|
||||
self.db.execute('INSERT OR IGNORE INTO binding VALUES (1, ?)', (digest,))
|
||||
if self.db.execute('SELECT digest FROM binding WHERE id=1').fetchone()[0] != digest:
|
||||
self.db.close()
|
||||
raise ValueError('contract drift: explicit migration required')
|
||||
|
||||
def close(self):
|
||||
self.db.close()
|
||||
|
||||
def ingest(self, event, now):
|
||||
c = self.contract
|
||||
if set(event) != {'schema', 'id', 'stream', 'producer', 'observed_at', 'states'}:
|
||||
raise ValueError('event fields')
|
||||
if (event['schema'] != 'railiance-telemetry.signal.v1'
|
||||
or event['stream'] != c['stream'] or event['producer'] != c['producer']
|
||||
or str(uuid.UUID(event['id'])) != event['id']):
|
||||
raise ValueError('event identity')
|
||||
states = event['states']
|
||||
if (not isinstance(states, dict) or set(states) != set(c['signals'])
|
||||
or not all(isinstance(s, str) and s in STATES for s in states.values())):
|
||||
raise ValueError('signal scope')
|
||||
emitted = instant(event['observed_at']).timestamp()
|
||||
age = now.timestamp() - emitted
|
||||
if not 0 <= age <= c['max_event_age_seconds']:
|
||||
raise ValueError('stale or future event')
|
||||
digest = hashlib.sha256(json.dumps(event, sort_keys=True).encode()).hexdigest()
|
||||
with self.db:
|
||||
self.db.execute('BEGIN IMMEDIATE')
|
||||
existing = self.db.execute('SELECT digest FROM events WHERE id=?', (event['id'],)).fetchone()
|
||||
if existing:
|
||||
if existing[0] != digest:
|
||||
raise ValueError('event id collision')
|
||||
return {'status': 'duplicate', 'id': event['id']}
|
||||
latest = self.db.execute('SELECT MAX(emitted) FROM events').fetchone()[0]
|
||||
if latest is not None and emitted <= latest:
|
||||
raise ValueError('out of order event')
|
||||
if self.db.execute('SELECT COUNT(*) FROM events').fetchone()[0] >= 10000:
|
||||
raise ValueError('capacity reached; explicit retention required')
|
||||
self.db.execute('INSERT INTO events VALUES (?, ?, ?, ?, ?)',
|
||||
(event['id'], digest, emitted, now.timestamp(), json.dumps(states, sort_keys=True)))
|
||||
self.db.execute('INSERT INTO notices(kind, created, event_id) VALUES (?, ?, ?)',
|
||||
('healthy' if all(s == 'healthy' for s in states.values()) else 'unhealthy',
|
||||
now.timestamp(), event['id']))
|
||||
return {'status': 'accepted', 'id': event['id'], 'delivery': 'local-inbox-only'}
|
||||
|
||||
def check(self, now):
|
||||
"""Must be invoked by an independent scheduler; absence is not self-executing."""
|
||||
with self.db:
|
||||
self.db.execute('BEGIN IMMEDIATE')
|
||||
last = self.db.execute('SELECT MAX(emitted) FROM events').fetchone()[0]
|
||||
if last is not None and now.timestamp() < last:
|
||||
raise ValueError('clock moved backwards')
|
||||
absent = last is None or now.timestamp() - last > self.contract['heartbeat_seconds']
|
||||
previous = self.db.execute('SELECT kind FROM notices ORDER BY id DESC LIMIT 1').fetchone()
|
||||
if absent and (previous is None or previous[0] != 'missing-emission'):
|
||||
self.db.execute('INSERT INTO notices(kind, created) VALUES (?, ?)',
|
||||
('missing-emission', now.timestamp()))
|
||||
return {'state': 'missing-emission' if absent else 'receiving',
|
||||
'recipient': self.contract['recipient'], 'delivery': 'local-inbox-only'}
|
||||
|
||||
def inbox(self):
|
||||
rows = self.db.execute('''SELECT n.id, n.kind, n.created, n.event_id, e.states
|
||||
FROM notices n LEFT JOIN events e ON e.id=n.event_id
|
||||
WHERE n.acknowledged IS NULL ORDER BY n.id''').fetchall()
|
||||
return {'recipient': self.contract['recipient'], 'delivery': 'local-inbox-only',
|
||||
'notices': [dict(id=r[0], kind=r[1], created_at=datetime.fromtimestamp(r[2], timezone.utc).isoformat(),
|
||||
event_id=r[3], states=json.loads(r[4]) if r[4] else None) for r in rows]}
|
||||
|
||||
def ack(self, notice_id, now):
|
||||
with self.db:
|
||||
updated = self.db.execute('UPDATE notices SET acknowledged=? WHERE id=? AND acknowledged IS NULL',
|
||||
(now.timestamp(), notice_id)).rowcount
|
||||
return {'acknowledged': bool(updated), 'id': notice_id}
|
||||
|
||||
def prune(self, now):
|
||||
cutoff = now.timestamp() - self.contract['retention_days'] * 86400
|
||||
with self.db:
|
||||
self.db.execute('BEGIN IMMEDIATE')
|
||||
# Keep the latest observation and notice as replay/absence anchors.
|
||||
self.db.execute('''DELETE FROM notices WHERE created < ? AND acknowledged IS NOT NULL
|
||||
AND id != (SELECT MAX(id) FROM notices)''', (cutoff,))
|
||||
deleted = self.db.execute('''DELETE FROM events WHERE received < ?
|
||||
AND emitted != (SELECT MAX(emitted) FROM events)
|
||||
AND id NOT IN (SELECT event_id FROM notices WHERE event_id IS NOT NULL)''', (cutoff,)).rowcount
|
||||
return {'events_pruned': deleted}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--contract', required=True, type=Path)
|
||||
p.add_argument('--database', required=True, type=Path)
|
||||
sub = p.add_subparsers(dest='command', required=True)
|
||||
sub.add_parser('ingest').add_argument('event', type=Path)
|
||||
for name in ('check', 'inbox', 'prune'):
|
||||
sub.add_parser(name)
|
||||
sub.add_parser('ack').add_argument('notice_id', type=int)
|
||||
a = p.parse_args()
|
||||
receiver = None
|
||||
try:
|
||||
receiver = Receiver(a.database, read_json(a.contract))
|
||||
now = datetime.now(timezone.utc)
|
||||
if a.command == 'ingest': result = receiver.ingest(read_json(a.event), now)
|
||||
elif a.command == 'inbox': result = receiver.inbox()
|
||||
elif a.command == 'ack': result = receiver.ack(a.notice_id, now)
|
||||
else: result = getattr(receiver, a.command)(now)
|
||||
print(json.dumps(result))
|
||||
return 0
|
||||
except (ValueError, TypeError, KeyError, AttributeError, OSError, sqlite3.Error):
|
||||
print(json.dumps({'status': 'rejected', 'error': 'invalid-input-or-unavailable'}))
|
||||
return 2
|
||||
finally:
|
||||
if receiver is not None: receiver.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
102
tests/test_receiver.py
Normal file
102
tests/test_receiver.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from datetime import datetime, timezone, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import uuid
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
from receiver import Receiver, strict_json
|
||||
from platform_event import translate
|
||||
|
||||
NOW = datetime(2026, 9, 6, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class ReceiverTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.path = Path(self.temp.name) / 'receiver.db'
|
||||
self.contract = dict(schema='railiance-telemetry.stream.v1', stream='test',
|
||||
producer='platform', recipient='test-operator', signals=['db.restore'],
|
||||
max_event_age_seconds=900, heartbeat_seconds=900, retention_days=30)
|
||||
self.receiver = Receiver(self.path, self.contract)
|
||||
self.addCleanup(lambda: self.receiver.close())
|
||||
|
||||
def event(self, state='healthy', when=NOW):
|
||||
return dict(schema='railiance-telemetry.signal.v1', id=str(uuid.uuid4()),
|
||||
stream='test', producer='platform', observed_at=when.isoformat(), states={'db.restore': state})
|
||||
|
||||
def test_failure_is_durable_and_addressed(self):
|
||||
self.receiver.ingest(self.event('failed'), NOW)
|
||||
self.receiver.close()
|
||||
self.receiver = Receiver(self.path, self.contract)
|
||||
inbox = self.receiver.inbox()
|
||||
self.assertEqual(inbox['recipient'], 'test-operator')
|
||||
self.assertEqual(inbox['notices'][0]['states'], {'db.restore': 'failed'})
|
||||
self.assertEqual(inbox['notices'][0]['kind'], 'unhealthy')
|
||||
self.assertTrue(self.receiver.ack(inbox['notices'][0]['id'], NOW)['acknowledged'])
|
||||
self.assertEqual(self.receiver.inbox()['notices'], [])
|
||||
|
||||
def test_never_seen_and_stopped_producer_detected(self):
|
||||
self.assertEqual(self.receiver.check(NOW)['state'], 'missing-emission')
|
||||
self.receiver.check(NOW)
|
||||
self.assertEqual(len(self.receiver.inbox()['notices']), 1)
|
||||
self.receiver.ingest(self.event(), NOW)
|
||||
self.assertEqual(self.receiver.check(NOW)['state'], 'receiving')
|
||||
self.assertEqual(self.receiver.check(NOW + timedelta(seconds=901))['state'], 'missing-emission')
|
||||
|
||||
def test_replay_does_not_renew_heartbeat(self):
|
||||
event = self.event()
|
||||
self.receiver.ingest(event, NOW)
|
||||
self.assertEqual(self.receiver.ingest(event, NOW + timedelta(seconds=899))['status'], 'duplicate')
|
||||
self.assertEqual(self.receiver.check(NOW + timedelta(seconds=901))['state'], 'missing-emission')
|
||||
with self.assertRaises(ValueError):
|
||||
self.receiver.ingest(self.event(when=NOW - timedelta(seconds=1)), NOW)
|
||||
|
||||
def test_duplicate_id_changed_payload_rejected(self):
|
||||
event = self.event(); self.receiver.ingest(event, NOW)
|
||||
event['states']['db.restore'] = 'failed'
|
||||
with self.assertRaises(ValueError): self.receiver.ingest(event, NOW)
|
||||
|
||||
def test_invalid_scope_future_stale_and_extra_data(self):
|
||||
variants = [dict(producer='other'), dict(states={'other': 'healthy'}),
|
||||
dict(states={'db.restore': 'secret'}), dict(password='PRIVATE_CANARY'),
|
||||
dict(observed_at=(NOW + timedelta(seconds=1)).isoformat()),
|
||||
dict(observed_at=(NOW - timedelta(seconds=901)).isoformat()),
|
||||
dict(observed_at='2026-09-06T00:00:00')]
|
||||
for variant in variants:
|
||||
with self.subTest(variant=variant), self.assertRaises(ValueError):
|
||||
self.receiver.ingest(dict(self.event(), **variant), NOW)
|
||||
self.assertEqual(self.receiver.inbox()['notices'], [])
|
||||
|
||||
def test_contract_drift_refused(self):
|
||||
with self.assertRaises(ValueError): Receiver(self.path, dict(self.contract, recipient='other'))
|
||||
|
||||
def test_retention_keeps_unacknowledged_and_latest_anchors(self):
|
||||
self.receiver.ingest(self.event('failed'), NOW)
|
||||
self.receiver.ingest(self.event(when=NOW + timedelta(seconds=1)), NOW + timedelta(seconds=1))
|
||||
later = NOW + timedelta(days=31)
|
||||
self.assertEqual(self.receiver.prune(later)['events_pruned'], 0)
|
||||
first = self.receiver.inbox()['notices'][0]['id']
|
||||
self.receiver.ack(first, later)
|
||||
self.assertEqual(self.receiver.prune(later)['events_pruned'], 1)
|
||||
self.assertEqual(self.receiver.check(later)['state'], 'missing-emission')
|
||||
|
||||
def test_duplicate_fields_and_oversize_rejected(self):
|
||||
for raw in (b'{"a":1,"a":2}', b' ' * 32769):
|
||||
with self.assertRaises(ValueError): strict_json(raw)
|
||||
|
||||
def test_adapter_keeps_time_and_owner_meaning(self):
|
||||
report = dict(schema='railiance-platform.assurance-signal.v1',
|
||||
cluster_uid='a553c742-0115-43d4-99a4-a5ca56fe0786', evaluated_at=NOW.isoformat(),
|
||||
signals={'db.restore': {'state': 'stale', 'owner': 'railiance-platform'}},
|
||||
transport='unmonitored', guarantees='unsupported', threshold_status='local-diagnostic-only', healthy=False)
|
||||
event = translate(report, self.contract)
|
||||
self.assertEqual(event['observed_at'], report['evaluated_at'])
|
||||
self.assertEqual(event['states'], {'db.restore': 'stale'})
|
||||
report['healthy'] = True
|
||||
with self.assertRaises(ValueError): translate(report, self.contract)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
79
workplans/RTEL-WP-0002-signal-contract.md
Normal file
79
workplans/RTEL-WP-0002-signal-contract.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
id: RTEL-WP-0002
|
||||
type: workplan
|
||||
title: "Provide the Q2 receiving contract and prove signal delivery"
|
||||
domain: financials
|
||||
repo: railiance-telemetry
|
||||
status: active
|
||||
owner: codex
|
||||
created: "2026-09-06"
|
||||
updated: "2026-09-06"
|
||||
related:
|
||||
- RTELE-WP-0001
|
||||
- RPF-WP-0036
|
||||
state_hub_workstream_id: "08a5db92-7293-50d3-b589-55287b9850b3"
|
||||
---
|
||||
|
||||
Bounded initial Q2 reference implementation for platform assurance. Producer
|
||||
meaning stays in S3; deployable monitoring packaging stays with a selected
|
||||
package owner. No public listener, credential custody or incident workflow.
|
||||
|
||||
## Define metadata-only receiving and retention contract
|
||||
|
||||
```task
|
||||
id: RTEL-WP-0002-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ac73553d-5bd4-5dff-8307-95a29bffe474"
|
||||
```
|
||||
|
||||
Implemented `docs/signal-contract.md` and a pinned platform stream contract.
|
||||
Exact identities/signals, finite states, payload limit, freshness, replay,
|
||||
proposed retention and local recipient semantics are explicit. Live acceptance
|
||||
of the proposed budgets and recipient remains T04, not implied by this schema.
|
||||
|
||||
## Implement durable receipt and operator inbox reference
|
||||
|
||||
```task
|
||||
id: RTEL-WP-0002-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "dacc9e2d-9321-5d39-afc4-88326f5c6930"
|
||||
```
|
||||
|
||||
SQLite acceptance and notices commit together; strict rejection, duplicate
|
||||
idempotence, explicit acknowledgments and bounded retention are tested. This
|
||||
is a local private CLI implementation; acceptance says local-inbox-only.
|
||||
|
||||
## Prove platform adaptation and absent-emission semantics locally
|
||||
|
||||
```task
|
||||
id: RTEL-WP-0002-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a8ed3588-4240-5278-b03f-04c3237a52f9"
|
||||
```
|
||||
|
||||
Adapter preserves producer evaluation time and classifications. Tests prove
|
||||
failure survives receiver restart, inbox addressing/acknowledgment, never-seen
|
||||
and stopped-producer detection, replay refusal, wrong-scope rejection and
|
||||
retention of unacknowledged evidence. Local simulated-time tests establish
|
||||
implementation behavior, not actual scheduled notification delivery.
|
||||
|
||||
## Accept private runtime and controlled end-to-end delivery
|
||||
|
||||
```task
|
||||
id: RTEL-WP-0002-T04
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "ceb14fdc-b3fd-5a22-870d-d835a9d52055"
|
||||
```
|
||||
|
||||
Choose package/runtime execution owner with cluster/activity-core; confirm the
|
||||
operator recipient, producer/watchdog cadence, retention/storage/backup and
|
||||
private authenticated access. Install only through accepted authority. Prove a
|
||||
controlled failure and stopped producer reach and are acknowledged by the named
|
||||
operator, and receiver/scheduler failure is independently detectable. Preserve
|
||||
receipts across restart; demonstrate capacity/retention handling. Only these
|
||||
receipts can satisfy RPF-WP-0036-T04. No package repo name or deployment grant is
|
||||
invented here. This live task holds the residual explicitly.
|
||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
|||
title: "Bootstrap State Hub integration"
|
||||
domain: financials
|
||||
repo: railiance-telemetry
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-11"
|
||||
updated: "2026-08-15"
|
||||
updated: "2026-09-06"
|
||||
related:
|
||||
- ADR-0008
|
||||
- RMASTER-WP-0023
|
||||
|
|
@ -25,14 +25,14 @@ seed the first *signal-contract* workplan. Do not deploy a monitoring
|
|||
stack here. Placement is Quality / Q2 (INTENT/SCOPE). That is not an
|
||||
open `railiance-master` question.
|
||||
|
||||
This id uses the historical `RAILIANCE-WP-` prefix. New telemetry
|
||||
This id uses the historical `RTELE-WP-` prefix. New telemetry
|
||||
workplans use `RTEL-WP-`. Do not change this file's `id` or hub UUID.
|
||||
|
||||
## Review Generated Integration Files
|
||||
|
||||
```task
|
||||
id: RTELE-WP-0001-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4aca4677-6712-553b-ac26-0682fdf211de"
|
||||
```
|
||||
|
|
@ -46,7 +46,7 @@ open S3-vs-Quality question). Set the *next* workplan prefix to
|
|||
|
||||
```task
|
||||
id: RTELE-WP-0001-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b6632ce1-dc84-532d-b277-adce8510a304"
|
||||
```
|
||||
|
|
@ -59,7 +59,7 @@ the first verification command.
|
|||
|
||||
```task
|
||||
id: RTELE-WP-0001-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4877f6e2-8298-5bab-b6a8-9a1ec1718289"
|
||||
```
|
||||
|
|
@ -78,3 +78,7 @@ checkout:
|
|||
```bash
|
||||
statehub fix-consistency
|
||||
```
|
||||
|
||||
Completed September 6: reviewed integration and Q2 boundary, corrected README,
|
||||
documented the standard-library test workflow and seeded RTEL-WP-0002. Live
|
||||
runtime/notification acceptance remains explicitly in RTEL-WP-0002-T04.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue