FLEX-WP-0031-T03: durable decision outbox and the FLEX-DEC-2026-018 release rule
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m27s

internal/emission commits one audit-core-shaped event per decision with
fsync before release (one sync per batch), random event ids with the
decision id as correlation_id, torn-tail and failed-commit truncation, and
per-class committed/released_uncommitted counts at GET /v1/emission.
The engine releases restrictions whose record failed to commit and
withholds allow/audit_only (503). api.DecisionEffects() is pinned by a
source-parsing test and cadence.yaml must classify exactly it.

T03 split under the task budget: heartbeat+drain is T05, reconciliation,
profile check and PVC are T06. Nothing deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 307130@bnt-lap001
Assistant-Session: 270c79f7-0823-4b0d-990d-aad5af9935ce
This commit is contained in:
tegwick 2026-09-23 20:20:58 +02:00
parent 7f0e2e37f9
commit cd14a34e33
9 changed files with 842 additions and 23 deletions

View file

@ -18,6 +18,7 @@ import (
"github.com/netkingdom/flex-auth/internal/audit"
"github.com/netkingdom/flex-auth/internal/callerauth"
decisioncore "github.com/netkingdom/flex-auth/internal/decision"
"github.com/netkingdom/flex-auth/internal/emission"
"github.com/netkingdom/flex-auth/internal/policy"
"github.com/netkingdom/flex-auth/internal/registry"
"github.com/netkingdom/flex-auth/pkg/api"
@ -326,6 +327,8 @@ func runServe(args []string, stdout, stderr io.Writer) int {
registryPath := fs.String("registry", "", "registry snapshot JSON file")
policyPath := fs.String("policy", "", "policy package Markdown file")
logPath := fs.String("log", "", "optional JSONL decision log path")
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
callerAuthMode := fs.String("caller-auth-mode", "disabled", "disabled, warn, or enforce")
callerAudience := fs.String("caller-audience", "flex-auth", "required caller token audience")
callerKubernetesURL := fs.String("caller-kubernetes-url", "https://kubernetes.default.svc", "Kubernetes API base URL for TokenReview")
@ -341,10 +344,24 @@ func runServe(args []string, stdout, stderr io.Writer) int {
return 64
}
if *outboxDir != "" && (*logPath != "" || *emissionSource == "") {
fmt.Fprintln(stderr, "serve --outbox-dir requires --emission-source and excludes --log")
return 64
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
return fail(stderr, err)
}
var outbox *emission.Outbox
if *outboxDir != "" {
outbox, err = emission.Open(*outboxDir, *emissionSource)
if err != nil {
return fail(stderr, err)
}
defer outbox.Close()
engine.SetDecisionLog(outbox)
}
authenticator, err := buildCallerAuthenticator(
callerauth.Mode(*callerAuthMode),
@ -360,6 +377,9 @@ func runServe(args []string, stdout, stderr io.Writer) int {
}
mux := newServeMuxWithCallerAuth(engine, authenticator)
if outbox != nil {
mountEmission(mux, outbox)
}
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
@ -520,8 +540,25 @@ func writeStatus(w io.Writer, status string, extra map[string]any) int {
return 0
}
// mountEmission exposes the local half of reconciliation: committed and
// released-uncommitted counts per class. Counts only, never records.
func mountEmission(mux *http.ServeMux, outbox *emission.Outbox) {
mux.HandleFunc("/v1/emission", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
writeHTTP(w, outbox.Status(), nil)
})
}
func writeHTTP(w http.ResponseWriter, value any, err error) {
w.Header().Set("content-type", "application/json")
if errors.Is(err, decisioncore.ErrRecordNotCommitted) {
// The consumer's declared stance applies (FLEX-DEC-2026-018).
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return