AxonFlow v9.19.0 Release Notes
v9.19.0 is a correctness release across two surfaces. In the operator console, the pages that report what the platform did now agree with what it actually recorded: audit rows carry policy names rather than raw identifiers, the compliance summary refreshes with the table it sits above, date ranges follow the viewer's own timezone, and approvals raised by the decision API appear in the approvals queue instead of being invisible. Underneath, in both editions, five separately maintained implementations of policy-condition matching converge onto one shared evaluator.
It is a MINOR by the semver policy: no capability is removed, no new credential is required, and no new refusal is added at boot. No migrations run this release. It does, however, change what some stored policies match, so read Before you upgrade rather than treating it as a routine bump.
Am I affected?
Everyone with policies stored in the database should read the two behavior notes below: the condition-matcher convergence changes five named matching behaviors, and policies stored with an explicitly empty conditions list stop being evaluated. Both are described precisely, and the empty-conditions note includes a query you can run before you upgrade.
Enterprise deployments whose operators use the console get the whole portal correctness wave. If your team has been reading policy identifiers off audit rows, cross-checking the compliance summary by hand, or driving the decision API directly to find pending approvals, those workarounds are no longer needed.
Deployments that use the detection posture lever (the profile or environment setting that decides whether personal-data detections warn, redact, or block) gain the observability described under Policy action authority. Nothing about the resulting verdicts changes.
Community edition: this is a substantive release, not a lockstep version bump. The policy-evaluation work, the audit name stamping, and the posture observability are all core code and ship in both editions.
The operator console now agrees with the record
(Enterprise)
Audit rows show policy names. Blocked rows previously showed raw policy identifiers in the policy column, and the expanded row's policy-name field rendered as an empty dash, while step-up rows beside them showed proper names. Two things were wrong and both are fixed: the writers on the decision and tool-call planes recorded identifiers without names, and the console's two render paths disagreed about what to do when a name was missing. Writers now stamp the display name the engine actually matched on, and every audit surface resolves identity the same way. A row that genuinely carries no recorded name renders its identifiers with an explicit (name not recorded) marker rather than a silent blank or an identifier dressed up as a name. Names are never looked up from the current policy catalog at display time, because a policy renamed after the fact must not appear on a row that never carried that name. Rows written before this release are unchanged and render with the marker. The storage-plane audit rows written for Cowork and Claude Code sessions carry the same stamped names, threaded from the same evaluation-time matches.
The compliance summary can no longer contradict the rows beneath it. The summary tiles were fetched only on page load and on a date change, while the table refetched on every filter change. Rows driven into the system while the page was open therefore appeared beneath tiles still showing their page-load values: visible blocked rows under a 0.0% block rate, on a compliance surface. The summary now refreshes whenever the table does, and dims rather than disappearing while it refreshes, so paging through results cannot reopen the same staleness. The summary window remains dates-only, so its caption's statement that it is not narrowed by the user, action, or tenant filters stays true.
Date ranges follow your timezone. Timestamps rendered in local time while the range was computed as a UTC day. West of UTC, an evening row on the end date fell outside the window in both the table and the summary and simply vanished; east of UTC, rows stamped with the next local day leaked in; the end date's final fraction of a second was always dropped; and the default range itself was derived from the UTC date, so late in the day the default end could be tomorrow. One shared conversion now produces a local start-of-day through local end-of-day window for every date picker, and the same class was swept out of the compliance, usage, export, policies, and evidence-export surfaces.
Approvals raised by the decision API appear in the queue. The approvals page read the workflow-step queue only. A step-up raised on the decision API created a pending approval that rendered nowhere in the console: an operator watched an apparently empty queue while oversight work waited, and had to call the human-review API directly to see or act on it. Both planes now render as one merged list with per-plane badges, per-plane actions and detail panels, and a sidebar count drawn from the same fetch. Approvals that expired before the sweeper ran are marked and their actions disabled rather than failing when clicked, and a plane that cannot be reached is named in a banner instead of being silently counted as empty. Enforcement is unchanged and still lives on the platform side.
The policies page reports one consistent set of numbers. Filters, pagination totals, and summary tiles could each disagree with the others and with the platform, because the console forwarded filters to two upstreams and trusted each to apply them, while one of the two applies none. Rows, totals, and tiles now derive from a single filtered collection. Converging that surfaced nine further defects in the same code path, all fixed here, including: an enabled-count tile that used a different definition of "enabled" than the badges beside it; a type filter that did nothing at all; an effective-policies view whose system-policy half was permanently empty yet reported as complete; a single-policy fetch that returned an all-zero policy with a success status; a page size the platform rejects, which silently truncated results to a much smaller default; an enabled filter that selected disabled rows on one half; and an override creation timestamp that was fabricated at fetch time, so every override reported that it had just been created.
A newly created MAS FEAT kill switch no longer claims an automatic trigger. MAS FEAT ships a manual-only kill switch, where every trigger is an explicit human-initiated call, but new switches recorded an automatic-disable setting the system does not implement. New switches now record what the system actually does; the setting itself stays on the API for compatibility and is currently inert. Existing switches are unchanged.
Policy evaluation converges onto one evaluator
(Community and Enterprise)
The agent and the orchestrator had accumulated five independent implementations of dynamic-policy condition matching, which had drifted onto different semantics. They now share one evaluator, and five behaviors converge. A stored policy can start or stop matching after upgrading, so each is stated plainly:
containsandnot_containsare case-insensitive on every plane, including the tool-call, connector, and content-type planes, which previously required an exact-case match. A blocking policy of this shape now fires on requests it previously let through.contains_anymatches on a non-string item in its value list (a number, boolean, or null) on every plane. The database-backed engine previously skipped such items silently, so a condition mixing a number and a string never matched on the number.greater_thanandless_thanno longer produce false positives. An operand that is neither a number nor a numeric-looking string used to compare as zero, so a non-numeric value could spuriously satisfy a blocking rule with a positive threshold; it now simply does not match. Independently, a numeric-looking string such as"5"now compares correctly on every plane, including the in-memory engine, which previously rejected numeric strings outright.- A
regexcondition's pattern must be a string. The in-memory engine previously accepted any value and converted it to a string before compiling, so a numeric value like1.5compiled as the pattern1.5, in which.is a wildcard rather than a literal dot. A policy authored that way stops matching. This is a narrowing, and it is paired with an authoring-time rejection so the shape cannot be created going forward. - Every plane now enforces all ten policy operators. The tool-call, connector, and content-type planes previously recognised only four of the ten, even though the console has always offered all ten and never indicated that six of them were inert there. A policy using
not_contains,contains_any,greater_than,less_than,in, ornot_inthat looked live in the console but silently never matched on those planes begins enforcing there for the first time.
Two long-standing defects in policy-override precedence are fixed in the same work. A later-created organization-level override could beat an earlier, more specific tenant-level one, contradicting the intended contract that the tenant scope always wins; precedence is now resolved independently of creation order. And because an override row carries independently nullable action and enabled columns, a tenant's deliberate disable could be silently dropped and an action-less tenant row could discard a valid organization-level action override. The two settings now resolve independently, so a tenant disable and an organization action change can both be in effect on the same policy at once, each attributed to its own row.
Policy action authority is now documented and observable
(Community and Enterprise)
On every plane governed by the detection posture lever, the lever's action replaces whatever action a matched policy row carries. That is the intended authority order, and it is why a row set to block can match a request that is nonetheless allowed with a redaction obligation. Until now that displacement left no trace: the action stored on the row read as authoritative in the API and the console while being unused at evaluation time.
Displacement toward a less restrictive outcome is now counted in a metric and, on the decision API, reported as an advisory reason on the response. No verdict, obligation, redaction, or approval outcome changes: the advisory is additive and is never attached to a denial or a step-up. The resolution order, a per-plane table mapping each action to its actual outcome, and authoring guidance are now written down in the policy action authority guide.
A companion metric reports conditions that could not genuinely be evaluated (an unknown operator, a numeric comparison against a non-numeric operand, a pattern that is not a string, an unparseable stored conditions blob, or a field that could not be resolved). Previously such a condition was indistinguishable from a legitimate non-match, so a policy that silently failed to enforce was invisible.
Licensing
Beginning with v9.19.0, free production use under the BSL 1.1 Additional Use Grant is conditioned on the integrity of the copy you run. The operative wording is in the LICENSE file in the platform repository and its community mirror; read it there rather than relying on this summary. In outline: production use of a copy or derivative work is permitted only while, in that copy, licence key functionality has not been tampered with, and no usage limit or feature gate has been raised, avoided or bypassed relative to the corresponding unmodified AxonFlow distribution it was made from. Supplying a configuration value through a mechanism the shipped software provides for that purpose is expressly outside the condition, so documented environment variables and configuration files are unaffected.
This is a new prospective condition on free production use, not a restatement of the previous grant; earlier releases remain governed by the terms they shipped with. The rights to copy, modify, create derivative works, redistribute, and make non-production use are unchanged, and the licence family (BSL 1.1), the Change Date, and the Change License are unchanged. A new COMMUNITY_LICENSE_BOUNDARY.md in the repository documents what the Community build includes; it is product documentation, expressly not a licence term.
Security
The indirect golang.org/x/mod dependency is raised to v0.40.0 in every Go module that declares it, in both editions (CVE-2026-56864 and CVE-2026-56865). The package reaches the module graph transitively and is not imported by any AxonFlow code, so no runtime behavior changes; the bump keeps source builds and image scans clear of the finding.
Community edition
This release carries real community changes, not a lockstep version bump. The condition-evaluator convergence, the override-precedence fixes, the writer-side policy-name stamping on the decision and tool-call planes, the policy action authority observability, and the exclusion of empty-conditions policies are all core code and ship in both editions from the same source.
The operator console is an Enterprise surface, so the console work above applies to Enterprise deployments only. The community repository additionally gains the local development scripts its contributing guide has always referenced but which were never published alongside it.
Before you upgrade
-
No migrations this release. No schema changes and no boot-requirement changes. From a v9.18.0 baseline the upgrade is images only.
-
Policies stored with an explicitly empty conditions list stop being evaluated. No release ever let you create a policy with zero conditions, but the update API before this release accepted an explicitly emptied conditions list, so a long-lived database can hold rows nobody could author today. Because a policy with no conditions applies to everything, leaving those rows in place would have turned each one into a match on every governed request at upgrade, converting a blocking row into a database-wide denial nobody wrote. They are now excluded on the database-backed evaluation and listing paths and in the policy-test preview instead. Such a row stays visible in the API and the console on purpose, because you need to see it to fix it, and the policy test endpoint reports the exclusion and names the remedy: give the row real conditions, or delete it. A policy with a null conditions field is a different shape and keeps applying to everything on the planes where it already did. One plane changes: the tool-call, connector, and content-type plane previously refused to match any policy that carried no conditions at all, and the convergence removes that fail-safe, so a stored condition-less
blockpolicy begins matching, and denying, on that plane for the first time. Audit any such rows with the same query-first care as the empty-list rows above. To find affected rows before upgrading:SELECT policy_id, tenant_id, action, enabledFROM dynamic_policiesWHERE jsonb_typeof(conditions) = 'array'AND jsonb_array_length(conditions) = 0;After upgrading, the condition-evaluability metric reports these exclusions under an
empty_conditionsreason, so a non-zero rate tells you residue is still present and the query above tells you which rows. -
Review the five condition-matching convergences in Policy evaluation converges onto one evaluator. If you author policies with
contains,contains_any, numeric comparisons, or regular expressions, at least one of them may change what your policies match. Policies on the tool-call, connector, and content-type planes deserve particular attention: six operators that were silently inert there now enforce. -
From older versions: v9.19.0 is cumulative. If you are coming from v9.16.x or earlier, read the intermediate release notes; the v9.17.0 notes carry an additive migration that runs when you cross that version.
SDK and plugin compatibility
Three client releases accompany this train, and the recommended versions reported on /health advance to match. Minimum-version floors are unchanged, so clients below the recommended versions keep working.
| Client | Recommended | What changed |
|---|---|---|
| Go SDK | 9.1.1 | A plan execution stopped by policy reported a completed status with no error, hiding enforcement from callers that branch on status. |
| Rust SDK | 0.8.2 | Five security advisories cleared from the dependency tree, plus a fix to the connectors example; no API changes. |
| OpenClaw plugin | 2.8.6 | Every surface that renders a remote-supplied endpoint or identity on a terminal now sanitises it, and a forbidden response under an allow-on-error setting announces that the call proceeded ungoverned instead of running with no signal. |
The Python, TypeScript, and Java SDKs are unchanged this train and their recommended versions stay at 9.1.0.
