Skip to main content

AxonFlow v9.12.2 Release Notes

v9.12.2 is a security patch release in two parts. First, it completes the v9.12.1 read-path repair for deployments running the hardened least-privilege database posture: the static-policy readers were still unscoped on the restricted role, so the request and response content gates — SQL-injection detection, dangerous-command blocking, PII block/redact — evaluated zero policies and allowed everything. Second, it ships tenant-isolation and fail-safety hardening that benefits every deployment: the policy engine now fails closed when a policy load returns an empty system policy set, the shared global baseline policies are no longer writable through the tenant policy API, and the HITL approval queue is now organization-isolated.

Am I affected?

The enforcement and visibility fixes apply to the same population as v9.12.1: you are affected only if your database pools actually run as axonflow_app_role — that is, AXONFLOW_DB_USE_APP_ROLE=true with the app-role DSNs provisioned:

  • CloudFormation deployments that completed the two-step app-role provisioning and flipped AppRoleProvisioned=true.
  • Self-hosted deployments whose operator followed the least-privilege provisioning guide (provision-app-role.sh + the app-role environment variables).

You are NOT affected by the silent-enforcement issue on the defaults: the docker-compose stack, the self-hosted install bundle, and CloudFormation stacks that never flipped AppRoleProvisioned all connect as the table-owning database role, which bypasses row-level security. Their content gates enforced correctly throughout.

Every deployment benefits from the hardening in this release regardless of posture: the fail-closed empty-policy-set guard, the global-baseline write lockdown, and HITL organization isolation.

If you are affected, upgrade promptly: until you do, the request and response content gates are not enforcing static policies — prompts that should be blocked for SQL injection or dangerous commands pass through, and PII that should be blocked or redacted is not.

What was broken on the app-role posture, now fixed

Content gates evaluated zero static policies

The agent's static_policies readers and the shared policy loader ran unscoped on the restricted database role, so row-level security matched zero rows: policy loads succeeded but loaded nothing, and the request and response content gates allowed everything with policies_evaluated: 0. All static-policy readers now run organization-scoped in two disjoint passes — the caller's own organization plus the shared global baseline — restoring full enforcement of the SQL-injection, dangerous-command, and PII block/redact policies.

Enforcement gates pass the caller's organization

All policy-evaluation gates — MCP check-input / check-output, the Gateway pre-check, the OpenAI-compatible endpoint, Cowork telemetry redaction, and response processing — now scope their policy loads by the validated caller organization. This closes a silent under-enforcement gap for organizations whose organization and tenant identifiers differ, where a gate could previously load policies against the wrong key and evaluate a smaller set than the organization had configured.

Portal, HITL, integration, and export reads returned nothing

The same unscoped-read defect class affected a family of read paths on least-privilege deployments, all fixed the same way:

  • The customer portal's "Static (Read-only)" policy count and the unified policies view showed 0 static policies, and the dashboard "Total Policies" tile counted only dynamic policies.
  • HITL approve/reject flows returned "approval request not found".
  • Integration activation enabled zero int_* policies.
  • Policy explain, override lookups, decision-chain reads, LLM provider listing, connector runtime configuration, and policy version history returned empty results. Version history now also includes organization-tier parent policies for all tenants of the owning organization.
  • On Enterprise: the enterprise HITL approval repository, SCIM role readers, the EU-AI-Act and evidence export repositories, and the portal API-key handlers returned empty results.

Hardening for every deployment

The policy engine fails closed on an empty system policy set

A successful policy load that contains zero system-tier policies is an impossible state on a healthy deployment — the system policies are migration-seeded — so it is now treated exactly like a load failure on both the request and response planes, with a distinct policy_load_empty_system_set log line and metric so the condition is diagnosable rather than silent. This is defense in depth: any future defect class that produces an empty policy load blocks at the gate instead of silently allowing, which is exactly the guard that would have caught the zero-row reads above the moment they happened.

Global baseline policies are write-locked

The shared global baseline policy rows — the SQL-injection guards, PII detection, EU-AI-Act templates, and integration policies every organization inherits — are no longer mutable by tenant callers. Update, delete, and disable of a global baseline row through the tenant policy API now return 403 for every caller. Previously only system-tier rows were protected, so a tenant could weaken a shared baseline policy for every organization on the deployment.

The HITL approval queue is organization-isolated

Queue listing, request lookup, history, approve, reject, and override now bind to the authenticated caller's organization; a request belonging to another organization returns not-found. Previously these flows were unavailable on least-privilege deployments (the "not found" fix above) and deployment-wide on owner-connection multi-tenant deployments — one organization's approver could see and act on another organization's pending approvals.

Behavior changes

  • Tenant-API writes to global baseline policy rows now return 403 (previously only system-tier rows were rejected). If an integration was editing shared baseline rows through the tenant API, it must stop; per-organization customization belongs in organization-tier policies and overrides.
  • A deployment booted with no migrations applied fails closed at the enforcement gates instead of running ungoverned, because it has no seeded system policies and the empty-set guard treats that as a load failure. Run your migrations (a normal boot applies them automatically) before sending traffic.
  • Effective-policy responses collapse duplicate overrides. When multiple live overrides exist for one policy, the effective-policy response now returns only the latest-created entry; previously duplicate rows could appear. Adjust any client that depended on seeing every override row.

Migration

One migration ships in this release: core/154 re-runs the global backfill for policy rows seeded under the legacy global sentinel (covering industry template seeds applied after core/153's backfill ran), and installs BEFORE INSERT triggers on the static and dynamic policy tables that default the organization key on global-sentinel rows so future seeds cannot reintroduce the gap. It is additive and idempotent, and no maintenance window is needed.

Known limitations carried from v9.12.0

The v9.12.0 known limitation regarding owner backfills on managed-PostgreSQL deployments where the migration role is subject to row-level security remains open in this release; the in-product break-glass owner endpoint is the documented recovery path.

Upgrading

Standard patch upgrade: pull the v9.12.2 images and restart; migration core/154 applies automatically on boot. After upgrading, verify enforcement with a content-gate round-trip: send a request that a static policy should block (for example a SQL-injection probe) and confirm it is blocked with a non-zero policies_evaluated, and confirm the portal's static policy count is non-zero. Review the behavior changes if you operate a multi-tenant deployment, edit policies through the tenant API programmatically, or consume the effective-policy endpoint.