Skip to content

Commit 97fcafc

Browse files
feat: default to Community SaaS + 7-day heartbeat + mode-clarity gate (#27)
* feat: default to Community SaaS + 7-day heartbeat + mode-clarity gate ADR-048 Gate 4 for the Codex plugin. Mirrors the claude-plugin shape so the contract stays consistent across all four hook-based plugins. Endpoint resolution: try.getaxonflow.com when neither AXONFLOW_ENDPOINT nor AXONFLOW_AUTH is set; user values honoured untouched otherwise. Bootstrap (scripts/community-saas-bootstrap.sh, NEW): - POST /api/v1/register on first run, persist {tenant_id, secret, expires_at, endpoint} to ~/.config/axonflow/try-registration.json (mode 0600 inside 0700 dir) - Refuses to load file with non-0600 perms (credential-leak defense) - In-flight gate via flock; 429 → 1-hour backoff stamp - Loads AXONFLOW_AUTH = base64(tenant_id:secret) for the hook lifecycle Telemetry heartbeat — 7-day cadence with full design rules (per feedback_telemetry_heartbeat_design_rules.md): stamp-on-delivery, in-flight gate, opt-out FIRST, mtime-as-truth, atomic write, persistent instance_id, defensive against future-dated stamps, cross-platform stat. Mode-clarity gate (tests/mode-clarity.sh, REQUIRED CI): Sandboxed-HOME run of pre-tool-check.sh × 5 input combinations of AXONFLOW_ENDPOINT × AXONFLOW_AUTH. Asserts: - Exactly 1 canary on stderr - Stdout silent of [AxonFlow] markers (hook-protocol cleanliness) - URL parses to expected scheme + host + port (anti-spoof) - mode token matches Wired into install-smoke.yml. 5/5 passing locally. One-time setup disclosure (Community-SaaS only, separate stamp at ~/.cache/axonflow/codex-plugin-disclosure-shown). Version bumped 0.4.2 → 0.5.0. * fix(telemetry): correctly classify Community-SaaS sessions Bootstrap exports AXONFLOW_AUTH but intentionally leaves AXONFLOW_ENDPOINT unset. The previous resolution rule fell into the localhost branch when invoked as a backgrounded subprocess of pre-tool-check.sh (AUTH was inherited), so /health probes targeted localhost, platform_version shipped as null, and DEPLOYMENT_MODE was tagged "production". Honour AXONFLOW_MODE first so the heartbeat ships the canonical try.getaxonflow.com endpoint and a dedicated deployment_mode=community-saas classification. * fix(telemetry): GNU stat -f %m doesn't fail on Linux — guard with numeric check On Linux GNU stat, `-f %m FILE` doesn't fail — it treats `%m` as a filesystem path and prints garbage with exit 0, poisoning the freshness check (non-numeric → is_fresh false → heartbeat fires every invocation). Probe GNU first, validate numeric, fall back to BSD. * fix: restore Codex hook protocol clobbered by ADR-048 templating The ADR-048 community-saas-default work earlier in this branch wholesale- copied claude-plugin's pre-tool-check.sh and post-tool-audit.sh over codex's, replacing Codex-specific behaviour with claude-specific behaviour (exit 2 → permissionDecision JSON, codex.* → claude_code.*, plus the same afterShellExecution and PII-on-write losses as cursor). Restore from main and re-overlay only the ADR-048 changes: endpoint resolution honouring Community-SaaS default, mode-clarity canary on stderr, sourcing community-saas-bootstrap.sh, one-time disclosure stamp, and the backgrounded telemetry-ping subprocess. * fix: bump marketplace.json to 0.5.0 to match plugin.json The earlier ADR-048 commit on this branch bumped .codex-plugin/plugin.json to 0.5.0 but missed marketplace.json, which still pinned 0.4.2. The "Versions match" regression test catches the drift; CI was failing because marketplace.json's metadata.version and per-plugin version diverged from plugin.json. * fix: macOS bootstrap+telemetry, dir-mode tightening, README/CHANGELOG hygiene Same set of self-review fixes applied to the codex plugin: - flock(1) is Linux-only; macOS users got no Community-SaaS bootstrap and no telemetry heartbeat. Fall back to a mkdir-based atomic lock with stale-lock reclamation when flock is unavailable. - mcp-auth-headers.sh now emits the mode-clarity canary and one-time disclosure when MCP starts before any tool runs. - load_registration_into_env's "unsafe permissions" warning is no longer silenced at the call site. - mkdir -p -m 0700 followed by explicit chmod 0700 — the mode flag only sets perms on directories it creates, not on existing ones. - README telemetry block updated to describe the 7-day heartbeat. - mcp-auth-headers.sh + telemetry-ping.sh + community-saas-bootstrap.sh + tests/mode-clarity.sh comments scrubbed of claude-isms. - bootstrap correctly classifies Community-SaaS sessions in deployment_mode. - CHANGELOG: drop `### CI / development` block (internal); add Security section for the dir-mode tightening. Local test run: 51 passed / 0 failed on macOS. * test: real-stack E2E harness + cross-platform CI New tests/heartbeat-real-stack/ harness drives the plugin through its actual public hook entry point against a localhost Python http.server emulating /api/v1/register, /v1/ping, /health, and /api/v1/mcp-server. Cold-start + warm-cache regression: 12/12 assertions green on macOS. New AXONFLOW_HARNESS=1 + AXONFLOW_HARNESS_REGISTER_URL + AXONFLOW_HARNESS_AGENT_ENDPOINT env hooks. Production paths leave AXONFLOW_HARNESS unset and URLs stay pinned to try.getaxonflow.com. Renamed bootstrap's `ENDPOINT` to `BOOTSTRAP_ENDPOINT` so sourcing into pre-tool-check.sh doesn't clobber the parent's resolved agent endpoint. New PR-blocking workflow at .github/workflows/heartbeat-real-stack.yml covers ubuntu-latest + macos-latest. * fix(real-stack): GNU/BSD portability + slower server boot timeout CI matrix surfaced two more BSD/GNU portability bugs: 1. `stat -f %Lp` (BSD permission octal) doesn't fail cleanly on Linux GNU stat — it interprets `-f` as `--file-system` and emits filesystem-stat lines, exit 0. The bare `||` chain in load_registration_into_env's mode-check + the harness's stat-mode helpers broke the same way the mtime path did. Probe GNU first and validate numeric/octal output, fall back to BSD. 2. `date -j -f "<fmt>" <input> "+%s"` (BSD) doesn't fail cleanly on Linux GNU date either. registration_is_fresh's date parsing silently fell through to `echo 0` even though GNU `date -d` would have parsed the RFC3339 string fine — so warm-cache always re-registered. Same fix pattern: GNU first, validate numeric. 3. macOS GH runners cold-start Python http.server slower than 10s (saw 9-15s in CI logs). Bumped the readiness timeout to 30s. Local re-run on macOS: 12/12 green for all 3 bash plugins. CI matrix should now go ubuntu + macos green. * fix(real-stack): file-based readiness sentinel + fsync placement bug server.py: write a file-based readiness sentinel at <work>/_server_ready in addition to the stdout "server ready" line. Python stdout was being block-buffered when redirected to a file on macOS GH runners even with flush=True. The harness now waits on the sentinel — file existence is the authoritative readiness signal, stdout is informational. Caught a subtle bug along the way: the fsync call was placed AFTER the `with open(...)` block exited (the conditional ternary form `os.fsync(...) if ... else None` is a single expression evaluated AFTER the with-block closes the file), so server.py crashed with `ValueError: I/O operation on closed file` before binding the listening socket. Moved the fsync inside the with-block. All 4 plugins now 12/12 green locally on macOS. * chore(real-stack): macOS CI diagnostic + early Python heartbeat Real-stack E2E was timing out on macos-latest with empty server stdout even after the fsync fix. Added diagnostic logging in run_real_stack.sh (which python3 + version, which curl + version, ls of server.py) and an early "server starting" / "server modules imported" stdout flush in server.py so even an import-time crash surfaces in the harness output instead of leaving us with a 30s silent timeout. Plus PYTHONUNBUFFERED=1 on the python3 invocation as defence-in-depth (the file-based readiness sentinel is still the authoritative signal). * ci: re-trigger workflows via comment * fix(real-stack): subclass HTTPServer to skip getfqdn() on macOS CI revealed Python's HTTPServer constructor hangs for 30+ seconds on macos-15-arm64 GH runners during server_bind() — specifically the socket.getfqdn() call triggers a mDNS query that never resolves. We don't use the FQDN for anything (handler.log_message is overridden to no-op), so subclass HTTPServer to skip it: server_name = host instead of socket.getfqdn(host). This was the root cause of the macos-latest "server did not start within 30s" timeout that's been blocking the heartbeat-real-stack matrix. With this fix the server starts in <1s on macOS just like Linux.
1 parent 9a3a2b2 commit 97fcafc

14 files changed

Lines changed: 1215 additions & 71 deletions

.codex-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
},
88
"metadata": {
99
"description": "Runtime governance for OpenAI Codex. Policy enforcement on terminal commands, advisory governance via skills, PII detection, audit trails, and compliance-grade decision records.",
10-
"version": "0.4.2"
10+
"version": "0.5.0"
1111
},
1212
"plugins": [
1313
{
1414
"name": "axonflow",
1515
"source": "./",
1616
"description": "Policy enforcement, PII detection, and audit trails for OpenAI Codex. Hybrid governance — enforces policies on terminal commands (exec_command) via hooks, provides advisory governance for other tools via implicit-activation skills, and records compliance-grade audit trails. Self-hosted via Docker — all data stays on your infrastructure.",
17-
"version": "0.4.2",
17+
"version": "0.5.0",
1818
"author": {
1919
"name": "AxonFlow",
2020
"email": "hello@getaxonflow.com",

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "axonflow",
33
"displayName": "AxonFlow Governance",
44
"description": "Policy enforcement, PII detection, and audit trails for OpenAI Codex. Hybrid governance — enforces policies on terminal commands (exec_command) via hooks, provides advisory governance for other tools via implicit-activation skills, and records compliance-grade audit trails. Self-hosted via Docker — all data stays on your infrastructure.",
5-
"version": "0.4.2",
5+
"version": "0.5.0",
66
"author": {
77
"name": "AxonFlow",
88
"email": "hello@getaxonflow.com",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Heartbeat Real-Stack E2E
2+
3+
# Runs the localhost-fake-stack harness across Linux + macOS to verify the
4+
# plugin's Community-SaaS bootstrap and 7-day telemetry heartbeat work
5+
# end-to-end against a real socket without ever touching production.
6+
# Cold-start asserts: registration POST → 0600 file written → heartbeat
7+
# delivered → 0600 stamp written. Warm-cache asserts: no fresh
8+
# registration, telemetry suppressed by the stamp gate.
9+
#
10+
# Bash hooks don't run natively on Windows (the plugin's hook scripts
11+
# require bash; Claude Code on Windows runs through WSL, which is the
12+
# Linux runner). No `windows-latest` here.
13+
14+
on:
15+
pull_request:
16+
paths:
17+
- "scripts/**"
18+
- "tests/heartbeat-real-stack/**"
19+
- ".github/workflows/heartbeat-real-stack.yml"
20+
push:
21+
branches: [main]
22+
paths:
23+
- "scripts/**"
24+
- "tests/heartbeat-real-stack/**"
25+
- ".github/workflows/heartbeat-real-stack.yml"
26+
27+
jobs:
28+
real-stack:
29+
name: Real-stack E2E (${{ matrix.os }})
30+
runs-on: ${{ matrix.os }}
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
os: [ubuntu-latest, macos-latest]
35+
env:
36+
AXONFLOW_TELEMETRY: "off"
37+
steps:
38+
- uses: actions/checkout@v6
39+
- name: Set up Python
40+
uses: actions/setup-python@v6
41+
with:
42+
python-version: "3.12"
43+
- name: Install jq (Linux)
44+
if: runner.os == 'Linux'
45+
run: sudo apt-get update && sudo apt-get install -y jq
46+
- name: Install jq (macOS)
47+
if: runner.os == 'macOS'
48+
run: brew install jq
49+
- name: Run real-stack harness
50+
run: bash tests/heartbeat-real-stack/run_real_stack.sh

.github/workflows/install-smoke.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,12 @@ jobs:
4545
run: |
4646
chmod +x tests/install-smoke/*.sh tests/install-smoke/*.py scripts/*.sh
4747
bash tests/install-smoke/run.sh
48+
49+
- name: Mode-clarity gate (ADR-048)
50+
# Required gate: asserts the canary log line on stderr matches the
51+
# resolved endpoint exactly across all 5 input combinations of
52+
# AXONFLOW_ENDPOINT × AXONFLOW_AUTH. Defends against
53+
# "user thinks they're on localhost, data goes to SaaS" silently.
54+
run: |
55+
chmod +x tests/mode-clarity.sh
56+
bash tests/mode-clarity.sh

CHANGELOG.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
7+
- **Plugin defaults to AxonFlow Community SaaS on first run** when neither `AXONFLOW_ENDPOINT` nor `AXONFLOW_AUTH` is set. Existing self-hosted installs (anyone who has set either env var) are honoured untouched — no silent endpoint override. Hooks emit a one-line canary on stderr every invocation: `[AxonFlow] Connected to AxonFlow at <URL> (mode=community-saas|self-hosted)`. A required CI gate parses this canary and asserts it matches the actual outbound destination so users can never be misled about which AxonFlow they're connected to.
8+
- **Anonymous telemetry switched to a 7-day heartbeat** with stamp-on-delivery semantics. Previously: a single stamp at install time, then silence forever. Now: at most one ping per machine per 7 days, gated by an in-flight `flock` so concurrent hook invocations don't stampede, and the stamp file mtime advances ONLY after the HTTP POST returns 2xx. A transient network failure no longer silences telemetry for 7 days.
9+
510
### Added
611

7-
- **Plugin/platform version compatibility check.** A new `scripts/version-check.sh` runs once per install (stamp-file guard) on the first hook invocation. It queries the AxonFlow agent's `/health` endpoint and reads the new `plugin_compatibility.min_plugin_version["codex"]` field (advertised by platform v7.5.0+, axonflow-enterprise#1764). If the plugin's runtime version is below the floor the platform expects, the script logs a single upgrade hint to stderr; below recommended-but-above-min logs an info-level note; at or above recommended is silent. Failure modes (older platform without the field, network error, malformed response, missing dependencies) are swallowed and never block hook execution. Skippable via `AXONFLOW_PLUGIN_VERSION_CHECK=off`. Mirrors the SDK-side downgrade-warning gate that has run since v4.8.0.
12+
- **First-run Community-SaaS bootstrap** (`scripts/community-saas-bootstrap.sh`) registers against `try.getaxonflow.com/api/v1/register` and persists `{tenant_id, secret, expires_at, endpoint}` to `~/.config/axonflow/try-registration.json` (mode 0600 inside a 0700 directory). The Basic-auth credential is loaded into `AXONFLOW_AUTH` for the rest of the hook lifecycle. Refuses to load a registration file with non-0600 permissions to prevent silent credential leak via a world-readable file. Honours HTTP 429 with a 1-hour backoff stamp.
13+
- **One-time setup disclosure** on first Community-SaaS connection: a positive, journey-based message recommending self-host for real workflows. Stamped at `~/.cache/axonflow/codex-plugin-disclosure-shown` so it fires exactly once per install.
14+
- **Mode-clarity gate** (`tests/mode-clarity.sh`) covers all 5 input combinations of `AXONFLOW_ENDPOINT` × `AXONFLOW_AUTH`. Anti-spoof: parses the URL and compares scheme + host + port (a lookalike domain like `try.getaxonflow.com.attacker.com` cannot pass a substring check).
15+
- **Plugin/platform version compatibility check.** A new `scripts/version-check.sh` runs once per install (stamp-file guard) on the first hook invocation. It queries the AxonFlow agent's `/health` endpoint and reads the new `plugin_compatibility.min_plugin_version["codex"]` field. If the plugin's runtime version is below the floor the platform expects, the script logs a single upgrade hint to stderr; below recommended-but-above-min logs an info-level note; at or above recommended is silent. Fire-and-forget; failures never block hook execution. Skippable via `AXONFLOW_PLUGIN_VERSION_CHECK=off`. Mirrors the SDK-side downgrade-warning gate that has run since v4.8.0.
816

917
### Removed
1018

@@ -14,11 +22,12 @@
1422

1523
- Telemetry ping now fires once per install on Codex. The previous behavior silently exited at the `DO_NOT_TRACK=1` check (injected by Codex), so the install ping never reached the stamp-file guard.
1624
- The deprecation warning no longer leaks to stderr on every `PreToolUse` hook invocation, eliminating the noise that was getting concatenated with `AxonFlow policy violation` messages on blocked commands.
25+
- Telemetry heartbeat now correctly classifies Community-SaaS sessions. The previous endpoint resolution fell into the localhost branch after the Community-SaaS bootstrap exported `AXONFLOW_AUTH` but intentionally left `AXONFLOW_ENDPOINT` unset; `/health` probes targeted localhost, `platform_version` shipped as `null`, and `deployment_mode` was tagged `production`. Resolution now keys off `AXONFLOW_MODE` first, so Community-SaaS users see the canonical endpoint and a dedicated `deployment_mode=community-saas` value.
26+
- Community-SaaS bootstrap and telemetry heartbeat now run on macOS. Both scripts gated their in-flight lock on `flock(1)`, which ships in stock Linux but not in stock macOS; a missing `flock` short-circuited the entire bootstrap, leaving Community-SaaS users unauthenticated, and silenced the telemetry heartbeat. Both paths now fall back to a `mkdir`-based atomic lock with stale-lock reclamation when `flock` is unavailable.
1727

18-
### CI / development
19-
20-
- Test harness (`tests/test-hooks.sh`) and CI workflows (`test.yml`, `install-smoke.yml`, `smoke-e2e.yml`) now use `AXONFLOW_TELEMETRY=off` to suppress telemetry during automated runs.
28+
### Security
2129

30+
- `~/.config/axonflow/` and `~/.cache/axonflow/` now have their permissions restored to `0700` on every invocation (was: only set on creation via `mkdir -m 0700`). A user who already had `~/.config/` at the conventional `0755` would otherwise hold the `0600` registration credential file inside a traversable directory.
2231

2332
## [0.4.2] - 2026-04-22
2433

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ More troubleshooting in the [integration guide](https://docs.getaxonflow.com/doc
384384

385385
## Telemetry
386386

387-
Anonymous one-time ping on first hook invocation: plugin version, OS, architecture, bash version, AxonFlow platform version. **Never** tool arguments, message contents, or policy data.
387+
Anonymous heartbeat at most once every 7 days per machine: plugin version, OS, architecture, bash version, AxonFlow platform version, deployment mode (community-saas / self-hosted production / self-hosted development). **Never** tool arguments, message contents, or policy data. The stamp file mtime advances only after the HTTP POST returns 2xx, so a transient network failure does not silence telemetry until the next window.
388388

389389
Opt out: set `AXONFLOW_TELEMETRY=off` in the environment Codex runs in.
390390

0 commit comments

Comments
 (0)