From 7d9204d8f5c423f18ec5d972d2f3262dd11f9018 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:17:28 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=85=20(test):=20Add=20a=20negative=20?= =?UTF-8?q?control=20over=20the=20documented=20quick-start=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AAASM-5529 controls each call install_fake_core() before running init_assembly, supplying an authoritative native runtime that the configuration docs/quick-start.md hands a reader does not have. They are therefore structurally unable to observe what the documented path does. This control runs the page's four keyword arguments verbatim — with no enforcement_mode, because the page never mentions one — against a pure-Python install, and drives the governed call through Agno's own FunctionCall.execute rather than the SDK's internal chain, so "a hook is installed" is observed rather than assumed. Refs AAASM-5661 --- ...est_quickstart_documented_configuration.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 test/unit/test_quickstart_documented_configuration.py diff --git a/test/unit/test_quickstart_documented_configuration.py b/test/unit/test_quickstart_documented_configuration.py new file mode 100644 index 00000000..043ede4f --- /dev/null +++ b/test/unit/test_quickstart_documented_configuration.py @@ -0,0 +1,184 @@ +"""What the documented quick-start configuration actually does (AAASM-5661). + +Epic AAASM-5526. + +Why the AAASM-5529 controls cannot see this +------------------------------------------- + +``test_quickstart_negative_control.py`` proves that a runtime ``deny`` prevents +the effect a tool exists to produce. Each of its controls calls +``install_fake_core()`` first, which supplies an authoritative native runtime. +The configuration ``docs/quick-start.md`` §3 hands a reader has no such runtime: +a pure-Python ``pip install agent-assembly`` carries no ``agent_assembly._core`` +extension, and the quick-start's own example runs offline with nothing listening +on the gateway URL it passes. A control that installs the authority first is +therefore structurally unable to observe what happens when there is none — it is +not a weaker version of this control, it is a control over a different program. + +What this module runs +--------------------- + +The four keyword arguments the quick-start passes, and nothing else. In +particular ``enforcement_mode`` is left unset, because the page never mentions +it: the posture under test has to be the one a reader gets by following the page, +not one this file selects. + +The governed call is driven through **Agno's own** ``FunctionCall.execute`` — +the chokepoint ``AgnoPatch`` patches and the entry point the page's Agno tab +uses — rather than through the SDK's internal ``run_governed_async_tool``. Going +through the framework is what makes "an interceptor is installed" an observable +fact rather than an assumption: if the SDK installed nothing, or installed +something that waves calls through, Agno runs the body and the file appears. + +What it establishes, in ADR 0033 §6 terms +----------------------------------------- + +In this configuration the SDK **Evaluates** no policy — there is no authority to +produce a decision — and each governed tool call is **Denied before execution** +by the fail-closed posture (AAASM-4760), carrying a reason that names the missing +extension rather than a policy rule. Both halves are asserted, because the first +without the second would let a future silent pass-through look identical to a +policy allow. + +The ``FALSIFICATION`` case runs the same Agno tool with no ``init_assembly`` at +all. It must write the file; if it stops doing so, the absence asserted above has +some other cause and every assertion here is vacuous. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest +from agno.tools.function import Function, FunctionCall + +from agent_assembly import init_assembly +from agent_assembly.core import assembly as core_assembly +from agent_assembly.core.runtime_interceptor import _native_core_available + +from .negative_control import FileSideEffect + +#: The three connection arguments docs/quick-start.md §3 passes, verbatim in +#: shape: an http:// loopback gateway URL, a literal key, a per-framework agent +#: id. The values are the page's own; only the agent id is renamed so a parallel +#: run cannot collide with a real one. +_GATEWAY_URL = "http://localhost:7391" +_API_KEY = "demo-key" +_AGENT_ID = "quickstart-documented-config-agent" + +#: The prefix Agno's failure result carries when the governance hook refused the +#: call — the framework-visible evidence that the refusal happened before the +#: body, not inside it. +_BLOCKED_PREFIX = "[BLOCKED by governance policy]" + +#: A fragment of ``_NATIVE_MISSING_REASON``. Asserting on it distinguishes "the +#: SDK refused because it had no authority to ask" from "a policy rule denied +#: this tool" — two outcomes a reader of the page cannot tell apart from the +#: exception alone, and which the page previously described only as the latter. +_NO_AUTHORITY_FRAGMENT = "the native agent_assembly._core extension is not installed" + + +@pytest.fixture(autouse=True) +def _cleanup_active_context() -> None: + """Release the process-singleton context so each control inits cleanly.""" + active = core_assembly._ACTIVE_CONTEXT + if active is not None and not active.is_shutdown: + active.shutdown() + core_assembly._ACTIVE_CONTEXT = None + + +@pytest.fixture +def file_effect(tmp_path: Path) -> FileSideEffect: + return FileSideEffect(path=tmp_path / "agno-tool-write.txt") + + +@pytest.fixture +def documented_context() -> Iterator[Any]: + """``init_assembly`` with exactly the quick-start's arguments. + + Torn down through the real ``shutdown()`` so the Agno patch this installs + globally is unwound before the next test runs. + """ + context = init_assembly( + gateway_url=_GATEWAY_URL, + api_key=_API_KEY, + agent_id=_AGENT_ID, + mode="sdk-only", + ) + try: + yield context + finally: + context.shutdown() + + +def _agno_tool_call(effect: FileSideEffect) -> Any: + """Build a real Agno ``FunctionCall`` whose body writes ``effect``.""" + + def write_to_disk(path: str) -> str: + return FileSideEffect(path=Path(path)).write("the tool body ran") + + return FunctionCall( + function=Function.from_callable(write_to_disk), + arguments={"path": str(effect.path)}, + ) + + +class TestTheConfigurationTheQuickStartHandsAReader: + def test_the_environment_under_test_has_no_native_authority(self) -> None: + """Pin the premise, so the controls below cannot quietly change subject. + + Every assertion in this class is about the pure-Python install a reader + gets from ``pip install agent-assembly``. With the native extension + present the SDK takes a different branch entirely, and these controls + would still pass while measuring something else. + """ + assert _native_core_available() is False, ( + "this suite measures the pure-Python install the quick-start's reader gets; " + "agent_assembly._core is importable here, so the branch under test is not the " + "one being exercised" + ) + + def test_a_governance_hook_is_installed_on_agnos_own_tool_path( + self, documented_context: Any, file_effect: FileSideEffect + ) -> None: + """The load-bearing control for AAASM-5661. + + Absence of the file is the assertion; the failure result is corroboration. + Ordered that way on purpose — an assertion on the result placed first + would abort before the side effect is examined, so a regression that let + the body run *and* returned a failure would slip through. + """ + result = _agno_tool_call(file_effect).execute() + + assert file_effect.occurred() is False + assert file_effect.content() is None + assert result.status == "failure" + assert _BLOCKED_PREFIX in str(result.error) + + def test_the_refusal_is_the_fail_closed_posture_rather_than_a_policy_decision( + self, documented_context: Any, file_effect: FileSideEffect + ) -> None: + """The refusal names a missing authority, not a rule that matched. + + This is the half the quick-start got wrong: it described the outcome as a + policy gate answering, when what answers is a posture taken in the + absence of anything to ask. + """ + result = _agno_tool_call(file_effect).execute() + + assert file_effect.occurred() is False + assert _NO_AUTHORITY_FRAGMENT in str(result.error) + + def test_the_agent_is_not_registered_in_this_configuration(self, documented_context: Any) -> None: + """``registered`` is the programmatic counterpart of the stderr warning.""" + assert documented_context.registered is False + + def test_falsification_the_same_agno_tool_ungoverned_writes_the_file(self, file_effect: FileSideEffect) -> None: + """No ``init_assembly``, no hook. If this stops writing, the class above is vacuous.""" + result = _agno_tool_call(file_effect).execute() + + assert file_effect.occurred() is True + assert file_effect.content() == "the tool body ran" + assert result.status == "success" From a15f56531b519017d5fda581a62727b89010abfe Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:20:17 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=90=9B=20(runtime):=20Warn=20about=20?= =?UTF-8?q?enforcement=20on=20the=20fail-open=20governance=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _governance_unavailable's fail-open branch returns the bare GatewayClient, which exposes no check_tool_start, so the adapters' missing-interceptor fallback allows and a governed tool call reaches its body with no in-process decision behind it. It emitted nothing at all; the only loud signal on that configuration was _warn_agent_unregistered, which is about registration, so a caller who read it still had no way to learn that enforcement was gone too. Emit a stderr notice naming the gap and the remedy, scoped to the fail-open branch so a fail-closed caller is not told the opposite. Refs AAASM-5661 --- agent_assembly/core/runtime_interceptor.py | 45 +++++++++++++ test/unit/core/test_runtime_interceptor.py | 78 +++++++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/agent_assembly/core/runtime_interceptor.py b/agent_assembly/core/runtime_interceptor.py index c2fe5a09..6373cd8d 100644 --- a/agent_assembly/core/runtime_interceptor.py +++ b/agent_assembly/core/runtime_interceptor.py @@ -39,6 +39,7 @@ import json import os import stat +import sys import warnings from importlib import metadata from typing import Any @@ -118,6 +119,40 @@ def _warn_sdk_enforcement_unavailable() -> None: ) +def _warn_sdk_enforcement_not_applied(reason: str) -> None: + """Warn (loudly) that this build applies no SDK-layer enforcement (AAASM-5661). + + Emitted on the fail-open branch of :func:`_governance_unavailable`: an explicit + ``observe`` / ``disabled`` posture with no authoritative runtime to consult. The + bare ``GatewayClient`` returned there exposes no ``check_tool_start``, so the + adapters' missing-interceptor fallback allows and the governed call runs — the + genuine no-op path, and the quiet one. ``init_assembly`` already warns about + *registration* on the same configuration, and that warning has been mistaken for + the whole story: an agent can be registered and still run with no in-process + allow/deny. + + Written straight to ``sys.stderr`` rather than through ``warnings`` for the same + reason as :func:`~agent_assembly.core.assembly._warn_agent_unregistered`: a + ``logging`` or ``warnings`` filter must not be able to silence a statement about + what the SDK is not doing. Once per interceptor build, so it cannot become + per-call noise. + + :param reason: The clause naming why no authority was reachable, reused from the + deny reason the enforce branch would have carried (contains no credentials). + """ + sys.stderr.write( + "[agent-assembly] WARNING: SDK-layer enforcement is NOT applied on this path " + f"({reason}). Under enforcement_mode='observe' / 'disabled' the SDK stays " + "advisory, so a governed tool call reaches its body with no in-process " + "allow/deny decision behind it, and a policy DENY does not block it here. This " + "is a separate gap from the registration warning: an agent can be registered " + "and still run with no SDK-layer enforcement. Use the default enforce posture " + "with the native agent_assembly._core extension installed to obtain the " + "in-process decision; the proxy / eBPF layers remain authoritative either way " + "(AAASM-5661).\n" + ) + + def _resolve_runtime_socket_path(agent_id: str) -> str: """Resolve the runtime UDS path: ``AA_RUNTIME_SOCKET`` > default convention. @@ -562,8 +597,18 @@ def _governance_unavailable(client: Any, enforce: bool, reason: str, *, warn: bo ``warn`` gates the one-time loud warning to the native-missing case (a pure-Python install), matching the historical AAASM-4130 behavior; the unreachable-socket case denies without an extra warning. + + The fail-open branch warns unconditionally (AAASM-5661). It is the one + remaining path on which a governed tool call reaches its body with no + in-process decision behind it, and until now it was the quietest: the bare + ``GatewayClient`` exposes no ``check_tool_start``, so the adapters fall back + to an allow and the session looks governed. The only loud signal a caller got + was :func:`~agent_assembly.core.assembly._warn_agent_unregistered`, which is + about *registration* — a reader who saw it and shrugged had no way to learn + that enforcement was gone too. """ if not enforce: + _warn_sdk_enforcement_not_applied(reason) return client if warn: _warn_sdk_enforcement_unavailable() diff --git a/test/unit/core/test_runtime_interceptor.py b/test/unit/core/test_runtime_interceptor.py index 4fa4bf58..334ba56a 100644 --- a/test/unit/core/test_runtime_interceptor.py +++ b/test/unit/core/test_runtime_interceptor.py @@ -266,9 +266,18 @@ def _no_core_import(name: str, *args: Any, **kwargs: Any) -> Any: assert result.check_tool_start(serialized={"name": "t"}, input_str="i")["status"] == "deny" -def test_observe_mode_does_not_warn_when_native_core_missing(monkeypatch: pytest.MonkeyPatch) -> None: - """The warning is scoped to the enforce posture: an explicit ``observe`` dry-run - with no native extension legitimately fails open and must stay silent.""" +def test_observe_mode_reports_enforcement_is_not_applied_when_native_core_missing( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Two separate contracts, and the test name used to assert only the first. + + The enforce-specific ``UserWarning`` stays scoped to the enforce posture — an + explicit ``observe`` dry-run legitimately fails open, and telling that caller + their tools are being denied would be false. But silence about *enforcement* + was the AAASM-5661 gap: this branch hands back the bare client, whose missing + ``check_tool_start`` makes the adapters allow, and the only loud signal the + caller got was about registration. + """ monkeypatch.delitem(sys.modules, "agent_assembly._core", raising=False) import builtins @@ -289,6 +298,11 @@ def _no_core_import(name: str, *args: Any, **kwargs: Any) -> Any: result = build_governance_interceptor(client, "agent-001", "observe") assert result is client + stderr = capsys.readouterr().err + assert "SDK-layer enforcement is NOT applied" in stderr + # The remedy has to be in the same breath as the gap; a caller who reads only + # the registration warning has no reason to look for a second one. + assert "agent_assembly._core" in stderr def test_native_missing_deny_reason_is_explicit(monkeypatch: pytest.MonkeyPatch) -> None: @@ -426,6 +440,64 @@ def connect(_socket_path: str) -> Any: assert build_governance_interceptor(client, "agent-001", "observe") is client +def test_observe_unreachable_runtime_reports_enforcement_is_not_applied( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """The second fail-open entry: native present, socket unreachable (AAASM-5661). + + Distinct from the native-missing case above and previously the quieter of the + two — this branch never emitted anything at all, in either posture. + """ + + class _UnreachableRuntimeClient: + @staticmethod + def connect(_socket_path: str) -> Any: + raise OSError("no such socket") + + fake_core = types.ModuleType("agent_assembly._core") + fake_core.RuntimeClient = _UnreachableRuntimeClient # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "agent_assembly._core", fake_core) + + client = _FakeGatewayClient() + build_governance_interceptor(client, "agent-001", "observe") + + stderr = capsys.readouterr().err + assert "SDK-layer enforcement is NOT applied" in stderr + assert "runtime unreachable" in stderr + + +def test_enforce_posture_does_not_report_enforcement_as_unapplied( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """The notice is scoped to the fail-open branch — a positive control for it. + + Without this, deleting the ``if not enforce`` guard and warning on every path + would leave the two controls above green while telling a fail-closed caller + that enforcement is not applied, which is the opposite of what happens. + """ + monkeypatch.delitem(sys.modules, "agent_assembly._core", raising=False) + + import builtins + import warnings + + real_import = builtins.__import__ + + def _no_core_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "agent_assembly._core": + raise ImportError("native extension unavailable") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _no_core_import) + + client = _FakeGatewayClient() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + result = build_governance_interceptor(client, "agent-001", "enforce") + + assert isinstance(result, _FailClosedInterceptor) + assert "SDK-layer enforcement is NOT applied" not in capsys.readouterr().err + + def test_enforce_wraps_with_fail_closed_query_path(monkeypatch: pytest.MonkeyPatch) -> None: """build_governance_interceptor under enforce wraps a reachable runtime so a raising query denies (the wrapper carries enforce=True).""" From 34d4989264e1111f064c57a31fdf7a7247855f07 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:26:56 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=85=20(test):=20Pin=20the=20tabs'=20r?= =?UTF-8?q?evert=20workaround=20and=20the=20startup=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three framework tabs revert the hook init_assembly() installs and re-apply one wired to the example's own LocalPolicyEngine. Their comments call it a no-op hook, which has been false since AAASM-4760 — it is deny-all — and the tab bodies are generated from quickstart_snippets/, vendored from the examples repo, so their comments are not this repo's to rewrite. The prose that explains the step is, and it needs a control that turns red if an upstream snippet drops it. Also asserts both startup warnings the documented configuration emits: registration on stderr, the enforcement gap through warnings. Refs AAASM-5661 --- ...est_quickstart_documented_configuration.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/unit/test_quickstart_documented_configuration.py b/test/unit/test_quickstart_documented_configuration.py index 043ede4f..4a4aa243 100644 --- a/test/unit/test_quickstart_documented_configuration.py +++ b/test/unit/test_quickstart_documented_configuration.py @@ -175,6 +175,27 @@ def test_the_agent_is_not_registered_in_this_configuration(self, documented_cont """``registered`` is the programmatic counterpart of the stderr warning.""" assert documented_context.registered is False + def test_startup_reports_both_the_registration_and_the_enforcement_gap( + self, capsys: pytest.CaptureFixture[str] + ) -> None: + """The quick-start now tells a reader init warns about two things. This is both. + + They travel on different channels — registration on ``sys.stderr`` so a + ``logging`` filter cannot drop it, the enforcement gap through + ``warnings`` — so asserting one and assuming the other would leave the + documented sentence half-covered. + """ + with pytest.warns(UserWarning, match="native runtime extension"): + context = init_assembly( + gateway_url=_GATEWAY_URL, + api_key=_API_KEY, + agent_id=_AGENT_ID, + mode="sdk-only", + ) + context.shutdown() + + assert "the agent is NOT registered" in capsys.readouterr().err + def test_falsification_the_same_agno_tool_ungoverned_writes_the_file(self, file_effect: FileSideEffect) -> None: """No ``init_assembly``, no hook. If this stops writing, the class above is vacuous.""" result = _agno_tool_call(file_effect).execute() @@ -182,3 +203,32 @@ def test_falsification_the_same_agno_tool_ungoverned_writes_the_file(self, file_ assert file_effect.occurred() is True assert file_effect.content() == "the tool body ran" assert result.status == "success" + + +class TestTheWorkaroundTheFrameworkTabsCarry: + """The tabs' revert-and-re-apply step, which the page now explains rather than hides. + + Three tabs carried a comment saying ``init_assembly()`` installs a *no-op* + hook offline. That description predates AAASM-4760 — the hook installed there + is deny-all, not a no-op — and a workaround written down three times is the + strongest available evidence that the gap was known in practice. The tab + bodies are generated from ``quickstart_snippets/`` (vendored from the + ``examples`` repo), so their comments are not this repo's to rewrite; the + prose section this ticket added is. This control pins the step the prose now + describes, so an upstream snippet change that drops it turns the sentence red + instead of leaving it quietly false. + """ + + def test_the_tabs_still_revert_the_hook_init_assembly_installed(self) -> None: + quick_start = Path(__file__).resolve().parents[2] / "docs" / "quick-start.md" + generated = quick_start.read_text(encoding="utf-8").split("BEGIN GENERATED: quickstart-framework-tabs")[1] + generated = generated.split("END GENERATED: quickstart-framework-tabs")[0] + + # "several" in the prose, pinned to a floor rather than an exact count — + # a fourth tab adopting the same workaround should not fail a sentence + # that stays true, and a drop to one should not pass under a word that + # says more than one. + assert generated.count(".revert()") >= 2, ( + "the quick-start's generated tabs no longer revert the hook init_assembly() " + "installed; the 'What this offline example evaluates' section says they do" + ) From 0477c6c9372e508c27e8a99d0048aaded433d4e6 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:27:21 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20State=20what=20the?= =?UTF-8?q?=20offline=20quick-start=20configuration=20evaluates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page told a reader that after init_assembly() "every tool call from this point on is routed through the policy gate". Measured on the configuration the page hands them — no gateway listening, no native agent_assembly._core on a pure-Python install — nothing evaluates any policy: the SDK installs a deny-all fail-closed hook and refuses each governed tool call with a reason naming the absent extension. Installing a real interceptor there is not achievable: with no runtime and no gateway there is no authority to consult, and the SDK cannot synthesise one. So the sentences are corrected rather than the code, in ADR 0033 §6 terms — Degraded, not Evaluated — and a new section says what the reader gets and what the SDK needs to decide instead. Every reworded sentence is rebound to test_quickstart_documented_ configuration.py, which runs the page's arguments with no fake native core, so the claims no longer rest on controls that supply the authority the documented path lacks. Refs AAASM-5661 --- docs/quick-start.md | 42 ++++- test/unit/test_quickstart_claim_bindings.py | 174 ++++++++++++++++++-- 2 files changed, 191 insertions(+), 25 deletions(-) diff --git a/docs/quick-start.md b/docs/quick-start.md index 57a2a71f..cd5ee975 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -1,7 +1,7 @@ # Quick Start Govern your first agent in about five minutes. By the end you'll have an agent — in whichever -framework you already use — whose tool calls pass through the Agent Assembly policy gate, and it +framework you already use — whose tool calls pass through an Agent Assembly adapter, and it runs **offline** against a local policy, so you need no API keys and no network access to the outside world. @@ -666,14 +666,15 @@ with no API keys and no outbound network. ## What just happened -1. **`init_assembly()` wired in governance.** It registered the agent with the gateway and - auto-loaded the adapter for your framework — every tool call from this point on is routed - through the policy gate. +1. **`init_assembly()` installed the governance hook.** It attempted to register the agent with + the gateway and auto-loaded the adapter for your framework, which patches that framework's + tool-invocation path. 2. **`mode="sdk-only"` kept it offline.** The in-process adapter enforces on tool calls with no network sidecar, so the example runs deterministically with no real LLM or gateway round-trip. -3. **Tool calls were governed.** The adapter intercepts the framework's tool-invocation path and - asks the policy engine for an allow/deny verdict before the tool actually runs. +3. **Tool calls were governed.** The adapter intercepts the framework's tool-invocation path + and, when a policy authority is reachable, asks it for an allow/deny verdict before the tool + actually runs. 4. **The `with` block tore everything down on exit** — adapter hooks were unwound and the gateway connection closed, leaving the process exactly as it was before. @@ -682,11 +683,36 @@ call. That's the product working. See [Handling allow/deny decisions](guides/handling-decisions.md) for how to catch and respond to those, and [Troubleshooting](troubleshooting.md) if `init_assembly()` itself raised. +## What this offline example evaluates + +The example passes a `gateway_url`, and running it offline means nothing is listening there. +A pure-Python `{{ aa.commands.install_pip }}` carries no native `agent_assembly._core` +extension either. + +`init_assembly()` reaches no policy authority in that configuration, so it evaluates no policy — +under +[ADR 0033 §6](https://github.com/ai-agent-assembly/agent-assembly/blob/master/docs/src/adr/0033-canonical-governance-and-enforcement-architecture.md) +the term for that state is **Degraded**, not *Evaluated*. +Under the default enforce posture the SDK takes its fail-closed branch instead: a governed tool +call is **denied before execution**, carrying a reason that names the absent extension rather +than a policy rule. +`init_assembly()` says as much at startup — it warns that the agent is unregistered, and that no +in-process policy decision can be made. + +That is why several framework tabs above revert the hook `init_assembly()` installed and +re-apply one wired to the example's own `LocalPolicyEngine`. +The local engine, not the SDK, is what returns allow and deny in the offline demo. + +Getting a decision from the SDK instead needs the native `agent_assembly._core` extension this +example lacks; install it with `{{ aa.commands.install_pip_runtime }}`. +[Point the SDK at a gateway](#2-point-the-sdk-at-a-gateway) above covers the other half of that +setup. + ## `mode="sdk-only"` — why this example uses it `mode="sdk-only"` is the in-process-only interception layer: the framework adapter enforces on -tool calls, with no network sidecar to start. It's the most portable mode and the best choice -for deterministic, offline examples and tests. The other modes (`auto`, `proxy`, `ebpf`) add +tool calls against a reachable policy authority, and starts no network sidecar. It's the most +portable mode and the best choice for deterministic, offline examples and tests. The other modes (`auto`, `proxy`, `ebpf`) add network/kernel interception — see [Core Concepts → Modes](concepts/index.md#runtime-modes). ## Next steps diff --git a/test/unit/test_quickstart_claim_bindings.py b/test/unit/test_quickstart_claim_bindings.py index b09375ca..5cc384ab 100644 --- a/test/unit/test_quickstart_claim_bindings.py +++ b/test/unit/test_quickstart_claim_bindings.py @@ -81,8 +81,15 @@ _QUICK_START = _REPO_ROOT / "docs" / "quick-start.md" #: Modules a binding may name a control from. +#: +#: AAASM-5661 added the third. The first proves what a *reachable* runtime does +#: and installs a fake native core to get one; the third proves what the +#: documented configuration does, which is to have none. Keeping them in separate +#: modules keeps that difference visible at the import line rather than buried in +#: a fixture. _CONTROL_MODULES = ( Path(__file__).with_name("test_quickstart_negative_control.py"), + Path(__file__).with_name("test_quickstart_documented_configuration.py"), Path(__file__).with_name("test_assembly.py"), ) @@ -186,15 +193,33 @@ class ClaimBinding: ) #: AAASM-5661 measured the documented configuration: it reaches no gateway and -#: installs a deny-all fail-closed interceptor. Every control here calls -#: install_fake_core(), supplying an authoritative runtime the documented path -#: does not have, so none of them exercises what these sentences describe. -#: Binding one would launder that gap into evidence. +#: installs a deny-all fail-closed interceptor. Every control in +#: test_quickstart_negative_control.py calls install_fake_core(), supplying an +#: authoritative runtime the documented path does not have, so none of them +#: exercises what these sentences describe. Binding one would launder that gap +#: into evidence. +#: +#: AAASM-5661 also closed the gap for the sentences it could: the ones about what +#: happens *after* init now name controls in +#: test_quickstart_documented_configuration.py, which runs the page's arguments +#: with no fake core. What is left here is what that module still cannot reach — +#: claims about a live gateway, which no unit control in this repo can stand up. _DOCUMENTED_PATH_UNMEASURED = ( "AAASM-5661: the documented configuration was measured and does not behave as this " "sentence says. No control covers it — every control in " "test_quickstart_negative_control.py installs a fake native core the documented " - "path does not have." + "path does not have, and the documented-configuration controls cannot stand up a " + "live gateway to prove what one would return." +) + +#: The documented configuration, measured end to end through Agno's own tool +#: path: a hook is installed, the body does not run, and the refusal names the +#: absent authority rather than a policy rule. +_DOCUMENTED_CONFIGURATION_CONTROLS = ( + "TestTheConfigurationTheQuickStartHandsAReader::test_a_governance_hook_is_installed_on_agnos_own_tool_path", + "TestTheConfigurationTheQuickStartHandsAReader" + "::test_the_refusal_is_the_fail_closed_posture_rather_than_a_policy_decision", + "TestTheConfigurationTheQuickStartHandsAReader::test_falsification_the_same_agno_tool_ungoverned_writes_the_file", ) BINDINGS: tuple[ClaimBinding, ...] = ( @@ -202,14 +227,22 @@ class ClaimBinding: claim_id="tool-calls-pass-through-the-policy-gate", quote=( "By the end you'll have an agent — in whichever framework you already use — whose " - "tool calls pass through the Agent Assembly policy gate, and it runs **offline** " + "tool calls pass through an Agent Assembly adapter, and it runs **offline** " "against a local policy, so you need no API keys and no network access to the " "outside world." ), # Found by inverting the default. It states the page's central promise # and matches no enforcement keyword, so every earlier revision of this # gate was blind to it. - unproven_reason=_DOCUMENTED_PATH_UNMEASURED, + # + # AAASM-5661 narrowed it from "the Agent Assembly policy gate" to "an + # Agent Assembly adapter", which is what the documented configuration + # actually puts on the tool path — measured through Agno's own + # FunctionCall.execute, so the adapter's presence is observed rather than + # inferred from init returning cleanly. The page's *policy gate* is the + # example's own LocalPolicyEngine, and the section added by that ticket + # says so. + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, ), ClaimBinding( claim_id="governs-whichever-framework-you-use", @@ -259,10 +292,19 @@ class ClaimBinding: ClaimBinding( claim_id="init-routes-every-tool-call", quote=( - "It registered the agent with the gateway and auto-loaded the adapter for your " - "framework — every tool call from this point on is routed through the policy gate." + "It attempted to register the agent with the gateway and auto-loaded the adapter " + "for your framework, which patches that framework's tool-invocation path." + ), + # AAASM-5661. The previous wording — "every tool call from this point on + # is routed through the policy gate" — was measured false on the + # documented path (there is no policy gate to route to) and used a banned + # absolute besides. What survives is what the controls observe: the + # adapter patches the framework's tool path, and registration is + # *attempted*, which on this configuration does not succeed. + controls=( + *_DOCUMENTED_CONFIGURATION_CONTROLS, + "TestTheConfigurationTheQuickStartHandsAReader::test_the_agent_is_not_registered_in_this_configuration", ), - unproven_reason=_DOCUMENTED_PATH_UNMEASURED, ), ClaimBinding( claim_id="sdk-only-enforces-on-tool-calls", @@ -275,9 +317,13 @@ class ClaimBinding: ClaimBinding( claim_id="verdict-precedes-execution", quote=( - "The adapter intercepts the framework's tool-invocation path and asks the policy " - "engine for an allow/deny verdict before the tool actually runs." + "The adapter intercepts the framework's tool-invocation path and, when a policy " + "authority is reachable, asks it for an allow/deny verdict before the tool " + "actually runs." ), + # AAASM-5661 added the condition. The controls below supply a reachable + # authority via install_fake_core(); unconditionally, the sentence + # described a configuration this page's own example does not produce. # Both halves. The negative controls prove the "before" by absence of # the side effect; the positive controls prove the probe would have seen # that effect had it happened. Either alone is vacuous. @@ -286,10 +332,14 @@ class ClaimBinding: ClaimBinding( claim_id="init-wired-in-governance-label", # Split out from the bullet it leads, once the splitter learned to keep - # closing markup with its sentence. Short, but still a claim: "wired in - # governance" asserts an outcome. - quote="**`init_assembly()` wired in governance.**", - unproven_reason=_DOCUMENTED_PATH_UNMEASURED, + # closing markup with its sentence. Short, but still a claim: it asserts + # an outcome. + # + # AAASM-5661 replaced "wired in governance" — an undifferentiated verb of + # exactly the kind ADR 0033 §6 rules out — with the narrower outcome the + # controls observe: a hook on the framework's tool path. + quote="**`init_assembly()` installed the governance hook.**", + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, ), ClaimBinding( claim_id="tool-calls-were-governed-label", @@ -320,8 +370,12 @@ class ClaimBinding: claim_id="sdk-only-is-the-in-process-interception-layer", quote=( '`mode="sdk-only"` is the in-process-only interception layer: the framework adapter ' - "enforces on tool calls, with no network sidecar to start." + "enforces on tool calls against a reachable policy authority, and starts no network " + "sidecar." ), + # AAASM-5661 named the precondition the deny controls actually satisfy. + # Without it the sentence read as unconditional and was falsified by the + # page's own example, which reaches no authority at all. controls=_DENY_CONTROLS, ), ClaimBinding( @@ -341,6 +395,86 @@ class ClaimBinding: "mode is selected'. AAASM-5766 owns proving or qualifying it." ), ), + # ---------------------------------------------------------------- AAASM-5661 + # "What this offline example evaluates". The section exists because the + # sentences above it were measured false on the page's own configuration, so + # every sentence in it is bound to the module that made that measurement. + ClaimBinding( + claim_id="offline-example-has-no-native-extension", + quote=( + "A pure-Python `{{ aa.commands.install_pip }}` carries no native `agent_assembly._core` extension either." + ), + # The premise the rest of the section rests on, pinned by the control + # that refuses to run if the premise stops holding. + controls=( + "TestTheConfigurationTheQuickStartHandsAReader::test_the_environment_under_test_has_no_native_authority", + ), + ), + ClaimBinding( + claim_id="offline-example-evaluates-no-policy", + quote=( + "`init_assembly()` reaches no policy authority in that configuration, so it " + "evaluates no policy — under [ADR 0033 §6](https://github.com/ai-agent-assembly/" + "agent-assembly/blob/master/docs/src/adr/" + "0033-canonical-governance-and-enforcement-architecture.md) the term for that " + "state is **Degraded**, not *Evaluated*." + ), + # A negative capability claim, and the one that replaces the page's + # central over-claim. The control reads the refusal's reason: it names + # the absent extension, which a policy verdict could not. + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, + ), + ClaimBinding( + claim_id="offline-example-denies-before-execution", + quote=( + "Under the default enforce posture the SDK takes its fail-closed branch instead: a " + "governed tool call is **denied before execution**, carrying a reason that names " + "the absent extension rather than a policy rule." + ), + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, + ), + ClaimBinding( + claim_id="offline-example-warns-at-startup", + quote=( + "`init_assembly()` says as much at startup — it warns that the agent is " + "unregistered, and that no in-process policy decision can be made." + ), + controls=( + "TestTheConfigurationTheQuickStartHandsAReader" + "::test_startup_reports_both_the_registration_and_the_enforcement_gap", + "TestTheConfigurationTheQuickStartHandsAReader::test_the_agent_is_not_registered_in_this_configuration", + ), + ), + ClaimBinding( + claim_id="framework-tabs-revert-the-installed-hook", + quote=( + "That is why several framework tabs above revert the hook `init_assembly()` " + "installed and re-apply one wired to the example's own `LocalPolicyEngine`." + ), + # About the page's own generated tabs. Bound rather than allow-listed + # because the tabs are vendored from another repo: an upstream edit that + # drops the workaround would otherwise leave this sentence quietly false. + controls=( + "TestTheWorkaroundTheFrameworkTabsCarry::test_the_tabs_still_revert_the_hook_init_assembly_installed", + ), + ), + ClaimBinding( + claim_id="the-local-engine-decides-in-the-offline-demo", + quote="The local engine, not the SDK, is what returns allow and deny in the offline demo.", + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, + ), + ClaimBinding( + claim_id="native-extension-is-required-for-an-sdk-decision", + quote=( + "Getting a decision from the SDK instead needs the native `agent_assembly._core` " + "extension this example lacks; install it with " + "`{{ aa.commands.install_pip_runtime }}`." + ), + # A necessity claim, and only that. The controls show the SDK returns no + # decision without the extension; nothing here shows that installing it + # is sufficient, and the sentence deliberately does not say so. + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, + ), ) #: Every sentence in the quick-start that makes no capability claim, keyed @@ -539,6 +673,12 @@ class ClaimBinding: "**[Configuration](configuration.md)** — drop the hard-coded URL and key; let the resolver chain find them.": ( "A Next-steps link item about configuration ergonomics: where the URL and key come from, not what governance does." ), + "The example passes a `gateway_url`, and running it offline means nothing is listening there.": ( + "AAASM-5661. Describes the example's own setup — an argument it passes and a listener the reader was told not to start. A premise for the bound sentences that follow, asserting nothing the SDK does with it." + ), + "[Point the SDK at a gateway](#2-point-the-sdk-at-a-gateway) above covers the other half of that setup.": ( + "AAASM-5661. A cross-reference to §2 of this same page. It names where the gateway setup is written down and asserts nothing about what the SDK evaluates, denies or observes once one is running." + ), } From d743389814dac285480ecc7b470faa44ee1b1668 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:27:23 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=94=A7=20(ci):=20Run=20the=20document?= =?UTF-8?q?ed-configuration=20controls=20in=20the=20bindings=20job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The claim-bindings gate checks a named control still exists; it cannot check the control still passes. ci.yaml does not run on a docs-only PR, which is precisely the change that rewords a claim, so the named evidence went unexecuted on exactly those PRs. Refs AAASM-5661 --- .github/workflows/quickstart-tabs-check.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quickstart-tabs-check.yml b/.github/workflows/quickstart-tabs-check.yml index 6c680457..feacb760 100644 --- a/.github/workflows/quickstart-tabs-check.yml +++ b/.github/workflows/quickstart-tabs-check.yml @@ -19,6 +19,9 @@ on: # these entries matter for the docs-only PR, which ci.yaml deliberately skips. - "test/unit/test_quickstart_claim_bindings.py" - "test/unit/test_quickstart_negative_control.py" +# AAASM-5661: the second control module the gate reads — the one that runs the +# documented configuration with no fake native core. + - "test/unit/test_quickstart_documented_configuration.py" - "agent_assembly/exceptions/**" push: branches: @@ -79,5 +82,13 @@ jobs: - name: Install the SDK and its dev dependencies run: uv sync + # AAASM-5661: the controls run here too, not only the bindings. A binding + # names a control by node id, and the gate checks the id still exists — it + # cannot tell whether that control still passes. On a docs-only PR ci.yaml + # does not run, so without this the named evidence went unexecuted. - name: Every documented enforcement claim names the control that proves it - run: uv run pytest test/unit/test_quickstart_claim_bindings.py -q --no-cov + run: >- + uv run pytest + test/unit/test_quickstart_claim_bindings.py + test/unit/test_quickstart_documented_configuration.py + -q --no-cov From 7e2f4aec2ce4d67062f96d0cf8881ea35b4e717e Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:44:54 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=90=9B=20(test):=20Repoint=20the=20un?= =?UTF-8?q?proven=20claims=20off=20the=20ticket=20that=20closes=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three unproven_reason referents named AAASM-5661 — the ticket this PR closes. On merge each would have resolved to finished work that never intended to deliver the capability, which is the stale-referent failure AAASM-5750 exists to eliminate. auto-start-probes-and-starts-a-gateway -> AAASM-5760 no-arg-init-connects-and-appears-in-dashboard -> AAASM-5760 gateway-returns-allow-deny-decisions -> AAASM-5758 AAASM-5760 carries both measurements verbatim: the aasm console script shadowing the bundled binary is its defect #1, and the gateway-less call raising rather than degrading is its defect #2. AAASM-5758 runs each documented quick-start from published artifacts against a real gateway, which is the only place a claim about what a gateway returns can be shown true; it lists AAASM-5661 among its blockers, so it cannot close first. The shared _DOCUMENTED_PATH_UNMEASURED body no longer carries a ticket of its own — that is what let one referent serve two claims with different owners. Refs AAASM-5661, AAASM-5760, AAASM-5758 --- test/unit/test_quickstart_claim_bindings.py | 55 ++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/test/unit/test_quickstart_claim_bindings.py b/test/unit/test_quickstart_claim_bindings.py index 5cc384ab..e0a4c2ee 100644 --- a/test/unit/test_quickstart_claim_bindings.py +++ b/test/unit/test_quickstart_claim_bindings.py @@ -202,14 +202,21 @@ class ClaimBinding: #: AAASM-5661 also closed the gap for the sentences it could: the ones about what #: happens *after* init now name controls in #: test_quickstart_documented_configuration.py, which runs the page's arguments -#: with no fake core. What is left here is what that module still cannot reach — -#: claims about a live gateway, which no unit control in this repo can stand up. +#: with no fake core. What is left is what that module cannot reach — claims that +#: need a runtime or a gateway actually standing up. +#: +#: The shared body carries NO ticket. It used to, and the ticket it carried was +#: AAASM-5661 — the one that closes on this merge, which is precisely the +#: stale-referent shape AAASM-5750 exists to eliminate: a pointer aimed at a +#: ticket that finishes without delivering the capability, so a reader who +#: follows it finds completed work that never intended to. Each claim below now +#: names the ticket that will actually resolve *it*, prefixed to this body. _DOCUMENTED_PATH_UNMEASURED = ( - "AAASM-5661: the documented configuration was measured and does not behave as this " - "sentence says. No control covers it — every control in " - "test_quickstart_negative_control.py installs a fake native core the documented " - "path does not have, and the documented-configuration controls cannot stand up a " - "live gateway to prove what one would return." + "the documented configuration was measured and does not behave as this sentence " + "says. No control covers it — every control in test_quickstart_negative_control.py " + "installs a fake native core the documented path does not have, and the " + "documented-configuration controls cannot stand up a live gateway to prove what one " + "would return." ) #: The documented configuration, measured end to end through Agno's own tool @@ -269,11 +276,17 @@ class ClaimBinding: # on PATH, so find_aasm_binary() resolves the Python one, which has no # `start` subcommand. The documented auto-start therefore cannot work # from a clean install. + # + # Owned by AAASM-5760, whose description carries this exact measurement + # as its defect #1. It previously named AAASM-5661, which measured the + # defect but does not fix it — a packaging change, not a documentation + # one — so that pointer would have resolved to closed work. unproven_reason=( - "AAASM-5661: no control covers the documented auto-start path, and it was " + "AAASM-5760: no control covers the documented auto-start path, and it was " "measured not to work from a clean install — the [project.scripts] aasm " "console script shadows the bundled binary, and the shadowing one has no " - "'start' subcommand." + "'start' subcommand. AAASM-5760 owns resolving the binary or naming the " + "command that exists." ), ), ClaimBinding( @@ -282,12 +295,32 @@ class ClaimBinding: "You don't configure `:50051` yourself — registration dials it automatically — so a " "no-argument `init_assembly()` both connects and shows the agent in the dashboard." ), - unproven_reason=_DOCUMENTED_PATH_UNMEASURED, + # Owned by AAASM-5760's defect #2, "a gateway-less call raises instead of + # degrading", whose AC is that the gateway-less path either degrades with + # a stated posture or the documentation says it raises. That is this + # sentence: measured, a no-argument init_assembly() with the native core + # present and no gateway raises ConfigurationError, and without the + # native core registration never runs, so the agent does not appear. + unproven_reason=( + f"AAASM-5760: {_DOCUMENTED_PATH_UNMEASURED} Neither half of 'connects and " + "shows the agent in the dashboard' holds on the documented path, and " + "AAASM-5760 owns making the gateway-less path degrade or saying that it " + "raises." + ), ), ClaimBinding( claim_id="gateway-returns-allow-deny-decisions", quote=("`init_assembly()` needs to reach a **gateway** — the policy brain that returns allow/deny decisions."), - unproven_reason=_DOCUMENTED_PATH_UNMEASURED, + # What a *live gateway* returns cannot be shown by any unit control in + # this repo; it needs the documented path standing up end to end. That is + # AAASM-5758, which runs each documented quick-start from published + # artifacts only — and which lists AAASM-5661 among its blockers, so it + # cannot be the ticket that closes first. + unproven_reason=( + f"AAASM-5758: {_DOCUMENTED_PATH_UNMEASURED} AAASM-5758 owns running the " + "documented quick-start from published artifacts against a real gateway, " + "which is the only place this sentence can be shown true." + ), ), ClaimBinding( claim_id="init-routes-every-tool-call", From c94425e12768772472f0201e1368d3b5315fa408 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 09:46:38 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=9A=A8=20(test):=20Ban=20every=20impl?= =?UTF-8?q?ementing=20ticket=20as=20a=20referent,=20not=20just=20the=20fir?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale-referent rule compared against one string, so it banned the module's original author and nobody after them — the one author who was never going to break it. AAASM-5661 edited these bindings, pointed three unproven claims at itself, and the gate stayed green for the whole PR because the banned name was still AAASM-5529. Widen it to a tuple every subsequent ticket appends itself to, since each closes on merge and none may be a referent afterwards. Proven able to fail: repointing one claim back at AAASM-5661 turns test_an_unproven_reason_does_not_name_an_implementing_ticket red (exit 1); reverting it green (exit 0). Refs AAASM-5661 --- test/unit/test_quickstart_claim_bindings.py | 40 +++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/test/unit/test_quickstart_claim_bindings.py b/test/unit/test_quickstart_claim_bindings.py index e0a4c2ee..b6a86fe0 100644 --- a/test/unit/test_quickstart_claim_bindings.py +++ b/test/unit/test_quickstart_claim_bindings.py @@ -73,9 +73,16 @@ # makes a rename a *collection* error, aborting before the assertion meant to # catch it can run. -#: The ticket this module implements. An unproven claim may not name it — see -#: test_an_unproven_reason_does_not_name_the_implementing_ticket. -IMPLEMENTING_TICKET = "AAASM-5529" +#: Tickets whose work lands in this module. An unproven claim may not name any of +#: them — see test_an_unproven_reason_does_not_name_an_implementing_ticket. +#: +#: A tuple rather than one string (AAASM-5661). As a single value the rule caught +#: only the module's original author: AAASM-5661 edited these bindings and pointed +#: three unproven claims at *itself*, and the gate stayed green through the whole +#: PR because the banned name was still 5529. Every ticket that edits this module +#: appends itself here, because every one of them closes on merge and none may be +#: a referent afterwards. +IMPLEMENTING_TICKETS: tuple[str, ...] = ("AAASM-5529", "AAASM-5661") _REPO_ROOT = Path(__file__).resolve().parents[2] _QUICK_START = _REPO_ROOT / "docs" / "quick-start.md" @@ -176,7 +183,7 @@ class ClaimBinding: #: ``ClassName::test_name`` or ``test_name`` ids from _CONTROL_MODULES. controls: tuple[str, ...] = () #: Set when no control proves the claim. Must name a ticket, and must not - #: name IMPLEMENTING_TICKET. + #: name any of IMPLEMENTING_TICKETS. unproven_reason: str = "" #: Backticked SDK identifiers the claim names -> the module they live in. symbols: dict[str, str] = field(default_factory=dict) @@ -1027,18 +1034,23 @@ def test_a_claim_is_either_proven_or_openly_unproven(self, binding: ClaimBinding ) @pytest.mark.parametrize("binding", BINDINGS, ids=lambda b: b.claim_id) - def test_an_unproven_reason_does_not_name_the_implementing_ticket(self, binding: ClaimBinding) -> None: - """An unproven claim may not point at the ticket that closes it. + def test_an_unproven_reason_does_not_name_an_implementing_ticket(self, binding: ClaimBinding) -> None: + """An unproven claim may not point at a ticket that closes it. + + A reason naming one of this module's own tickets resolves to a *closed* + issue the moment that work merges, and nothing would notice: the + ticket-shaped check above is satisfied by any AAASM-nnnn, open or not. - A reason naming this module's own ticket resolves to a *closed* issue the - moment this work merges, and nothing would notice: the ticket-shaped - check above is satisfied by any AAASM-nnnn, open or not. + Checked against every entry, not only the first (AAASM-5661). A + single-value rule bans the author who wrote the rule and nobody after + them, which is the one author who was never going to break it. """ - assert IMPLEMENTING_TICKET not in binding.unproven_reason, ( - f"Claim {binding.claim_id!r} is registered unproven against {IMPLEMENTING_TICKET}, " - "the ticket this module implements. On merge that pointer resolves to a closed " - "issue and the claim is silently orphaned. Name the ticket that will actually " - "resolve it, or file one." + named = [ticket for ticket in IMPLEMENTING_TICKETS if ticket in binding.unproven_reason] + assert not named, ( + f"Claim {binding.claim_id!r} is registered unproven against {named}, which this " + "module implements. On merge that pointer resolves to a closed issue and the " + "claim is silently orphaned. Name the ticket that will actually resolve it, or " + "file one." ) @pytest.mark.parametrize("binding", BINDINGS, ids=lambda b: b.claim_id) From e4c411b93cc5dc34c91b809fa5f922aa105c4bc4 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 10:00:28 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=85=20(test):=20Run=20the=20real=20ne?= =?UTF-8?q?twork=20layer=20to=20show=20sdk-only=20starts=20no=20sidecar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AAASM-5529 controls monkeypatch _start_network_layer away, so nothing observed what mode="sdk-only" actually does. Assert the no-op shutdown hook rather than the network_mode string: the string is what the caller asked for, a started sidecar would leave a real teardown behind. Refs AAASM-5661 --- .../unit/test_quickstart_documented_configuration.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/test_quickstart_documented_configuration.py b/test/unit/test_quickstart_documented_configuration.py index 4a4aa243..ee7e58a6 100644 --- a/test/unit/test_quickstart_documented_configuration.py +++ b/test/unit/test_quickstart_documented_configuration.py @@ -175,6 +175,18 @@ def test_the_agent_is_not_registered_in_this_configuration(self, documented_cont """``registered`` is the programmatic counterpart of the stderr warning.""" assert documented_context.registered is False + def test_no_network_sidecar_starts_in_sdk_only_mode(self, documented_context: Any) -> None: + """What `mode="sdk-only"` actually buys the example: determinism, not enforcement. + + The AAASM-5529 controls monkeypatch ``_start_network_layer`` away, so they + cannot speak to this; here the real one runs. Asserting the shutdown hook + is the no-op — not merely that ``network_mode`` reads ``"sdk-only"`` — + because the mode string is what the caller asked for, and a started + sidecar would leave a real teardown behind. + """ + assert documented_context.network_mode == "sdk-only" + assert documented_context._network_shutdown is core_assembly._noop_shutdown + def test_startup_reports_both_the_registration_and_the_enforcement_gap( self, capsys: pytest.CaptureFixture[str] ) -> None: From 89cd4b5867081ecdb9eaaccefd2c11d7797ba470 Mon Sep 17 00:00:00 2001 From: Bryant Date: Fri, 14 Aug 2026 10:00:34 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Stop=20three=20mor?= =?UTF-8?q?e=20sentences=20implying=20a=20policy=20decided?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full-page sweep for the class — a sentence asserting enforcement unconditionally while resting on controls that install a fake native core — found five. Two were corrected in the first pass; these are the rest. :672 "the in-process adapter enforces on tool calls with no network sidecar" — the same claim as the mode section below, left unconditional while that one was qualified. The enforcement half now lives only in the qualified sentence. :675 "**Tool calls were governed.**" — named by nobody; found by the sweep. A bare past-tense assertion about this example, where the calls were refused without a policy governing them. :681 "the policy denied the call" — the sentence a reader meets at the moment their tool blocks, ten lines above the section saying the opposite. Its own binding named a control whose point is that there is no verdict source. :681 now says what refused it and how to tell which, with a control per disjunct: the deny controls read the policy text out of the reason, the documented-path control reads the absent-extension text out of it. Refs AAASM-5661 --- docs/quick-start.md | 17 +++--- test/unit/test_quickstart_claim_bindings.py | 63 +++++++++++++++++++-- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/docs/quick-start.md b/docs/quick-start.md index cd5ee975..918ad001 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -669,17 +669,18 @@ with no API keys and no outbound network. 1. **`init_assembly()` installed the governance hook.** It attempted to register the agent with the gateway and auto-loaded the adapter for your framework, which patches that framework's tool-invocation path. -2. **`mode="sdk-only"` kept it offline.** The in-process adapter enforces on tool calls with no - network sidecar, so the example runs deterministically with no real LLM or gateway - round-trip. -3. **Tool calls were governed.** The adapter intercepts the framework's tool-invocation path - and, when a policy authority is reachable, asks it for an allow/deny verdict before the tool - actually runs. +2. **`mode="sdk-only"` kept it offline.** No network sidecar starts in that mode, so the example + runs deterministically with no real LLM or gateway round-trip. +3. **Tool calls went through the adapter.** The adapter intercepts the framework's + tool-invocation path and, when a policy authority is reachable, asks it for an allow/deny + verdict before the tool actually runs. 4. **The `with` block tore everything down on exit** — adapter hooks were unwound and the gateway connection closed, leaving the process exactly as it was before. -If a tool call raises a `ToolExecutionBlockedError`, that is not a bug — the policy denied the -call. That's the product working. See +If a tool call raises a `ToolExecutionBlockedError`, that is not a bug — something refused the +call before it ran. Read the exception's reason to see what refused it: a policy rule that denied +the call, or — as in this offline example — an SDK that had no authority to ask and refused +rather than run ungoverned. That's the product working. See [Handling allow/deny decisions](guides/handling-decisions.md) for how to catch and respond to those, and [Troubleshooting](troubleshooting.md) if `init_assembly()` itself raised. diff --git a/test/unit/test_quickstart_claim_bindings.py b/test/unit/test_quickstart_claim_bindings.py index b6a86fe0..a63655ae 100644 --- a/test/unit/test_quickstart_claim_bindings.py +++ b/test/unit/test_quickstart_claim_bindings.py @@ -349,10 +349,19 @@ class ClaimBinding: ClaimBinding( claim_id="sdk-only-enforces-on-tool-calls", quote=( - "The in-process adapter enforces on tool calls with no network sidecar, so the " - "example runs deterministically with no real LLM or gateway round-trip." + "No network sidecar starts in that mode, so the example runs deterministically " + "with no real LLM or gateway round-trip." ), - controls=_DENY_CONTROLS, + # AAASM-5661, second pass. This said "the in-process adapter enforces on + # tool calls with no network sidecar" — the same claim as the + # `mode="sdk-only"` section further down, which that pass qualified while + # leaving this one unconditional, and rested on _DENY_CONTROLS, both of + # which call install_fake_core(). Two sentences making one claim is one + # sentence too many; the enforcement half now lives only in the qualified + # one, and what is left here is what `sdk-only` actually buys the example. + # Bound to the control that runs the real _start_network_layer rather than + # monkeypatching it away. + controls=("TestTheConfigurationTheQuickStartHandsAReader::test_no_network_sidecar_starts_in_sdk_only_mode",), ), ClaimBinding( claim_id="verdict-precedes-execution", @@ -383,8 +392,15 @@ class ClaimBinding: ), ClaimBinding( claim_id="tool-calls-were-governed-label", - quote="**Tool calls were governed.**", - controls=_ALLOW_AND_DENY_CONTROLS, + quote="**Tool calls went through the adapter.**", + # AAASM-5661, second pass. Found by sweeping the page for the claim class + # rather than the sites review named — nobody flagged this one. "were + # governed" is a bare past-tense assertion about *this* example, in the + # undifferentiated register ADR 0033 §6 rules out, and it rested on + # controls that install a fake native core. On the configuration the page + # hands a reader the calls were refused without a policy governing them, + # so the label now says the part that held: they reached the adapter. + controls=_DOCUMENTED_CONFIGURATION_CONTROLS, ), ClaimBinding( claim_id="with-block-tears-everything-down", @@ -398,14 +414,49 @@ class ClaimBinding: ), ClaimBinding( claim_id="deny-surfaces-as-tool-execution-blocked", - quote=("If a tool call raises a `ToolExecutionBlockedError`, that is not a bug — the policy denied the call."), + quote=( + "If a tool call raises a `ToolExecutionBlockedError`, that is not a bug — something " + "refused the call before it ran." + ), + # AAASM-5661, second pass. This read "— the policy denied the call", + # which a reader meets at the moment their tool blocks. On the documented + # configuration every governed call raises this and no policy denied any + # of them. The tell was in the binding: it already named + # test_an_unavailable_native_runtime_denies_rather_than_silently_allowing, + # a control whose whole point is that there is no verdict source — the + # named evidence was the counter-example to the clause. + # + # "something refused" is the claim both halves support: a policy deny + # (the fake-core controls) and a no-authority refusal (the documented + # ones) each raise it. controls=( *_DENY_CONTROLS, + *_DOCUMENTED_CONFIGURATION_CONTROLS, "TestDegradedRuntimeCannotLookProtected" "::test_an_unavailable_native_runtime_denies_rather_than_silently_allowing", ), symbols={"ToolExecutionBlockedError": "agent_assembly.exceptions"}, ), + ClaimBinding( + claim_id="the-exception-reason-says-what-refused", + quote=( + "Read the exception's reason to see what refused it: a policy rule that denied the " + "call, or — as in this offline example — an SDK that had no authority to ask and " + "refused rather than run ungoverned." + ), + # AAASM-5661, second pass. The replacement for what "the policy denied the + # call" used to assert, and stronger than it: each disjunct has its own + # control reading the reason string. The deny controls assert the policy + # text reaches it ("policy forbids disk writes"); the documented-path + # control asserts the other branch names the absent extension. Binding + # only one of the two would leave the sentence half-evidenced in exactly + # the direction that misleads. + controls=( + *_DENY_CONTROLS, + "TestTheConfigurationTheQuickStartHandsAReader" + "::test_the_refusal_is_the_fail_closed_posture_rather_than_a_policy_decision", + ), + ), ClaimBinding( claim_id="sdk-only-is-the-in-process-interception-layer", quote=(