access-engine/docs/decision-emission-operations.md
tegwick 0a05383195
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Failing after 1m1s
Bound durable outbox storage and enforce process writer exclusion
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 18:43:52 +02:00

7.6 KiB

Decision emission preparation

FLEX-WP-0031 implements a durable local outbox, daily per-class heartbeats, asynchronous delivery and optional PVC. Every pin still defaults to no outbox and no delivery worker. cadence.yaml remains declared-not-yet-emitting; G2 does not close on local tests or chart rendering.

After sender admission and the exact approved rollout, enable the chart's outbox.enabled, choose a unique outbox.source for the pin, and set outbox.size against measured capacity and expected cumulative log growth. outbox.existingClaim can reuse its own retained claim. New claims carry Helm's keep policy so uninstall does not remove the evidence log; any eventual claim cleanup needs its own reviewed action. The chart enforces one replica and Recreate; each source requires its own outbox. Never run two processes against one outbox directory. The fsGroup permits the non-root process to write the mount while keeping its root filesystem read-only.

Delivery requires explicit serve flags:

--audit-url <admitted audit-core http(s) /v1/events endpoint>
--audit-token-file <governed mounted sender token path>

Neither flag is enabled by the chart or any checked-in production pin. These flags require --outbox-dir; the source is supplied by --emission-source. The worker reads the token file per event to support rotation, refuses HTTP redirects, and does not log tokens or receiver response bodies. Receiver authentication/network admission and token mounts remain owner dependencies.

The worker commits due heartbeats into the same log as decisions, then drains bounded batches in order. The daily timestamp is recovered from the committed log after restart. Each heartbeat has its own event/correlation ID and carries data.class, committed count and the process's released-uncommitted count. Known uncommitted releases produce released-uncommitted-finding instead of a nothing-to-report assertion. This count is volatile across restart, as the existing atomicity ruling states; the heartbeat does not strengthen that bound.

Only documented 200 (duplicate) and 202 (accepted) responses advance drain.json. The cursor is fsynced, atomically replaced and its directory synced. A rejection, timeout or cursor-write failure retains the event and replays its original idempotency key. A failing head event blocks subsequent delivery rather than being dropped. HTTP occurs outside the decision lock.

GET /v1/emission reports committed and released-uncommitted decision counts, pending event depth, oldest pending commit time and lag_exceeded when depth exceeds 100 or age exceeds 1h. Counts include no record payload. The drain cursor advances over heartbeats as well as decisions; pending depth includes both. Acknowledged entries are retained in the append-only log. There is no compaction/retention worker, so operators must plan storage and monitoring for cumulative log growth before rollout.

docs/contracts/decision-emission-cadence.json is the schema-valid wire declaration for receiver registration. It names all five classes explicitly; allow's additional reconciliation obligation is in a source extension because the generic expected-rate form prohibits a reconciliation block and the security profile prohibits duplicate classes. audit-core's parser accepts the declaration; that does not prove native sender admission or evaluation of the source extension.

Run make verify-emission-cadence against the sibling InfoTechCanon schema and NetKingdom profile checker. The declaration pins the schema SHA-256 and the check rejects drift. Go tests check inventory/heartbeat parity; chart checks exercise auth on/off, existing/new PVCs and refusal of multiple replicas.

A valid remote comparator remains blocked: audit-core windows use receiver accepted_at, and local windows would use commit time. The current ingest receipt contains no acceptance timestamp. Comparing these directly would manufacture divergence after delayed delivery. Requirements and native acceptance gates are in blocked-task-dependencies-2026-10-04.md.

Bounded single writer for native signer admission

Open now requires the admitted Linux filesystem locking semantics. It obtains an exclusive nonblocking OS flock on owner/current-group regular writer.lock before reading the cursor or inspecting/recovering the journal. The FD remains open for the outbox lifetime; another process refuses before journal mutation. Close waits for an in-flight drain/cursor commit, closes the journal and releases the lock; process death also releases it. The lock file is retained, never unlinked. It is created 0600; writable-PVC fsGroup reconciliation may widen it to 0660 for the same admitted UID/GID, which is supported. The lock, journal and cursor refuse foreign ownership/group, other-access, hardlinks, symlinks and nonregular files. The lock is nonsecret; private seed permissions remain separately strict. This protects competing admitted producers, not hostile same-UID unlink/rename or privileged host tampering. Native PVC admission must prove actual filesystem locking semantics; RWO and Recreate with one replica alone are insufficient.

serve --outbox-max-bytes and chart outbox.maxBytes select a positive integer at most 67108864 (64MiB), also the default and source hard ceiling. Each newline-terminated record and each atomic aggregate batch is capped at 2MiB. The cap is checked before any write; a rejected batch cannot partially commit. GET /v1/emission reports max/committed/remaining bytes and byte-cap exhaustion and writer_fenced without payloads. The requested PVC capacity is not a filesystem quota. The initial retained Secrets pin proposes a new 1Gi local-path claim; no existing claim was observed. Reclaim/retention, permissions, volume identity and native filesystem semantics require separate owner admission.

Startup refuses files above the selected ceiling and streams records with a 2MiB line bound, rather than loading the whole log. The cursor is bounded to 4KiB. A newline-terminated corrupt/oversized entry is preserved and refused; only the existing bounded unterminated final write repair truncates uncommitted bytes. Initial journal directory entries and successful repair truncation are synced. Startup failure releases the OS lock. Existing oversized committed state is never automatically deleted, compacted, rotated or accepted by raising a runtime cap above the hard maximum.

Write/fsync errors withhold all effects under current GH-DEC-2026-024, including DENY. A partial append is truncated back to the committed prefix and synced; if that rollback cannot be confirmed, the writer is fenced for its lifetime until Close/restart and independent recovery. Capacity refusal touches no prior bytes. Actual /dev/full ENOSPC and /dev/null fsync failure tests exercise closed failure/fencing. A filled bounded journal remains full across restart; acknowledged entries are still retained. At 75% capacity request an exact reviewed export/retention action, and at 90% stop initiating protected work. These are operator thresholds, not an invented automated retention worker or audit-core admission. No underlying disk-full condition is repaired by retrying a protected operation or deleting evidence.

The retained lifecycle signer rollout must use a new immutable CI candidate containing reviewed hardening. Native admission includes actual competing-process exclusion, capacity/failure/restart and response/original-record checks. Local source tests, Helm keep annotation and a volume request do not prove node-loss recovery, receiver custody or complete emission adoption. FLEX-WP-0031's remaining native/G2 obligations stay open.