Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
81 changes: 79 additions & 2 deletions tests/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Loading