Skip to content

v1.8.0

Choose a tag to compare

@saurabhjain1592 saurabhjain1592 released this 06 Sep 23:56
f0d0e51

Added

  • The plugin now declares what it can enforce (ADR-065 capability handshake; getaxonflow/axonflow-enterprise#3763). Set AXONFLOW_PEP_AUDIENCE to the audience your AxonFlow decision proofs are bound to, and every governed call carries X-Axonflow-PEP-Handshake: a short document naming this enforcement point and the obligation types it can discharge. A platform running v10.4.0 or later that would attach a mandatory obligation this plugin has declared it cannot carry out refuses the request instead of handing the content over and assuming the plugin will cope.

  • Off by default, and absent rather than empty when off. Leave AXONFLOW_PEP_AUDIENCE unset and no header is sent at all, so the plugin behaves byte for byte as 1.7.0 against every platform version. The distinction is load bearing: a header present with an empty value is malformed to the platform and would refuse the call, which an absent header does not. The per-call hooks append the flag only when non-empty, and the MCP installer does not write the key at all when there is no declaration.

  • Re-run scripts/install-mcp-with-headers.sh after changing AXONFLOW_PEP_AUDIENCE. The declaration is written into ~/.codex/config.toml at install time, because it is derived from the audience rather than being the audience itself and nothing in the shell would compute it for the long-lived MCP process.

  • The plugin declares no capabilities, and that is the honest answer. A redaction obligation is discharged by substituting the platform's engine-masked text for the original, and AxonFlow does not permit a client to redact for itself. This plugin's hooks submit a statement and act on the verdict; they perform no substitution, so the plugin cannot establish that such an obligation would be discharged and does not claim it can.

  • A malformed AXONFLOW_PEP_AUDIENCE builds no handshake and says so on stderr, rather than silently disabling itself and leaving an operator believing a control is in force.

  • The usage heartbeat now reports the licence tier the platform states about itself, as license_tier (axonflow-enterprise#3619). Telemetry could not previously attribute a ping to an edition or licence state: sdk, sdk_version and platform_version say which client and which build, and deployment_mode says which topology, but nothing said whether the platform behind it was running Community, Evaluation, Professional, Enterprise or Plus. The receiver has accepted license_tier since the v8 train (omitempty, normalised server-side) — only the clients never populated it.

  • Read from the /health probe that already runs, so no new request is made. scripts/telemetry-ping.sh already fetched /health once per heartbeat to detect platform_version; the response body is now captured once and both fields are read from it. The probe is unchanged in count, timeout and headers, and a test pins it at exactly one GET /health per heartbeat.

  • Relayed verbatim, never interpreted. The plugin does not normalise, case-fold, or map the value. The receiver owns the canonical mapping, so a tier issued after this plugin shipped still buckets correctly server-side instead of being flattened by a client that predates it. The lowercase community a community-mode build defaults to, and the transient starting an agent reports before initialisation completes, are both real answers and are relayed as such.

  • Three dimensions that sound alike are kept apart. license_tier is what the platform says its licence is; deployment_mode is where this plugin is pointed, classified locally from the endpoint host; endpoint_type is that endpoint's network reachability. A self_hosted endpoint is routinely Enterprise-licensed and community_saas is a hosting topology rather than the Community tier, so none of the three may be derived from another. A test pins all three to different values in one payload.

Security

  • Fails open in every direction, and the field is omitted rather than guessed. An unreachable endpoint, a 4xx or 5xx, a malformed or non-object body, an answer slower than the 2s probe budget, or a tier that is absent, blank or not a string all leave the heartbeat delivered, the exit code 0, both streams silent, and the key absent — not "unknown", not null, not "". Omission is the wire's existing "this client did not report" signal; sending "unknown" would instead assert that the platform answered and said it did not know.
  • The /health probe now uses curl --fail, so a non-2xx response contributes neither license_tier nor platform_version instead of having its error body parsed for one. This matches the openclaw plugin's existing if (!resp.ok) return null.
  • An endpoint-supplied tier longer than 64 characters is dropped whole rather than truncated — the longest canonical value is 14 characters, and a truncated value would be a tier the platform never reported.
  • Tests: tests/telemetry-license-tier/ (30 assertions over the round-trip and fail-open matrix, driving the real telemetry-ping.sh), its mutation gate (eight planted defects that must each turn the matrix red, one of which restores the platform_version JSON splice and so pins the silent-heartbeat bug fixed below against returning, plus two behaviour-preserving controls that must survive), runtime-e2e/license_tier_telemetry/ (adds a verbatim round-trip against a live agent's own reported tier), and a new leg in tests/heartbeat-real-stack/ so the pre-existing hook-entry-point E2E covers the field too.

Fixed

  • A /health response whose version contained a double quote or backslash silently suppressed the entire heartbeat. Pre-existing, found while building the fail-open matrix for the change above. platform_version was spliced into the payload by hand-building a JSON string around the value and passing it through jq --argjson; an embedded quote made that argument invalid JSON, jq -n failed, PAYLOAD came back empty, and the script hit its [ -z "$PAYLOAD" ] && exit 0 guard having sent no ping at all, with exit code 0 and no diagnostic. A probe that answered was more dangerous than one that did not — the exact inverse of the property this heartbeat is supposed to have. It is now passed as a plain --arg and turned into null-or-string inside the jq filter, so jq owns the escaping. The wire shape is unchanged: JSON null when unknown, a JSON string otherwise. Pinned by three matrix cases (embedded quote, embedded backslash, and the unknown case still serialising as JSON null under a present key) and by a mutation-gate mutant that restores the splice and must turn the matrix red.

Changed

  • Telemetry disclosure in README.md now names the licence tier and states explicitly that no licence key, expiry, seat count, or customer name is read or sent. The same section's stale deployment_mode values (self-hosted production / self-hosted development, retired in the v1 telemetry schema) were corrected to the values actually sent, and the source-tree comment claiming the heartbeat "fires once per install" now states the real 7-day cadence.