Skip to main content

AxonFlow v10.3.0 Release Notes

v10.3.0 is a minor with one live addition and a large dark half, and the two are separated here rather than blended.

Live: the agent gains an AuthZEN-native authorization endpoint, POST /api/v1/access/evaluation. It is registered in every edition, at every tier, with no feature flag and no kill switch. It is a new route, so nothing that previously worked stops working, and it is an adapter rather than a new engine: every entry becomes the same internal request POST /api/v1/decide already builds, and is answered by the same evaluator. The verdict it returns is real and enforcing.

Dark: every enforcement plane now dual-evaluates against the new policy decision point and records what it would have decided, behind a switch that is off by default; the identity compatibility window shipped in v10.2.0 gains the metrics that let you tell "no divergences" apart from "no comparisons"; and trust realms, reservations, proof executions and decision-proof key custody all gain durable Postgres stores. None of the dark half changes an authorization outcome, and most of it has no writer in any shipped code path yet.

It is a MINOR by the semver policy. Five additive migrations run this release, and unlike v10.2.0 one of them is core, so a Community deployment does apply a migration. 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.2.0.

Am I affected?

A deployment that sets none of the new variables and calls no new route sees one new startup log line per process ([DECISION-SHADOW] agent: AXONFLOW_DECISION_SHADOW_MODE=off (...)), applies the migrations described below, and behaves exactly as on v10.2.0 on every existing request path.

Anyone whose gateway or policy enforcement point would rather speak AuthZEN. The new endpoint is available immediately, with no configuration. Read The AuthZEN evaluation endpoint for the envelope shapes, the closed vocabularies, the refusal body and the profile header, all of which are stricter than the AuthZEN baseline on purpose.

Anyone scraping /prometheus, and anyone whose dashboards or alert rules name a Go runtime series. Ten new AxonFlow metric families appear with no configuration at all, and the Go client library moved from 1.17.0 to 1.24.0, which changes the built-in Go collector output. go_memstats_lookups_total is removed; go_gc_gogc_percent, go_gc_gomemlimit_bytes, go_sched_gomaxprocs_threads and, on Linux, process_network_receive_bytes_total and process_network_transmit_bytes_total are added by default; and every Go-collector HELP string gains a Sourced from ... suffix. No AxonFlow series changes. See What changes on /prometheus.

Anyone who calls the identity settings admin API. On PUT /api/v1/admin/organizations/{org_id}/identity-settings, an absent compat_mode now preserves the stored mode; "compat_mode": null clears it. Until v10.2.x an absent field cleared it. See One admin API semantic change.

Anyone sizing images. The agent and orchestrator binaries each grow by roughly 16 MB, because the policy decision point is now linked in-process so it can be dual-evaluated.

Anyone waiting on the durable reservation store or the durable proof-execution record. They ship, and nothing constructs them yet, so the behaviour they exist to correct is not corrected by this release. See What this release deliberately does not claim.

Everyone else. Nothing changes.

The AuthZEN evaluation endpoint

POST /api/v1/access/evaluation accepts an AuthZEN 1.0 evaluation and answers with a decision. It is authenticated with the ordinary API credential and is available at all tiers, because the evaluation behind it is the one Decision Mode already performs.

One envelope, two shapes, chosen on the key set. A body carries exactly one of evaluation (a single evaluation) or evaluations (a bulk envelope with shared subject, action, resource and context plus an evaluations array). Sending both keys is malformed even when one of them is null, because presence is decided on the key set rather than on nullness.

A bulk envelope returns ONE decision, not one per entry. Its entries are preconditions of a single operation, so a denied entry denies the operation.

A bulk envelope is capped at 64 entries. The 1 MiB body cap bounds bytes, not entries, and {} is a fully valid entry that inherits everything from the shared base - so a size cap alone would still let a single request carry hundreds of thousands of policy evaluations. An envelope with more than 64 entries is refused with a 413 and a typed refusal naming the cap and the count you sent, before anything is evaluated. Because the entries are the preconditions of one operation, 64 is far beyond legitimate use. Evaluation also stops at the next entry boundary if you disconnect mid-request.

{
"evaluation": {
"subject": { "type": "gateway", "id": "edge-1" },
"action": { "name": "llm.completion" },
"resource": { "type": "llm", "id": "gpt-4o" },
"context": { "args": { "query": "..." } }
}
}

The vocabularies are closed, and anything outside them is refused rather than ignored. subject.type must be gateway. Actions are llm.completion, tool.call and agent.invoke. Resource types are llm, tool and agent. The only context members are args and correlation, and args.query is required. Caller-supplied subject.properties, an unrecognized context member, an argument beside query, and an action and resource naming two different stages are each a refusal, not a silently dropped field.

A refusal is a different body shape from a decision, so a client cannot read one as the other. It carries code, pointer, message, supported and request_id, and has no decision member. pointer is an RFC 6901 JSON Pointer into your own request. The codes are closed: malformed_envelope, incomplete_evaluation, unsupported_subject, unsupported_action, unsupported_resource, unevaluable_attribute, missing_evaluable_content and evaluation_unavailable. Only evaluation_unavailable is retryable.

Profile negotiation is explicit. Send X-Axonflow-AuthZEN-Profile: axonflow-authzen-profile-2026-08-29 to receive the AxonFlow context payload (state, category, reason, obligations, approval, decision id, schema version) alongside the boolean. An absent or empty header returns the bare AuthZEN 1.0 boolean - with one exception, and it is a fail-closed one. An otherwise-allowed decision that carries a mandatory obligation (for example, "you must redact these fields before forwarding") answers {"decision":false} to a caller that did not negotiate the profile, because the obligation rides in the context payload that caller will never see, and an enforcement point cannot be handed a permission whose precondition it cannot receive. Callers that negotiate the profile get the allow with its obligation, exactly as documented; denied, challenged and errored evaluations are unchanged; and an allow carrying no mandatory obligation is still true for every caller. These withheld allows are counted under their own outcome label and logged, so you can find the integrations that need to start sending the header, and the audit record states both facts: that policy permitted the request, and that the platform refused it anyway because the caller could not receive the precondition. A profile version this build does not emit is a 406 naming the version it does emit, never a silent fall-through to something else.

Statuses the endpoint emits: 200, 400, 401, 406, 413 (bodies are capped at 1 MiB and bulk envelopes at 64 entries), 422, 500 and 502.

A new audit plane, access_evaluation. A direct POST /api/v1/decide still records plane=decision, so the two surfaces are separable in your audit records from the first request.

Reference: Agent Endpoints.

The decision shadow: every plane dual-evaluates, recorded only

Twelve enforcement planes now evaluate the new policy decision point alongside the verdict they already computed, and record the comparison. decide, gateway_request, mcp, openai_compatible, proxy_request, proxy_tier, orchestrator_response, wcp, map, policy_simulation, policy_test, and one Enterprise ingest plane.

# Agent AND orchestrator. Default: unset, which is off.
AXONFLOW_DECISION_SHADOW_MODE=off # the engines never build an observation
AXONFLOW_DECISION_SHADOW_MODE=shadow # dual-evaluate, and RECORD ONLY

The shadow cannot change a decision, by construction rather than by discipline. The observation entry point returns no value at all, so there is no result a call site could act on, and the evaluation happens on a bounded worker after the plane's response has already been decided. The legacy side of every comparison is the verdict the plane already produced, never a second evaluation of the same request.

enforce is not a value this switch may hold. It is refused by name at boot, by a database constraint, and again at the single read site. The decision plane becomes an authority in a future major, not through this switch.

A comparison across two different policy sets is not comparable, and is counted as its own thing rather than as agreement or as an unexplained difference. That matters because otherwise every policy edit would either inflate your agreement rate or turn your dashboard red.

Seven counters are exported, and the denominator comes first: total observations by plane and disposition, comparisons by plane and classification, fail-open counts by plane and direction, bundle builds, evaluation duration, enqueue duration (the only cost a caller waits for), and organization-mode resolution failures.

The window ships with its own vacuity guard. An eighth series, axonflow_decision_shadow_mode, is a gauge published per plane at process start - in both editions, and in mode off too - reading 1 for the mode each plane booted in and 0 for the others; it is per plane because the plane list can be narrowed, and a plane excluded from the list reads off whatever the process mode is. Alongside it, the observation counter's compared child is pre-created at zero for every plane. The readings this window feeds are ratios, and before this only the numerator existed ahead of traffic: a zero fail-open count read identically for a plane that was watched and clean and a plane that was never measured at all. With the gauge and the pre-created denominator, "watched and silent" and "not watched" are different query results rather than the same empty one.

An unrecognized value of AXONFLOW_DECISION_SHADOW_MODE is fatal at boot, in every mode, because this switch has no safe direction to fall back to.

On Enterprise the same switch is settable per organization, through decision_shadow_mode on the same identity settings row the compatibility mode already uses: one query, one TTL, two axes. A record wins over the process flag in both directions, an absent record means the process flag, and an unreadable one means the process flag and is counted. Putting one organization into the shadow on a deployment that is otherwise off is the recommended first look, and it is reversible by writing null.

The shadow window gets a denominator

v10.2.0 put the identity compatibility adapters into shadow mode and reported zero divergences. It had no way to tell that apart from zero comparisons: agreements were sampled one in a hundred thousand, the counterfactual recorder had no runtime consumer, and no metric carried compatibility data.

Six new series now appear on the /prometheus endpoint both binaries already serve, on the default registry, with no new port, no new route and no configuration:

SeriesLabels
axonflow_identity_compat_comparisons_totalcomponent, path, mode, legacy, identity_state, divergence, fail_open, synthetic, version
axonflow_identity_compat_org_comparisons_totalcomponent, org, synthetic
axonflow_identity_compat_mode (gauge)component, mode
axonflow_identity_compat_build_info (gauge, always 1)component, version, adapter_contract
axonflow_identity_compat_org_mode_failures_totalcomponent
axonflow_identity_compat_org_settings_read_failures_totalcomponent

Four design points worth knowing before you build a panel on them:

  • The counters are never sampled. The agreement log sampling interval still governs one log line and nothing else. This release adds no environment variable to the identity axis.
  • fail_open is its own axis, derived from the two admission decisions rather than from the divergence class, with three values: none, legacy_permitted_new_denied (the safe direction) and new_permitted_legacy_denied (the direction that matters).
  • The mode gauge publishes at boot with a series for every declared mode, the configured one at 1 and the others at 0. With zero traffic the comparison counter has no series at all, so a threshold over it reads as "no reading" rather than as a low reading.
  • Synthetic traffic is labelled, so a canary can never be counted as organic volume. A request carrying X-Axonflow-Synthetic-Probe: 1 (or true) is stamped at authentication and the label travels with the comparison. It is a metric label only and is never an authorization input.

Per-organization comparison volume is capped at 100 distinct organizations per process, first come and never evicted, with overflow in __over_cap__. The uncapped total lives on the comparison counter, which carries no organization label.

One live defect is fixed with it. A deployment whose schema does not include the Enterprise migration category was wiring the per-organization identity settings store anyway, producing one failed read per organization per refresh window and a recurring "relation does not exist" log line. Both the agent and the orchestrator now decide that from the deployment mode and log the reason once. Enterprise deployments are unaffected.

What changes on /prometheus

Linking the policy decision point in-process moves the Prometheus client library from 1.17.0 to 1.24.0, and its built-in Go collector output changes with it:

  • Removed: go_memstats_lookups_total.
  • Added by default: go_gc_gogc_percent, go_gc_gomemlimit_bytes, go_sched_gomaxprocs_threads, and on Linux process_network_receive_bytes_total and process_network_transmit_bytes_total.
  • Changed: every Go-collector HELP string gains a Sourced from ... suffix.

No AxonFlow series changes. If a dashboard, a recording rule or an alert names a Go runtime series by hand, check it against the list above before upgrading.

Ten new AxonFlow metric families appear with no configuration: the six identity-compatibility series above, three for the AuthZEN endpoint (request totals by outcome, shape and origin; refusals by code; and a histogram of entries per envelope), and the decision-shadow mode gauge, which publishes at boot on every plane in both editions even when the mode is off. Cardinality is bounded by construction: closed label vocabularies, a plane set derived from the compiler's plane model, and the 100-organization cap.

Durable stores that nothing writes to yet

Four bodies of state that previously lived in process memory or nowhere at all now have Postgres schemas. This is schema and library work. With one narrow exception described below, nothing in a shipped code path reads or writes any of it, and no behaviour changes.

  • Trust realms and the identity epoch (Community and Enterprise). Two new core tables hold an organization's trust realms and its identity epoch. A realm's whole definition is round-tripped through a JSON document, and the columns beside it exist so the database can enforce issuer uniqueness and an operator can read the table. Issuer uniqueness is per organization, not global, so two customers may federate the same public identity provider. Version advance is now enforced by the database as well as per replica: a re-registration at an equal or lower version is refused rather than silently accepted.
  • Reservations (Enterprise). The admission path contains no SELECT: the check is the write, an update guarded by the capacity predicate whose zero-row result means "no room". Counters are charged in a fixed order so two concurrent charges of the same pair cannot deadlock. There is one window-key generator, always in UTC, supporting exactly day, month and hour windows; a rolling window is refused rather than guessed, because falling back to a day key would silently make a rolling hour a budget twenty-four times larger. Only the administrative limit setter creates a counter row, so an unconfigured counter is a refusal and never "unlimited".
  • Decision proof executions (Enterprise). A single-use record whose primary key is the nonce alone, not the pair of organization and nonce: under a composite key "single use" would have meant "once per tenant".
  • Decision-proof key custody (Enterprise, and off by default). Two separate signing roots in a cloud key service, so certifying an ephemeral signing key and distributing a key set are different authorities and a compromise of one is not enough. A certificate binds the environment, deployment, organization scope, purpose, validity window, both algorithms and the issuer epoch; freshness fails closed on two independent bounds; a monotonic sequence refuses a replayed older key set; and a rotation-readiness check refuses over an empty roster rather than answering "ready" because nobody is listed. A revoked key stays published until its expiry specifically so a compromise reports "revoked" and not "unknown". This is the narrow exception: it has request handlers, but they are registered only when custody is explicitly enabled and every one of its settings is present, and there is as yet no automatic client that drives a rotation.

One admin API semantic change

On PUT /api/v1/admin/organizations/{org_id}/identity-settings, an absent compat_mode now preserves the stored mode; "compat_mode": null clears it.

Until v10.2.x an absent field cleared it, which was safe while it was the only mode on that body. With a second mode on the same body, a caller who set one would silently clear the other. Clearing is still available, and is now something a caller says rather than something a caller omits.

Security

Three dependency advisories are fixed by version bumps, none of which changes AxonFlow behaviour:

  • golang.org/x/crypto moves from 0.53.0 to 0.55.0 for CVE-2026-56854, a critical advisory in its SSH package. AxonFlow does not run an SSH server.
  • google.golang.org/grpc moves from 1.82.1 to 1.83.1 for CVE-2026-84304 (high), carrying its own minimum-required versions of two Google API libraries forward with it.
  • golang.org/x/image moves from 0.43.0 to 0.45.0 for CVE-2026-46603 (high, a denial of service in the WebP lossless decoder).

One hardening entry on the new AuthZEN endpoint, recorded for the reasoning rather than for remediation: no released version is affected, because the endpoint itself is new in v10.3.0, so the first build you can run already behaves as documented. Two properties were tightened while the route was still unreleased. First, the bulk envelope is capped at 64 entries with a 413, because the 1 MiB body cap bounds bytes rather than evaluations and an empty object is a valid entry - without the cap, one authenticated request could carry hundreds of thousands of serial policy evaluations that edge rate limiting cannot see. Second, an otherwise-allowed decision carrying a mandatory obligation is answered {"decision":false} to a caller that did not negotiate the profile header: earlier in the train it returned a bare {"decision":true} with the obligation silently dropped, which would have let a bare AuthZEN 1.0 caller forward unredacted content believing it had been permitted to. Both behaviours are described in The AuthZEN evaluation endpoint.

What this release deliberately does not claim

  • The durable stores do not fix what they exist to fix, yet. Nothing constructs the reservation store or the durable proof-execution record, so every deployment still runs the in-memory reference. A budget of 100 still admits 200 across two replicas, and one decision proof is still consumed once per replica. There is also no metric export for the reservation counters and no scheduler for either expiry sweep.
  • Nothing reads or writes the trust realm tables. They are created so the configuration has somewhere durable to live; loading them into the running registry is future work.
  • Decision-proof key custody is not usable unattended. There is no automatic rotation loop and no enforcement-point key-set client; an enforcement point's identity on a key report is self-asserted; and on a stack where the services share one task role, the key policies deny signing to that role, so the roles must be separated before anything can sign.
  • The decision shadow has one rollback width fewer than intended. The switch is settable per deployment and per organization. There is no per-plane switch, so mitigating one misbehaving plane for one organization means taking all twelve out of the shadow for that organization or shipping a build. The blast radius is loss of observation, not customer impact.
  • No plugin release accompanies this train. The five SDKs do release with it - see SDK and plugin compatibility - but every plugin's recommended version is unchanged.
  • The AuthZEN endpoint is an adapter over the existing evaluator, not the new policy decision point. Which engine answers it changes in a future major, with no change to the wire.

Migration

Five additive migrations run this release. Four are Enterprise only and one is core, so unlike v10.2.0 a Community deployment does apply a migration.

MigrationEditionApplied by a Community deployment
trust realms and the identity epochcoreyes
decision-proof key custodyEnterpriseno
reservation storeEnterpriseno
proof execution recordEnterpriseno
per-organization decision shadow modeEnterpriseno

Between them they create nine tables and alter exactly one existing table, by adding a nullable column to it. Nothing is backfilled, no data is rewritten, no index is built on an existing relation, and no lock is taken on anything that carries rows today. Every new table is empty after the migration, and five of the nine have no writer in any shipped code path.

Row-level security is enabled and forced on every new organization-scoped table, with isolation policies on the current organization in both the read and the write direction, so a query that forgets its scope matches nothing rather than everything. The decision-proof key custody tables deliberately carry no organization column and no row-level security: a signing key belongs to the deployment rather than to a tenant, and inventing a tenancy the object does not have would produce a policy that either matches everything or hides the key set from the process that has to serve it.

Rolling the binaries before applying the migrations keeps v10.2.0 behaviour. For the per-organization decision shadow column the settings read fails and falls back to the process flag; for the other four there is nothing to fall back from, because no shipped code path reads those tables at all. In practice the ordering is moot, since the agent applies migrations at boot.

No boot requirement changes for a deployment that sets none of the new variables. A deployment that sets AXONFLOW_DECISION_SHADOW_MODE to an unrecognized value does not start, by design. Every decision-proof custody setting is non-fatal: a missing or malformed one registers no routes and logs the reason.

From older versions: v10.3.0 is cumulative. If you are upgrading from v10.1.0, read the v10.2.0 release notes for the identity compatibility mode and its Enterprise migration; from v10.0.0, also read the v10.1.0 release notes; from v9.x, start with the v10.0.0 release notes and the v9 to v10 migration guide.

SDK and plugin compatibility

All five SDKs release with this train: Go, Python, TypeScript and Java v9.2.0, and Rust v0.9.0 (preview). The platform's /health endpoint now recommends those versions. The new SDK line adds the client surface for the AuthZEN evaluation endpoint - wire types generated from the published surface document, typed refusals with the retryable set marked, and the profile header sent on every request - and, in the Go, Python, TypeScript and Java SDKs, a license_tier field on the anonymous telemetry heartbeat that relays the tier your platform's /health endpoint already reports, exactly as reported and never read from your licence key; the aggregate adoption reporting built on it keeps client-reported and platform-reported figures separate, each broken down by tier. The telemetry page carries the full disclosure.

No plugin release rides this train; all five plugins stay at their current recommended versions.

The release is fully backward compatible on the wire. Nothing new is required of a client: a 9.1.x SDK keeps working against v10.3.0 unchanged - it simply has no helper for the new endpoint - the AuthZEN endpoint is a new route rather than a change to an existing one, minimum supported SDK versions do not move, and with the new switch unset every existing request is answered with the same status and verdict as before. See the version compatibility page.