AxonFlow v9.7.0 Release Notes
v9.7.0 is a minor release with one new capability and four enforcement-plane hardening fixes. Enterprise deployments gain per-client version-distribution telemetry — a Prometheus view of which client (plugin, proxy, SDK) versions are actually calling the platform. The fixes close real fail-open and data-exposure gaps: the response-leg redaction pipeline now reports whether it actually ran (redaction_evaluated on check-output), the request plane fails closed when the policy engine cannot load its policies, a blocked gateway pre-check no longer returns the connector data it just blocked, and the portal's per-session drill-down is now reachable end to end — filter, deep-link, and export.
There are no database migrations and no configuration changes, so it is a drop-in upgrade from v9.6.x.
Added
Per-client version-distribution telemetry (Enterprise)
Enterprise deployments now record which client versions are calling the governed planes. AxonFlow clients identify themselves with the standard X-Axonflow-Client: <client>/<version> header (for example claude-code-plugin/1.9.1, mcp-proxy/0.3.1, sdk-go/8.5.1); from v9.7.0 the agent captures the validated pair into a Prometheus counter:
axonflow_client_version_requests_total{plane, client, client_version}— requests by client id and version, on the decision plane (POST /api/v1/decide, counted per attempt) and the MCP check-output plane (counted per authenticated request).axonflow_client_version_dropped_total{reason}— values that were not recorded, by reason (absent/invalid/overflow).
This answers fleet questions like "is anyone still running the old proxy?" directly from Grafana, and complements the existing origin label on the Decision Mode metrics (which buckets clients into six fixed integrations and deliberately discards versions).
The capture is telemetry-only and fail-open: it never participates in authentication and can never affect a verdict. Cardinality is strictly bounded — raw header values never become label values (both parts must pass a shape allowlist), and a hard cap of 512 distinct series routes further new pairs to the overflow drop reason. In Community the metric is not registered at all, so its absence reads as "not measured" rather than "no clients".
The Claude Desktop governance proxy joins the reporting clients in its releases after v0.3.0, identifying itself as mcp-proxy/<version> on both of its governed calls — see the Claude Desktop integration page. Full label and validation reference: Metrics Reference.
Rust SDK and the Claude Desktop proxy join the /health compatibility matrix (Community)
/health on both ports now advertises minimum and recommended versions for the Rust SDK (recommended v0.8.1) and the Claude Desktop MCP governance proxy (recommended v0.3.1), alongside the SDK and plugin versions it already publishes. Both clients can now run the same upgrade-warning gate the other SDKs and plugins use, comparing their own version against the platform's advertised floor.
Fixed
check-output now emits the redaction_evaluated fail-closed signal (Enterprise)
The response-phase redaction endpoint POST /api/v1/mcp/check-output now returns the same redaction_evaluated boolean the request-phase check-input endpoint has carried since v8.6.0: true means the response-redaction pipeline actually ran (regardless of whether anything was masked), false or absent means it did not run — so the absence of redacted content cannot be trusted as "nothing to mask". Previously the response leg had no way to distinguish "scanned clean" from "never scanned", and a policy enforcement point fulfilling a response-phase redaction obligation had to rely on transport-level failure signals alone.
A PEP fulfilling a response-phase redact_pii obligation must fail closed when the field is false or absent on a v9.7.0+ engine. The Enterprise MCP server's check_output tool mirrors the same field, and the public API specification documents the contract. See Building a Policy Enforcement Point for the updated fail-closed rules.
Request plane now fails closed on a policy-load error (Community)
When the policy engine could not load its policies mid-flight (for example, a transient policy-store outage after the in-memory cache expired), the request plane previously logged the error and let the request through ungoverned under the default graceful-degradation posture. That silently disabled SQL-injection, dangerous-command, and PII blocking on every request-plane gate — the decision endpoint, the MCP check-input / resource / tool-execution paths, the gateway pre-check, and the OpenAI-compatible endpoint.
From v9.7.0 a policy-load error on the request plane always blocks, with the reason Policy engine unavailable — regardless of the graceful-degradation setting, because on the request plane there is no safe middle ground between proceeding ungoverned and blocking. The result also distinguishes "could not scan" from "scanned and found nothing", so an availability-failure block is labeled distinctly in the audit trail. This matches the response plane, which gained the equivalent fail-closed behavior in v9.4.0.
Gateway pre-check no longer returns connector data on a blocked request (Enterprise)
Gateway Mode's pre-check endpoint accepts an optional list of data sources and, when the request is approved, prefetches the governed connector data alongside the verdict. That prefetch was gated incorrectly: a blocked pre-check (or one held for human approval) still executed the caller's query against the live connector and returned the rows next to the deny — the response carried the very data the block was meant to withhold.
From v9.7.0 the connector prefetch runs only for a cleanly approved request: blocked and approval-pending pre-checks return no connector data, and approved-with-redaction requests continue to skip the prefetch as before.
Portal: session drill-down wired through the Log Explorer and export (Enterprise portal; export fix in every edition)
v9.6.1 fixed the audit-search API's session_id filter, but the surfaces around it were still half-wired. v9.7.0 completes the loop:
- Log Explorer session filter. The portal's Log Explorer gains a Session ID filter (exact match), a
?session_id=deep-link — which is what the session-summary view's bucket rows now target — and the Session ID shown in a record's detail panel is clickable, drilling the explorer into that session. Previously the drill-down existed only at the API level and was unreachable from the UI. - Export honors the session filter. The audit export endpoint now decodes and applies
session_id, so exporting a session-filtered view exports that session — not the tenant's whole time window, as before. The export also gains asession_idoutput column. - Export filter parity. The export endpoint additionally honors the
decision_id,policy_name, andoverride_idfilters the search endpoint already supported, so a filtered export always matches the filtered view.
All drill-down and export paths remain tenant-scoped; the fix only narrows results within your own tenant.
Retired unavailable Anthropic model defaults (Community)
A retired Claude model id that the Anthropic API now rejects was baked into the fallback model defaults across several surfaces — compose defaults, provider-config fallbacks, the Anthropic provider's model constants (which the unified router's failover uses), Bedrock adapter defaults, and example configs. Any provider failover through Anthropic (or the equivalent non-region-prefixed Bedrock id) could then fail with "all providers failed" when the primary provider was unavailable. The fallback ids are now centralized in a shared defaults package pinned to current catalog ids, with tests that keep the retired ids out of every config surface. This only affects the built-in fallback — deployments that set ANTHROPIC_MODEL / BEDROCK_MODEL explicitly were never impacted.
Companion SDK releases
An SDK release train ships alongside v9.7.0 — Go / Python / TypeScript v8.5.1 and Rust v0.8.1 (Java is unchanged at v8.5.1). These are fix releases; see each SDK's getting-started page for details:
- Go v8.5.1 — production (fail-open) mode no longer converts a definitive 4xx rejection, such as invalid credentials, into a synthetic success; fail-open now applies only to transport failures, 5xx, and 429. Debug response-body logging is gated behind the debug flag.
- Python v8.5.1 — the LLM interceptors now work when a sync client is driven from inside a running event loop (FastAPI handlers, notebooks), and async provider clients such as
AsyncOpenAIare correctly detected as async through decorator wrappers. - TypeScript v8.5.1 —
getPlanStatusnow sends authentication headers (it previously failed against auth-enforcing deployments), andqueryConnectoraccepts an optional user token for deployments that validate per-user identity. - Rust v0.8.1 —
execute_planno longer reports an empty status on success: the status iscompletedon genuine success andfailedon a blocked or failed plan.
Upgrading
v9.7.0 is a drop-in upgrade from v9.6.x. There are no migrations and no configuration changes.
# Community
docker compose pull && docker compose up -d
# Enterprise
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml pull
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml up -d
Behavioral note: if your deployment relied on the request plane's previous fail-open behavior during policy-store outages, be aware that those requests are now blocked with Policy engine unavailable until the policy store recovers. This is the intended posture — the previous behavior silently disabled enforcement.
In-VPC Enterprise (AWS CFN)
Redeploy with the v9.7.0 images. No migration runs on startup.
