Skip to content

[AAASM-4013] 🐛 (sdk): Fail closed on runtime-down + unknown decision under enforce#224

Merged
Chisanan232 merged 3 commits into
masterfrom
v0.0.1/AAASM-4013/fix/enforce_fail_closed
Jul 3, 2026
Merged

[AAASM-4013] 🐛 (sdk): Fail closed on runtime-down + unknown decision under enforce#224
Chisanan232 merged 3 commits into
masterfrom
v0.0.1/AAASM-4013/fix/enforce_fail_closed

Conversation

@Chisanan232

@Chisanan232 Chisanan232 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What changed

Under enforcementMode: "enforce" + napi-inprocess, a killed / stalled / unreachable runtime made the native query_policy return a non-throwing Ok({ decision: "allow", reason: FAIL_OPEN_REASON }) (the shim folds QueryFailed / ChannelClosed / Shutdown onto allow), and an unspecified verdict onto the empty decision "". The AAASM-3996 gateway guard is catch-only, so these non-throwing sentinels slipped past it and a policy-denied tool executed.

This makes the raw-verdict mapping (mapDecisionToPolicyResult in src/native/client.ts) fail-closed-aware:

  • Adds a JS-side FAIL_OPEN_REASON constant kept byte-for-byte in sync with native/aa-ffi-node/src/lib.rs.
  • Under failClosed (set only for enforce, plumbed via a new InitAssemblyOptions.failClosed from both createNativeClient call sites), the fail-open sentinel and the unknown/empty decision map to { denied: true } instead of allow.
  • Genuine allow / redact, and the observe / disabled / unset postures, are unchanged (fail open).

This is the JS-layer fix (option 2): aa-sdk-client is pinned by git rev and the native shim intentionally folds error cases onto the sentinel, so no native rebuild is needed. It complements the gateway-layer catch guard (3996) as a second, non-throwing-path defense.

Why

The SDK is advisory, but under enforce it is a security control and must fail closed — a stalled/killed sidecar must not turn deny-on-failure into allow-on-failure. This restores parity with the Python SDK's enforce posture (enforce = enforcement_mode == "enforce"; error-sentinel / unspecified decisions deny under enforce).

How to verify

  • pnpm test — full suite green (353 passed, 2 skipped).
  • New tests/native-client.test.ts cases exercise the REAL non-throwing allow sentinel (decision "allow" + FAIL_OPEN_REASON) and the unknown/empty decision under failClosed{ denied: true }, plus the fail-open counterparts (sentinel without failClosed still allows; a genuine authoritative allow still proceeds under enforce).
  • New end-to-end tests/native-gateway-enforcement.test.ts cases drive the real native client through createNativeGatewayClient + withAssembly, asserting the sentinel and the unknown verdict both block the tool (PolicyViolationError) under enforce, while observe fails open.
  • pnpm typecheck and pnpm lint clean.

Closes AAASM-4013

🤖 Generated with Claude Code

https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73

Chisanan232 and others added 2 commits July 2, 2026 22:20
The napi shim folds an unreachable/slow/closed runtime (QueryFailed,
ChannelClosed, Shutdown) onto a NON-throwing `allow` + FAIL_OPEN_REASON,
and an unspecified verdict onto the empty decision "". The AAASM-3996
gateway guard is catch-only, so these non-throwing sentinels bypass it
and a policy-denied tool executes under `enforcementMode: "enforce"`.

Make the raw-verdict mapping (`mapDecisionToPolicyResult`) fail-closed
aware: under `failClosed` (set only for enforce), the fail-open sentinel
and the unknown/empty decision map to `{ denied: true }` instead of
allow. Genuine allow/redact and observe/disabled/unset postures are
unchanged (fail open). This matches the Python SDK's enforce posture.

Refs AAASM-4013

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
…orce

Add native-client tests exercising the REAL non-throwing allow sentinel
(decision "allow" + FAIL_OPEN_REASON) and the unknown/empty decision
under failClosed → denied, plus the fail-open counterparts. Add an
end-to-end gateway+wrapper test proving the sentinel and unknown verdict
block the tool under enforce through the real native client.

Refs AAASM-4013

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Chisanan232

Copy link
Copy Markdown
Contributor Author

🤖 Claude Code review — approve

AAASM-4013 enforce fail-closed. JS-layer fix: failClosed (set when enforce) plumbed into mapDecisionToPolicyResult; the non-throwing fail-open sentinel AND unknown/"" decision now map to {denied:true} under enforce (complements the catch-only 3996 guard). Genuine allow/redact + observe stay fail-open. 353 pass incl. real-sentinel + unknown e2e tests.

Note: codecov/patch red = coverage-delta acceptance gate only (code CI green, new paths are tested) — ignorable per policy.

@Chisanan232

Copy link
Copy Markdown
Contributor Author

🤖 Claude Code — deep review: APPROVE (merge-ready)

CI: green except codecov/patch (coverage acceptance-only, ignored). Scope: complete for the napi path — the JS-layer failClosed maps the fail-open sentinel (covers QueryFailed/ChannelClosed/Shutdown, all folded to one sentinel) AND unknown/"" decision to {denied:true} under enforce; the ticket explicitly offered this JS option.

Side-effects (regression check): observe/disabled stay fail-open — failClosed is derived ONLY from enforcementMode==="enforce" at both call sites; the OBSERVE e2e drives the real sentinel and asserts the tool still runs. Genuine allow/redact/pending not over-blocked (only allow+reason===FAIL_OPEN_REASON denies). The gRPC-sidecar path early-returns before failClosed is consumed → byte-identical to master. No regression.

Non-blocking note: the FAIL_OPEN_REASON string is duplicated Rust↔JS with no compile-time guard (documented in TSDoc) — a future edit to the Rust constant would silently break the JS guard; consider exporting it from native.

…M-4013)

Add tests for the previously-uncovered new lines flagged by codecov/patch:
- mapDecisionToPolicyResult: a REDACT verdict is authoritative and proceeds
  even under failClosed; an unknown/empty decision without failClosed still
  fails open (observe/disabled).
- createClient: under enforcementMode enforce a runtime-down fail-open sentinel
  is denied end-to-end through check(); under observe the same sentinel
  proceeds — exercising the failClosed=enforce plumbing (both branches).
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

@Chisanan232 Chisanan232 merged commit 1ac2e73 into master Jul 3, 2026
25 checks passed
@Chisanan232 Chisanan232 deleted the v0.0.1/AAASM-4013/fix/enforce_fail_closed branch July 3, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant