AxonFlow v10.2.0 Release Notes
v10.2.0 is a dark minor. It carries the first three phases of the ADR-065 policy decision and identity control plane: the canonical authorization contract and a deterministic in-process policy decision point, the identity plane (trust realms, realm-qualified principals, actor chains, a directory graph), the stateful requirements plane (typed obligations, compound approval clauses, signed decision proofs, atomic reservations), typed policy authoring with its registries, and the compiler plus shadow semantic-diff gate that measures how far the legacy policy tables are from the new model. None of it evaluates a request in this release. The one thing that touches live traffic is the compatibility mode, which is off by default and, when set to shadow, records what the identity plane would have decided while legacy authentication continues to decide every request. It is selected either by the deployment-wide AXONFLOW_IDENTITY_COMPAT_MODE or per organization through the new identity settings admin API, and an organization with no record follows the deployment.
It is a MINOR by the semver policy. One additive Enterprise migration runs this release (enterprise/146, which creates the per-organization identity settings table; a Community deployment applies nothing). No capability is removed, no new configuration is required, and with the new switch unset and no organization opted in, every existing endpoint answers with the same status and verdict as on v10.1.0. Two things to know before you upgrade rather than after: two of the three new variables refuse to boot on a typo (so set them correctly or not at all), and a licence's days-remaining number is now counted in whole days between UTC dates, which for an installed licence can move the number by one day in the widening direction.
Am I affected?
A deployment that sets none of the new variables and opts no organization in sees one new startup log line per process ([IDENTITY-COMPAT] agent: AXONFLOW_IDENTITY_COMPAT_MODE=off (...)), an Enterprise deployment additionally applies enterprise/146 and logs that no Shared Signals receiver was registered, and nothing else changes.
Anyone who displays or alerts on a licence's days remaining. The number is now whole days between UTC dates on every validator, and both licence generators format dates in UTC. For a licence already installed, the number moves by at most one day in the widening direction; a licence minted after the upgrade encodes the intended expiry date, so a fresh 30-day licence reports 30 where the old pipeline could report 28. No licence becomes expired earlier than it was. See Licence expiry counted in whole UTC days.
Anyone who wants to start the ADR-065 observation window. Set AXONFLOW_IDENTITY_COMPAT_MODE=shadow on both the agent and the orchestrator and read the [IDENTITY-COMPAT] records. In shadow, legacy authentication still decides every request and no status or verdict changes; the one caller-visible effect is on the Enterprise outage legs, where a 401 body carries the outage wording described below instead of v10.1.0's bytes. Read Identity Compatibility Mode before enabling enforce.
Enterprise operators running the fleet per-user token validators. With the identity plane running (shadow or enforce), the outage errors in those validators are reworded so that a revocation store or JWKS that cannot be consulted says so instead of reading as a tampered token; with the mode unset every error body is byte-identical to v10.1.0. Status 401 and the verdict are unchanged in every mode. See Outages are named as outages.
Everyone else. Nothing changes. The new packages have no caller on any request path.
Community
The ADR-065 policy decision plane, as a module nothing imports
A separate Go module, platform/decision, carries the canonical authorization contract (realm-qualified identifiers, tri-state attributes with provenance, a normalized request with per-hop actor identity, a four-valued decision, typed obligations with one composition algebra per family, an audience-scoped explain trace, versioned JSON Schemas, and a strict AuthZEN 1.0 adapter), a deterministic in-process policy decision point (typed documents compiled losslessly into Rego v1, an OPA runtime with an allow-listed capabilities document and strict compilation, ed25519-signed digest-pinned bundles under separate system and organization authority roots), the executable conformance corpus with its 47-row source-case disposition ledger, the typed policy authoring control plane, the action, tool, resource and enforcement-point registries, and the legacy policy compiler with its shadow semantic-diff gate.
The module is separate so that OPA, which is security critical and heavy, stays out of the dependency graph of every shipping binary during the contracts phase. platform/go.mod does not reference it and no file outside the module imports it. Nothing in it evaluates a request.
The shadow semantic-diff gate
The compiler reads static_policies and dynamic_policies rows through both of the legacy read paths, per plane, and produces exactly one record per input row. The shadow harness dual-evaluates every plane, organization scope and phase, a model of the legacy decision procedure on one side and the real policy decision point on the other, and classifies every difference. A difference is unexplained by default, and the gate enforces its denominator as well as its numerator: an empty corpus, a plane with no cases, an unreached row and a report that compiled nothing all fail. In CI it runs over a fixture corpus and, in the real-Postgres lane, over a capture from a freshly migrated database (112 rows, 1880 comparisons, zero unexplained).
This is a measurement harness over captured policy rows, run offline and in CI. It is not a shadow evaluation of live requests, and no request plane consults the policy decision point in this release.
Identity plane primitives
platform/shared/identity gains organization-scoped trust realms with per-realm verification, canonical realm-qualified principals (a bare identifier is a hard error and is never completed with a default realm; email, username, display name, SCIM externalId and token sub are aliases with provenance, never keys), and ordered root-first actor chains with attenuation expressed as a meet. An undeclared issuer denies before policy loads, and every tri-state on a realm is refused at registration if left at its zero value. The directory graph closure and SCIM ingestion are Enterprise. These files had no production caller until the compatibility adapters below.
Obligations and detector assurance
platform/shared/requirements/obligation declares nine initial obligation types with one composition algebra per family, and the pre-permit proofs: a mandatory obligation whose applicability is unknown denies, an enforcement point must advertise the exact obligation version it can discharge, an incompatible same-kind pair denies, a broad redaction and a narrow hash over one payload resolve per leaf, and absent evidence is never read as satisfied. requirements/assurance separates a detector's failure behaviour from authorization: an advisory outage and a required-control outage have different outcomes, and inspection cannot grant authorization. Approval, decision proofs and reservations are Enterprise. No caller outside the packages.
Identity compatibility mode: the identity plane in shadow
The four legacy credential paths (the API credential, the AxonFlow-minted per-user token, the tenant's OIDC issuer, and the trust-gated identity header) now build the credential the identity plane verifies realm policy over, without changing what the legacy path decided.
# Agent AND orchestrator. Default: unset, which is off.
AXONFLOW_IDENTITY_COMPAT_MODE=off # verification does not run
AXONFLOW_IDENTITY_COMPAT_MODE=shadow # runs, and is RECORDED ONLY
AXONFLOW_IDENTITY_COMPAT_MODE=enforce # runs, records, and REFUSES what the identity plane does not admit
offreturns before consulting the registry or the recorder; the feature's runtime suite proves the authentication outcome is identical to a build without it, request for request.shadowwrites a[IDENTITY-COMPAT]record per evaluation (agreements sampled, divergences never sampled) while legacy authentication decides every request.enforceadditionally refuses, with a401, a credential the legacy path accepted and the identity plane did not admit. The adapter can only ever refuse: it has no path that turns a legacy rejection into an acceptance.
Two companions: AXONFLOW_IDENTITY_COMPAT_ENFORCE_REASONS narrows what enforce refuses to an allow-list of reason codes (it can only narrow), and AXONFLOW_IDENTITY_COMPAT_AGREEMENT_LOG_EVERY sets the agreement sampling interval (default 100000). An unrecognized value of the mode or of the reason list is fatal at boot, in every mode, because this switch has no safe direction to fall back to. The sampling interval is never fatal.
The divergence classes to expect in shadow are UNKNOWN_REALM (the issuer is declared by no realm), ORG_BINDING_MISMATCH (the token asserts an organization other than the credential's), REVOCATION_UNAVAILABLE (a realm declaring a revocation source cannot check a jti-less token) and SUBJECT_MISSING (an upstream asserting only X-User-Email has asserted an alias). Full behaviour, what each record carries, what enforce refuses and what it will mean at v11: Identity Compatibility Mode.
Licence expiry counted in whole UTC days
days_until_expiry was computed by subtracting a mid-afternoon now from an expiry date parsed as midnight UTC and truncating, which loses most of a day, and both licence generators formatted their bare dates in the generator's local zone, so a licence minted in a western timezone encoded yesterday's date. A 30-day licence minted west of Greenwich could validate as 28 days, and every expiry warning could fire up to two days early. Every validator now anchors on today's midnight UTC and counts whole days between dates, and both generator paths format their date in UTC, including the plugin-claim generator's fallback for a zero issued-at instant, which now reads the UTC clock, so no generator path formats a local-zone date. For a licence already installed the number moves by at most one day in the widening direction (the local-zone date is already baked into a licence minted before the upgrade); a licence minted after the upgrade encodes the intended date.
Smaller Community fixes
axonctlsent only the deprecatedX-Tenant-IDheader; it now sends the canonicalX-Client-IDalongside it. Neither header authenticates; the canonical name matters to servers and proxies in front of the agent that route and log on it.- The Community SaaS activity-update worker replaced its channel on every registration call while the previous goroutine was still reading it; it now starts exactly once. A production binary calls it once and never noticed.
- The policy decision point returns an indeterminate
ERRORdecision for a nil request rather than an error a caller could drop, and an approval plane's resolver must answer clause reachability rather than optionally skipping the check.
Community CI lane
The community lane now runs the decision module's named gates (the disposition ledger, the tri-state corpus, the monotonicity and delegation property tests, the corpus and mutation proofs, and the authoring checks) plus a full-module race run, and runs the axonctl and integration modules, which are separate Go modules no workflow had executed before.
Enterprise
Directory graph and SCIM ingestion
A normalized directory graph with a bounded breadth-first closure that emits shortest witness paths, names every group on any loop, quarantines cross-realm and orphan edges under separate codes, and fails closed on truncation: a realm with no group graph is authoritatively empty, an unreadable graph is unreachable, a bounded traversal is truncated, and the three never share a code path. SCIM ingestion sits behind it and refuses to guess nesting support, identifiers or membership disagreement between the two provider views.
OIDC realm, CAEP receiver and the segment-projection proof
The tenant's configured OIDC issuer is derived into a per-organization realm that is withdrawn when the provider is disabled, and the ADR-060 segment projection from the directory graph is diffed against the legacy resolver on the real SCIM schema with zero unexplained differences. Nothing re-points the policy gates at the projection yet.
The Shared Signals / CAEP intake gains an HTTP surface: POST /api/v1/identity/caep/events on the agent, authenticated with the organization's ordinary client credential, with RFC 8935 delivery semantics. It is registered only on a process wired to act on it and refuses every delivery until an organization opts in, so it is inert on an untouched deployment. It grants and denies nothing; its whole effect is to drop cached identity data early when an IdP says a session ended. See Identity Plane APIs.
Per-organization identity settings
The compatibility mode becomes settable per organization, through GET/PUT/DELETE /api/v1/admin/organizations/{org_id}/identity-settings on the customer portal's admin API. A record wins over the deployment's AXONFLOW_IDENTITY_COMPAT_MODE in both directions: an organization can be observed on a deployment whose flag is unset, and exempted on one running enforce. An organization with no record follows the process flag exactly as before. Backed by enterprise/146, org-scoped under row-level security, re-read on a TTL (AXONFLOW_IDENTITY_ORG_SETTINGS_TTL_SECONDS, default 60s). Reference: Identity Plane APIs.
Approval clauses, decision proofs and reservations
Compound approval clauses are a conjunction of immutable threshold clauses that is never flattened; separation of duties is a real bipartite matching; timeout, cancellation, revocation, a policy-epoch change and any bound-input change all deny. The approval plane holds no database handle: queue writes go through the existing HITL enqueue chokepoint, so the tier gate, pending cap, dedup and history trail all apply. Decision proofs are signed and audience-bound, with a binding that walks the struct by reflection so a new bound field is bound and mutation-tested automatically. Reservations are a reserved, committed, released, expired state machine with fencing on every transition and one linearizable critical section covering the check and the charge; 64 concurrent racers against a capacity of one admit exactly one. The reservation store in this release is an in-memory reference implementation; the durable store is a written recommendation awaiting an operator decision.
Outages are named as outages
In the fleet per-user token validators (Enterprise-only files), with the identity plane running (shadow or enforce), a revocation check that cannot be consulted, a JWKS that cannot be fetched or still cooling down, and an unknown key id whose covering refetch itself failed now say the revocation status or the key material is unavailable, where before all of them read as an invalid token; an unknown key id under refetch cooldown whose refetch succeeded names the age of the key set it was checked against and stays a determinate rejection. The rewording is gated on the mode: with AXONFLOW_IDENTITY_COMPAT_MODE unset the validators emit v10.1.0's exact bytes, including their errors.Is semantics. The portal SSO login verifier carries the same classification under the same gate, so with the plane running an IdP key rotation during login is no longer reported to the user as an invalid id_token; the login still fails either way. Status 401 and the verdict are unchanged in every mode; community binaries do not compile these files and carry no change.
What this release deliberately does not claim
- No request plane consults the policy decision point. The shadow diff harness is offline and CI-bound over captured policy rows. Per-plane shadow evaluation of live requests, and the AuthZEN-native wire surface in the SDKs, are not in this release; the existing check and decide endpoints keep their status, verdict and wire shapes.
- No SDK or plugin release accompanies this train.
- Enablement has two axes and no third. An organization (the identity settings record) and a reason code (the enforce allow-list). There is still no per-reason allow-list within an organization:
AXONFLOW_IDENTITY_COMPAT_ENFORCE_REASONSstays process-wide and applies wherever the resolved mode isenforce. - Bare
enforcerefuses on two indeterminate codes (REVOCATION_UNAVAILABLEandIDENTITY_INTERNAL_ERROR), which is why the guidance is to list the reasons you enforce rather than enable it bare. - No machine-readable refusal code reaches the wire yet. An identity-plane refusal is distinguishable by its message text on four of the five renderings; a code field is owed.
Migration
One additive Enterprise migration runs this release. enterprise/146 creates the identity_org_settings table, org-scoped under row-level security, and takes no long lock. A Community deployment applies no migration this release. Nothing reads a row that does not exist, so an Enterprise deployment that opts no organization in is unaffected by it.
No boot requirement changes for a deployment that sets none of the three new variables. A deployment that sets AXONFLOW_IDENTITY_COMPAT_MODE or AXONFLOW_IDENTITY_COMPAT_ENFORCE_REASONS to an unrecognized value does not start, by design.
From older versions: v10.2.0 is cumulative. If you are upgrading from v10.0.0, read the v10.1.0 release notes for its five migrations and the audit-verification authorization change; from v9.x, start with the v10.0.0 release notes and the v9 to v10 migration guide.
SDK and plugin compatibility
No SDK or plugin release accompanies this train. The recommended client versions are unchanged: Go SDK v9.1.1, Python, TypeScript and Java SDKs v9.1.0, Rust SDK v0.8.2 (preview), and all five plugins at their current recommended versions. The release is fully backward compatible on the wire: nothing new is required of a client, and with the new switch unset every existing request is answered with the same status and verdict as before. See the version compatibility page.
