Skip to main content

v9.13 → v9.14 Upgrade Guide

v9.14.0 is mostly a compliance-surface release: one report facade across all five regulators (EU AI Act, SEBI, RBI, MAS FEAT, OJK), real renderers behind every format, and honest three-state answers where an empty 200 used to stand in for everything. Almost all of it lands on restart and asks nothing of you.

Three changes do need a decision before you pull the image, and this guide is about those. The largest by far is a refusal change: the whole-tenant compliance and audit export endpoints now require administrative authority, not merely tenant-wide read scope. Export automation that ran on a tenant-scoped license or internal-service credential worked on v9.12 and v9.13 and returns 403 on v9.14 until you give it an admin per-user token. The other two are OJK specific: identity resolution is now org-first (a blank org header is refused 400), and OJK readiness is now measured rather than asserted (so a score that looked high before can drop, correctly).

There is no new preflight for v9.14. The four migrations are additive and mutate no rows, so the v9.13 preflight's backup check is the only preflight step that still applies. If you are jumping from v9.12 you must work the v9.13 guide first - read Start here before anything below.

Start here: you also need the v9.13 guide

This guide covers only the delta from v9.13 to v9.14. A jump from v9.12.x straight to v9.14.0 crosses both release boundaries, and the v9.13 boundary carries the changes with irreversible data effects - core/155 disables policy rows whose identifiers are unrecoverable afterwards, and core/156 stamps a sentinel its own down migration deliberately leaves behind.

If you are on v9.12.x:

  1. Work through the v9.12 → v9.13 Upgrade Guide in full first - run its preflight against your still-running v9.12 stack, resolve every FAIL, read every WARNING, and take the snapshot it asks for. Nothing on v9.14 supersedes that procedure.
  2. Then return here for the v9.14 delta.

The two boundaries can be crossed in a single image pull (migrations from both apply in order at boot), but the decisions in the v9.13 guide have to be made before that pull, not after. The v9.13 preflight reads your old stack; once you are on v9.14 the rows it would have named no longer exist to be named.

If you are already on v9.13, skip straight to What changed.

Before you upgrade: take a snapshot

All four v9.14 migrations are additive and rewrite no existing rows, so v9.14 adds no new irreversible data effect of its own. Take the usual pre-upgrade database snapshot anyway - if you are crossing the v9.13 boundary in the same pull, its core/155 and core/156 data changes are the reason the snapshot matters, and a snapshot is their only rollback. Re-pinning the previous image digests rolls the images back exactly and undoes neither migration's data change.

What changed - the things that need a decision

1. Whole-tenant compliance and audit exports now require ADMIN authority

This is the change most likely to break a working self-hosted integration, and it is silent in the sense that nothing in your database records who is affected - it is a property of the credential your export automation carries, not of your deployment.

What is gated. On v9.14 these endpoints require the caller to hold administrative authority over the tenant, and answer 403 otherwise:

  • POST /api/v1/ojk/audit/export
  • POST /api/v1/sebi/audit/export
  • POST /api/v1/euaiact/export
  • the evidence export and media-governance export
  • compliance-report generate (POST /api/v1/compliance/reports) and download (GET /api/v1/compliance/reports/{id}/download)

The compliance-report status poll (GET /api/v1/compliance/reports/{id}) is deliberately left readable by any session in the tenancy - only generate and download are gated.

Separately, the audit REST read endpoints (/api/v1/audit/report, /search, /session-summary, /export) have been RBAC read-scoped since v9.13 (#2922): a non-admin per-user token, or a license/internal-service credential, is scoped to its own rows and a full audit trail returns empty or near-empty rather than 403. That is the same axis. If any of those reads is driven by a license or internal-service credential and expects the whole tenant's trail, it needs the same admin token described below.

Why it changed. Administrative authority and tenant-wide read scope used to be one field. A portal session with only viewer-level read permission carries tenant-wide read scope, and that was enough to POST a whole-tenant export. The two axes are now separate: read scope answers "how wide may this caller see within its own tenant," and admin authority answers "is this caller an administrator." Exports gate on the second. (#3248)

The symptom. Export automation that returned 200 on v9.12/v9.13 now returns 403 with a body like tenant-wide audit export requires an admin/owner role. A tenant-scoped license key or internal-service credential carries tenant-wide read but not admin authority, so this hits export automation that authenticates with the license credential alone.

Portal sessions in admin, owner or policy_admin roles are unaffected - the portal stamps a trusted admin-authority assertion for exactly those roles. Portal sessions in viewer or developer roles now receive 403 on export POSTs.

The remedy is an admin per-user token, sent to the AGENT

The admin-authority assertion the orchestrator honors is stamped by the agent from a validated per-user token and is stripped from inbound client requests at the agent boundary - you cannot set it directly. Restore export automation by minting an admin (or owner) role per-user token and sending it as the X-User-Token header to the agent, alongside your existing HTTP Basic license credential. A token sent straight to the orchestrator does not elevate.

Mint the token. The install bundle ships mint-admin-token.sh, which signs a short-lived HS256 user_token with role=admin locally over your AXONFLOW_JWT_SECRET - it does not call the platform and does not need the stack up. It fails closed if the secret is unset (an empty key signs forgeable tokens). Read its header comment for the full claim shape; the common calls:

# From the axonflow-install bundle directory. Org and JWT secret are read
# from ./.env; default TTL is 24h; default role is admin.
./mint-admin-token.sh

# Explicit org and a 1-hour owner token:
./mint-admin-token.sh --org acme --role owner --ttl-hours 1

# Capture the token for scripting (human output and the sample curl go to
# stderr, so this captures only the token):
TOKEN="$(./mint-admin-token.sh)"

Only --role admin and --role owner are accepted; the script refuses any other role because only those restore tenant-wide authority.

Wire it into your export automation. Add the minted token as X-User-Token on the request to the agent, keeping your existing license credential:

curl -sS -u "$AXONFLOW_ORG_ID:$AXONFLOW_LICENSE_KEY" \
-H "X-User-Token: $TOKEN" \
-X POST "$AXONFLOW_AGENT_URL/api/v1/ojk/audit/export" \
-H 'Content-Type: application/json' \
-d '{ ... }'

Mint on a schedule that stays inside the token TTL (default 24h; set --ttl-hours to match your automation cadence, minimum 1). Sign with the same AXONFLOW_JWT_SECRET the agent uses.

If your export automation instead drives the portal (a browser or portal-session flow rather than a direct API call), move it to an admin, owner or policy_admin portal role - a viewer or developer session now receives 403 on export actions. Portal org-login sessions in an admin role need no token change.

2. Four additive migrations run at boot

Parallel to how the v9.13 guide names core/155 and core/156: here is what runs on the v9.14 pull. All four are additive and mutate no existing rows, so there is no maintenance-window concern and no data to capture beforehand.

MigrationApplies toWhat it does
core/157every deployment modeAdds nullable static_policies.segment_id. NULL on every existing row; no behavior change until a segment-scoped policy is authored.
enterprise/136enterprise migration setsNew compliance_report_jobs table (org- and tenant-keyed, RLS enabled and forced, CHECK-constrained completion states).
enterprise/137enterprise migration setsNew indonesia_pii_detection_events table (RLS on org_id, masked values only).
enterprise/138enterprise migration setsAdds storage columns to euaiact_exports, fixing a GET /api/v1/euaiact/export 500 that hit every non-travel deployment. Its down migration deliberately retains the columns.

in-vpc-enterprise runs all four. There is no ACCESS EXCLUSIVE scan window here as there was for core/156 on v9.13 - these add a nullable column and three new tables, which do not scan existing data.

3. OJK identity resolution is org-first; a blank org header is refused

OJK routes now resolve their scoping organization from X-Org-ID first, falling back to X-Tenant-ID only when the org header is absent, and the value is trimmed. A blank or whitespace-only org identity is now refused with 400 missing_org instead of reaching the repositories as an empty scope. The previous resolver read the tenant header first into org-labelled columns. (#3250)

What to do. Ensure every OJK caller sends a valid, non-blank org or tenant header. Callers going through the AxonFlow Agent or the Customer Portal already do - both stamp those headers from the validated credential - so this bites only a caller that reaches OJK routes with an empty or whitespace org value. On v9 licenses where the license organization and the tenant string legitimately differ, sending the correct X-Org-ID also matters for correctness: the tenancy predicate on audit_logs-backed sections now distinguishes org from tenant rather than treating either as interchangeable.

Distinct org and tenant values

If your deployment holds ojk_breach_notifications rows and sends distinct org and tenant values, read docs/compliance/ojk-org-scope-upgrade.md in the platform tree before upgrading - the tenancy predicate on those rows changed shape.

4. OJK readiness is now MEASURED, not asserted

Set expectations here so a lower number after the upgrade is not read as a regression. Before v9.14, four of the five OJK readiness checks returned unconditional pass literals and the dashboard carried hardcoded counts - a deployment with no reachable database could still score 80 or better. Every check now queries the state it names, or reports unknown, which scores zero and stays in the denominator. (#3250)

The consequence: OJK readiness scores on real deployments will move, typically down, on upgrade. That is the score becoming honest, not the platform regressing. The OJK module also now appears in the /health components map, so /health gains an OJK entry it did not carry before.

What you need to do - by consumer type

Compliance export automation (the one that matters here)

If any automation calls a whole-tenant compliance or audit export endpoint (OJK, SEBI, EU AI Act, evidence, media-governance, or compliance-report generate/download) using a license key or internal-service credential, it will 403 on v9.14. Mint an admin per-user token with mint-admin-token.sh and send it as X-User-Token to the agent alongside the license credential - see section 1. This is the single most likely break on an existing self-hosted integration.

OJK consumers

Confirm every OJK caller sends a non-blank X-Org-ID (or X-Tenant-ID) - a blank org identity is now 400 (section 3) - and expect the OJK readiness score to drop to its real value (section 4). Agent- and portal-proxied callers already send the header.

SDK / plugin / decision / MCP / policy enforcement consumers

Nothing, with one edge condition below. Enforcement callers on /api/v1/decide, /api/policy/pre-check, the MCP evaluate plane, and the v9.13-era tenancy-header requirements are unchanged by v9.14. The compliance and export gates above do not touch the enforcement path.

Only if a caller uses the governed /api/request plane

On enterprise mode, the segment gate (#3057, ADR-060) now participates in policy selection on /api/request. A genuine segment-resolution error fails the request closed, and a governed /api/request call whose token carries no email claim currently fail-closes to 403. Most integrations use /api/v1/decide, /api/policy/pre-check or the MCP evaluate plane - not /api/request - and are entirely unaffected. If any caller does use /api/request, mint its token with an email claim (for example --kind user --email you@your-org --org-id your-org) so it resolves an identity. The empty-email case is loosened to proceed org-only in v9.14.2; until then, carry the email claim.

SEBI export consumers only

POST /api/v1/sebi/audit/export?format=xml now returns 501 XML_NOT_IMPLEMENTED (it previously returned a JSON body mislabeled under an XML content type, so nothing could have consumed it as real XML), and format=csv now returns genuine CSV rather than JSON under a text/csv header. Audit any SEBI export automation for a hardcoded format=xml or a JSON parse of the csv response before upgrading. OJK-only deployments are unaffected. (#3248)

Self-hosted operators (summary)

  1. If on v9.12.x, complete the v9.13 upgrade guide in full first.
  2. Take a database snapshot.
  3. Give export automation an admin per-user token via X-User-Token (section 1).
  4. Confirm OJK callers send a non-blank org header, and expect the OJK readiness score to drop to its measured value.
  5. Pull the image. The four migrations apply additively at boot; no window sizing is needed.

Getting help

If something behaves differently after upgrading, the most useful information for support:

  1. The exact endpoint and HTTP status code. A 403 on an export endpoint is almost always the admin-authority gate - say whether the caller was a license credential or a portal session, and which role.
  2. Your DEPLOYMENT_MODE, quoted, on both the agent and the orchestrator.
  3. For an OJK 400, the org and tenant header values the caller sent (redact as needed).

[email protected] - please mention "v9.14 upgrade" in the subject so it routes correctly.

See also