Repository navigation
v1.8.0
Added
-
The plugin now declares what it can enforce (ADR-065 capability handshake; getaxonflow/axonflow-enterprise#3763). Set
AXONFLOW_PEP_AUDIENCEto the audience your AxonFlow decision proofs are bound to, and every governed call carriesX-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_AUDIENCEunset 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.shafter changingAXONFLOW_PEP_AUDIENCE. The declaration is written into~/.codex/config.tomlat 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_AUDIENCEbuilds 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_versionandplatform_versionsay which client and which build, anddeployment_modesays which topology, but nothing said whether the platform behind it was running Community, Evaluation, Professional, Enterprise or Plus. The receiver has acceptedlicense_tiersince the v8 train (omitempty, normalised server-side) — only the clients never populated it. -
Read from the
/healthprobe that already runs, so no new request is made.scripts/telemetry-ping.shalready fetched/healthonce per heartbeat to detectplatform_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 oneGET /healthper 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
communitya community-mode build defaults to, and the transientstartingan agent reports before initialisation completes, are both real answers and are relayed as such. -
Three dimensions that sound alike are kept apart.
license_tieris what the platform says its licence is;deployment_modeis where this plugin is pointed, classified locally from the endpoint host;endpoint_typeis that endpoint's network reachability. Aself_hostedendpoint is routinely Enterprise-licensed andcommunity_saasis 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
tierthat 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", notnull, 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
/healthprobe now usescurl --fail, so a non-2xx response contributes neitherlicense_tiernorplatform_versioninstead of having its error body parsed for one. This matches the openclaw plugin's existingif (!resp.ok) return null. - An endpoint-supplied
tierlonger 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 realtelemetry-ping.sh), its mutation gate (eight planted defects that must each turn the matrix red, one of which restores theplatform_versionJSON 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 intests/heartbeat-real-stack/so the pre-existing hook-entry-point E2E covers the field too.
Fixed
- A
/healthresponse whoseversioncontained a double quote or backslash silently suppressed the entire heartbeat. Pre-existing, found while building the fail-open matrix for the change above.platform_versionwas spliced into the payload by hand-building a JSON string around the value and passing it throughjq --argjson; an embedded quote made that argument invalid JSON,jq -nfailed,PAYLOADcame back empty, and the script hit its[ -z "$PAYLOAD" ] && exit 0guard 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--argand turned into null-or-string inside the jq filter, so jq owns the escaping. The wire shape is unchanged: JSONnullwhen unknown, a JSON string otherwise. Pinned by three matrix cases (embedded quote, embedded backslash, and the unknown case still serialising as JSONnullunder a present key) and by a mutation-gate mutant that restores the splice and must turn the matrix red.
Changed
- Telemetry disclosure in
README.mdnow 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 staledeployment_modevalues (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.