AxonFlow v9.13.0 Release Notes
v9.13.0 is the output of a cross-tenant remediation programme. Almost every fix in it is the same shape: the platform trusted values the caller supplied, in places that decided authorization, tenancy, or policy outcomes.
This release carries two migrations (core/155, core/156), and two deliberate behaviour changes can affect an upgrade:
- A stack whose
DEPLOYMENT_MODEis not a recognised value now refuses to boot. - Rows with a blank tenancy key become permanently unreachable, so an execution spanning the upgrade fails its final update instead of completing.
See Before you upgrade.
Am I affected?
Every deployment benefits from the authoritative-principal, tenancy-binding, authentication, and egress fixes.
Deployments on the hardened least-privilege database posture (AXONFLOW_DB_USE_APP_ROLE=true with the app-role DSNs provisioned) additionally get fixes for pre-authentication lookups that were failing valid credentials. On those deployments, a valid portal API key or SCIM bearer token could be reported as an authentication failure, and the SCIM directory could return an empty result that identity providers acted on. If you run SCIM on this posture, read SCIM on the app-role posture.
Deployments on the defaults — docker-compose, the install bundle, CloudFormation stacks that never flipped AppRoleProvisioned — connect as the table-owning database role, which bypasses row-level security, so the pre-authentication lookups worked throughout.
The request body decided things it should never have decided
Policy conditions could be satisfied from the request body
The governed request plane took user.role and user.email from the request body. user.role is resolvable as a policy-condition field, is offered in the customer portal's policy builder, and appears in the shipped HIPAA PHI-access template — so a tenant policy of the form {query contains …} AND {user.role not_equals "admin"} → block was defeated by putting "user":{"role":"admin"} in the body. That makes it a policy-evasion primitive, not merely an attribution problem.
Be precise about the severity: no enabled policy keyed on user.role is seeded on a default deployment, so this was latent by default rather than live on a stock install. Principals are now bound from the validated credential and the body values are ignored.
Governed routes require an authenticated tenancy
POST /api/v1/process, /api/v1/workflows/execute, /api/v1/plan, and /api/v1/plan/execute previously fell back to a tenancy named in the request body. They now return 401 unless the request carries both X-Org-ID and X-Tenant-ID with usable values — both dimensions, so a deployment resolving an organization but no tenant is refused rather than half-scoped.
A body naming a different tenancy is now 403 — refused, not silently corrected. Previously a divergent value was replaced whenever the header happened to be present, so a misconfigured client got a working request under a tenancy it never asked for.
Callers going through the AxonFlow Agent or the customer portal are unaffected. Both stamp these headers from the validated credential.
The same pattern, in six more places
- MAP HITL approve/reject took the approver's name from the request body.
- RBI compliance actors were read from the request body, and RBI compliance routes took their tenant scope from a client-supplied query parameter.
- Workflow-control routes took tenancy from client-supplied headers.
- MCP dynamic-policy evaluation took its tenant scope from the request body.
POST /api/v1/mcp/evaluate-policiesnow derives tenancy from the authenticated caller: naming another tenant is 403, and omitting the field — previously400 tenant_id is required— evaluates your own policies. - Workflow execution and multi-agent planning stamped rows with a body-supplied, or entirely absent, tenancy.
Authentication and exposure
- The orchestrator API required no authentication at all, and was internet-reachable on affected topologies. It now requires an internal-service credential.
- The customer portal's orchestrator catch-all forwarded any path to any authenticated session.
- The portal admin API served anonymously outside
DEPLOYMENT_MODE=saas, and in-VPC deployments could not enable admin authentication at all. OPTIONSrequests ran auth-gated handlers anonymously.- The orchestrator's embedded Execution Viewer UI is no longer reachable by addressing the orchestrator directly, and no Compose file in the repository publishes the orchestrator host-wide any more.
- Portal CORS origins are now operator-configured through
AXONFLOW_CORS_ALLOWED_ORIGINS(CloudFormation: theCorsAllowedOriginsparameter), instead of an allowlist compiled into the image.
Tenant isolation
- Organization binding failed open on eight or more governance call sites. A fail-closed choke point now makes an unscoped call unrepresentable rather than merely discouraged.
- The dynamic-policy verdict cache was tenant-blind.
- The dynamic-policy list endpoint disclosed every tenant's policies, and policy simulation disclosed the deployment-wide policy count. List and enforcement scoping now share one decision point.
- The MAS FEAT compliance module never scoped a single statement to its organization.
- MCP tool governance silently evaluated no tenant policies.
contentpolicies — the default type — were dropped before evaluation, and the evaluator understood neither thequeryfield nor theregexoperator that the policy-creation tool writes. See MCP tool governance for the behaviour change this unlocks.
The least-privilege database posture
- The portal's pre-authentication API-key lookup ran inside the row-level-security scope it exists to discover. A lookup that exists to find which tenant owns a credential cannot set the scope first — that is what it is trying to determine. It matched nothing, and the handler reported a valid key as an authentication failure. Operators rotated credentials that were never wrong.
- The app-role boot guard passed when the administrative pool was configured but unusable — it checked that an environment variable was set, never that a pool had been obtained.
SCIM on the app-role posture
SCIM bearer-token authentication rejected every valid token, and the SCIM directory returned an empty directory instead of an error. That second one matters more than it looks: an identity provider that receives "no users" acts on it. If you run SCIM on this posture, review your directory state after upgrading.
Egress filtering
Nine SSRF egress classifiers with five distinct behaviours are unified onto one range table. 2001:db8::/32, NAT64, and 6to4 were treated as public by all nine. The HTTP connector had no socket-level egress guard at all. Four callback dialers validated the DNS answer and then dialled the hostname, which resolved again — a DNS-rebinding window.
Circuit-breaker notifications and orchestrator webhooks now refuse ranges they previously accepted. If either targets an internal address, re-check it before upgrading.
Before you upgrade
DEPLOYMENT_MODE is now validated
A stack whose DEPLOYMENT_MODE is not a recognised value now refuses to boot, instead of silently applying the SaaS schema. Recognised values:
community, community-saas, enterprise, evaluation, in-vpc-banking, in-vpc-enterprise, in-vpc-healthcare, in-vpc-travel, invpc, saas
A leading space or a capitalised spelling now stops the agent. Check it with:
docker compose exec axonflow-agent printenv DEPLOYMENT_MODE
An unset DEPLOYMENT_MODE is unchanged: it still selects core/ only, and it is still not fatal.
DEPLOYMENT_MODE=enterprise was not a recognised mode, so every self-hosted enterprise stack had been applying the SaaS schema through a permissive fallback. It is now an alias for in-vpc-enterprise. Nothing needs to change, and nothing is dropped — the industry tables your stack acquired through the old fallback stay exactly where they are and simply stop being re-selected. No corrective migration removes them.
On a fresh install, DEPLOYMENT_MODE=enterprise no longer creates the industry tables, and the compliance routes that read them answer 500 — the routes are registered by the enterprise build independently of the mode, so they exist and the queries fail. Existing stacks keep everything they have. If you run a compliance module, name its vertical (in-vpc-banking, in-vpc-healthcare, in-vpc-travel) or saas.
Note that the RBI kill switch sits on the Decision Mode hot path and fails open on a database without its tables. That is the state in-vpc-enterprise has always been in; what is new is that enterprise now joins it.
Migrations
core/155 and core/156 ship with this release.
core/156 makes the tenancy keys NOT NULL and adds a blank-string CHECK on plans, workflows, workflow_checkpoints, execution_summaries, and webhook_subscriptions, after stamping orphaned rows with an inert sentinel. SET NOT NULL and ADD CHECK are scans rather than table rewrites, so there is no full rewrite — but all five ALTERs run in one transaction, so the ACCESS EXCLUSIVE locks on all five tables are held until COMMIT rather than released per table. Size the maintenance window for the sum of the five scans, not for the largest table.
The migration is idempotent, and tables whose columns are absent are skipped rather than raising.
A row with a blank tenancy key was previously readable by every tenant — that is the vulnerability being closed. Every such row is stamped with the inert sentinel __axonflow_unowned__ and becomes unreachable and unwritable.
The migration does not attempt to determine an owner, so no such row is recovered to a real tenant; re-attributing one requires direct SQL.
The practical consequence: an execution that started before the upgrade and finishes after it will fail its final update rather than be marked completed. That window is the upgrade itself. Drain in-flight executions if you can.
budgets is deliberately not constrained by this migration: its scope lookup treats an empty organization as apply-to-all, so adding the constraint would silently disable deployment-global spend caps — a loosening of control, not a tightening.
Fixture data
AxonFlow's own end-to-end test fixtures and demo tenants were being seeded into every customer deployment. They are no longer applied, and rows already present are not removed — they are scoped to tenants you do not use and enforce nothing for yours. Removing them is an operator decision, taken on your schedule.
MCP tool governance
content policies now govern MCP tool calls where the dynamic plane is enabled (MCP_DYNAMIC_POLICIES_ENABLED=true). Because content is the default policy type, existing tenant policies of the form {query contains|regex …} + block begin governing MCP tool calls on upgrade, where previously they governed only the LLM, MAP, and WCP planes.
Deployments on the default posture (MCP_DYNAMIC_POLICIES_ENABLED=false, both docker-compose and the community-SaaS CloudFormation template) are unaffected until they opt in.
A condition naming a field the MCP evaluator cannot resolve fails to no-match — but a known field with a negated operator can match on an empty value. For example {user.role not_equals "admin"} is true when the role is unset, which is common on this plane.
axonflow_create_tenant_policy now reports real enforcement state — an enforced boolean, and an enforcement_blocked_reason naming the lever to flip when the policy is inert — instead of an unconditional success promise that was false on every default install.
Upgrading
Self-hosted partners: follow UPGRADING.md in the install bundle, which carries a v9.13.0 section covering the three pre-upgrade checks above.
The full changelog, including the reasoning and precise blast radius of each item, is in CHANGELOG.md.
