Derive heartbeat cadence from the shared coordination frame
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
parent
db850aded5
commit
a786b061da
9 changed files with 98 additions and 27 deletions
|
|
@ -9,6 +9,7 @@ from typing import Any, Callable
|
|||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from .coordination import FRAME_SECONDS
|
||||
from .store import Engine
|
||||
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ class OutboxWorker:
|
|||
engine: Engine,
|
||||
sink: Callable[[dict[str, Any]], None],
|
||||
*,
|
||||
heartbeat_interval_seconds: int = 300,
|
||||
heartbeat_interval_seconds: int = FRAME_SECONDS,
|
||||
) -> None:
|
||||
self.engine = engine
|
||||
self.sink = sink
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from wsgiref.simple_server import make_server
|
|||
|
||||
from .api import App
|
||||
from .audit import AuditCoreSink, OutboxWorker
|
||||
from .coordination import FRAME_SECONDS
|
||||
from .auth import Identity, JWTAuthenticator, StaticTokenAuthenticator
|
||||
from .store import Engine
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ def _parser() -> argparse.ArgumentParser:
|
|||
serve.add_argument("--audit-url")
|
||||
serve.add_argument("--audit-token-file")
|
||||
serve.add_argument("--outbox-poll-seconds", type=float, default=5)
|
||||
serve.add_argument("--heartbeat-seconds", type=int, default=86400)
|
||||
serve.add_argument("--heartbeat-seconds", type=int, default=FRAME_SECONDS)
|
||||
migrate = sub.add_parser("migrate", help="apply repeatable schema migrations")
|
||||
_common_db(migrate)
|
||||
verify = sub.add_parser("verify", help="verify schema and SQLite integrity")
|
||||
|
|
|
|||
6
approval_engine/coordination.py
Normal file
6
approval_engine/coordination.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""Generated cadence timings; regenerate with NetKingdom coordination-frame tool."""
|
||||
# Source: coordination-engine/config/coordination-frame.json
|
||||
FRAME_CONFIG_SHA256 = "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67"
|
||||
FRAME_SECONDS = 3600
|
||||
RECEIVER_WAIT_SECONDS = 7200
|
||||
EFFECTIVE_DEADLINE_SECONDS = 10800
|
||||
38
cadence.yaml
38
cadence.yaml
|
|
@ -25,14 +25,15 @@
|
|||
},
|
||||
"heartbeat": {
|
||||
"event_class": "audit-core.heartbeat",
|
||||
"interval_seconds": 86400,
|
||||
"interval_seconds": 3600,
|
||||
"assertion": "nothing-to-report",
|
||||
"missing": "finding",
|
||||
"extensions": {
|
||||
"approval-engine": {
|
||||
"asserted_class": "issuance",
|
||||
"requested_observer_max_gap_seconds": 172800,
|
||||
"observer_registration_status": "pending"
|
||||
"requested_observer_max_gap_seconds": 7200,
|
||||
"observer_registration_status": "pending",
|
||||
"effective_deadline_seconds": 10800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,14 +60,15 @@
|
|||
},
|
||||
"heartbeat": {
|
||||
"event_class": "audit-core.heartbeat",
|
||||
"interval_seconds": 86400,
|
||||
"interval_seconds": 3600,
|
||||
"assertion": "nothing-to-report",
|
||||
"missing": "finding",
|
||||
"extensions": {
|
||||
"approval-engine": {
|
||||
"asserted_class": "use",
|
||||
"requested_observer_max_gap_seconds": 172800,
|
||||
"observer_registration_status": "pending"
|
||||
"requested_observer_max_gap_seconds": 7200,
|
||||
"observer_registration_status": "pending",
|
||||
"effective_deadline_seconds": 10800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,14 +95,15 @@
|
|||
},
|
||||
"heartbeat": {
|
||||
"event_class": "audit-core.heartbeat",
|
||||
"interval_seconds": 86400,
|
||||
"interval_seconds": 3600,
|
||||
"assertion": "nothing-to-report",
|
||||
"missing": "finding",
|
||||
"extensions": {
|
||||
"approval-engine": {
|
||||
"asserted_class": "supersession",
|
||||
"requested_observer_max_gap_seconds": 172800,
|
||||
"observer_registration_status": "pending"
|
||||
"requested_observer_max_gap_seconds": 7200,
|
||||
"observer_registration_status": "pending",
|
||||
"effective_deadline_seconds": 10800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,14 +130,15 @@
|
|||
},
|
||||
"heartbeat": {
|
||||
"event_class": "audit-core.heartbeat",
|
||||
"interval_seconds": 86400,
|
||||
"interval_seconds": 3600,
|
||||
"assertion": "nothing-to-report",
|
||||
"missing": "finding",
|
||||
"extensions": {
|
||||
"approval-engine": {
|
||||
"asserted_class": "revocation",
|
||||
"requested_observer_max_gap_seconds": 172800,
|
||||
"observer_registration_status": "pending"
|
||||
"requested_observer_max_gap_seconds": 7200,
|
||||
"observer_registration_status": "pending",
|
||||
"effective_deadline_seconds": 10800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,6 +195,16 @@
|
|||
"status": "candidate",
|
||||
"schema_sha256": "6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae"
|
||||
}
|
||||
},
|
||||
"netkingdom": {
|
||||
"coordination_frame": {
|
||||
"ref": "coordination-engine/config/coordination-frame.json",
|
||||
"sha256": "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67",
|
||||
"frame_seconds": 3600,
|
||||
"heartbeat_frames": 1,
|
||||
"registered_wait_frames": 2,
|
||||
"effective_deadline_frames": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ spec:
|
|||
app.kubernetes.io/name: approval-engine
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
coordination.coulomb.social/frame-sha256: "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67"
|
||||
labels:
|
||||
app.kubernetes.io/name: approval-engine
|
||||
spec:
|
||||
|
|
@ -72,6 +74,8 @@ spec:
|
|||
- approval-engine
|
||||
- --jwks-url
|
||||
- http://keycape.sso.svc.cluster.local:8080/jwks
|
||||
- --heartbeat-seconds
|
||||
- "3600"
|
||||
- --audit-url
|
||||
- http://audit-core.audit-core.svc.cluster.local:8080
|
||||
- --audit-token-file
|
||||
|
|
|
|||
|
|
@ -35,15 +35,18 @@ whichever class happens to be busy and says nothing about the quiet one — here
|
|||
that is `revocation`, whose silence is the only silence that matters. Requested
|
||||
`heartbeat_classes`, all four at the same bound:
|
||||
|
||||
| Class | Longest gap that is not yet a finding |
|
||||
| Class | Registered wait before receiver grace |
|
||||
| --- | --- |
|
||||
| `issuance` | 172800s (48h) |
|
||||
| `use` | 172800s (48h) |
|
||||
| `supersession` | 172800s (48h) |
|
||||
| `revocation` | 172800s (48h) |
|
||||
| `issuance` | 2f = 7200s (2h) |
|
||||
| `use` | 2f = 7200s (2h) |
|
||||
| `supersession` | 2f = 7200s (2h) |
|
||||
| `revocation` | 2f = 7200s (2h) |
|
||||
|
||||
Twice the deployed `--heartbeat-seconds` default of 86400, so one missed cycle
|
||||
is not a finding and two consecutive ones are. The engine emits all four in a
|
||||
These are registered base waits, not the effective alert threshold. Founder
|
||||
coordination-frame decision 2026-10-15 sets f=3600s. The prepared emitter
|
||||
interval is f; the receiver wait is 2f=7200s, and Audit Core's existing 1.5
|
||||
grace gives an effective deadline of 3f=10800s. This source request is not a
|
||||
live registry receipt or proof that deployment has changed pace. The engine emits all four in a
|
||||
single transaction: a partial emission would report some classes healthy and
|
||||
others stalled, which is a worse signal than none.
|
||||
|
||||
|
|
@ -83,3 +86,12 @@ owners and bounds; this registration does not claim their completion.
|
|||
First provisioning still requires the platform-owned custody record linking
|
||||
the receiver's protected sender registry and this mounted credential. No
|
||||
credential value belongs in this document, Git, Hub or a rollout receipt.
|
||||
|
||||
|
||||
Current timing values derive from coordination-engine/config/coordination-frame.json
|
||||
through net-kingdom/tools/coordination-frame/render.py. The generated declaration,
|
||||
consumer constants and deployment pod annotation carry its SHA-256. Existing
|
||||
credential scope and the native September 15 bearer-revocation proof are
|
||||
retained; do not repeat that unchanged proof as if it were missing. Actual
|
||||
lifecycle reconciliation, class registration, lag findings and operational
|
||||
observer readback remain APPROVAL-WP-0002-T04.
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ becomes a missing positive rather than a quiet month.
|
|||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Class | `heartbeat` |
|
||||
| Interval | 24 hours (SHOULD also be emittable on demand) |
|
||||
| Interval | f = 1 hour; SHOULD also be emittable on demand |
|
||||
| Assertion | `nothing-to-report` |
|
||||
| Counts | `{issuance, use, supersession, revocation}` of committed outbox rows |
|
||||
| Missing heartbeat | **finding** after the requested 172800s receiver bound; registration pending |
|
||||
| Missing heartbeat | **finding** after effective 3f = 3h (registration 2f = 2h plus receiver 1.5 grace); admission pending |
|
||||
|
||||
### Reconciliation
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ case §9.6 names.
|
|||
|
||||
## What is a finding
|
||||
|
||||
- No per-class heartbeat arrives beyond the requested 48h receiver bound (24h emission interval).
|
||||
- No per-class heartbeat arrives beyond the effective 3f deadline (f emission interval).
|
||||
- `audit-core` count for a class is less than this engine's drained count
|
||||
for that class.
|
||||
- Outbox depth or age exceeds the lag bound in `cadence.yaml` (drain stuck).
|
||||
|
|
@ -143,3 +143,19 @@ snapshot proves completeness or resistance to source compromise.
|
|||
This implements T04's missing bounded source computation. Receiver registration,
|
||||
observed lag/findings and native reconciliation remain blocked owner acceptance;
|
||||
no existing live image is promoted by this change.
|
||||
|
||||
|
||||
## Configurable coordination frame — 2026-10-15
|
||||
|
||||
The timing paragraphs in the dated October 3 migration section describe its
|
||||
historical daily defaults. Current source defaults and the prepared manifest
|
||||
are derived from coordination-engine/config/coordination-frame.json, initially
|
||||
f=3600s by founder decision. The generated approval_engine/coordination.py and
|
||||
cadence.yaml retain the config digest; CLI and direct OutboxWorker defaults
|
||||
use FRAME_SECONDS. The deployment argument explicitly supplies the same value
|
||||
and its pod annotation retains the frame digest. Runtime applies remain under
|
||||
the owner change gate; no hourly live cadence is claimed by this source edit.
|
||||
|
||||
Class mappings, ownership, atomic outbox behavior and count-window accounting
|
||||
are unchanged. A different frame is regenerated for emitter and observer
|
||||
settings together using net-kingdom/tools/coordination-frame/render.py.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ the workplan remains `blocked` after completing reachable source work.
|
|||
| Task | Owner return required | Evidence that clears the wait |
|
||||
| --- | --- | --- |
|
||||
| T01 — authenticated approver evidence | key-cape and informed-decision; founder for release rollout | Exact deployed issuer revision and client/profile receipt for requester and human paths; human sub/type, tenant/source, resource audience, scopes and assurance captured as non-secret facts and correlated with a stored human-control entry. Promote scanned schema-v6 image under per-change approval, back up/migrate once, then verify native entry and issuance/use provenance. No tokens or secrets in receipts. |
|
||||
| T04 — delivery and reconciliation | audit-core, with railiance-platform for deployed sender/custody acceptance | Accept current cadence.yaml inventory for source approval-engine and tenant tenant:platform, explicit redact policy and four transition heartbeat classes at the requested 172800s bound. Return actual lifecycle event ids/drain acknowledgment and independent accepted counts for an agreed bounded comparison, accounting for occurrence versus receiver acceptance time, delayed delivery, duplicate acknowledgments and boundary crossings; source pending rows remain lag. Prove startup beats, missing-class and lag findings (depth 100 / age 3600s), and scoped sender bearer revocation. |
|
||||
| T04 — delivery and reconciliation | audit-core, with railiance-platform for deployed sender/custody acceptance | Accept current cadence.yaml inventory for source approval-engine and tenant tenant:platform, explicit redact policy and four transition heartbeat classes at the frame-derived 2f=7200s registered wait (effective 3f=10800s after receiver grace). Return actual lifecycle event ids/drain acknowledgment and independent accepted counts for an agreed bounded comparison, accounting for occurrence versus receiver acceptance time, delayed delivery, duplicate acknowledgments and boundary crossings; source pending rows remain lag. Prove startup beats, missing-class and lag findings (depth 100 / age 3600s), and retain the delivered September 15 scoped sender bearer-revocation receipt. |
|
||||
| T05 — live PEP consumption | secrets-engine, with informed-decision for the act-specific human review and access-engine for decision binding | Name the served PEP route/workload, immutable component revision, engine image/endpoint, authenticated caller identities and exact binding. Prove claim-before-decision and CAS consume after ALLOW before protected side effect; same-digest retry, different-digest conflict, spent-on-failure, revoked/expired/superseded/wrong-binding/unavailable denial with no protected callback. Preserve the native approval and decision digests, declared human_control where required, and consumer-owned side-effect/refusal evidence. |
|
||||
|
||||
For live whitehat T-06 only, T05's named surface must additionally be covered by
|
||||
|
|
@ -76,3 +76,11 @@ population and reconcile lag/duplicate/boundary cases; matching since/until
|
|||
strings against occurrence counts is insufficient. Existing owner task bindings
|
||||
remain key-cape (T01), audit-core (T04) and secrets-engine (T05). No new owner
|
||||
receipt or named live T-06 surface arrived since the preceding review.
|
||||
|
||||
|
||||
2026-10-15 timing follow-up: founder selected shared coordination f=1h,
|
||||
replacing a fixed day for this adoption. Generated source/config bindings are
|
||||
prepared, not deployed. Canonical base is coordination-engine/config/coordination-frame.json;
|
||||
NetKingdom retains regeneration and drift checks. Lifecycle drain/count-window,
|
||||
registry admission and independent observation remain open. No credential,
|
||||
JWT/approval validity, issuer scope or production image pin is changed.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
from pathlib import Path
|
||||
|
||||
from approval_engine.audit import audit_envelope
|
||||
from approval_engine.coordination import FRAME_SECONDS, RECEIVER_WAIT_SECONDS
|
||||
from approval_engine.store import EVENT_CLASSES, HEARTBEAT_CLASSES
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
|
@ -30,8 +31,16 @@ def test_cadence_inventory_matches_durable_emitter(engine):
|
|||
assert beat["event_class"] == observed[cls]["type"] == "audit-core.heartbeat"
|
||||
assert beat["assertion"] == observed[cls]["data"]["assertion"]
|
||||
assert beat["extensions"]["approval-engine"]["asserted_class"] == cls
|
||||
assert beat["interval_seconds"] == 86400
|
||||
assert beat["extensions"]["approval-engine"]["requested_observer_max_gap_seconds"] == 172800
|
||||
assert beat["interval_seconds"] == FRAME_SECONDS
|
||||
assert beat["extensions"]["approval-engine"]["requested_observer_max_gap_seconds"] == RECEIVER_WAIT_SECONDS
|
||||
assert entries[cls]["extensions"]["netkingdom"]["rate_monitoring"] == "forbidden"
|
||||
assert entries[cls]["reconciliation"]["undrained_local"] == "lag-not-divergence"
|
||||
assert "heartbeat" not in entries["heartbeat"] # transport control has no recursive heartbeat
|
||||
|
||||
|
||||
def test_default_runtime_heartbeat_matches_declared_coordination_frame():
|
||||
from approval_engine.cli import _parser
|
||||
|
||||
args = _parser().parse_args(["serve"])
|
||||
assert args.heartbeat_seconds == FRAME_SECONDS
|
||||
assert _parser().parse_args(["serve", "--heartbeat-seconds", "600"]).heartbeat_seconds == 600
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue