Implement local decision delivery and reconcile blocked workplan requirements
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m1s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-fcad-7ea3-bf36-361fc8d679e8
This commit is contained in:
tegwick 2026-10-04 11:40:37 +02:00
parent 257a83e997
commit 3cb1cfaedf
25 changed files with 1258 additions and 50 deletions

View file

@ -64,6 +64,10 @@ ci: vet lint test build overlay-render
verify-posture:
@bash tools/verify-posture.sh
.PHONY: verify-emission-cadence
verify-emission-cadence:
python3 tools/verify_emission_cadence.py
overlay-render:
@tests/stage1.sh

View file

@ -35,7 +35,7 @@
| workplan | FLEX-WP-0024 | finished | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
| workplan | FLEX-WP-0025 | finished | — | workplans/FLEX-WP-0025-fact-versus-assertion.md |
| workplan | FLEX-WP-0026 | finished | — | workplans/FLEX-WP-0026-openrouter-native-contract.md |
| workplan | FLEX-WP-0027 | blocked | — | workplans/FLEX-WP-0027-t03-human-review.md |
| workplan | FLEX-WP-0027 | finished | — | workplans/FLEX-WP-0027-t03-human-review.md |
| workplan | FLEX-WP-0028 | finished | — | workplans/FLEX-WP-0028-compact-sitting-review.md |
| workplan | FLEX-WP-0029 | finished | — | workplans/FLEX-WP-0029-stance-register-second-edition.md |
| workplan | FLEX-WP-0030 | finished | — | workplans/FLEX-WP-0030-boundary-declaration-cleanup.md |
@ -165,7 +165,7 @@
| task | FLEX-WP-0026-T02 | done | — | workplans/FLEX-WP-0026-openrouter-native-contract.md |
| task | FLEX-WP-0027-T01 | done | — | workplans/FLEX-WP-0027-t03-human-review.md |
| task | FLEX-WP-0027-T02 | done | — | workplans/FLEX-WP-0027-t03-human-review.md |
| task | FLEX-WP-0027-T03 | wait | — | workplans/FLEX-WP-0027-t03-human-review.md |
| task | FLEX-WP-0027-T03 | done | — | workplans/FLEX-WP-0027-t03-human-review.md |
| task | FLEX-WP-0028-T01 | done | — | workplans/FLEX-WP-0028-compact-sitting-review.md |
| task | FLEX-WP-0028-T02 | done | — | workplans/FLEX-WP-0028-compact-sitting-review.md |
| task | FLEX-WP-0029-T01 | done | — | workplans/FLEX-WP-0029-stance-register-second-edition.md |
@ -184,8 +184,10 @@
| task | FLEX-WP-0031-T02 | wait | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T03 | done | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T04 | wait | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T05 | wait | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T05 | done | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T06 | wait | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T07 | done | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0031-T08 | wait | — | workplans/FLEX-WP-0031-decision-record-emission.md |
| task | FLEX-WP-0032-T01 | done | — | workplans/FLEX-WP-0032-informed-decision-list-action.md |
| task | FLEX-WP-0032-T02 | done | — | workplans/FLEX-WP-0032-informed-decision-list-action.md |
| task | FLEX-WP-0032-T03 | done | — | workplans/FLEX-WP-0032-informed-decision-list-action.md |

View file

@ -36,10 +36,11 @@ form: heartbeat-or-reconciliation
rate_monitoring: forbidden # class-level exception stated on `allow` only
# STATE. The classification below is published and in force as a declaration.
# The emission pipeline is not built: no sender named flex-auth or
# access-engine is registered with audit-core, no token lane, no scope row, and
# the decision record reaches consumers in the /v1/check response rather than
# into any custody. This file is therefore the declaration §11 requires and not
# The outbox, disabled-by-default heartbeat/drain and optional PVC are built
# (FLEX-WP-0031 T03/T05/T07). No pin enables them in production, no sender is
# registered with audit-core, and token/scope/observer admission is pending.
# The decision record still reaches consumers in the /v1/check response rather
# than into custody. This file is therefore the declaration §11 requires and not
# a claim that the guarantee is delivered today. The delivery gap is G2 in
# docs/conformance/security-layer-conformance.md, with owner, blocker and
# review date, and it fails CLOSED on its distinguishing case: a stream with no
@ -150,10 +151,10 @@ classes:
removed. An allow carries a TTL lifetime, so a suppressed allow expires
into a denial of service rather than into a standing grant.
heartbeat:
action: flex-auth.decision.heartbeat
action: audit-core.heartbeat
evidence_class: operational
rarity: scheduled
note: The positive assertion above; not a decision.
note: Per-class data.class assertion above; not a decision. Wire declaration is docs/contracts/decision-emission-cadence.json.
bound: >-
Heartbeat and reconciliation detect loss, outage, drain failure and accident.

View file

@ -5,6 +5,13 @@ metadata:
labels:
{{- include "flex-auth.labels" . | nindent 4 }}
spec:
{{- if .Values.outbox.enabled }}
{{- if ne (int .Values.replicaCount) 1 }}
{{- fail "outbox requires replicaCount=1: one writer per source and PVC" }}
{{- end }}
strategy:
type: Recreate
{{- end }}
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
@ -31,6 +38,12 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
{{- toYaml .Values.args | nindent 12 }}
{{- if .Values.outbox.enabled }}
- --outbox-dir
- {{ .Values.outbox.mountPath | quote }}
- --emission-source
- {{ required "outbox.source is required when outbox.enabled=true" .Values.outbox.source | quote }}
{{- end }}
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
{{- $binding := required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }}
- --caller-auth-mode
@ -65,14 +78,26 @@ spec:
drop:
- ALL
readOnlyRootFilesystem: true
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
{{- if or .Values.outbox.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
volumeMounts:
{{- if .Values.outbox.enabled }}
- mountPath: {{ .Values.outbox.mountPath | quote }}
name: flex-auth-outbox
{{- end }}
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
- mountPath: {{ .Values.callerAuth.reviewer.mountPath }}
name: flex-auth-reviewer
readOnly: true
{{- end }}
{{- end }}
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
{{- if or .Values.outbox.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
volumes:
{{- if .Values.outbox.enabled }}
- name: flex-auth-outbox
persistentVolumeClaim:
claimName: {{ default (printf "%s-outbox" (include "flex-auth.name" .)) .Values.outbox.existingClaim | quote }}
{{- end }}
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
- name: flex-auth-reviewer
projected:
defaultMode: 0440
@ -88,4 +113,5 @@ spec:
items:
- key: ca.crt
path: ca.crt
{{- end }}
{{- end }}

View file

@ -0,0 +1,19 @@
{{- if and .Values.outbox.enabled (not .Values.outbox.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "flex-auth.name" . }}-outbox
annotations:
helm.sh/resource-policy: keep
labels:
{{- include "flex-auth.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.outbox.storageClassName }}
storageClassName: {{ .Values.outbox.storageClassName | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.outbox.size | quote }}
{{- end }}

View file

@ -9,6 +9,15 @@ image:
replicaCount: 1
# Inert until enabled for an approved pin. The outbox is single-writer.
outbox:
enabled: false
source: ""
mountPath: /var/lib/flex-auth/outbox
size: 1Gi
storageClassName: ""
existingClaim: ""
args:
- serve
- --addr

View file

@ -12,6 +12,7 @@ import (
"net/http"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
@ -329,6 +330,8 @@ func runServe(args []string, stdout, stderr io.Writer) int {
logPath := fs.String("log", "", "optional JSONL decision log path")
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats")
auditTokenFile := fs.String("audit-token-file", "", "mounted sender token file, never a token value")
callerAuthMode := fs.String("caller-auth-mode", "disabled", "disabled, warn, or enforce")
callerAudience := fs.String("caller-audience", "flex-auth", "required caller token audience")
callerKubernetesURL := fs.String("caller-kubernetes-url", "https://kubernetes.default.svc", "Kubernetes API base URL for TokenReview")
@ -348,6 +351,17 @@ func runServe(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "serve --outbox-dir requires --emission-source and excludes --log")
return 64
}
delivery := emission.DeliveryConfig{URL: *auditURL, TokenFile: *auditTokenFile}
if *auditURL != "" || *auditTokenFile != "" {
if *outboxDir == "" {
fmt.Fprintln(stderr, "audit delivery requires --outbox-dir")
return 64
}
if err := delivery.Validate(); err != nil {
fmt.Fprintln(stderr, err)
return 64
}
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
@ -380,6 +394,15 @@ func runServe(args []string, stdout, stderr io.Writer) int {
if outbox != nil {
mountEmission(mux, outbox)
}
if *auditURL != "" {
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
runEmissionDelivery(ctx, outbox, delivery, stderr)
}()
defer func() { cancel(); <-done }()
}
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
@ -388,6 +411,31 @@ func runServe(args []string, stdout, stderr io.Writer) int {
return 0
}
func runEmissionDelivery(ctx context.Context, outbox *emission.Outbox, config emission.DeliveryConfig, stderr io.Writer) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
if ctx.Err() != nil {
return
}
if err := outbox.AppendDueHeartbeats(); err != nil {
fmt.Fprintf(stderr, "emission heartbeat commit: %v\n", err)
}
n, err := outbox.Drain(ctx, config, 100)
if err != nil && ctx.Err() == nil {
fmt.Fprintf(stderr, "emission drain: %v\n", err)
}
if err == nil && n == 100 {
continue // Drain the next bounded batch without imposing a rate cap.
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
return newServeMuxWithCallerAuth(engine, callerauth.Disabled())
}

View file

@ -0,0 +1,40 @@
# Blocked task review — 2026-10-04
Review scope: all nonterminal task blocks in access-engine workplans. Changes
are confined to this repository and its State Hub projection. Owner source
records were inspected read-only. This record establishes acceptance
requirements; it records no new live approval, credential, identity or window.
| Our task | Owner dependency and record | Required return / acceptance evidence |
| --- | --- | --- |
| FLEX-WP-0031-T02 | audit-core, accepted AUDIT-IN-0006 | Exact sender rows for `flex-auth.ops-warden`, `.tenant-engine`, `.user-engine`, `.secrets-engine`, `.informed-decision-t03`, `.informed-decision-sitting`; `may_write: true`, `may_read: false`, `tenants: ["*"]` with the accepted deny-for-not-yet-created-tenant justification, load-bearing evidence kind, redact policy, four heartbeat classes at 86400s, and the source-owned wire cadence declaration. Return owning revision, registration references, token custody/mount references, and native acceptance status; prepared rows alone do not establish live admission. The new fetch pin needs separate admission if it is added to the emission rollout; the six-pin intake does not cover it. |
| FLEX-WP-0031-T02 | gate-house, GH-WP-0004-T12 (todo in owner source) | A published decision disposing of FLEX-DEC-2026-018: deny/redact/not_applicable may leave after outbox failure while allow/audit_only are withheld. audit-core's accepted intake is not a statutory waiver. State whether the direction-limited exception is admitted, its declaration/detection conditions, or the required replacement semantics. |
| FLEX-WP-0031-T02 / T08 | railiance-platform custody; ops-warden routing | AUDIT-IN-0006 names the attended OpenBao sender mint into `platform/data/workloads/audit-core/senders`, using the platform attended helper. Supply the governed lane and per-pin token mount/rotation references plus owner admission receipts; never send token values to Git, State Hub or chat. Before any credential request, run `warden route find` and `warden route show` as AGENTS.md requires. No credential request or mint was made by this review. |
| FLEX-WP-0031-T06 | audit-core, reconciliation contract | Current `audit_core/postgres_backend.py:event_counts` and `sqlite_backend.py:event_counts` count **accepted_at** in `[since, until)`. Our log has **CommittedAt/OccurredAt**. Ingest returns only status/reference, with no acceptance timestamp. Return an approved matching basis: counts by source event time, or stable per-event acceptance timestamps (including duplicates) that our sender can persist. Define bounds, clock/late-delivery handling, source/tenant scope and retry/crash treatment. Identical bounds alone cannot reconcile these two clocks; missing admission is not the only blocker. |
| FLEX-WP-0031-T08 | operator, platform/network owners and audit-core observer | Approve exact immutable image, enabled pin/source, PVC size/node capacity, storage growth/retention, governed token mount and ingress/egress changes. Return native 202/duplicate-200 delivery, restart/cursor and lag evidence, plus an audit-core silence finding in a separately authorized non-production exercise. This review sets no test window or engagement. The optional chart and worker remain disabled on every pin. |
| FLEX-WP-ADHOC-2026-10-03-T02 | operator decision 877c268b-2089-42f1-a811-d98f549c1cb2, ops-bridge, ops-warden WARDEN-WP-0039-T03 | Decision is still open. Approve the exact CI digest and live install; record headroom/dry-run/rollout, Service IP and ops-bridge tunnel on 19091. ops-warden's unread return 26992e49 confirms fetch URL separation is implemented at 7d4381e/88c22d4; its task now needs configured `policy.fetch_flex_auth_url`, native health/allow/wrong-owner/fail-closed receipts. This is deployment acceptance, not missing evaluator code. |
| FLEX-WP-0020-T11 | sbom-nexus, open SBOM-IN-0001 | Owner receipt of the scheduled catch-up ingest: `access-engine` snapshot, frozen source SHA, repository UUID `fda8ad85-a7d7-4055-8f21-902a533e59df`. Owner source still shows open; elapsed time is not an ingest receipt. No manual production ingest was attempted. |
| FLEX-WP-0020-T11 | secrets-engine; railiance-fabric | secrets-engine still has `../flex-auth` in `docs/exec-owner-binding.md:89`; return the source-coordinate correction and existing handoff completion reference. railiance-fabric must return the owning residual record for its historical `repo:flex-auth` discovery node and explicitly state historical retention vs canonical mapping. No receiving task ID or completion is invented for either return. |
| FLEX-WP-0020-T11 | operator decision e071b34e-c4d4-49d0-824d-2c58bb0ae496 | Decision remains open. Name the soak bounds, observed deployment/decision/telemetry/package/automation evidence and residual owner receipts, then approve the exact obsolete checkout to remove. No soak success or cleanup is inferred. The protected flex-auth alias remains retained. |
The three remaining root workplans are `blocked`, with task status `wait` and
explicit dependency/human gates. Completed local work is not blocked on remote
registration: FLEX-WP-0031-T05 now covers the implementation, T07 the optional
PVC and profile validation, and T08 the remaining native delivery acceptance.
T06 retains the valid-window comparator and native acceptance requirements.
FLEX-WP-0027 is finished on the original task scope. secrets-engine's terminal
SECRETS-WP-0010-T03 and hashed native completion receipt establish human
acceptance/consumption for the September 16 version-2 cycle, actor `worsch`.
See `evidence/2026-10-04-t03-owner-completion-review.json`. The version-3
records now in access-engine are the separate IR-WP-0005-T02 cycle; consumed
approvals cannot be replayed. The old open human decision remains recorded as
history; this review does not answer a fresh human decision for the operator.
The whitehat return from this session also remains a gap: no recorded E2
tenant-A identity establishes absence of tenant-B authority. Reply
`ec96e1ba-27bd-4faa-8773-55ffeaba095c` on thread
`bd8cf37e-80e8-4df9-9181-1441b31baa30` returns that gap as access-engine. E2 and
ASM targets remain pending; terminal engagements were not reused and no test
packet was sent. A real tenant-scoped identity/authority receipt must come from
its identity/surface owner before access-engine can confirm it.

View file

@ -56,7 +56,7 @@ review date.
| # | Gap | Owner | Blocked on | Review |
| --- | --- | --- | --- | --- |
| G1 | Registry-snapshot digest absent from decision provenance. §9.7.2 promotes it to a conformance prerequisite: a decision that turned on registry content must be replayable from its own record. | `flex-auth` | implementation | `FLEX-WP-0019` |
| G2 | **Emission guarantee declared, not delivered.** Ruled 2026-09-21 (`GH-DEC-2026-018`): flex-auth is the source; custody is never source; `audit-core` confirmed its half and declined the role (`AUDIT-IN-0005`). The per-event-class inventory and classification are **published** in [`cadence.yaml`](../../cadence.yaml) and named from `INTENT.md` (`source_of_evidence: true`, `emission_guarantee`). Five decision classes are load-bearing; `deny`, `redact`, `not_applicable`, `audit_only` are **rare** — heartbeat **and** reconciliation, rate monitoring forbidden; `allow` is **volume** — expected-rate **and** reconciliation. Nothing emits yet: no sender is registered with `audit-core`, no outbox, no heartbeat. | `flex-auth` | outbox + heartbeat implementation, and an `audit-core` sender registration (intake + token lane, per `AUDIT-IN-0002`/`0003`) — `FLEX-WP-0031` | **2026-10-19** |
| G2 | **Emission guarantee declared, not delivered.** Ruled 2026-09-21 (`GH-DEC-2026-018`): flex-auth is the source; custody is never source; `audit-core` confirmed its half and declined the role (`AUDIT-IN-0005`). The per-event-class inventory and classification are **published** in [`cadence.yaml`](../../cadence.yaml) and named from `INTENT.md` (`source_of_evidence: true`, `emission_guarantee`). Five decision classes are load-bearing; `deny`, `redact`, `not_applicable`, `audit_only` are **rare** — heartbeat **and** reconciliation, rate monitoring forbidden; `allow` is **volume** — expected-rate **and** reconciliation. No production pin emits yet. Durable outbox, per-class heartbeat/drain and optional PVC are implemented and locally tested; sender admission, compatible reconciliation windows and native observer evidence remain pending. | `flex-auth` | audit-core `AUDIT-IN-0006` admission/token delivery, gate-house `GH-WP-0004-T12` ruling, compatible count windows and approved native heartbeat/reconciliation/silence-finding evidence — `FLEX-WP-0031` | **2026-10-19** |
| G3 | Published stance-register review is stale: written at two register rows, §13.1 now carries five, and its Finding 1 was ruled by v0.8 §6.4 obligation 3. | `flex-auth` | second edition | `FLEX-WP-0029` |
**G2 fails closed.** A stream with no delivered cadence produces no silence
@ -96,3 +96,10 @@ heartbeat and reconciliation and forbidding rate monitoring. That is the same pr
praised in `ops-warden`'s stance map: the published artifact is asserted equal to
the shipped one by test, rather than merely written down. It does **not** assert
the contents below the declaration — a reviewer still has to read this file.
2026-10-04 implementation review: source-owned
`docs/contracts/decision-emission-cadence.json` passes the supplied InfoTechCanon
schema and NetKingdom security profile. This is declaration validity, not native
emission evidence. `FLEX-DEC-2026-018` supplies the local release rule;
`GH-WP-0004-T12` still owes the statutory disposition of its failure-path
exception. G2 remains open and no atomicity waiver is inferred.

View file

@ -0,0 +1,178 @@
{
"schema_version": "0.1",
"declaration_id": "flex-auth-decision-emission-cadence-v0.1",
"source": "flex-auth",
"sources": [
{
"source_id": "flex-auth:deny",
"event_class": "flex-auth.decision.deny",
"form": "heartbeat-or-reconciliation",
"reconciliation": {
"compare_local": "committed outbox counts per class, tenant and explicit since/until window, with pending deliveries counted as lag",
"compare_observed": "audit-core GET /v1/reconciliation for the exact pin source and tenant, once receiver acceptance-time windows are compatible with local counts; time-basis contract pending",
"divergence": "finding",
"undrained_local": "lag-not-divergence"
},
"extensions": {
"netkingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
},
"heartbeat": {
"event_class": "audit-core.heartbeat",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding",
"extensions": {
"flex-auth": {
"asserted_class": "flex-auth.decision.deny",
"observer_registration_status": "pending"
}
}
}
},
{
"source_id": "flex-auth:redact",
"event_class": "flex-auth.decision.redact",
"form": "heartbeat-or-reconciliation",
"reconciliation": {
"compare_local": "committed outbox counts per class, tenant and explicit since/until window, with pending deliveries counted as lag",
"compare_observed": "audit-core GET /v1/reconciliation for the exact pin source and tenant, once receiver acceptance-time windows are compatible with local counts; time-basis contract pending",
"divergence": "finding",
"undrained_local": "lag-not-divergence"
},
"extensions": {
"netkingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
},
"heartbeat": {
"event_class": "audit-core.heartbeat",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding",
"extensions": {
"flex-auth": {
"asserted_class": "flex-auth.decision.redact",
"observer_registration_status": "pending"
}
}
}
},
{
"source_id": "flex-auth:not_applicable",
"event_class": "flex-auth.decision.not_applicable",
"form": "heartbeat-or-reconciliation",
"reconciliation": {
"compare_local": "committed outbox counts per class, tenant and explicit since/until window, with pending deliveries counted as lag",
"compare_observed": "audit-core GET /v1/reconciliation for the exact pin source and tenant, once receiver acceptance-time windows are compatible with local counts; time-basis contract pending",
"divergence": "finding",
"undrained_local": "lag-not-divergence"
},
"extensions": {
"netkingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
},
"heartbeat": {
"event_class": "audit-core.heartbeat",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding",
"extensions": {
"flex-auth": {
"asserted_class": "flex-auth.decision.not_applicable",
"observer_registration_status": "pending"
}
}
}
},
{
"source_id": "flex-auth:audit_only",
"event_class": "flex-auth.decision.audit_only",
"form": "heartbeat-or-reconciliation",
"reconciliation": {
"compare_local": "committed outbox counts per class, tenant and explicit since/until window, with pending deliveries counted as lag",
"compare_observed": "audit-core GET /v1/reconciliation for the exact pin source and tenant, once receiver acceptance-time windows are compatible with local counts; time-basis contract pending",
"divergence": "finding",
"undrained_local": "lag-not-divergence"
},
"extensions": {
"netkingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
},
"heartbeat": {
"event_class": "audit-core.heartbeat",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding",
"extensions": {
"flex-auth": {
"asserted_class": "flex-auth.decision.audit_only",
"observer_registration_status": "pending"
}
}
}
},
{
"source_id": "flex-auth:allow:rate",
"event_class": "flex-auth.decision.allow",
"form": "expected-rate",
"window_seconds": 3600,
"expected_min": 1,
"drop_below": "finding",
"extensions": {
"netkingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "permitted"
},
"flex-auth": {
"reconciliation": {
"compare_local": "committed outbox counts per class, tenant and explicit since/until window, with pending deliveries counted as lag",
"compare_observed": "audit-core GET /v1/reconciliation for the exact pin source and tenant, once receiver acceptance-time windows are compatible with local counts; time-basis contract pending",
"divergence": "finding",
"undrained_local": "lag-not-divergence"
},
"reason": "The generic expected-rate form forbids a reconciliation block; this source-owned extension preserves the additional reconciliation guarantee without a duplicate event class."
}
}
}
],
"extensions": {
"flex-auth": {
"state": "declared-not-yet-emitting",
"gap": "G2",
"inventory": {
"load_bearing": [
"flex-auth.decision.deny",
"flex-auth.decision.redact",
"flex-auth.decision.not_applicable",
"flex-auth.decision.audit_only",
"flex-auth.decision.allow"
],
"rare_load_bearing": [
"flex-auth.decision.deny",
"flex-auth.decision.redact",
"flex-auth.decision.not_applicable",
"flex-auth.decision.audit_only"
],
"attributive": []
},
"contract": {
"owner": "info-tech-canon",
"schema_sha256": "6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae"
},
"lag_bound": {
"outbox_depth": 100,
"outbox_age": "1h",
"exceed": "finding"
},
"bound": "Heartbeat and reconciliation detect loss, outage, drain failure and accident. Neither detects a compromised source suppressing an event and its own count together \u2014 \u00a79.6's stated residual, and \u00a716 puts the independent observer outside audit-core's scope. Conformance to this declaration is not a claim of stream completeness."
}
}
}

View file

@ -0,0 +1,70 @@
# 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:
```text
--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`.

View file

@ -0,0 +1,33 @@
{
"assessment_scope": "supplied-inventory",
"conformant": true,
"contract_schema": "/home/worsch/info-tech-canon/infospace/schemas/emission-cadence.schema.yaml",
"contract_valid": true,
"declaration": "/home/worsch/access-engine/docs/contracts/decision-emission-cadence.json",
"findings": [],
"inventory": {
"attributive": [],
"load_bearing": [
"flex-auth.decision.allow",
"flex-auth.decision.audit_only",
"flex-auth.decision.deny",
"flex-auth.decision.not_applicable",
"flex-auth.decision.redact"
],
"rare_load_bearing": [
"flex-auth.decision.audit_only",
"flex-auth.decision.deny",
"flex-auth.decision.not_applicable",
"flex-auth.decision.redact"
]
},
"profile": "netkingdom-emission-cadence-security-profile-v0.1",
"profile_assessed": true,
"summary": {
"must": 0,
"should": 0
},
"reviewed": "2026-10-04",
"declaration_sha256": "6a4af47ebeab0d387d7088adb9601a79fa8a1eeb5c581ec9e15f0ace2977ccc5",
"scope": "Source declaration/schema/profile only; no native audit-core delivery or silence finding claimed."
}

View file

@ -0,0 +1,47 @@
{
"reviewed": "2026-10-04",
"task": "FLEX-WP-0027-T03",
"owner_record": "SECRETS-WP-0010-T03",
"source_receipt": "secrets-engine/docs/evidence/2026-09-16-t03-completion.json",
"source_sha256": "f68b38a3968373fa0811a5906756b63d9dade40475b341b12937c3668b182b3f",
"scope": "Original three T03 reviews, renewed version-2 approvals completed on 2026-09-16; excludes later version-3 IR-WP-0005 run",
"owner_human_acceptance_record": "secrets-engine/workplans/SECRETS-WP-0010-openrouter-native-access.md, T03 completed with native delivery \u2014 2026-09-16",
"native_actions": [
{
"action": "apply",
"actor": "worsch",
"result": "applied",
"record_id": "fb256f9f-303b-4a7f-a060-71b713e2c085",
"decision_id": "decision:dc57b3b9614aa15b",
"ts": "2026-09-16T00:05:09.827448+00:00",
"approval_id": "9935335c-8e9a-566e-a48e-6a5b5f4882eb",
"approval_consumed_at": "2026-09-16T00:05:09+00:00",
"request_digest": "sha256:c5c44adc715385640dffeb3305ff854fc69bd800929f8c31f93a536524089831"
},
{
"action": "verify",
"actor": "worsch",
"result": "pass",
"record_id": "39f8084d-c3f4-443a-8272-2cda87c46c89",
"decision_id": "decision:b4fca706e4c55b9a",
"ts": "2026-09-16T00:05:12.565647+00:00",
"approval_id": "273d6882-6253-5dc9-ac54-544f92ef5e56",
"approval_consumed_at": "2026-09-16T00:05:11+00:00",
"request_digest": "sha256:a735896e9d208032ea08b073bcc819864b12116bdf60852fcdd2e54dad2e62bf"
},
{
"action": "exec",
"actor": "worsch",
"result": "exit-0",
"record_id": "3823a903-1a13-4d85-aef0-cc2e05c477a0",
"decision_id": "decision:82cec64febcf635f",
"ts": "2026-09-16T00:08:30.937892+00:00",
"approval_id": "7ba0c13b-68cd-5b3e-9481-42ba9e385e68",
"approval_consumed_at": "2026-09-16T00:08:32+00:00",
"request_digest": "sha256:4082d2ccad78097a8be34ff0b0221ed2c4b6407fdbaf36e4a583927ae9acc820"
}
],
"approval_state": "All three native claims inspected as consumed after completion",
"new_execution_performed": false,
"consumed_approvals_reused": false
}

View file

@ -0,0 +1,233 @@
package emission
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
const HeartbeatType = "audit-core.heartbeat"
// RareClasses is the source-owned inventory in cadence.yaml.
func RareClasses() []string {
return []string{ClassPrefix + "deny", ClassPrefix + "redact", ClassPrefix + "not_applicable", ClassPrefix + "audit_only"}
}
type heartbeatData struct {
Class string `json:"class"`
Assertion string `json:"assertion"`
Committed uint64 `json:"committed"`
ReleasedUncommitted uint64 `json:"released_uncommitted"`
}
func (o *Outbox) noteHeartbeat(event Event) {
if event.Type != HeartbeatType {
return
}
b, err := json.Marshal(event.Data)
if err != nil {
return
}
var data heartbeatData
if json.Unmarshal(b, &data) != nil {
return
}
at, err := time.Parse(time.RFC3339Nano, event.OccurredAt)
if err == nil {
o.heartbeats[data.Class] = at
}
}
// AppendDueHeartbeats commits one heartbeat per rare class at most every 24h.
// The timestamp is rebuilt from the log on restart, never from a process timer.
func (o *Outbox) AppendDueHeartbeats() error {
o.mu.Lock()
defer o.mu.Unlock()
now := o.clock().UTC()
var events []Event
for _, class := range RareClasses() {
if at, ok := o.heartbeats[class]; ok && now.Sub(at) < 24*time.Hour {
continue
}
id, err := eventID()
if err != nil {
return err
}
assertion := "nothing-to-report"
if o.uncommitted[class] > 0 {
assertion = "released-uncommitted-finding"
}
events = append(events, Event{ID: id, Type: HeartbeatType, Source: o.source, Subject: o.source,
Tenant: PlatformTenant, CorrelationID: id, OccurredAt: now.Format(time.RFC3339Nano),
Data: heartbeatData{Class: class, Assertion: assertion, Committed: o.committed[class], ReleasedUncommitted: o.uncommitted[class]}})
}
if len(events) == 0 {
return nil
}
return o.appendEvents(now.Format(time.RFC3339Nano), events)
}
type drainCursor struct {
Seq uint64 `json:"seq"`
Offset int64 `json:"offset"`
}
func (o *Outbox) loadCursor() error {
b, err := os.ReadFile(filepath.Join(filepath.Dir(o.path), "drain.json"))
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("read drain cursor: %w", err)
}
if err := json.Unmarshal(b, &o.cursor); err != nil {
return fmt.Errorf("decode drain cursor: %w", err)
}
if o.cursor.Offset < 0 || (o.cursor.Seq == 0) != (o.cursor.Offset == 0) {
return errors.New("invalid drain cursor")
}
return nil
}
// nextEntry reads only the committed prefix; mu protects the size snapshot.
func (o *Outbox) nextEntry() (entry, int64, error) {
f, err := os.Open(o.path)
if err != nil {
return entry{}, 0, err
}
defer f.Close()
r := bufio.NewReader(io.NewSectionReader(f, o.cursor.Offset, o.size-o.cursor.Offset))
line, err := r.ReadBytes('\n')
if err != nil {
return entry{}, 0, err
}
var e entry
if err := json.Unmarshal(line, &e); err != nil {
return entry{}, 0, err
}
if e.Seq != o.cursor.Seq+1 || e.Event.Source != o.source {
return entry{}, 0, errors.New("inconsistent next outbox entry")
}
return e, o.cursor.Offset + int64(len(line)), nil
}
func (o *Outbox) saveCursor(cursor drainCursor) error {
dir := filepath.Dir(o.path)
f, err := os.CreateTemp(dir, ".drain-*")
if err != nil {
return err
}
defer os.Remove(f.Name())
b, _ := json.Marshal(cursor)
if _, err = f.Write(b); err == nil {
err = f.Sync()
}
err = errors.Join(err, f.Close())
if err != nil {
return err
}
if err = os.Rename(f.Name(), filepath.Join(dir, "drain.json")); err != nil {
return err
}
d, err := os.Open(dir)
if err != nil {
return err
}
defer d.Close()
return d.Sync()
}
// DeliveryConfig is supplied only after sender admission. Tokens are read from
// a mounted file for each event to support rotation; they never enter logs.
type DeliveryConfig struct {
URL string
TokenFile string
}
func (c DeliveryConfig) Validate() error {
u, err := url.Parse(c.URL)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/v1/events" {
return errors.New("audit URL must be an explicit http(s) /v1/events endpoint without credentials, query or fragment")
}
if c.TokenFile == "" {
return errors.New("audit delivery requires a token file")
}
return nil
}
// Drain sends a bounded batch sequentially. Only 200/202 advance the durable
// cursor. Failed or ambiguous requests replay the same event/idempotency key.
// No receiver error payload or token is included in an error.
func (o *Outbox) Drain(ctx context.Context, config DeliveryConfig, limit int) (int, error) {
if err := config.Validate(); err != nil {
return 0, err
}
if limit < 1 {
return 0, errors.New("drain limit must be positive")
}
o.drainMu.Lock()
defer o.drainMu.Unlock()
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
delivered := 0
for delivered < limit {
o.mu.Lock()
if o.cursor.Seq == o.seq {
o.mu.Unlock()
return delivered, nil
}
e, offset, err := o.nextEntry()
o.mu.Unlock()
if err != nil {
return delivered, fmt.Errorf("read pending event: %w", err)
}
token, err := os.ReadFile(config.TokenFile)
if err != nil {
return delivered, errors.New("audit token file unavailable")
}
value := strings.TrimSpace(string(token))
if value == "" || strings.ContainsAny(value, "\r\n") {
return delivered, errors.New("audit token file is empty or invalid")
}
body, err := json.Marshal(e.Event)
if err != nil {
return delivered, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, config.URL, bytes.NewReader(body))
if err != nil {
return delivered, errors.New("construct audit request failed")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+value)
req.Header.Set("Idempotency-Key", e.Event.ID)
response, err := client.Do(req)
if err != nil {
return delivered, errors.New("audit delivery failed; event retained")
}
response.Body.Close()
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted {
return delivered, fmt.Errorf("audit delivery HTTP %d; event retained", response.StatusCode)
}
o.mu.Lock()
cursor := drainCursor{Seq: e.Seq, Offset: offset}
err = o.saveCursor(cursor)
if err == nil {
o.cursor = cursor
}
o.mu.Unlock()
if err != nil {
return delivered, fmt.Errorf("persist drain cursor: %w", err)
}
delivered++
}
return delivered, nil
}

View file

@ -0,0 +1,246 @@
package emission_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/netkingdom/flex-auth/internal/emission"
"github.com/netkingdom/flex-auth/pkg/api"
)
func TestHeartbeatScheduleSurvivesRestart(t *testing.T) {
dir := t.TempDir()
now := time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)
o, err := emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
o.SetClock(func() time.Time { return now })
if err := o.AppendDueHeartbeats(); err != nil {
t.Fatal(err)
}
if err := o.AppendDueHeartbeats(); err != nil {
t.Fatal(err)
}
o.Close()
o, err = emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
defer o.Close()
o.SetClock(func() time.Time { return now })
if err := o.AppendDueHeartbeats(); err != nil {
t.Fatal(err)
}
if got := len(readEvents(t, dir)); got != 4 {
t.Fatalf("heartbeats after restart = %d", got)
}
seen := map[string]bool{}
for _, e := range readEvents(t, dir) {
if e["type"] != emission.HeartbeatType || e["id"] != e["correlation_id"] {
t.Fatalf("invalid heartbeat: %v", e)
}
if _, err := time.Parse(time.RFC3339Nano, e["occurred_at"].(string)); err != nil {
t.Fatal(err)
}
data := e["data"].(map[string]any)
seen[data["class"].(string)] = true
}
for _, class := range emission.RareClasses() {
if !seen[class] {
t.Fatalf("no heartbeat for %s", class)
}
}
now = now.Add(24 * time.Hour)
o.NoteReleasedUncommitted(api.DecisionEffectDeny)
if err := o.AppendDueHeartbeats(); err != nil {
t.Fatal(err)
}
if got := len(readEvents(t, dir)); got != 8 {
t.Fatalf("daily heartbeats = %d", got)
}
for _, event := range readEvents(t, dir)[4:] {
data := event["data"].(map[string]any)
if data["class"] == "flex-auth.decision.deny" && data["assertion"] != "released-uncommitted-finding" {
t.Fatal("known emission failure concealed by nothing-to-report heartbeat")
}
}
}
func TestDrainRetainsRejectionsAndResumesWithStableIDs(t *testing.T) {
for _, rejection := range []int{400, 401, 403, 500, 302, 204} {
t.Run(http.StatusText(rejection), func(t *testing.T) {
dir := t.TempDir()
tokenFile := filepath.Join(dir, "sender-token")
if err := os.WriteFile(tokenFile, []byte("test-token"), 0600); err != nil {
t.Fatal(err)
}
o, err := emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
if err := o.AppendBatch([]api.DecisionEnvelope{deny("decision:1", "tenant:a"), deny("decision:2", "tenant:a")}); err != nil {
t.Fatal(err)
}
var ids []string
status := rejection
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var event emission.Event
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
t.Error(err)
}
if r.URL.Path != "/v1/events" || r.Header.Get("Idempotency-Key") != event.ID || r.Header.Get("Authorization") != "Bearer test-token" {
t.Errorf("bad delivery headers/path")
}
ids = append(ids, event.ID)
w.Header().Set("Location", "/redirect-must-not-be-followed")
w.WriteHeader(status)
w.Write([]byte("sensitive receiver payload"))
}))
defer server.Close()
cfg := emission.DeliveryConfig{URL: server.URL + "/v1/events", TokenFile: tokenFile}
if n, err := o.Drain(context.Background(), cfg, 100); n != 0 || err == nil || strings.Contains(err.Error(), "sensitive") {
t.Fatalf("rejection advanced or exposed response: %d %v", n, err)
}
if o.Status().Pending != 2 {
t.Fatal("rejected event lost")
}
status = 202
if n, err := o.Drain(context.Background(), cfg, 1); n != 1 || err != nil {
t.Fatalf("accepted: %d %v", n, err)
}
o.Close()
o, err = emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
defer o.Close()
if o.Status().Pending != 1 {
t.Fatal("cursor did not survive restart")
}
status = 200
if n, err := o.Drain(context.Background(), cfg, 100); n != 1 || err != nil {
t.Fatalf("duplicate: %d %v", n, err)
}
if len(ids) != 3 || ids[0] != ids[1] || ids[1] == ids[2] {
t.Fatalf("unstable/reused event IDs: %v", ids)
}
if o.Status().Pending != 0 {
t.Fatal("pending after drain")
}
})
}
}
func TestDrainAmbiguousAcceptanceReplaysAndTokenRotates(t *testing.T) {
dir := t.TempDir()
tokenFile := filepath.Join(dir, "token")
os.WriteFile(tokenFile, []byte("first"), 0600)
o, err := emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
defer o.Close()
o.Append(deny("decision:1", "tenant:a"))
var ids, tokens []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ids = append(ids, r.Header.Get("Idempotency-Key"))
tokens = append(tokens, r.Header.Get("Authorization"))
w.WriteHeader(202)
}))
defer server.Close()
cfg := emission.DeliveryConfig{URL: server.URL + "/v1/events", TokenFile: tokenFile}
// Receiver accepts, but persisting the cursor fails. Retain and retry.
os.Mkdir(filepath.Join(dir, "drain.json"), 0700)
if n, err := o.Drain(context.Background(), cfg, 1); n != 0 || err == nil {
t.Fatalf("cursor failure: %d %v", n, err)
}
os.Remove(filepath.Join(dir, "drain.json"))
os.WriteFile(tokenFile, []byte("rotated"), 0600)
if n, err := o.Drain(context.Background(), cfg, 1); n != 1 || err != nil {
t.Fatalf("retry: %d %v", n, err)
}
if len(ids) != 2 || ids[0] != ids[1] || tokens[1] != "Bearer rotated" {
t.Fatalf("retry/rotation: %v %v", ids, tokens)
}
}
func TestOutboxReportsLagAndRefusesSourceReuse(t *testing.T) {
dir := t.TempDir()
o, err := emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
o.SetClock(func() time.Time { return now })
o.Append(deny("decision:1", "tenant:a"))
if o.Status().LagExceeded {
t.Fatal("fresh event treated as lag finding")
}
now = now.Add(2 * time.Hour)
if !o.Status().LagExceeded {
t.Fatal("overdue event not reported")
}
o.Close()
if other, err := emission.Open(dir, "flex-auth.other"); err == nil {
other.Close()
t.Fatal("source changed on existing outbox")
}
}
func TestSlowReceiverDoesNotBlockDecisionCommit(t *testing.T) {
dir := t.TempDir()
tokenFile := filepath.Join(dir, "token")
if err := os.WriteFile(tokenFile, []byte("local-test-token"), 0600); err != nil {
t.Fatal(err)
}
o, err := emission.Open(dir, "flex-auth.test")
if err != nil {
t.Fatal(err)
}
defer o.Close()
if err := o.Append(deny("decision:1", "tenant:a")); err != nil {
t.Fatal(err)
}
arrived, release := make(chan struct{}), make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
close(arrived)
<-release
w.WriteHeader(202)
}))
defer server.Close()
drained := make(chan error, 1)
go func() {
_, err := o.Drain(context.Background(), emission.DeliveryConfig{URL: server.URL + "/v1/events", TokenFile: tokenFile}, 1)
drained <- err
}()
defer func() {
close(release)
if err := <-drained; err != nil {
t.Error(err)
}
}()
<-arrived
committed := make(chan error, 1)
go func() { committed <- o.Append(deny("decision:2", "tenant:a")) }()
select {
case err := <-committed:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("remote HTTP holds the decision commit lock")
}
// Deferred release lets the drain finish even on test failure. Assert the
// pending count while HTTP is still blocked rather than waiting for it.
if o.Status().Pending != 2 {
t.Fatal("new commit lost during delivery")
}
}

View file

@ -61,7 +61,10 @@ type Status struct {
ReleasedUncommitted map[string]uint64 `json:"released_uncommitted"`
// ReleasedUncommitted is held in memory: a restart during an outbox outage
// loses it (FLEX-DEC-2026-018, consequences).
Means string `json:"means"`
Means string `json:"means"`
Pending uint64 `json:"pending"`
OldestPendingAt string `json:"oldest_pending_at,omitempty"`
LagExceeded bool `json:"lag_exceeded"`
}
// Outbox is a durable, append-only, fsync-per-commit event log.
@ -76,6 +79,9 @@ type Outbox struct {
size int64 // bytes committed; a failed commit is cut back to it
committed map[string]uint64
uncommitted map[string]uint64
drainMu sync.Mutex
cursor drainCursor
heartbeats map[string]time.Time
}
// Open opens or creates the outbox in dir and rebuilds committed counts from it.
@ -92,10 +98,17 @@ func Open(dir, source string) (*Outbox, error) {
clock: time.Now,
committed: map[string]uint64{},
uncommitted: map[string]uint64{},
heartbeats: map[string]time.Time{},
}
if err := o.loadCursor(); err != nil {
return nil, err
}
if err := o.load(); err != nil {
return nil, err
}
if o.cursor.Seq > o.seq || o.cursor.Offset > o.size {
return nil, errors.New("drain cursor is ahead of committed outbox")
}
file, err := os.OpenFile(o.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640)
if err != nil {
return nil, fmt.Errorf("open outbox: %w", err)
@ -126,11 +139,21 @@ func (o *Outbox) load() error {
if err := json.Unmarshal(data[good:good+end], &e); err != nil {
return fmt.Errorf("outbox line %d is corrupt: %w", line, err)
}
if e.Seq != o.seq+1 || e.Event.Source != o.source {
return fmt.Errorf("outbox line %d has inconsistent sequence or source", line)
}
o.seq = e.Seq
o.committed[e.Event.Type]++
good += end + 1
if e.Seq == o.cursor.Seq && int64(good) != o.cursor.Offset {
return errors.New("drain cursor does not match outbox")
}
o.noteHeartbeat(e.Event)
}
o.size = int64(good)
if o.cursor.Seq > o.seq {
return errors.New("drain cursor is ahead of outbox")
}
return nil
}
@ -159,15 +182,24 @@ func (o *Outbox) AppendBatch(decisions []api.DecisionEnvelope) error {
o.mu.Lock()
defer o.mu.Unlock()
now := o.clock().UTC().Format(time.RFC3339Nano)
var buf []byte
seq := o.seq
var events []Event
for _, decision := range decisions {
id, err := eventID()
if err != nil {
return err
}
events = append(events, o.event(id, now, decision))
}
return o.appendEvents(now, events)
}
// appendEvents requires mu; heartbeats use the same durable commit as decisions.
func (o *Outbox) appendEvents(now string, events []Event) error {
var buf []byte
seq := o.seq
for _, event := range events {
seq++
line, err := json.Marshal(entry{Seq: seq, CommittedAt: now, Event: o.event(id, now, decision)})
line, err := json.Marshal(entry{Seq: seq, CommittedAt: now, Event: event})
if err != nil {
return fmt.Errorf("marshal outbox entry: %w", err)
}
@ -181,8 +213,9 @@ func (o *Outbox) AppendBatch(decisions []api.DecisionEnvelope) error {
}
o.seq = seq
o.size += int64(len(buf))
for _, decision := range decisions {
o.committed[Class(decision.Effect)]++
for _, event := range events {
o.committed[event.Type]++
o.noteHeartbeat(event)
}
return nil
}
@ -217,6 +250,16 @@ func (o *Outbox) Status() Status {
"released_uncommitted is decisions released without a record " +
"since this process started, and any non-zero value is a finding",
}
status.Pending = o.seq - o.cursor.Seq
if status.Pending > 0 {
if e, _, err := o.nextEntry(); err == nil {
status.OldestPendingAt = e.CommittedAt
at, _ := time.Parse(time.RFC3339Nano, e.CommittedAt)
status.LagExceeded = status.Pending > 100 || o.clock().Sub(at) > time.Hour
} else {
status.LagExceeded = true
}
}
for _, effect := range api.DecisionEffects() {
class := Class(effect)
status.Committed[class] = o.committed[class]

View file

@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"testing"
"gopkg.in/yaml.v3"
@ -150,6 +151,11 @@ func TestCadenceClassifiesEveryEffect(t *testing.T) {
Action string `yaml:"action"`
EvidenceClass string `yaml:"evidence_class"`
} `yaml:"classes"`
Heartbeat struct {
Classes []struct {
Class string `yaml:"class"`
} `yaml:"classes"`
} `yaml:"heartbeat"`
}
if err := yaml.Unmarshal(data, &cadence); err != nil {
t.Fatal(err)
@ -177,4 +183,17 @@ func TestCadenceClassifiesEveryEffect(t *testing.T) {
t.Fatalf("cadence.yaml load-bearing classes %v; want %v", declared, want)
}
}
var heartbeatClasses []string
for _, class := range cadence.Heartbeat.Classes {
heartbeatClasses = append(heartbeatClasses, class.Class)
}
rare := emission.RareClasses()
sort.Strings(heartbeatClasses)
sort.Strings(rare)
if strings.Join(heartbeatClasses, ",") != strings.Join(rare, ",") {
t.Fatalf("runtime heartbeat inventory %v differs from declared %v", rare, heartbeatClasses)
}
if cadence.Classes["heartbeat"].Action != emission.HeartbeatType {
t.Fatal("heartbeat wire class differs from declaration")
}
}

View file

@ -95,14 +95,16 @@ func TestInformedDecisionAttributeAdmission(t *testing.T) {
}
}
func TestSittingImagePinUnchanged(t *testing.T) {
func TestSittingImagePinMatchesRecordedBuild(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(repoRoot(t), "values", "informed-decision-sitting.yaml"))
if err != nil {
t.Fatal(err)
}
text := string(raw)
if !strings.Contains(text, "sha256:f6fa2062f7095e66c7a91f20cd130914b5481d46d9d909339e028f7ecdf0065d") {
t.Fatal("sitting image digest changed")
// values/informed-decision-sitting.yaml records the v9 image of 103d8b6,
// pinned in 257a83e. The earlier v7 pin is no longer the baseline.
if !strings.Contains(text, "sha256:aa7ccbafb2a8f884c97119e295fd168fa4e67e974682a8c30b35149201469eff") {
t.Fatal("sitting image digest differs from the recorded v9 build")
}
if !strings.Contains(text, "/opt/flex-auth/examples/informed-decision-sitting/policy.md") {
t.Fatal("sitting policy path changed")

View file

@ -5,6 +5,7 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
python3 tests/validate_app_toml.py
python3 tests/test_outbox_chart.py
for values in values/stage1.yaml values/stage2-canary.yaml values/stage3-production.yaml \
values/user-engine.yaml values/tenant-engine.yaml values/ops-warden.yaml; do

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Render the optional outbox with caller auth on/off, without cluster access."""
import subprocess
import yaml
def render(*settings, succeeds=True):
command = ["helm", "template", "flex-auth", "charts/flex-auth", "-f", "values/stage2-canary.yaml"]
for setting in settings:
command += ["--set", setting]
result = subprocess.run(command, capture_output=True, text=True, check=False)
if not succeeds:
assert result.returncode != 0, "invalid multi-writer configuration was accepted"
return []
assert result.returncode == 0, result.stderr
return list(yaml.safe_load_all(result.stdout))
def deployment(documents):
return next(d for d in documents if d["kind"] == "Deployment")
base = render()
assert not any(d["kind"] == "PersistentVolumeClaim" for d in base)
assert "--outbox-dir" not in deployment(base)["spec"]["template"]["spec"]["containers"][0]["args"]
enabled = ("outbox.enabled=true", "outbox.source=flex-auth.test")
for auth in ((), ("callerAuth.mode=enforce", "callerAuth.binding=test=system:serviceaccount:test:test")):
documents = render(*enabled, *auth)
pod = deployment(documents)["spec"]["template"]["spec"]
container = pod["containers"][0]
args = container["args"]
assert args[args.index("--outbox-dir") + 1] == "/var/lib/flex-auth/outbox"
assert args[args.index("--emission-source") + 1] == "flex-auth.test"
assert deployment(documents)["spec"]["strategy"]["type"] == "Recreate"
pvc = next(d for d in documents if d["kind"] == "PersistentVolumeClaim")
assert pvc["spec"]["accessModes"] == ["ReadWriteOnce"]
assert pvc["spec"]["resources"]["requests"]["storage"] == "1Gi"
assert any(v.get("persistentVolumeClaim", {}).get("claimName") == pvc["metadata"]["name"] for v in pod["volumes"])
assert any(v["name"] == "flex-auth-outbox" and not v.get("readOnly", False) for v in container["volumeMounts"])
if auth:
assert any(v["name"] == "flex-auth-reviewer" for v in pod["volumes"])
existing = render(*enabled, "outbox.existingClaim=retained-outbox")
assert not any(d["kind"] == "PersistentVolumeClaim" for d in existing)
pod = deployment(existing)["spec"]["template"]["spec"]
assert any(v.get("persistentVolumeClaim", {}).get("claimName") == "retained-outbox" for v in pod["volumes"])
render(*enabled, "replicaCount=2", succeeds=False)
render("outbox.enabled=true", succeeds=False)
print("optional outbox chart checks passed")

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Verify the source declaration against the pinned owner schema/profile."""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import sys
root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser()
parser.add_argument("--contract-schema", type=Path, default=root.parent / "info-tech-canon/infospace/schemas/emission-cadence.schema.yaml")
parser.add_argument("--checker", type=Path, default=root.parent / "net-kingdom/tools/emission-cadence-profile/emission_cadence_profile.py")
args = parser.parse_args()
declaration = root / "docs/contracts/decision-emission-cadence.json"
document = json.loads(declaration.read_text())
extension = document["extensions"]["flex-auth"]
if not args.contract_schema.is_file() or not args.checker.is_file():
parser.error("owner schema/profile is unavailable; supply explicit --contract-schema and --checker paths")
if hashlib.sha256(args.contract_schema.read_bytes()).hexdigest() != extension["contract"]["schema_sha256"]:
parser.error("owner schema changed; review and update the declared schema pin before assessment")
command = [sys.executable, str(args.checker), "--contract-schema", str(args.contract_schema)]
inventory = extension["inventory"]
for event in inventory["load_bearing"]:
command += ["--load-bearing", event]
for event in inventory["rare_load_bearing"]:
command += ["--rare-load-bearing", event]
command += [str(declaration)]
sys.exit(subprocess.run(command, check=False).returncode)

View file

@ -91,3 +91,14 @@ Steps, none run yet:
Close-down checklist row 1 (the-custodian) is updated by message, not by edit.
2026-10-04 CUST-WP-0075 owner routing: `FLEX-WP-ADHOC-2026-10-03-T02` remains a human wait under open decision `877c268b-2089-42f1-a811-d98f549c1cb2`. Exact scoped human act and native receipts remain required; this metadata records no approval or execution.
2026-10-04 blocked-task review: ops-warden message 26992e49 confirms its
fetch/sign separation is implemented at 7d4381e (sync 88c22d4). The evaluator
code is already complete; do not repeat that implementation or fall back to
the signing URL. Remaining requirements are the exact CI digest/live install,
ops-bridge 19091 tunnel, configured policy.fetch_flex_auth_url and native
health/positive/wrong-owner/fail-closed receipts under WARDEN-WP-0039-T03.
Decision 877c268b-2089-42f1-a811-d98f549c1cb2 was checked and remains open.
This task and workplan stay blocked on the approved rollout; no live change was
made. Requirements are recorded in
`docs/blocked-task-dependencies-2026-10-04.md`.

View file

@ -479,7 +479,8 @@ generic error is treated as proof of rollback.
id: FLEX-WP-0020-T11
status: wait
needs_human: true
blocking_reason: "Soak window after T10; destructive cleanup needs separate human approval."
blocked_on: message-from:sbom-nexus
blocking_reason: "Needs SBOM-IN-0001 catch-up receipt, secrets-engine coordinate correction and railiance-fabric residual disposition, observed soak evidence, and exact checkout cleanup approval under e071b34e-c4d4-49d0-824d-2c58bb0ae496."
priority: high
state_hub_task_id: "46d8318b-d1fb-52e3-b632-52230ed1438f"
decision_id: "e071b34e-c4d4-49d0-824d-2c58bb0ae496"
@ -514,3 +515,12 @@ the historical repo:flex-auth node in the railiance-fabric discovery snapshot. T
destructive cleanup of old aliases/redirects needs separate human approval) remains.
2026-10-04 CUST-WP-0075 owner routing: `FLEX-WP-0020-T11` remains a human wait under open decision `e071b34e-c4d4-49d0-824d-2c58bb0ae496`. Exact scoped human act and native receipts remain required; this metadata records no approval or execution. Completed predecessor edges removed: FLEX-WP-0020-T10; completed source evidence remains in the history and does not satisfy the human gate.
2026-10-04 blocked-task review: owner source still records SBOM-IN-0001 open,
and secrets-engine/docs/exec-owner-binding.md:89 still names ../flex-auth.
Neither is closed by the rename's completed journal. The discovery snapshot
residual needs railiance-fabric's receiving record and disposition. No soak
window or success is invented; no checkout or alias is removed. Exact owner
receipts and operator acceptance requirements are in
`docs/blocked-task-dependencies-2026-10-04.md`. T11 and this workplan remain
blocked until those returns and the still-open cleanup decision permit closure.

View file

@ -4,7 +4,7 @@ type: workplan
title: "Admit scoped human review for the three T03 actions"
domain: infotech
repo: flex-auth
status: blocked
status: finished
flavor: implementation
owner: codex
topic_slug: netkingdom
@ -44,9 +44,8 @@ checks pass with synthetic identities; evidence is retained in docs/evidence.
```task
id: FLEX-WP-0027-T03
status: wait
needs_human: true
blocking_reason: "Needs the operator's exact signed-in account and actual human acknowledgements; synthetic policy checks are not acceptance."
status: done
needs_human: false
priority: high
state_hub_task_id: "9417d64a-308c-566f-af29-217c5c45d294"
decision_id: "e1868a8d-cad7-48c6-84d1-78a5a566d4a4"
@ -62,3 +61,16 @@ account exercising the review), so the workplan is marked `blocked` rather
than `active` until that happens.
2026-10-04 CUST-WP-0075 owner routing: `FLEX-WP-0027-T03` remains a human wait under open decision `e1868a8d-cad7-48c6-84d1-78a5a566d4a4`. Exact scoped human act and native receipts remain required; this metadata records no approval or execution.
2026-10-04 owner-evidence review: the historical wait above is superseded.
`SECRETS-WP-0010-T03` is done; its 2026-09-16 completion record explicitly
records real human acceptance of all three renewed version-2 memos. The native
receipt retains actor `worsch`, approved human-controlled CAS consumption and
decision/request bindings for apply, verify and exec; all three approvals are
consumed. Reviewed receipt hash and action references are retained in
`docs/evidence/2026-10-04-t03-owner-completion-review.json`. This closes the
original scoped review task and finishes this workplan without new execution.
The version-3 records now in `examples/informed-decision-t03/records.json`
serve the next `IR-WP-0005-T02` cycle; they do not reopen this original task or
permit reuse of the consumed approvals. The old decision link is retained as
history, not a claim that the later human decision has been answered.

View file

@ -14,7 +14,7 @@ related_workplans:
- FLEX-WP-0030
- FLEX-WP-0019
created: "2026-09-21"
updated: "2026-09-27"
updated: "2026-10-04"
state_hub_workstream_id: "84f5d9fe-b4c9-584a-b964-efe3e48af095"
---
@ -63,7 +63,8 @@ and the node disk is at 84 %.
id: FLEX-WP-0031-T02
status: wait
blocked_on: message-from:audit-core
blocking_reason: "audit-core intake: sender registration, evidence_kind and heartbeat classes as cadence.yaml publishes them; gate is a registered sender."
depends_on: [GH-WP-0004-T12]
blocking_reason: "AUDIT-IN-0006 sender rows and governed token delivery remain pending; gate-house GH-WP-0004-T12 must rule on the load-bearing failure-path exception."
priority: high
state_hub_task_id: "89661908-ca9a-5e4a-a0a5-62d1a9e02568"
```
@ -135,33 +136,36 @@ Done 2026-09-23:
- `make test` (race) passes, and a local serve smoke test committed and counted
one deny and one allow. Nothing is deployed and no chart changed.
## 5. Heartbeat and drain to audit-core
## 5. Implement heartbeat and drain to audit-core
```task
id: FLEX-WP-0031-T05
status: wait
depends_on: [FLEX-WP-0031-T02]
blocking_reason: "Per-class heartbeat sender and drain need the registered sender from T02."
status: done
priority: high
state_hub_task_id: "14bd6648-11da-53d7-a512-027005bd4772"
```
2026-09-27: `cadence.yaml`'s heartbeat declaration corrected — it now names one
class per rare load-bearing event (`deny`, `redact`, `not_applicable`,
`audit_only`) instead of a single combined `flex-auth.decision.heartbeat`
class. `go build`/`go test ./...` pass unchanged. The remaining scope — the
actual per-class heartbeat sender and the drain to `POST /v1/events` — is
correctly blocked on T02's sender registration with `audit-core`; the task
stays `wait` rather than in progress here.
2026-10-04: split implementation from native delivery acceptance (new T08).
`internal/emission/delivery.go` implements daily per-class `audit-core.heartbeat`
events with independent correlation IDs and offset timestamps, persisted in
our durable outbox. Restart rebuilds each class's last heartbeat from the log.
Heartbeats expose committed and released-uncommitted counts; a known failure
changes the assertion to `released-uncommitted-finding`.
- Emit a daily `audit-core.heartbeat` event per rare class, with `data.class`
set to the class, following audit-core's `stream_findings` shape.
- Correct `cadence.yaml`, which still names a single
`flex-auth.decision.heartbeat` class.
- Drain committed events to `POST /v1/events` with `Idempotency-Key` set to the
event id, keep a drain cursor, and expose outbox depth and age against the
`lag_bound`.
- The drain stays disabled until T02 admits the senders.
The asynchronous drain sends the eight-field envelope with event-ID
idempotency, reads a mounted token file per event for rotation, refuses
redirects, and advances a fsynced/renamed cursor only on 200 or 202. Every other
response, transport failure or cursor-write failure retains the event. A
bounded drain runs each minute, without holding the decision lock during HTTP.
`GET /v1/emission` exposes pending depth, oldest pending timestamp and lag-bound
exceedance (100 events / 1h). No response body or token is logged.
`serve --audit-url <explicit /v1/events endpoint> --audit-token-file <mount>`
enables the worker only with an outbox. Both default empty, and no pin enables
it. Local tests cover per-class cadence, restart, rejection, stable replay,
ambiguous acceptance, token rotation and cursor durability. The current outbox
retains acknowledged log entries; retention/compaction is not implemented, so
capacity planning must include cumulative log growth.
## 6. Reconciliation, profile check, and storage
@ -169,7 +173,8 @@ stays `wait` rather than in progress here.
id: FLEX-WP-0031-T06
status: wait
depends_on: [FLEX-WP-0031-T02]
blocking_reason: "Reconciliation compare needs audit-core GET /v1/reconciliation, which exists only once T02's sender is registered."
blocked_on: message-from:audit-core
blocking_reason: "Receiver counts use accepted_at, while local counts use commit time; audit-core must supply compatible window semantics or durable acceptance timestamps before a valid comparator can be implemented."
priority: high
state_hub_task_id: "bf92a951-3b69-59dd-8907-f6748c91a264"
```
@ -193,13 +198,22 @@ undone. Stays `wait`.
(84 % on 2026-09-23). Rolling it out to any pin is a production change and
needs the founder's go-ahead.
2026-10-04: storage/profile preparation is now complete as T07. The remaining
comparator is independently blocked by receiver time semantics, not just a
missing registration: audit-core counts accepted_at in half-open windows,
while our log timestamps local commits. Ingest returns status/reference only.
A receiver-approved matching basis or stable acceptance-time receipts (also
on duplicates) is required before implementing a comparison. Do not widen
may_read to fetch archive payloads as a workaround. T02 and T08 still gate
native custody/rollout evidence; see the dependency record for exact returns.
## 4. Close G2
```task
id: FLEX-WP-0031-T04
status: wait
depends_on: [FLEX-WP-0031-T05, FLEX-WP-0031-T06]
blocking_reason: "Closing G2 needs the silence-finding gate, which needs T05's sender and T06's reconciliation."
depends_on: [FLEX-WP-0031-T08, FLEX-WP-0031-T06]
blocking_reason: "G2 needs T08 native heartbeat/drain and silence-finding evidence plus T06 reconciliation; local tests cannot close the delivery gap."
priority: medium
state_hub_task_id: "423b3090-1b72-58fd-9353-5c907c7683bb"
```
@ -212,3 +226,57 @@ Change `cadence.yaml` `state` to emitting, move G2 out of the gap table with the
evidence, and tell `gate-house`, `audit-core` and `kings-guard`. Gate: a silence
finding is observed on a deliberately withheld heartbeat in a non-production
run.
## 7. Prepare persistent storage and validate the cadence contract
```task
id: FLEX-WP-0031-T07
status: done
priority: high
state_hub_task_id: "151fe609-ba68-506b-9c2c-9fdb0b149eef"
```
Split from T06 on 2026-10-04; its historical schema/storage blockers above are
superseded by this implementation. `charts/flex-auth` has optional outbox PVC,
mount, source and flags, defaults disabled, a one-replica constraint and
Recreate strategy to avoid two writers during rollout. It supports an existing
claim. Rendering is tested with caller auth enabled and disabled. No values pin
or live deployment is changed; 1Gi is a configurable preparation value, not an
assertion of available node space.
Published `docs/contracts/decision-emission-cadence.json` against the actual
InfoTechCanon wire schema and NetKingdom profile. All five decision classes
are explicitly supplied; contract/profile assessment passes. The generic
expected-rate schema forbids reconciliation on that entry and the profile
forbids duplicate classes, so allow's additional reconciliation guarantee is
in the `extensions.flex-auth` namespace. Receiver acceptance of that mapping
remains part of T02; a profile pass does not establish observer delivery.
## 8. Admit and exercise native heartbeat/drain delivery
```task
id: FLEX-WP-0031-T08
status: wait
priority: high
depends_on: [FLEX-WP-0031-T02]
needs_human: true
blocking_reason: "Needs admitted sender/token mounts, founder approval of exact image/PVC/network rollout, and audit-core observer evidence; no production delivery is enabled."
state_hub_task_id: "b3b52b30-c73d-5a90-9935-d98afe5a9eaf"
```
Remaining acceptance split from T05: audit-core's owner must return admitted
exact per-pin sources, scope, heartbeat classes and cadence mapping under
AUDIT-IN-0006. Platform must provide governed attended token custody and
mount references, never values. The founder must approve exact immutable image,
PVC capacity and rollout; network owners must admit the decision pins to the
receiver. Record native 202 and idempotent 200 delivery, restart cursor/lag
behaviour, and an audit-core silence finding from a separately authorized
non-production withheld-heartbeat exercise. No test engagement/window is
created by this workplan. T06 still owns the compatible-time reconciliation
implementation and its native acceptance. See
`docs/blocked-task-dependencies-2026-10-04.md` for the concrete dependency gates.
2026-10-04 review: workplan stays blocked. Local heartbeat/drain and storage
preparation no longer wait on another repository, but admission, statutory
ruling, valid reconciliation time basis and approved native acceptance do.
`cadence.yaml` remains `declared-not-yet-emitting` and G2 remains open.