Skip to content

Commit 9a3a2b2

Browse files
feat: warn when plugin version is below platform floor (#28)
Platform v7.5.0 advertises plugin_compatibility on /health (axonflow-enterprise#1764) symmetric to the long-running sdk_compatibility shape. New scripts/version-check.sh consumes the "codex" entry from min_plugin_version and recommended_plugin_version and emits a single log line when the plugin's runtime version is below the floor. Behavior matrix: - Below min: stderr warn, stamp written, exit 0 - Between min/recommended: stderr info, stamp written, exit 0 - At/above recommended: silent, stamp written, exit 0 - Older platform (no field): silent, stamp written, exit 0 - Missing per-plugin entry: silent, stamp written, exit 0 - Platform unreachable: silent, NO stamp (retry next time) - AXONFLOW_PLUGIN_VERSION_CHECK=off: silent, no stamp, exit 0 - Stamp already exists: silent, exit 0 (no curl) Wired fire-and-forget from scripts/pre-tool-check.sh, same shape as telemetry-ping.sh. The version check NEVER exits non-zero, never blocks the hook hot path, and never affects the policy decision. Tests in tests/test-version-check.sh pin all 8 behavioural cases above (18 individual asserts) via a Python stub HTTP server. Sister PRs filed for OpenClaw, Claude Code, and Cursor plugins implementing the same pattern.
1 parent 15a9a73 commit 9a3a2b2

4 files changed

Lines changed: 317 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
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.
8+
59
### Removed
610

711
- **BREAKING:** `DO_NOT_TRACK=1` is no longer honored as an AxonFlow telemetry opt-out in this plugin. Use `AXONFLOW_TELEMETRY=off` instead. The Codex CLI injects `DO_NOT_TRACK=1` into every hook subprocess regardless of user intent, so it cannot represent a real user choice in this context.

scripts/pre-tool-check.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ fi
3434
# Telemetry: fire-and-forget on first invocation (stamp file guard inside script)
3535
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
3636
"${SCRIPT_DIR}/telemetry-ping.sh" </dev/null &
37+
# Plugin/platform version compatibility check — fire-and-forget, runs once
38+
# per install, warns to stderr if the plugin is below the platform's
39+
# min_plugin_version (axonflow-enterprise#1764). Same fire-and-forget shape
40+
# as telemetry-ping; never blocks the hook hot path.
41+
"${SCRIPT_DIR}/version-check.sh" </dev/null &
3742

3843
# Read hook input from stdin
3944
INPUT=$(cat)

scripts/version-check.sh

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
# Plugin/platform version compatibility check — runs once per install.
3+
#
4+
# Queries the AxonFlow agent's /health endpoint (advertised since
5+
# platform v7.5.0 via axonflow-enterprise#1764) and compares the
6+
# plugin's runtime version to plugin_compatibility.min_plugin_version
7+
# ["codex"]. Logs a single warning to stderr if the plugin is below
8+
# the floor the platform expects. Mirrors the SDK pattern that has run
9+
# on every SDK client construction since v4.8.0.
10+
#
11+
# Stamp file at $HOME/.cache/axonflow/codex-plugin-version-check-stamp
12+
# prevents repeat warnings. Delete the stamp file to re-check on next
13+
# hook invocation. AXONFLOW_PLUGIN_VERSION_CHECK=off skips the check
14+
# entirely (separate from telemetry opt-out — this is not telemetry).
15+
#
16+
# This script NEVER exits non-zero — all errors are silently swallowed.
17+
# It NEVER blocks hook execution (called with & from pre-tool-check.sh).
18+
19+
# Dependencies — exit silently if missing
20+
command -v curl &>/dev/null || exit 0
21+
command -v jq &>/dev/null || exit 0
22+
23+
if [ "${AXONFLOW_PLUGIN_VERSION_CHECK:-}" = "off" ]; then
24+
exit 0
25+
fi
26+
27+
# Stamp file guard — only check once per install
28+
STAMP_DIR="${HOME}/.cache/axonflow"
29+
STAMP_FILE="${STAMP_DIR}/codex-plugin-version-check-stamp"
30+
31+
if [ -f "$STAMP_FILE" ]; then
32+
exit 0
33+
fi
34+
35+
mkdir -p "$STAMP_DIR" 2>/dev/null || exit 0
36+
37+
ENDPOINT="${AXONFLOW_ENDPOINT:-http://localhost:8080}"
38+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
39+
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
40+
PLUGIN_ID="codex"
41+
42+
PLUGIN_VERSION=$(jq -r '.version // empty' "$PLUGIN_DIR/.codex-plugin/plugin.json" 2>/dev/null)
43+
if [ -z "$PLUGIN_VERSION" ]; then
44+
# Can't determine plugin version — record the attempt and exit silently
45+
: > "$STAMP_FILE" 2>/dev/null || true
46+
exit 0
47+
fi
48+
49+
HEALTH_BODY=$(curl -s --max-time 2 "${ENDPOINT}/health" 2>/dev/null)
50+
if [ -z "$HEALTH_BODY" ]; then
51+
# Platform unreachable — don't stamp, retry next time
52+
exit 0
53+
fi
54+
55+
MIN_VERSION=$(echo "$HEALTH_BODY" \
56+
| jq -r --arg id "$PLUGIN_ID" '.plugin_compatibility.min_plugin_version[$id] // empty' 2>/dev/null)
57+
RECOMMENDED_VERSION=$(echo "$HEALTH_BODY" \
58+
| jq -r --arg id "$PLUGIN_ID" '.plugin_compatibility.recommended_plugin_version[$id] // empty' 2>/dev/null)
59+
60+
# Older platform doesn't advertise plugin_compatibility — stamp and exit
61+
# (no signal is not a regression; same posture SDKs use).
62+
if [ -z "$MIN_VERSION" ] && [ -z "$RECOMMENDED_VERSION" ]; then
63+
: > "$STAMP_FILE" 2>/dev/null || true
64+
exit 0
65+
fi
66+
67+
# compare_versions a b → echoes -1 / 0 / 1 for a<b / a==b / a>b
68+
# Strips leading 'v', drops pre-release/build segments, pads to 3 parts.
69+
compare_versions() {
70+
local a="${1#v}"
71+
local b="${2#v}"
72+
a="${a%%[-+]*}"
73+
b="${b%%[-+]*}"
74+
local IFS=.
75+
read -r -a aa <<<"$a"
76+
read -r -a bb <<<"$b"
77+
local i
78+
for i in 0 1 2; do
79+
local av="${aa[$i]:-0}"
80+
local bv="${bb[$i]:-0}"
81+
av=$((10#${av:-0}))
82+
bv=$((10#${bv:-0}))
83+
if [ "$av" -lt "$bv" ]; then echo -1; return; fi
84+
if [ "$av" -gt "$bv" ]; then echo 1; return; fi
85+
done
86+
echo 0
87+
}
88+
89+
if [ -n "$MIN_VERSION" ]; then
90+
cmp=$(compare_versions "$PLUGIN_VERSION" "$MIN_VERSION")
91+
if [ "$cmp" = "-1" ]; then
92+
echo "[axonflow] Plugin v${PLUGIN_VERSION} is below the platform's minimum supported version (v${MIN_VERSION}). Upgrade with \`codex plugin update axonflow\` — older releases may mis-handle newer platform contract fields." >&2
93+
: > "$STAMP_FILE" 2>/dev/null || true
94+
exit 0
95+
fi
96+
fi
97+
98+
if [ -n "$RECOMMENDED_VERSION" ]; then
99+
cmp=$(compare_versions "$PLUGIN_VERSION" "$RECOMMENDED_VERSION")
100+
if [ "$cmp" = "-1" ]; then
101+
echo "[axonflow] Plugin v${PLUGIN_VERSION} is below the recommended version (v${RECOMMENDED_VERSION}). Plugin will keep working; upgrade for the full feature set." >&2
102+
fi
103+
fi
104+
105+
: > "$STAMP_FILE" 2>/dev/null || true
106+
exit 0

tests/test-version-check.sh

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env bash
2+
# Regression tests for scripts/version-check.sh.
3+
#
4+
# Spawns a tiny stub HTTP server that returns a configurable /health body,
5+
# runs version-check.sh against it with a clean HOME (so the stamp-file
6+
# guard always lets the check fire), and asserts the script behaves
7+
# correctly across:
8+
#
9+
# - Below min: warn-line on stderr, stamp written, exit 0
10+
# - Between min and recommended: info-line on stderr, stamp written
11+
# - At or above recommended: silent, stamp written
12+
# - Older platform without plugin_compatibility: silent, stamp written
13+
# - Platform unreachable: silent, stamp NOT written (retry next time)
14+
# - Missing per-plugin entry: silent, stamp written
15+
# - AXONFLOW_PLUGIN_VERSION_CHECK=off: silent, stamp NOT written
16+
# - Stamp already exists: silent, exits early
17+
#
18+
# Run locally:
19+
# bash tests/test-version-check.sh
20+
21+
set -euo pipefail
22+
23+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
24+
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
25+
CHECK_SCRIPT="$PLUGIN_DIR/scripts/version-check.sh"
26+
PLUGIN_ID="codex"
27+
28+
if [ ! -x "$CHECK_SCRIPT" ]; then
29+
echo "FAIL: $CHECK_SCRIPT not executable"
30+
exit 1
31+
fi
32+
33+
# Spawn a stub HTTP server. /health responds with whatever JSON is
34+
# currently in $HEALTH_BODY_FILE; non-/health 404s.
35+
HEALTH_BODY_FILE="$(mktemp)"
36+
PYTHON="$(command -v python3 || command -v python)"
37+
if [ -z "$PYTHON" ]; then
38+
echo "skip: python not available — version-check tests need a stub server"
39+
exit 0
40+
fi
41+
42+
STUB_PORT=$((RANDOM + 30000))
43+
"$PYTHON" - "$STUB_PORT" "$HEALTH_BODY_FILE" >/dev/null 2>&1 &
44+
STUB_PID=$!
45+
trap 'kill "$STUB_PID" 2>/dev/null || true; rm -f "$HEALTH_BODY_FILE"' EXIT
46+
47+
# Inline stub
48+
"$PYTHON" -c "
49+
import http.server, socketserver, sys
50+
port = int(sys.argv[1])
51+
body_file = sys.argv[2]
52+
class H(http.server.BaseHTTPRequestHandler):
53+
def do_GET(self):
54+
if self.path != '/health':
55+
self.send_response(404); self.end_headers(); return
56+
try:
57+
with open(body_file) as f: body = f.read().strip()
58+
except OSError:
59+
body = ''
60+
if not body:
61+
self.send_response(503); self.end_headers(); return
62+
self.send_response(200)
63+
self.send_header('Content-Type', 'application/json')
64+
self.end_headers()
65+
self.wfile.write(body.encode())
66+
def log_message(self, *a, **k): pass
67+
with socketserver.TCPServer(('127.0.0.1', port), H) as srv:
68+
srv.serve_forever()
69+
" "$STUB_PORT" "$HEALTH_BODY_FILE" >/dev/null 2>&1 &
70+
STUB_PID=$!
71+
72+
# Wait up to 3s for the stub to bind
73+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
74+
curl -s --max-time 1 "http://127.0.0.1:${STUB_PORT}/health" >/dev/null 2>&1 && break
75+
sleep 0.25
76+
done
77+
78+
PASS_COUNT=0
79+
FAIL_COUNT=0
80+
ASSERT() {
81+
local label="$1"
82+
local expected_pattern="$2"
83+
local actual="$3"
84+
if echo "$actual" | grep -qE "$expected_pattern"; then
85+
echo " pass: $label"
86+
PASS_COUNT=$((PASS_COUNT + 1))
87+
else
88+
echo " FAIL: $label"
89+
echo " expected match for: $expected_pattern"
90+
echo " actual: $actual"
91+
FAIL_COUNT=$((FAIL_COUNT + 1))
92+
fi
93+
}
94+
95+
run_check() {
96+
local fake_home
97+
fake_home="$(mktemp -d)"
98+
local stderr_file
99+
stderr_file="$(mktemp)"
100+
HOME="$fake_home" \
101+
AXONFLOW_ENDPOINT="http://127.0.0.1:${STUB_PORT}" \
102+
"$CHECK_SCRIPT" 2>"$stderr_file"
103+
local rc=$?
104+
local stamp_exists="no"
105+
if [ -f "$fake_home/.cache/axonflow/codex-plugin-version-check-stamp" ]; then
106+
stamp_exists="yes"
107+
fi
108+
cat "$stderr_file"
109+
echo "EXIT=$rc"
110+
echo "STAMP=$stamp_exists"
111+
rm -rf "$fake_home" "$stderr_file"
112+
}
113+
114+
set_health_body() {
115+
printf '%s' "$1" > "$HEALTH_BODY_FILE"
116+
}
117+
118+
# ---- Test 1: below min → warn ----
119+
echo "Test 1: plugin below min → warn"
120+
set_health_body "{\"plugin_compatibility\":{\"min_plugin_version\":{\"$PLUGIN_ID\":\"99.0.0\"},\"recommended_plugin_version\":{\"$PLUGIN_ID\":\"99.0.0\"}}}"
121+
out=$(run_check)
122+
ASSERT "exits 0" "EXIT=0" "$out"
123+
ASSERT "warns on stderr" "below the platform's minimum" "$out"
124+
ASSERT "stamp written" "STAMP=yes" "$out"
125+
126+
# ---- Test 2: between min and recommended → info ----
127+
echo "Test 2: between min and recommended → info"
128+
set_health_body "{\"plugin_compatibility\":{\"min_plugin_version\":{\"$PLUGIN_ID\":\"0.0.1\"},\"recommended_plugin_version\":{\"$PLUGIN_ID\":\"99.0.0\"}}}"
129+
out=$(run_check)
130+
ASSERT "exits 0" "EXIT=0" "$out"
131+
ASSERT "info on stderr" "below the recommended" "$out"
132+
ASSERT "stamp written" "STAMP=yes" "$out"
133+
134+
# ---- Test 3: at or above recommended → silent ----
135+
echo "Test 3: at or above recommended → silent"
136+
set_health_body "{\"plugin_compatibility\":{\"min_plugin_version\":{\"$PLUGIN_ID\":\"0.0.1\"},\"recommended_plugin_version\":{\"$PLUGIN_ID\":\"0.0.1\"}}}"
137+
out=$(run_check)
138+
ASSERT "exits 0" "EXIT=0" "$out"
139+
ASSERT "no stderr output" "^EXIT=0" "$(echo "$out" | grep -v '^STAMP=' | head -1)"
140+
ASSERT "stamp written" "STAMP=yes" "$out"
141+
142+
# ---- Test 4: older platform without plugin_compatibility → silent ----
143+
echo "Test 4: older platform → silent"
144+
set_health_body '{"status":"healthy"}'
145+
out=$(run_check)
146+
ASSERT "exits 0" "EXIT=0" "$out"
147+
ASSERT "stamp written" "STAMP=yes" "$out"
148+
149+
# ---- Test 5: missing per-plugin entry → silent ----
150+
echo "Test 5: missing per-plugin entry → silent"
151+
set_health_body '{"plugin_compatibility":{"min_plugin_version":{"openclaw":"1.0.0"},"recommended_plugin_version":{"openclaw":"1.0.0"}}}'
152+
out=$(run_check)
153+
ASSERT "exits 0" "EXIT=0" "$out"
154+
ASSERT "stamp written" "STAMP=yes" "$out"
155+
156+
# ---- Test 6: platform unreachable → silent, no stamp ----
157+
echo "Test 6: platform unreachable → silent, no stamp"
158+
fake_home="$(mktemp -d)"
159+
stderr_file="$(mktemp)"
160+
HOME="$fake_home" \
161+
AXONFLOW_ENDPOINT="http://127.0.0.1:1" \
162+
"$CHECK_SCRIPT" 2>"$stderr_file" || true
163+
out=$(cat "$stderr_file")
164+
stamp_exists="no"
165+
[ -f "$fake_home/.cache/axonflow/codex-plugin-version-check-stamp" ] && stamp_exists="yes"
166+
ASSERT "no stderr" "^$" "$out"
167+
ASSERT "no stamp written" "^no$" "$stamp_exists"
168+
rm -rf "$fake_home" "$stderr_file"
169+
170+
# ---- Test 7: AXONFLOW_PLUGIN_VERSION_CHECK=off → silent, no stamp ----
171+
echo "Test 7: opt-out env var"
172+
set_health_body "{\"plugin_compatibility\":{\"min_plugin_version\":{\"$PLUGIN_ID\":\"99.0.0\"}}}"
173+
fake_home="$(mktemp -d)"
174+
stderr_file="$(mktemp)"
175+
HOME="$fake_home" \
176+
AXONFLOW_ENDPOINT="http://127.0.0.1:${STUB_PORT}" \
177+
AXONFLOW_PLUGIN_VERSION_CHECK=off \
178+
"$CHECK_SCRIPT" 2>"$stderr_file"
179+
stamp_exists="no"
180+
[ -f "$fake_home/.cache/axonflow/codex-plugin-version-check-stamp" ] && stamp_exists="yes"
181+
out=$(cat "$stderr_file")
182+
ASSERT "no stderr" "^$" "$out"
183+
ASSERT "no stamp written" "^no$" "$stamp_exists"
184+
rm -rf "$fake_home" "$stderr_file"
185+
186+
# ---- Test 8: stamp already exists → exits early, no curl ----
187+
echo "Test 8: stamp guard"
188+
set_health_body "{\"plugin_compatibility\":{\"min_plugin_version\":{\"$PLUGIN_ID\":\"99.0.0\"}}}"
189+
fake_home="$(mktemp -d)"
190+
mkdir -p "$fake_home/.cache/axonflow"
191+
touch "$fake_home/.cache/axonflow/codex-plugin-version-check-stamp"
192+
stderr_file="$(mktemp)"
193+
HOME="$fake_home" \
194+
AXONFLOW_ENDPOINT="http://127.0.0.1:${STUB_PORT}" \
195+
"$CHECK_SCRIPT" 2>"$stderr_file"
196+
out=$(cat "$stderr_file")
197+
ASSERT "no stderr" "^$" "$out"
198+
rm -rf "$fake_home" "$stderr_file"
199+
200+
echo
201+
echo "PASS: $PASS_COUNT, FAIL: $FAIL_COUNT"
202+
[ "$FAIL_COUNT" -eq 0 ]

0 commit comments

Comments
 (0)