From fc97b7aeeca6cd91b4b083929129f205ebbbff5b Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Thu, 9 Apr 2026 00:49:48 +0200 Subject: [PATCH] prep(v0.3.1): decision-matrix test coverage + changelog cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.3.1 prep for the planned 2026-04-09 release. Not cut here. Review H3: v0.3.0's new fail-open decision matrix was untested beyond a stderr-string assertion. Added 5 mock-server cases (FAIL_CLOSED_METHOD, FAIL_CLOSED_PARAMS, FAIL_OPEN_INTERNAL, FAIL_OPEN_PARSE, FAIL_OPEN_UNKNOWN) and 5 new test blocks. Changelog cleanup: - 'issue #1545 Direction 3' ref stripped from v0.3.0 section - v0.3.0 date corrected 2026-04-09 → 2026-04-08 Manifest bump: .cursor-plugin/plugin.json 0.3.0 → 0.3.1 Tests: 27/27 pass (up from 20 in v0.3.0). --- .cursor-plugin/plugin.json | 2 +- CHANGELOG.md | 10 ++++- tests/test-hooks.sh | 81 +++++++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 584ce68..61eab98 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "axonflow", "displayName": "AxonFlow Governance", "description": "Policy enforcement, PII detection, and audit trails for Cursor. Automatically evaluates governed tool inputs against 80+ governance policies, scans outputs for sensitive data, and records every decision in a compliance-grade audit trail. Self-hosted via Docker — all data stays on your infrastructure.", - "version": "0.3.0", + "version": "0.3.1", "author": { "name": "AxonFlow", "email": "hello@getaxonflow.com", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb6aef..0a7733b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Changelog -## [0.3.0] - 2026-04-09 +## [0.3.1] - Release Pending (2026-04-09) + +### Added + +- **Decision-matrix regression tests** for the v0.3.0 hook fail-open/fail-closed behavior. The v0.3.0 release only added a single stderr-string assertion update; the new branches (JSON-RPC -32601 method-not-found, -32602 invalid-params, -32603 internal, -32700 parse, and unknown error codes) were completely untested. This release adds mock-server cases for every branch so the decision matrix is now covered end-to-end. + +## [0.3.0] - 2026-04-08 ### Changed -- **Hook fail-open/fail-closed hardening (issue #1545 Direction 3).** `scripts/pre-tool-check.sh` now distinguishes curl exit code (network failure) from HTTP success with an error body. Fail-closed (exit 2, block tool) only on operator-fixable JSON-RPC errors: auth failures (-32001), method-not-found (-32601), and invalid-params (-32602). Fail-open (exit 0, allow) on everything else: curl timeouts/DNS failures/connection refused, empty response, server-internal errors (-32603), parse errors (-32700), and unknown error codes. Prevents transient governance infrastructure issues from blocking legitimate dev workflows while still catching broken configurations. +- **Hook fail-open/fail-closed hardening.** `scripts/pre-tool-check.sh` now distinguishes curl exit code (network failure) from HTTP success with an error body. Fail-closed (exit 2, block tool) only on operator-fixable JSON-RPC errors: auth failures (-32001), method-not-found (-32601), and invalid-params (-32602). Fail-open (exit 0, allow) on everything else: curl timeouts/DNS failures/connection refused, empty response, server-internal errors (-32603), parse errors (-32700), and unknown error codes. Prevents transient governance infrastructure issues from blocking legitimate dev workflows while still catching broken configurations. ### Security diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index fab513a..6785bac 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -71,8 +71,20 @@ class Handler(http.server.BaseHTTPRequestHandler): args = params.get('arguments', {}) statement = args.get('statement', '') - # Simulate different responses based on statement content - if 'AUTH_ERROR' in statement: + # Simulate different responses based on statement content. + # v0.3.1: additional trigger strings for the v0.3.0 decision matrix + # that was untested — see tests below each FAIL_* case. + if 'FAIL_CLOSED_METHOD' in statement: + resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -32601, 'message': 'Method not found'}} + elif 'FAIL_CLOSED_PARAMS' in statement: + resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -32602, 'message': 'Invalid params'}} + elif 'FAIL_OPEN_INTERNAL' in statement: + resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -32603, 'message': 'Internal error'}} + elif 'FAIL_OPEN_PARSE' in statement: + resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -32700, 'message': 'Parse error'}} + elif 'FAIL_OPEN_UNKNOWN' in statement: + resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -99999, 'message': 'Unknown code'}} + elif 'AUTH_ERROR' in statement: # JSON-RPC auth error resp = {'jsonrpc': '2.0', 'id': body.get('id'), 'error': {'code': -32001, 'message': 'Authentication failed'}} elif 'BLOCKED' in statement: @@ -184,6 +196,71 @@ EXIT_CODE=$? assert_eq "Exit code is 0 (fail-open)" "0" "$EXIT_CODE" assert_empty "No output (silent allow on network failure)" "$OUTPUT" +# v0.3.1 decision-matrix coverage (review finding H3) +echo "" +echo "--- PreToolUse: -32601 method not found → exit 2 (block) ---" +if [ "${1:-}" = "--live" ]; then + echo " SKIP: mock-only trigger" + ((PASS++)) || true +else + STDERR_FILE=$(mktemp) + set +e + echo '{"tool_name":"Bash","tool_input":{"command":"FAIL_CLOSED_METHOD"}}' | "$PRE_HOOK" 2>"$STDERR_FILE" + EXIT_CODE=$? + set -e + STDERR_OUT=$(cat "$STDERR_FILE") + rm -f "$STDERR_FILE" + assert_eq "Exit code is 2 (block)" "2" "$EXIT_CODE" + assert_contains "Has governance blocked on stderr" "$STDERR_OUT" "governance blocked" +fi + +echo "" +echo "--- PreToolUse: -32602 invalid params → exit 2 (block) ---" +if [ "${1:-}" = "--live" ]; then + echo " SKIP: mock-only trigger" + ((PASS++)) || true +else + set +e + echo '{"tool_name":"Bash","tool_input":{"command":"FAIL_CLOSED_PARAMS"}}' | "$PRE_HOOK" 2>/dev/null + EXIT_CODE=$? + set -e + assert_eq "Exit code is 2 (block)" "2" "$EXIT_CODE" +fi + +echo "" +echo "--- PreToolUse: -32603 internal → exit 0 (fail-open) ---" +if [ "${1:-}" = "--live" ]; then + echo " SKIP: mock-only trigger" + ((PASS++)) || true +else + OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"FAIL_OPEN_INTERNAL"}}' | "$PRE_HOOK" 2>/dev/null) + EXIT_CODE=$? + assert_eq "Exit code is 0 (fail-open on -32603)" "0" "$EXIT_CODE" + assert_empty "No output" "$OUTPUT" +fi + +echo "" +echo "--- PreToolUse: -32700 parse error → exit 0 (fail-open) ---" +if [ "${1:-}" = "--live" ]; then + echo " SKIP: mock-only trigger" + ((PASS++)) || true +else + echo '{"tool_name":"Bash","tool_input":{"command":"FAIL_OPEN_PARSE"}}' | "$PRE_HOOK" 2>/dev/null + EXIT_CODE=$? + assert_eq "Exit code is 0 (fail-open on -32700)" "0" "$EXIT_CODE" +fi + +echo "" +echo "--- PreToolUse: unknown error code → exit 0 (fail-open) ---" +if [ "${1:-}" = "--live" ]; then + echo " SKIP: mock-only trigger" + ((PASS++)) || true +else + echo '{"tool_name":"Bash","tool_input":{"command":"FAIL_OPEN_UNKNOWN"}}' | "$PRE_HOOK" 2>/dev/null + EXIT_CODE=$? + assert_eq "Exit code is 0 (fail-open on unknown code)" "0" "$EXIT_CODE" +fi + echo "" echo "--- PreToolUse: empty tool_name → allow ---" OUTPUT=$(echo '{"tool_name":"","tool_input":{}}' | "$PRE_HOOK" 2>/dev/null)