Implement local decision delivery and reconcile blocked workplan requirements
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1046e-fcad-7ea3-bf36-361fc8d679e8
This commit is contained in:
parent
257a83e997
commit
3cb1cfaedf
25 changed files with 1258 additions and 50 deletions
4
Makefile
4
Makefile
|
|
@ -64,6 +64,10 @@ ci: vet lint test build overlay-render
|
||||||
verify-posture:
|
verify-posture:
|
||||||
@bash tools/verify-posture.sh
|
@bash tools/verify-posture.sh
|
||||||
|
|
||||||
|
.PHONY: verify-emission-cadence
|
||||||
|
verify-emission-cadence:
|
||||||
|
python3 tools/verify_emission_cadence.py
|
||||||
|
|
||||||
overlay-render:
|
overlay-render:
|
||||||
@tests/stage1.sh
|
@tests/stage1.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
| workplan | FLEX-WP-0024 | finished | — | workplans/FLEX-WP-0024-decision-envelope-authenticity.md |
|
| 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-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-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-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-0029 | finished | — | workplans/FLEX-WP-0029-stance-register-second-edition.md |
|
||||||
| workplan | FLEX-WP-0030 | finished | — | workplans/FLEX-WP-0030-boundary-declaration-cleanup.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-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-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-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-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-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 |
|
| 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-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-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-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-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-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-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 |
|
| task | FLEX-WP-0032-T03 | done | — | workplans/FLEX-WP-0032-informed-decision-list-action.md |
|
||||||
|
|
|
||||||
13
cadence.yaml
13
cadence.yaml
|
|
@ -36,10 +36,11 @@ form: heartbeat-or-reconciliation
|
||||||
rate_monitoring: forbidden # class-level exception stated on `allow` only
|
rate_monitoring: forbidden # class-level exception stated on `allow` only
|
||||||
|
|
||||||
# STATE. The classification below is published and in force as a declaration.
|
# STATE. The classification below is published and in force as a declaration.
|
||||||
# The emission pipeline is not built: no sender named flex-auth or
|
# The outbox, disabled-by-default heartbeat/drain and optional PVC are built
|
||||||
# access-engine is registered with audit-core, no token lane, no scope row, and
|
# (FLEX-WP-0031 T03/T05/T07). No pin enables them in production, no sender is
|
||||||
# the decision record reaches consumers in the /v1/check response rather than
|
# registered with audit-core, and token/scope/observer admission is pending.
|
||||||
# into any custody. This file is therefore the declaration §11 requires and not
|
# 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
|
# a claim that the guarantee is delivered today. The delivery gap is G2 in
|
||||||
# docs/conformance/security-layer-conformance.md, with owner, blocker and
|
# docs/conformance/security-layer-conformance.md, with owner, blocker and
|
||||||
# review date, and it fails CLOSED on its distinguishing case: a stream with no
|
# 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
|
removed. An allow carries a TTL lifetime, so a suppressed allow expires
|
||||||
into a denial of service rather than into a standing grant.
|
into a denial of service rather than into a standing grant.
|
||||||
heartbeat:
|
heartbeat:
|
||||||
action: flex-auth.decision.heartbeat
|
action: audit-core.heartbeat
|
||||||
evidence_class: operational
|
evidence_class: operational
|
||||||
rarity: scheduled
|
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: >-
|
bound: >-
|
||||||
Heartbeat and reconciliation detect loss, outage, drain failure and accident.
|
Heartbeat and reconciliation detect loss, outage, drain failure and accident.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ metadata:
|
||||||
labels:
|
labels:
|
||||||
{{- include "flex-auth.labels" . | nindent 4 }}
|
{{- include "flex-auth.labels" . | nindent 4 }}
|
||||||
spec:
|
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 }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
|
|
@ -31,6 +38,12 @@ spec:
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
args:
|
args:
|
||||||
{{- toYaml .Values.args | nindent 12 }}
|
{{- 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" }}
|
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
|
||||||
{{- $binding := required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }}
|
{{- $binding := required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }}
|
||||||
- --caller-auth-mode
|
- --caller-auth-mode
|
||||||
|
|
@ -65,14 +78,26 @@ spec:
|
||||||
drop:
|
drop:
|
||||||
- ALL
|
- ALL
|
||||||
readOnlyRootFilesystem: true
|
readOnlyRootFilesystem: true
|
||||||
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
|
{{- if or .Values.outbox.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||||
volumeMounts:
|
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 }}
|
- mountPath: {{ .Values.callerAuth.reviewer.mountPath }}
|
||||||
name: flex-auth-reviewer
|
name: flex-auth-reviewer
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }}
|
{{- if or .Values.outbox.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||||
volumes:
|
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
|
- name: flex-auth-reviewer
|
||||||
projected:
|
projected:
|
||||||
defaultMode: 0440
|
defaultMode: 0440
|
||||||
|
|
@ -88,4 +113,5 @@ spec:
|
||||||
items:
|
items:
|
||||||
- key: ca.crt
|
- key: ca.crt
|
||||||
path: ca.crt
|
path: ca.crt
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
||||||
19
charts/flex-auth/templates/outbox-pvc.yaml
Normal file
19
charts/flex-auth/templates/outbox-pvc.yaml
Normal 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 }}
|
||||||
|
|
@ -9,6 +9,15 @@ image:
|
||||||
|
|
||||||
replicaCount: 1
|
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:
|
args:
|
||||||
- serve
|
- serve
|
||||||
- --addr
|
- --addr
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"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")
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||||
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
|
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")
|
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")
|
callerAuthMode := fs.String("caller-auth-mode", "disabled", "disabled, warn, or enforce")
|
||||||
callerAudience := fs.String("caller-audience", "flex-auth", "required caller token audience")
|
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")
|
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")
|
fmt.Fprintln(stderr, "serve --outbox-dir requires --emission-source and excludes --log")
|
||||||
return 64
|
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)
|
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -380,6 +394,15 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
||||||
if outbox != nil {
|
if outbox != nil {
|
||||||
mountEmission(mux, outbox)
|
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)
|
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
|
||||||
if err := http.ListenAndServe(*addr, mux); err != nil {
|
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||||
|
|
@ -388,6 +411,31 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
||||||
return 0
|
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 {
|
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
|
||||||
return newServeMuxWithCallerAuth(engine, callerauth.Disabled())
|
return newServeMuxWithCallerAuth(engine, callerauth.Disabled())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
docs/blocked-task-dependencies-2026-10-04.md
Normal file
40
docs/blocked-task-dependencies-2026-10-04.md
Normal 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.
|
||||||
|
|
@ -56,7 +56,7 @@ review date.
|
||||||
| # | Gap | Owner | Blocked on | Review |
|
| # | 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` |
|
| 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` |
|
| 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
|
**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
|
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 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.
|
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.
|
||||||
|
|
|
||||||
178
docs/contracts/decision-emission-cadence.json
Normal file
178
docs/contracts/decision-emission-cadence.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
docs/decision-emission-operations.md
Normal file
70
docs/decision-emission-operations.md
Normal 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`.
|
||||||
33
docs/evidence/2026-10-04-decision-emission-profile.json
Normal file
33
docs/evidence/2026-10-04-decision-emission-profile.json
Normal 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."
|
||||||
|
}
|
||||||
47
docs/evidence/2026-10-04-t03-owner-completion-review.json
Normal file
47
docs/evidence/2026-10-04-t03-owner-completion-review.json
Normal 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
|
||||||
|
}
|
||||||
233
internal/emission/delivery.go
Normal file
233
internal/emission/delivery.go
Normal 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
|
||||||
|
}
|
||||||
246
internal/emission/delivery_test.go
Normal file
246
internal/emission/delivery_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,7 +61,10 @@ type Status struct {
|
||||||
ReleasedUncommitted map[string]uint64 `json:"released_uncommitted"`
|
ReleasedUncommitted map[string]uint64 `json:"released_uncommitted"`
|
||||||
// ReleasedUncommitted is held in memory: a restart during an outbox outage
|
// ReleasedUncommitted is held in memory: a restart during an outbox outage
|
||||||
// loses it (FLEX-DEC-2026-018, consequences).
|
// 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.
|
// 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
|
size int64 // bytes committed; a failed commit is cut back to it
|
||||||
committed map[string]uint64
|
committed map[string]uint64
|
||||||
uncommitted 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.
|
// 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,
|
clock: time.Now,
|
||||||
committed: map[string]uint64{},
|
committed: map[string]uint64{},
|
||||||
uncommitted: 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 {
|
if err := o.load(); err != nil {
|
||||||
return nil, err
|
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)
|
file, err := os.OpenFile(o.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open outbox: %w", err)
|
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 {
|
if err := json.Unmarshal(data[good:good+end], &e); err != nil {
|
||||||
return fmt.Errorf("outbox line %d is corrupt: %w", line, err)
|
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.seq = e.Seq
|
||||||
o.committed[e.Event.Type]++
|
o.committed[e.Event.Type]++
|
||||||
good += end + 1
|
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)
|
o.size = int64(good)
|
||||||
|
if o.cursor.Seq > o.seq {
|
||||||
|
return errors.New("drain cursor is ahead of outbox")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,15 +182,24 @@ func (o *Outbox) AppendBatch(decisions []api.DecisionEnvelope) error {
|
||||||
o.mu.Lock()
|
o.mu.Lock()
|
||||||
defer o.mu.Unlock()
|
defer o.mu.Unlock()
|
||||||
now := o.clock().UTC().Format(time.RFC3339Nano)
|
now := o.clock().UTC().Format(time.RFC3339Nano)
|
||||||
var buf []byte
|
var events []Event
|
||||||
seq := o.seq
|
|
||||||
for _, decision := range decisions {
|
for _, decision := range decisions {
|
||||||
id, err := eventID()
|
id, err := eventID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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++
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal outbox entry: %w", err)
|
return fmt.Errorf("marshal outbox entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -181,8 +213,9 @@ func (o *Outbox) AppendBatch(decisions []api.DecisionEnvelope) error {
|
||||||
}
|
}
|
||||||
o.seq = seq
|
o.seq = seq
|
||||||
o.size += int64(len(buf))
|
o.size += int64(len(buf))
|
||||||
for _, decision := range decisions {
|
for _, event := range events {
|
||||||
o.committed[Class(decision.Effect)]++
|
o.committed[event.Type]++
|
||||||
|
o.noteHeartbeat(event)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +250,16 @@ func (o *Outbox) Status() Status {
|
||||||
"released_uncommitted is decisions released without a record " +
|
"released_uncommitted is decisions released without a record " +
|
||||||
"since this process started, and any non-zero value is a finding",
|
"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() {
|
for _, effect := range api.DecisionEffects() {
|
||||||
class := Class(effect)
|
class := Class(effect)
|
||||||
status.Committed[class] = o.committed[class]
|
status.Committed[class] = o.committed[class]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
@ -150,6 +151,11 @@ func TestCadenceClassifiesEveryEffect(t *testing.T) {
|
||||||
Action string `yaml:"action"`
|
Action string `yaml:"action"`
|
||||||
EvidenceClass string `yaml:"evidence_class"`
|
EvidenceClass string `yaml:"evidence_class"`
|
||||||
} `yaml:"classes"`
|
} `yaml:"classes"`
|
||||||
|
Heartbeat struct {
|
||||||
|
Classes []struct {
|
||||||
|
Class string `yaml:"class"`
|
||||||
|
} `yaml:"classes"`
|
||||||
|
} `yaml:"heartbeat"`
|
||||||
}
|
}
|
||||||
if err := yaml.Unmarshal(data, &cadence); err != nil {
|
if err := yaml.Unmarshal(data, &cadence); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -177,4 +183,17 @@ func TestCadenceClassifiesEveryEffect(t *testing.T) {
|
||||||
t.Fatalf("cadence.yaml load-bearing classes %v; want %v", declared, want)
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
raw, err := os.ReadFile(filepath.Join(repoRoot(t), "values", "informed-decision-sitting.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
text := string(raw)
|
text := string(raw)
|
||||||
if !strings.Contains(text, "sha256:f6fa2062f7095e66c7a91f20cd130914b5481d46d9d909339e028f7ecdf0065d") {
|
// values/informed-decision-sitting.yaml records the v9 image of 103d8b6,
|
||||||
t.Fatal("sitting image digest changed")
|
// 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") {
|
if !strings.Contains(text, "/opt/flex-auth/examples/informed-decision-sitting/policy.md") {
|
||||||
t.Fatal("sitting policy path changed")
|
t.Fatal("sitting policy path changed")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ set -euo pipefail
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
|
||||||
python3 tests/validate_app_toml.py
|
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 \
|
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
|
values/user-engine.yaml values/tenant-engine.yaml values/ops-warden.yaml; do
|
||||||
|
|
|
||||||
50
tests/test_outbox_chart.py
Normal file
50
tests/test_outbox_chart.py
Normal 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")
|
||||||
29
tools/verify_emission_cadence.py
Normal file
29
tools/verify_emission_cadence.py
Normal 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)
|
||||||
|
|
@ -91,3 +91,14 @@ Steps, none run yet:
|
||||||
Close-down checklist row 1 (the-custodian) is updated by message, not by edit.
|
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 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`.
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,8 @@ generic error is treated as proof of rollback.
|
||||||
id: FLEX-WP-0020-T11
|
id: FLEX-WP-0020-T11
|
||||||
status: wait
|
status: wait
|
||||||
needs_human: true
|
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
|
priority: high
|
||||||
state_hub_task_id: "46d8318b-d1fb-52e3-b632-52230ed1438f"
|
state_hub_task_id: "46d8318b-d1fb-52e3-b632-52230ed1438f"
|
||||||
decision_id: "e071b34e-c4d4-49d0-824d-2c58bb0ae496"
|
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.
|
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 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.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
||||||
title: "Admit scoped human review for the three T03 actions"
|
title: "Admit scoped human review for the three T03 actions"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: flex-auth
|
repo: flex-auth
|
||||||
status: blocked
|
status: finished
|
||||||
flavor: implementation
|
flavor: implementation
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: netkingdom
|
topic_slug: netkingdom
|
||||||
|
|
@ -44,9 +44,8 @@ checks pass with synthetic identities; evidence is retained in docs/evidence.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLEX-WP-0027-T03
|
id: FLEX-WP-0027-T03
|
||||||
status: wait
|
status: done
|
||||||
needs_human: true
|
needs_human: false
|
||||||
blocking_reason: "Needs the operator's exact signed-in account and actual human acknowledgements; synthetic policy checks are not acceptance."
|
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "9417d64a-308c-566f-af29-217c5c45d294"
|
state_hub_task_id: "9417d64a-308c-566f-af29-217c5c45d294"
|
||||||
decision_id: "e1868a8d-cad7-48c6-84d1-78a5a566d4a4"
|
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.
|
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 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.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ related_workplans:
|
||||||
- FLEX-WP-0030
|
- FLEX-WP-0030
|
||||||
- FLEX-WP-0019
|
- FLEX-WP-0019
|
||||||
created: "2026-09-21"
|
created: "2026-09-21"
|
||||||
updated: "2026-09-27"
|
updated: "2026-10-04"
|
||||||
state_hub_workstream_id: "84f5d9fe-b4c9-584a-b964-efe3e48af095"
|
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
|
id: FLEX-WP-0031-T02
|
||||||
status: wait
|
status: wait
|
||||||
blocked_on: message-from:audit-core
|
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
|
priority: high
|
||||||
state_hub_task_id: "89661908-ca9a-5e4a-a0a5-62d1a9e02568"
|
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
|
- `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.
|
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
|
```task
|
||||||
id: FLEX-WP-0031-T05
|
id: FLEX-WP-0031-T05
|
||||||
status: wait
|
status: done
|
||||||
depends_on: [FLEX-WP-0031-T02]
|
|
||||||
blocking_reason: "Per-class heartbeat sender and drain need the registered sender from T02."
|
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "14bd6648-11da-53d7-a512-027005bd4772"
|
state_hub_task_id: "14bd6648-11da-53d7-a512-027005bd4772"
|
||||||
```
|
```
|
||||||
|
|
||||||
2026-09-27: `cadence.yaml`'s heartbeat declaration corrected — it now names one
|
2026-10-04: split implementation from native delivery acceptance (new T08).
|
||||||
class per rare load-bearing event (`deny`, `redact`, `not_applicable`,
|
`internal/emission/delivery.go` implements daily per-class `audit-core.heartbeat`
|
||||||
`audit_only`) instead of a single combined `flex-auth.decision.heartbeat`
|
events with independent correlation IDs and offset timestamps, persisted in
|
||||||
class. `go build`/`go test ./...` pass unchanged. The remaining scope — the
|
our durable outbox. Restart rebuilds each class's last heartbeat from the log.
|
||||||
actual per-class heartbeat sender and the drain to `POST /v1/events` — is
|
Heartbeats expose committed and released-uncommitted counts; a known failure
|
||||||
correctly blocked on T02's sender registration with `audit-core`; the task
|
changes the assertion to `released-uncommitted-finding`.
|
||||||
stays `wait` rather than in progress here.
|
|
||||||
|
|
||||||
- Emit a daily `audit-core.heartbeat` event per rare class, with `data.class`
|
The asynchronous drain sends the eight-field envelope with event-ID
|
||||||
set to the class, following audit-core's `stream_findings` shape.
|
idempotency, reads a mounted token file per event for rotation, refuses
|
||||||
- Correct `cadence.yaml`, which still names a single
|
redirects, and advances a fsynced/renamed cursor only on 200 or 202. Every other
|
||||||
`flex-auth.decision.heartbeat` class.
|
response, transport failure or cursor-write failure retains the event. A
|
||||||
- Drain committed events to `POST /v1/events` with `Idempotency-Key` set to the
|
bounded drain runs each minute, without holding the decision lock during HTTP.
|
||||||
event id, keep a drain cursor, and expose outbox depth and age against the
|
`GET /v1/emission` exposes pending depth, oldest pending timestamp and lag-bound
|
||||||
`lag_bound`.
|
exceedance (100 events / 1h). No response body or token is logged.
|
||||||
- The drain stays disabled until T02 admits the senders.
|
|
||||||
|
`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
|
## 6. Reconciliation, profile check, and storage
|
||||||
|
|
||||||
|
|
@ -169,7 +173,8 @@ stays `wait` rather than in progress here.
|
||||||
id: FLEX-WP-0031-T06
|
id: FLEX-WP-0031-T06
|
||||||
status: wait
|
status: wait
|
||||||
depends_on: [FLEX-WP-0031-T02]
|
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
|
priority: high
|
||||||
state_hub_task_id: "bf92a951-3b69-59dd-8907-f6748c91a264"
|
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
|
(84 % on 2026-09-23). Rolling it out to any pin is a production change and
|
||||||
needs the founder's go-ahead.
|
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
|
## 4. Close G2
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLEX-WP-0031-T04
|
id: FLEX-WP-0031-T04
|
||||||
status: wait
|
status: wait
|
||||||
depends_on: [FLEX-WP-0031-T05, FLEX-WP-0031-T06]
|
depends_on: [FLEX-WP-0031-T08, FLEX-WP-0031-T06]
|
||||||
blocking_reason: "Closing G2 needs the silence-finding gate, which needs T05's sender and T06's reconciliation."
|
blocking_reason: "G2 needs T08 native heartbeat/drain and silence-finding evidence plus T06 reconciliation; local tests cannot close the delivery gap."
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "423b3090-1b72-58fd-9353-5c907c7683bb"
|
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
|
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
|
finding is observed on a deliberately withheld heartbeat in a non-production
|
||||||
run.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue