Audit¶
SHA256-chained audit logging, protobuf event streaming, and deterministic replay for regulatory compliance, debugging, and formal verification. Every supervisor decision, regime transition, and actuation command can be recorded with tamper-evident JSONL records and a parallel event-sourced protobuf stream.
Motivation¶
SPO is designed for safety-critical applications (power grids, plasma control, medical devices). These domains require:
- Traceability — every control decision must be attributable to a specific input state and policy rule
- Tamper evidence — audit records must detect insertion, deletion, or modification after the fact
- Reproducibility — given the same inputs and code version, the system must produce identical outputs
The audit subsystem provides all three via hash-chained logging, event-sourced streaming, and deterministic replay.
Hash Chain Structure¶
Each audit record contains:
| Field | Description |
|---|---|
step |
Monotonic step counter |
timestamp |
ISO 8601 UTC timestamp |
event_type |
regime_transition, actuation, boundary_breach, etc. |
payload |
Event-specific data (JSON-serialisable) |
prev_hash |
SHA256 of the previous record |
hash |
SHA256 of this record (step + event_type + payload + prev_hash) |
The first record uses prev_hash = "0" * 64. To verify the chain,
recompute each hash and check it matches the stored value and the
next record's prev_hash.
The spo run --audit header includes the resolved binding summary under
binding_config and binding_summary. For N-channel domainpacks this summary
includes channel_algebra, covering required and optional channels, derived
channels, runtime evidence channels, group membership, coupling participants,
and missing required channel evidence.
Keyed Audit Signatures¶
Set SPO_AUDIT_KEY to enable HMAC-SHA256 signatures on JSONL audit records.
Audit records include canonical payload metadata for replay verification, with
HMAC fields added when signing is enabled:
| Field | Description |
|---|---|
_audit_mode |
hmac-signed or unsigned-development |
_audit_schema_version |
Signature metadata schema version |
_audit_stream_id |
Logical JSONL stream id |
_audit_sequence |
Monotonic record sequence |
_audit_timestamp_unix_ns |
Signing timestamp in Unix nanoseconds |
_previous_hash |
Previous JSONL record hash used by the signature |
_payload_hash |
SHA256 of the canonical payload without audit metadata |
_signature |
HMAC algorithm, key id, and signature value |
The raw key is never written to the audit file. The stored key id is
sha256(key)[:16], which lets replay choose the right verification key without
logging secret material.
When SPO_AUDIT_KEY is configured, ReplayEngine.verify_integrity() and
spo replay --verify fail closed if a record is unsigned, malformed, signed by
an unknown key, or modified after signing. Without SPO_AUDIT_KEY, legacy
unsigned development logs remain readable and hash-chain verification keeps its
previous behaviour.
The protobuf event stream uses the same environment policy. When
event_stream is enabled on AuditLogger, every envelope records its
audit mode, signature algorithm, key id, and HMAC value alongside the existing
sequence, payload hash, previous hash, and event hash.
verify_event_stream_integrity() and spo watch reject unsigned or
signature-invalid envelopes whenever SPO_AUDIT_KEY or SPO_AUDIT_KEYRING is
configured.
When simulate() receives an AuditLogger with a protobuf event stream, it
flushes the stream after the final event and stores the whole-stream integrity
summary on SimulationResult.audit_event_stream_integrity. This is a run-end
integrity check, not a per-step action gate; append-time hash chaining remains
the live tamper-evidence during the run.
Unsigned logs and streams are allowed only when no audit key is configured.
They are marked explicitly as unsigned-development and still carry
canonical payload hashes so reviewers can distinguish local development traces
from operational signed evidence without losing deterministic payload evidence.
For key rotation, keep historical keys only in the operator environment and pass
them as a JSON object through SPO_AUDIT_KEYRING:
export SPO_AUDIT_KEY="$(openssl rand -hex 32)"
export SPO_AUDIT_KEYRING='{
"<sha256-old-secret-prefix>": "<old-generated-secret>",
"<sha256-new-secret-prefix>": "<new-generated-secret>"
}'
spo replay audit.jsonl --verify
Each keyring object key must match sha256(secret)[:16]; mismatches fail
closed. Do not commit these environment values, include them in diagnostics, or
store them in audit artefacts.
Audit Logger¶
Appends timestamped, SHA256-chained records to a JSONL audit trail. When
event_stream is supplied, the same stored records are also appended to a
length-delimited protobuf stream.
The compatibility facade scpn_phase_orchestrator.audit exposes the audit
logger, replay engine, and event-stream helpers lazily. Its __all__ and
dir() output list the same public exports before import-time resolution, so
interactive tools and documentation generators see the facade contract without
eagerly importing the runtime audit stack.
from scpn_phase_orchestrator.runtime.audit_logger import AuditLogger
logger = AuditLogger("audit.jsonl", event_stream="audit.spoa")
logger.log_event("regime_transition", {"from": "nominal", "to": "degraded"})
integrity = logger.verify_event_stream_integrity()
assert integrity is not None and integrity.ok
logger.close()
audit_logger ¶
Append-only JSONL logger for replayable SPO runs.
AuditLogger records headers, simulation steps, supervisor actions, and named
events with a SHA-256 hash chain. When SPO_AUDIT_KEY is configured, existing
unsigned streams are rejected and new records carry HMAC metadata so downstream
replay and reporting can verify provenance before trusting an audit trail.
Classes¶
AuditStreamIntegrityResult
dataclass
¶
Close-time integrity summary for a protobuf audit event stream.
Parameters¶
event_stream_path : str
Filesystem path to the verified protobuf audit event stream.
ok : bool
Whether payload digests, sequence numbers, hash links, and required
signatures verified.
verified_events : int
Number of consecutive events verified before the first failure, or all
events when ok is True.
Methods:¶
to_audit_record ¶
Return a JSON-safe audit-stream integrity record.
Returns¶
dict[str, object] A JSON-safe integrity summary for downstream reports.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
AuditLogger ¶
Append-only JSONL audit log for UPDE simulation steps.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
Attributes¶
event_stream_integrity
property
¶
Return the most recent event-stream integrity summary, if any.
Returns¶
AuditStreamIntegrityResult | None
The last computed event-stream integrity result, or None when no
protobuf stream has been verified.
Methods:¶
log_header ¶
log_header(
*,
n_oscillators: int,
dt: float,
method: str = "euler",
seed: int | None = None,
amplitude_mode: bool = False,
control_mode: str = "supervisor_policy",
binding_config: dict[str, object] | None = None,
binding_summary: dict[str, object] | None = None,
) -> None
Engine configuration record for replay reconstruction.
Parameters¶
n_oscillators : int
Number of oscillators in the system.
dt : float
Integration step size.
method : str
Integration method (euler, rk4, or rk45).
seed : int | None
Seed for the deterministic RNG, or None.
amplitude_mode : bool
Whether the engine runs in Stuart-Landau amplitude mode.
control_mode : str
Live control surface used by the simulation core.
binding_config : dict[str, object] | None
Resolved binding configuration, or None.
binding_summary : dict[str, object] | None
Resolved binding summary, or None.
Raises¶
AuditError If the audit log cannot be written.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
log_step ¶
log_step(
step: int,
upde_state: UPDEState,
actions: list[ControlAction],
*,
phases: FloatArray | None = None,
omegas: FloatArray | None = None,
knm: FloatArray | None = None,
alpha: FloatArray | None = None,
zeta: float = 0.0,
psi_drive: float = 0.0,
amplitudes: FloatArray | None = None,
mu: FloatArray | None = None,
knm_r: FloatArray | None = None,
epsilon: float | None = None,
channel_runtime: dict[str, object] | None = None,
) -> None
Write one simulation step to the audit log with optional full state.
Parameters¶
step : int
Zero-based simulation step index.
upde_state : UPDEState
The UPDE state to record or export.
actions : list[ControlAction]
The control actions recorded for the step.
phases : FloatArray | None
Oscillator phases in radians, shape (N,).
omegas : FloatArray | None
Natural frequencies in rad/s, shape (N,).
knm : FloatArray | None
Coupling matrix K_nm, shape (N, N).
alpha : FloatArray | None
Phase-lag matrix in radians, shape (N, N), or None for no lag.
zeta : float
External drive strength ζ.
psi_drive : float
External drive reference phase in radians.
amplitudes : FloatArray | None
Oscillator amplitudes, shape (N,), or None.
mu : FloatArray | None
Per-oscillator linear growth parameters, or None.
knm_r : FloatArray | None
Amplitude coupling matrix, shape (N, N), or None.
epsilon : float | None
Stuart-Landau amplitude coupling factor, or None.
channel_runtime : dict[str, object] | None
N-channel runtime evidence, or None.
Raises¶
AuditError If the audit log cannot be written.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | |
log_event ¶
Write a named event with arbitrary data to the audit log.
Parameters¶
event_type : str
Named event type, or None.
data : dict[str, Any]
Arbitrary JSON-safe event payload.
Raises¶
AuditError If the audit log cannot be written.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
close ¶
Flush and close the audit log file handle.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
verify_event_stream_integrity ¶
Verify the configured protobuf event stream after flushing writes.
Returns¶
AuditStreamIntegrityResult | None
The integrity summary when this logger owns an event stream, else
None for JSONL-only audit logs.
Source code in src/scpn_phase_orchestrator/runtime/audit_logger.py
Functions:¶
Audit Signing Helpers¶
scpn_phase_orchestrator.runtime.audit_signing centralises audit signature constants,
key identifiers, and verification key loading. It is used by JSONL audit replay
and protobuf event-stream verification so keyring validation stays consistent
across both audit transports.
audit_signing ¶
HMAC key discovery helpers for signed audit verification.
The module derives stable non-secret key identifiers and loads current or
historical verification keys from SPO_AUDIT_KEY and SPO_AUDIT_KEYRING.
Empty, malformed, or mismatched key material raises ValueError so signed
audit verification never falls back to an ambiguous trust state.
Functions:¶
key_id_for_secret ¶
Return the audit key identifier stored in signed audit metadata.
Parameters¶
key_material : str The audit signing-key material.
Returns¶
str The audit key identifier stored in signed audit metadata.
Raises¶
ValueError If the inputs are invalid or inconsistent.
Source code in src/scpn_phase_orchestrator/runtime/audit_signing.py
audit_verification_keys ¶
Load current and historical audit verification keys from the environment.
SPO_AUDIT_KEY supplies the current operational key. SPO_AUDIT_KEYRING
supplies historical keys as a JSON object mapping sha256(secret)[:16] to
the corresponding secret. Invalid or mismatched keyrings fail closed by
raising ValueError.
Returns¶
dict[str, str] Load current and historical audit verification keys from the environment.
Raises¶
ValueError If the inputs are invalid or inconsistent.
Source code in src/scpn_phase_orchestrator/runtime/audit_signing.py
Deterministic Replay¶
Replays an audit trail against a fresh SPO instance to reproduce the exact sequence of states. Replay verifies that the same inputs produce the same outputs — detecting non-determinism, floating-point platform differences, or code regressions.
Replay guarantees:
- Same binding spec + same audit trail → identical phase trajectories
- Any divergence is flagged with the step number and magnitude
- Platform-specific float differences (x86 vs ARM extended precision) are handled via configurable tolerance
- JSONL parsing rejects non-finite constants, duplicate object keys, and non-object lines before replay or hash verification
from scpn_phase_orchestrator.runtime.replay import ReplayEngine
replay = ReplayEngine("audit.jsonl")
entries = replay.load()
header = replay.load_header(entries)
if header is not None:
engine = replay.build_engine(header)
ok, verified = replay.verify_determinism_chained(engine, entries)
print(f"verified={verified} ok={ok}")
replay ¶
Replay and integrity verification for SPO audit logs.
The replay engine reconstructs UPDE or Stuart-Landau state from audit JSONL, checks hash-chain and optional HMAC integrity, and reruns chained state transitions against logged next-step phases. Malformed headers, unsupported methods, invalid signatures, and non-replayable records fail closed instead of silently accepting unverifiable provenance.
Classes¶
ReplayEngine ¶
Replay and verify determinism of JSONL audit logs.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
Methods:¶
load ¶
Read and parse all JSONL entries from the audit log file.
Returns¶
list[dict[str, Any]] Read and parse all JSONL entries from the audit log file.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
replay_step ¶
Reconstruct UPDEState from a log entry.
Parameters¶
step_data : dict[str, Any] A single audit-log entry to reconstruct.
Returns¶
UPDEState The reconstructed UPDE state.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
load_header ¶
Extract the header record (engine config) if present.
Parameters¶
entries : list[dict[str, Any]] Audit-log entries to operate on.
Returns¶
dict[str, Any] | None
The header record, or None if absent.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
step_entries ¶
Filter to entries with full UPDE state (replayable).
Parameters¶
entries : list[dict[str, Any]] Audit-log entries to operate on.
Returns¶
list[dict[str, Any]] The replayable entries with full UPDE state.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
build_engine ¶
Construct engine from header (UPDE or Stuart-Landau).
Parameters¶
header : dict[str, Any] The audit header record (engine config).
Returns¶
UPDEEngine | StuartLandauEngine The engine reconstructed from the header.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
verify_determinism_chained ¶
verify_determinism_chained(
engine: UPDEEngine,
entries: list[dict[str, Any]],
atol: float = 1e-06,
) -> tuple[bool, int]
Chained multi-step replay: output of step N must match input of step N+1.
Returns (passed, n_verified).
Parameters¶
engine : UPDEEngine The engine used to replay logged steps. entries : list[dict[str, Any]] Audit-log entries to operate on. atol : float Absolute comparison tolerance.
Returns¶
tuple[bool, int]
A (passed, n_verified) pair.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
verify_integrity
staticmethod
¶
Verify the SHA256 hash chain of audit log entries.
Returns (all_valid, n_verified). Legacy logs without _hash
fields return (True, 0) unless SPO_AUDIT_KEY is configured.
Parameters¶
entries : list[dict[str, Any]] Audit-log entries to operate on.
Returns¶
tuple[bool, int]
A (all_valid, n_verified) pair for the hash chain.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
verify_determinism_sl_chained ¶
verify_determinism_sl_chained(
engine: StuartLandauEngine,
entries: list[dict[str, Any]],
atol: float = 1e-06,
) -> tuple[bool, int]
Chained multi-step replay for Stuart-Landau engine.
Supports two log formats: - New format: separate 'phases' (N) + 'amplitudes' (N) fields. - Legacy format: 'phases' holds the full SL state (2N) with 'mu' present. When neither mu nor amplitudes are present, skips with a warning. Returns (passed, n_verified).
Parameters¶
engine : StuartLandauEngine The engine used to replay logged steps. entries : list[dict[str, Any]] Audit-log entries to operate on. atol : float Absolute comparison tolerance.
Returns¶
tuple[bool, int]
A (passed, n_verified) pair for the Stuart-Landau replay.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | |
verify_determinism ¶
Re-run logged steps and compare global order parameter R.
Requires steps to include 'phases', 'omegas', 'knm', 'zeta', 'psi', 'alpha' fields for full replay. Compares replayed global R against logged 'R' (or 'r_global') field.
Parameters¶
engine : UPDEEngine The engine used to replay logged steps. steps : list[dict[str, Any]] Number of simulation steps to run.
Returns¶
bool
True when the replayed order parameter matches the log.
Source code in src/scpn_phase_orchestrator/runtime/replay.py
Functions:¶
Protobuf Event Stream¶
scpn_phase_orchestrator.runtime.audit_stream provides the event-sourced stream layer.
The schema is tracked in proto/audit.proto and packaged as
scpn_phase_orchestrator/audit/audit.proto.
from scpn_phase_orchestrator.runtime.audit_stream import (
read_event_stream,
verify_event_stream_integrity,
)
events = read_event_stream("audit.spoa")
ok, verified = verify_event_stream_integrity(events)
The stream is not a replacement for deterministic replay; it is the live transport for the same audit records. The JSONL file remains the compatibility format for existing reports and replay tooling.
audit_stream ¶
Event-sourced audit stream backed by length-delimited protobuf envelopes.
Classes¶
AuditStreamEvent
dataclass
¶
AuditStreamEvent(
schema_version: int,
stream_id: str,
sequence: int,
event_type: str,
recorded_at_unix_ns: int,
source: str,
previous_hash: str,
payload_json: str,
payload_sha256: str,
event_hash: str,
signature_algorithm: str,
signature_key_id: str,
signature: str,
audit_mode: str,
payload: Payload,
)
Decoded audit event envelope with parsed JSON payload.
EventStreamWriter ¶
Append length-delimited protobuf audit events to a stream file.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
Attributes¶
path
property
¶
Methods:¶
write ¶
Append one payload as a hashed and optionally signed audit event.
Parameters¶
payload : Payload
The event or wire payload.
event_type : str | None
Named event type, or None.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
flush ¶
close ¶
Functions:¶
read_event_stream ¶
Read all protobuf events from an SPO audit stream.
Parameters¶
path : str | Path Filesystem path to the target file.
Returns¶
list[AuditStreamEvent] The decoded audit stream events.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
iter_event_stream ¶
iter_event_stream(
path: str | Path,
*,
from_start: bool = False,
poll_interval_s: float = 0.2,
) -> Iterator[AuditStreamEvent]
Yield existing and newly appended stream events in order.
Parameters¶
path : str | Path Filesystem path to the target file. from_start : bool Whether to replay from the start of the stream. poll_interval_s : float Poll interval in seconds.
Returns¶
Iterator[AuditStreamEvent] An iterator over existing and newly appended stream events.
Raises¶
FileNotFoundError If the stream file does not exist.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
tail_event_stream ¶
tail_event_stream(
path: str | Path,
*,
from_start: bool = False,
max_events: int | None = None,
poll_interval_s: float = 0.2,
) -> list[AuditStreamEvent]
Tail a stream file until max_events decoded events are available.
Use :func:iter_event_stream for unbounded live streaming.
Parameters¶
path : str | Path
Filesystem path to the target file.
from_start : bool
Whether to replay from the start of the stream.
max_events : int | None
Maximum number of events to read, or None.
poll_interval_s : float
Poll interval in seconds.
Returns¶
list[AuditStreamEvent]
The decoded events, up to max_events.
Raises¶
ValueError If the inputs are invalid or inconsistent.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
verify_event_stream_integrity ¶
Verify payload digests, sequence continuity, and event hash chaining.
Parameters¶
events : list[AuditStreamEvent] The decoded audit stream events.
Returns¶
tuple[bool, int]
A (ok, count) pair: integrity flag and verified event count.
Source code in src/scpn_phase_orchestrator/runtime/audit_stream.py
Pipeline integration¶
The audit logger sits at the output of the supervisor loop:
SupervisorPolicy.decide() ──→ list[ControlAction]
│
┌──────┼──────┐
↓ ↓ ↓
Actuator Audit EventBus
Logger
│
audit.jsonl (append)
audit.spoa (append)
│
SHA-256 chain
Every regime transition, actuation command, and boundary violation is recorded. The audit trail is the authoritative record of what the system did and why.
AuditLogger API¶
| Method | Signature | Description |
|---|---|---|
log_header |
(n_oscillators, dt, method, seed, amplitude_mode) |
Engine config record |
log_step |
(step, upde_state, actions, *, phases, omegas, knm, alpha, zeta, psi_drive, amplitudes, mu, knm_r, epsilon) |
Full simulation step |
log_event |
(event_type: str, data: dict) |
Named event with arbitrary data |
close |
() |
Flush and close file handle |
Supports context manager (with AuditLogger(...) as logger:).
log_step optionally records full engine state (phases, omegas, knm, alpha)
for deterministic replay. When phases is provided, omegas, knm, and
alpha are required (raises AuditError otherwise). Stuart-Landau fields
(amplitudes, mu, knm_r, epsilon) are optional.
Audit in operational workflows¶
Audit records are used in three recurring workflows:
- Incident reconstruction: replay the same binding spec and input state,
then compare
verify_determinism()against the original trace. - Release review: use
log_headerand per-step metadata to confirm that backend choice, precision mode, and adapter set were constant across candidate runs. - Policy validation: compare regime transitions, action frequency, and boundary hits before promoting rules to wider environments.
Use signed mode when operational evidence is required (SPO_AUDIT_KEY or
SPO_AUDIT_KEYRING). Unsigned logs remain available for local development,
but they are explicitly flagged and must not be used as production evidence.
ReplayEngine API¶
| Method | Signature | Description |
|---|---|---|
load |
() → list[dict] |
Parse all JSONL entries |
load_header |
(entries) → dict \| None |
Extract engine config |
step_entries |
(entries) → list[dict] |
Filter to replayable steps |
build_engine |
(header) → UPDEEngine \| StuartLandauEngine |
Reconstruct engine from header |
verify_integrity |
(entries) → (bool, int) |
Verify SHA-256 chain |
verify_determinism |
(engine, steps) → bool |
Compare replayed R to logged R |
verify_determinism_chained |
(engine, entries, atol) → (bool, int) |
Multi-step replay: output N = input N+1 |
verify_determinism_sl_chained |
(engine, entries, atol) → (bool, int) |
Stuart-Landau chained replay |
Hash chain verification algorithm¶
prev_hash = "0" * 64 # genesis hash
for record in records:
stored_hash = record.pop("_hash")
content = json.dumps(record, separators=(",", ":"))
expected = sha256((prev_hash + content).encode()).hexdigest()
assert stored_hash == expected # tamper detection
prev_hash = stored_hash
Compliance references¶
- NIST SP 800-92: Guide to Computer Security Log Management
- IEC 62443: Industrial communication networks security
- ISO 27001 A.12.4: Logging and monitoring