# Decision-record contract Status: published Contract: `flex-auth.decision-record.v1` Schema: [`../schemas/decision_envelope.schema.json`](../schemas/decision_envelope.schema.json) Date: 2026-09-02 This is flex-auth's output artifact under the NetKingdom Security Layer Model v0.7 §17. Taxonomy holds only the shared field vocabulary. Consumers may rely on this schema. A decision record is a `DecisionEnvelope` returned by `POST /v1/check` and the CLI `check` / `batch-check` / `list-allowed` commands. Standalone evaluation and every delegated adapter (Topaz, relationship, rule, Keycloak) emit the same shape. ## Required fields | Field | Meaning | | --- | --- | | `id` | Deterministic decision identifier | | `effect` | `allow`, `deny`, `redact`, `audit_only`, or `not_applicable` | | `subject` / `resource` | Normalized refs the evaluator used | | `provenance` | Who evaluated, over which policy and facts | ## Contract fields consumers may rely on | Field | Meaning | | --- | --- | | `contract_version` | `flex-auth.decision-record.v1` | | `binding` | Structured subject, action, resource, context, and `request_digest` | | `binding.approval_binding_digest` | Present only when the request carried `context.approval`. The digest an approval's `pdp_digest` must equal — see [`canonical-request-digest.md`](canonical-request-digest.md). Not a replay identity | | `lifetime` | **Required on every allow.** A TTL with `not_before` and `expires_at` | | `provenance.policy_package` / `policy_version` | Named package pin | | `provenance.policy_package_digest` | SHA-256 of package metadata plus compiled Rego | | `provenance.registry_snapshot_digest` | SHA-256 of the canonical registry snapshot | | `provenance.directory_etag` | Directory consistency token when a delegated directory was joined | | `provenance.input_claim_digests` | SHA-256 per request-time claim class (`context`, `caring_context`) | | `provenance.decision_time` | UTC timestamp used to compute `lifetime` | `reason`, `diagnostics`, and CARING prose are not an authorization contract. ## Allow lifetime Every allow carries `lifetime.kind = ttl`. The duration comes from the policy package `allow_ttl` field, or from the engine default of `15m` when the package omits it. A package that declares `allow_ttl: none` (or `0s`) produces a deny with reason `allow_lifetime_unstated` instead of a standing grant. Replay is permitted only while `lifetime.expires_at` is still in the future. See [`canonical-request-digest.md`](canonical-request-digest.md) and [`decision-input-freshness.md`](decision-input-freshness.md). ## Versioning This is contract version 1. Additive optional fields may appear. Removing or redefining a required field requires a new `contract_version` value and a new schema id. ## What this record does not prove: who answered **A consumer must not read digest recomputation as verification of the responder.** Recomputing `binding.request_digest`, `provenance.policy_package_digest`, and `provenance.registry_snapshot_digest` and finding all three correct says nothing about who produced the envelope. Every input to those digests is either sent by the caller or published: the request material is what the caller just transmitted, and both the package and registry digests are computable from files in this repo. A responder that knows the package id and version can reproduce all three exactly. The digests establish **integrity of the binding**, never **authenticity of the source**. `flex-auth.decision-record.v1` carries no signature today, and pins serve plain HTTP. So the response channel is unauthenticated, stated as a stance rather than left as an assumption (`FLEX-DEC-2026-010`). For a fail-closed consumer the distinction that matters is this: **fail-closed protects against a PDP that is absent, not against one that lies.** An unreachable PDP denies; a lying PDP allows. A detached signature over the canonical envelope is the intended fix (`FLEX-WP-0024`). Until it lands, responder authenticity comes from the channel alone — and of the available channels only `kubectl port-forward` supplies it, by targeting one named pod over the API server's TLS with no DNS name resolved.