Skip to content

[AAASM-4760] 🐛 (sdk): fail closed instead of silently fail-open when ungoverned - #272

Merged
Chisanan232 merged 2 commits into
masterfrom
v0.0.1/AAASM-4760/fix/fail_closed
Jul 17, 2026
Merged

[AAASM-4760] 🐛 (sdk): fail closed instead of silently fail-open when ungoverned#272
Chisanan232 merged 2 commits into
masterfrom
v0.0.1/AAASM-4760/fix/fail_closed

Conversation

@Chisanan232

@Chisanan232 Chisanan232 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a HIGH-severity fail-open in the SDK's in-process enforcement path (AAASM-4760).

When the native agent_assembly._core extension is absent — the pure-Python / containerized install with no native wheel — under an enforce posture (the None default or an explicit enforce), build_governance_interceptor returned the bare GatewayClient. The framework adapters were then handed a client with no check_tool_start, so every governed tool call proceeded UN-governed while the session still reported as governed. A developer relying on the SDK layer believed they were protected; enforcement was completely inert.

This PR makes that state impossible to miss: the native-missing enforce path now fails CLOSED.

Before / after enforcement behavior

Situation (native _core absent) Before After
enforce / None (default) posture Bare client returned → tool calls proceed ungoverned (silent fail-open) Deny-all _FailClosedInterceptor with an explicit reason + loud one-time warning → every governed tool call is denied (fail closed)
observe / disabled posture Bare client (fail open) Bare client (fail open) — unchanged, the opt-in advisory path is preserved

The deny carries an actionable reason ("governance unavailable: … refusing to run ungoverned …") so the developer knows enforcement is unavailable (install the native extension, or opt into observe) rather than mistaking it for a policy deny. The registered flag on the returned context already surfaces the unregistered state (False) with a loud stderr warning; this PR closes the remaining hole where tool calls still ran ungoverned.

This matches the project's existing fail-closed-under-enforce semantics (the "native present but runtime socket unreachable" case already returned _FailClosedInterceptor; native-missing now behaves consistently) and mirrors Go's fail-closed / Node's ConfigurationError postures.

Scope note: this owns only the python-sdk behavior. The base-image side (shipping the native rc.6 wheel in the published python base image) is handled by AAASM-4761; together they close the gap.

Type of Change

  • 🔧 Bug fix

Breaking Changes

  • Yes (please describe below)

Under enforce with the native extension absent, governed tool calls now deny instead of silently proceeding. This is the intended security fix. Deployments that legitimately want the SDK to be advisory-only (relying on the proxy / eBPF layers) must opt into enforcement_mode="observe", which is the documented, clearly-signalled non-enforcing posture. No API surface changed.

Related Issues

  • Related JIRA ticket: AAASM-4760
  • Related: AAASM-4761 (base-image native-wheel side, separate lane)

Testing

  • Unit tests added/updated

  • Updated the two interceptor tests that encoded the old fail-open contract to assert the deny-all fail-closed behavior.

  • Added a builder-level regression asserting the deny reason is explicit (test_native_missing_deny_reason_is_explicit).

  • Added an end-to-end regression through init_assembly asserting both guarantees at once: ctx.registered is False and the wired interceptor denies a tool call rather than silently allowing it (test_native_absent_under_enforce_wires_fail_closed_interceptor).

  • Full suite: 804 passed, 16 skipped. ruff check clean on changed files; mypy agent_assembly unchanged from baseline (only pre-existing native-_core/grpc import-not-found notes); pre-commit green.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex logic
  • Documentation updated if needed
  • All tests passing

🤖 Generated with Claude Code

https://claude.ai/code/session_019NafDDcehs2ez4Ax9kuWfi

Chisanan232 and others added 2 commits July 17, 2026 14:02
Under an enforce posture (the None default or explicit "enforce") a
pure-Python / containerized install with no native agent_assembly._core
extension previously returned the bare GatewayClient from
build_governance_interceptor. The adapters got no check_tool_start, so
every governed tool call proceeded UN-governed while the session looked
governed — a silent fail-open (AAASM-4760).

Now the native-missing enforce path returns a deny-all
_FailClosedInterceptor with an explicit, logged reason and keeps the loud
one-time warning: no tool call is authoritatively allowed when there is no
native authority to consult. The explicit observe / disabled dry-run
postures remain the opt-in advisory path (fail open), unchanged.

Updates the two interceptor tests that encoded the old fail-open contract
and adds a builder-level regression asserting the deny reason is explicit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NafDDcehs2ez4Ax9kuWfi
End-to-end assertion for AAASM-4760: with the native extension absent under
the default enforce posture, init_assembly must not run
ungoverned-but-looks-governed. The test asserts both guarantees at once —
ctx.registered is False (not a governed/registered session) and the wired
interceptor's check_tool_start returns deny, never a silent allow.

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

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 17, 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 — PR review

CI: All checks green — CI Success, unit + integration test items (3.13/ubuntu), CodeQL, SonarCloud (Quality Gate passed, 100% coverage on new code), Codecov patch (all modified lines covered), pip-audit, LangChain contract test, docs build. Only reviewDecision: REVIEW_REQUIRED outstanding (≥1 Pioneer approval), which is the expected human gate, not a failure.

Scope (AAASM-4760, HIGH): Complete. The fail-open hole is closed. With native _core absent under an enforce posture (the None default or explicit enforce), build_governance_interceptor now returns a deny-all _FailClosedInterceptor carrying an actionable reason ("governance unavailable: … refusing to run ungoverned …") plus the loud one-time _warn_sdk_enforcement_unavailable(). There is no remaining path where the SDK silently allows while looking governed: the registered flag was already False for this case (AAASM-4130), and this PR closes the last gap where tool calls still executed ungoverned. Both native-missing call sites and the native-present-but-unreachable site now funnel through one _governance_unavailable() helper, so the enforce→deny / observe→pass decision is consistent. Regression tests assert both guarantees: test_native_absent_under_enforce_wires_fail_closed_interceptor checks ctx.registered is False and that the wired interceptor returns status == "deny" (not allow) on a governed tool call; test_native_missing_deny_reason_is_explicit asserts the reason contains "governance unavailable" / "ungoverned"; the two prior fail-open contract tests were flipped to assert _FailClosedInterceptor + deny.

Side-effects / security: Clean and correctly bounded.

  • The legitimate offline/observe path is preserved — observe/disabled still return the bare client (fail open) and emit no warning; the deny-all only triggers under enforce.
  • A correctly-configured native install is untouched: when _core is present and the runtime connects, the final RuntimeQueryInterceptor branch is unchanged, so this cannot fail-closed a working native deployment. The deny-all only fires when genuinely ungoverned (no native authority, or native present but socket unreachable — the latter already behaved this way).
  • No public API surface changed (documented as a behavioral breaking change only: enforce + no native now denies instead of silently proceeding — the intended fix).

Frontend: Playwright N/A — pure Python SDK, no UI.

Verdict: APPROVE (not merging — review-required / merge decision left to dev-lead). The HIGH-severity fail-open is genuinely closed, the fix is minimal and consistent with the existing fail-closed-under-enforce semantics, and it is well covered by regression tests.

@Chisanan232
Chisanan232 merged commit 5e5ba97 into master Jul 17, 2026
26 checks passed
@Chisanan232
Chisanan232 deleted the v0.0.1/AAASM-4760/fix/fail_closed branch July 17, 2026 08:11
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