From 53996eda0b8777e494681e5e70d25a6a91a01e64 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20Haystack=20e?= =?UTF-8?q?xample=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/haystack.md | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/examples/haystack.md diff --git a/docs/examples/haystack.md b/docs/examples/haystack.md new file mode 100644 index 00000000..3f671cea --- /dev/null +++ b/docs/examples/haystack.md @@ -0,0 +1,117 @@ +# Haystack + +Integrates Agent Assembly with [Haystack](https://haystack.deepset.ai/) (deepset) to enforce governance policy on a real Haystack agent's tool calls before they execute, using the SDK's native Haystack adapter. + +## What this example demonstrates + +- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode (which also auto-detects Haystack). +- Installing the native Haystack adapter — `HaystackPatch` patches `haystack.tools.Tool.invoke`, the single tool-execution chokepoint Haystack 2.x uses. +- Running real `haystack.tools.Tool` instances through a genuine `ToolInvoker` (the component a Haystack `Agent` uses to run a model-chosen tool call). +- An **allowed** tool call (`query_index`) and another **allowed** tool call (`summarize_docs`) — their tool bodies execute. +- A **denied** tool call (`execute_sql`, blocked by `deny_arbitrary_execution`) — the deny short-circuits `Tool.invoke` and returns a `[BLOCKED by governance policy]` result *before the tool body runs*. +- No LLM, API key, or running gateway is involved — the tools are driven offline through a hand-built `ToolCall`. + +## The framework / library + +[Haystack](https://haystack.deepset.ai/) (deepset; installed as `haystack-ai`, imported as `haystack`) is the agent framework governed in this example. Haystack 2.x is the line that exposes the agentic `Agent` / `ToolInvoker` tool-call path and the `haystack.tools.Tool.invoke` hook point the adapter patches; the 1.x line predates that API and is out of scope. + +Version pins (from `pyproject.toml`): + +| Dependency | Version | +|---|---| +| `haystack-ai` | `>=2.0.0,<3.0` | +| `agent-assembly` | `>=0.0.1b2` | +| Python | `>=3.12` | + +## How it works + +`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `haystack-demo-agent`: + +```python +with init_assembly( + gateway_url=gateway_url, + api_key=api_key, + agent_id="haystack-demo-agent", + mode="sdk-only", +) as ctx: + ... +``` + +**The hook point.** The native adapter patches `haystack.tools.Tool.invoke`. That single method is the execution chokepoint Haystack 2.x routes *every* tool through: it governs both the bare `Tool.invoke()` path and the full agent loop, because `haystack.components.agents.Agent` dispatches its tool calls via `haystack.components.tools.ToolInvoker`, which itself calls `tool_to_invoke.invoke(**final_args)`. Patching `Tool.invoke` therefore intercepts every tool execution — which is why governance is wired there and not at the higher-level `Agent` / `ToolInvoker` layer. `HaystackAdapter.get_supported_versions()` returns `[">=2.0.0,<3.0"]`. + +**Offline note.** No LLM is needed. The example builds three real `haystack.tools.Tool` instances (`src/tools.py`) and drives them through a genuine `ToolInvoker` fed a hand-built `ToolCall` — the exact shape a chat model would emit — so the governed `Tool.invoke` is exercised on the real agent tool-dispatch path with no model, credentials, or gateway in the loop. + +**How deny short-circuits.** The patched `Tool.invoke` first calls the interceptor's `check_tool_start` hook. The offline `LocalPolicyEngine` (`src/policy.py`) returns a gateway-format decision dict — `{"status": "deny", "reason": ...}` for the arbitrary-execution tools, `{"status": "allow"}` otherwise. On a deny the adapter returns a `[BLOCKED by governance policy]` message *without calling the original `invoke`*, so the tool's underlying function never runs. The policy carries `_enforce = True`, putting the adapter in the fail-closed `enforce` posture (an unknown or malformed verdict denies rather than silently allowing). + +In offline `sdk-only` mode `init_assembly()` wires a no-op interceptor (there is no live gateway to answer policy), so the example reverts that and re-installs the same native adapter against the `LocalPolicyEngine` to make a real allow/deny visible without a gateway: + +```python +HaystackPatch(LocalPolicyEngine()).revert() # drop the auto-applied no-op patch +patch = HaystackPatch(LocalPolicyEngine()) +installed = patch.apply() +``` + +In production you instead point `init_assembly()` at a gateway and let its auto-detected adapter enforce real policy — no manual re-install is needed. + +## Prerequisites & running it + +See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites. + +Then run the example (offline — no API key and no running gateway required): + +```bash +cd python/haystack-tool-policy +uv sync --extra dev +uv run python src/main.py +``` + +### Expected output + +The two safe tools run and the destructive tool is blocked before its body executes: + +``` +============================================================== + Agent Assembly — Haystack Tool Policy Demo +============================================================== + +Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode)... + Agent: haystack-demo-agent + Gateway: http://localhost:8080 + Mode: sdk-only (offline demo) + +Policy rules (local simulation of gateway policy): + DENY — execute_sql, run_shell_command (arbitrary execution) + ALLOW — everything else + +Installing the native Haystack adapter against the demo policy... + Adapter installed: True + +Running real Haystack tools through a ToolInvoker: +-------------------------------------------- + → query_index({'query': 'what is Agent Assembly?'}) + ✅ ALLOWED — Index results for 'what is Agent Assembly?': [chunk-12, chunk-44, chunk-07] (mock) + + → summarize_docs({'topic': 'policy enforcement'}) + ✅ ALLOWED — Summary for 'policy enforcement': Agent Assembly provides governance... (mock) + + → execute_sql({'sql': 'DROP TABLE users; --'}) + ❌ BLOCKED — [BLOCKED by governance policy] Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'... + +Tool bodies that actually executed: ['query_index', 'summarize_docs'] +``` + +`execute_sql` is absent from the executed list — the deny short-circuited it before the tool ran, proving real governance rather than a no-op. + +## Run the smoke tests + +The example ships offline smoke tests that assert an allowed tool runs and a denied tool's body never executes: + +```bash +uv run pytest tests/ -v +``` + +## Links + +- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/haystack-tool-policy) +- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/haystack-tool-policy/README.md) +- [Haystack documentation](https://haystack.deepset.ai/) From 066b3e42c71c3fe921818b79d935176edc8d11d1 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20Smolagents?= =?UTF-8?q?=20example=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/smolagents.md | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/examples/smolagents.md diff --git a/docs/examples/smolagents.md b/docs/examples/smolagents.md new file mode 100644 index 00000000..c67b0819 --- /dev/null +++ b/docs/examples/smolagents.md @@ -0,0 +1,117 @@ +# Smolagents + +Integrates Agent Assembly with [Smolagents](https://github.com/huggingface/smolagents) (Hugging Face) to enforce governance policy on tool calls before they execute. + +## What this example demonstrates + +- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode. +- Governing **real** `smolagents.Tool` instances — `SmolagentsAdapter` patches `smolagents.tools.Tool.__call__`, the single chokepoint every smolagents tool execution flows through (`Tool.__call__` runs `self.forward(...)`). +- Running an **allowed** tool call (`search_docs`, `summarize`) that executes its real body, and a **denied** tool call (`run_shell_command`) that is short-circuited with the `[BLOCKED by governance policy]` message **before** `forward()` runs. +- How a denied tool's `forward()` body genuinely never executes — proven by a negative-control smoke test that runs the same tool ungoverned and watches it execute. +- A fully **offline** run: no running gateway, no model, and no API credentials. + +## The framework / library + +[Smolagents](https://github.com/huggingface/smolagents) (Hugging Face) is the agent framework governed in this example. Every smolagents tool — whether driven by a `ToolCallingAgent` or a `CodeAgent` — executes through `Tool.__call__`, so a single hook governs both agent styles. + +Version pins (from `pyproject.toml`): + +| Dependency | Version | +|---|---| +| `smolagents` | `>=1.0.0,<2.0.0` | +| `agent-assembly` | `>=0.0.1b2` | +| Python | `>=3.12` | + +## How it works + +`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `smolagents-demo-agent`: + +```python +with init_assembly( + gateway_url=gateway_url, + api_key=api_key, + agent_id="smolagents-demo-agent", + mode="sdk-only", +) as ctx: + ... +``` + +**The hook point.** The Smolagents adapter patches `smolagents.tools.Tool.__call__` — the single tool-execution chokepoint in the 1.x line. Both agent styles reach it: the `ToolCallingAgent` path (`MultiStepAgent.execute_tool_call` resolves the tool and calls `tool(...)`) and the `CodeAgent` path (tools are injected into the sandbox namespace and invoked as plain callables, which dispatches to `Tool.__call__`). Because `__call__` runs `self.forward(*args, **kwargs)` to execute the tool body, wrapping it lets governance observe the tool name and arguments and decide **before** the body runs. + +When the policy returns a `deny` verdict, the wrapper returns the `[BLOCKED by governance policy]` message **instead of** calling `forward()`, so the denied tool body never executes (fail-closed under `enforce`). On `allow`, the real `forward()` runs and its result is recorded. + +**Offline note.** A smolagents agent (`ToolCallingAgent` / `CodeAgent`) drives its loop against an **LLM** that *decides* which tool to call, which needs a model and a network call. To keep the example runnable with no secrets, it does not start a live model. Instead it invokes the real governed `smolagents.Tool` instances directly — the exact call (`tool(**inputs)` → `Tool.__call__` → `forward`) a smolagents agent makes to execute a tool. The genuine allow / deny governance code runs; only the LLM that would *choose* the tools is absent. The tools are real `smolagents.Tool` subclasses and the governance is the production `SmolagentsPatch` — this is not a mock. + +**Wiring note.** `init_assembly()` auto-detects smolagents and installs the hook with its default interceptor. So that the **offline policy** wins, the example applies the adapter's patch with a `LocalPolicyEngine` **before** calling `init_assembly()` (the patch is idempotent, so init's auto-detect then leaves the already-governed `Tool.__call__` alone). In production you skip this — `init_assembly()` wires the real gateway-backed interceptor for you. + +```python +policy = LocalPolicyEngine() +patch = SmolagentsPatch(policy) +patch.apply() +``` + +The offline `LocalPolicyEngine` returns decisions in the gateway wire format (`src/policy.py`). It denies the destructive tools and allows everything else. + +## Prerequisites & running it + +See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites. + +Then run the example (offline — no model/API credentials and no running gateway required): + +```bash +cd python/smolagents-tool-policy +uv sync --extra dev +uv run python src/main.py +``` + +### Expected output + +``` +============================================================== + Agent Assembly — Smolagents Tool Policy Demo +============================================================== + +Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode)... + Agent: smolagents-demo-agent + Gateway: http://localhost:8080 + Mode: sdk-only (offline demo) + +Policy rules (local simulation of gateway policy): + DENY — run_shell_command, delete_records (destructive ops) + ALLOW — everything else + +Governing real smolagents.Tool instances via Tool.__call__: + Tools: search_docs, summarize, run_shell_command + +Running governed tool calls: +-------------------------------------------- + → search_docs({'query': 'what is Agent Assembly?'}) + ✅ ALLOWED — docs results for 'what is Agent Assembly?': [chunk-12, chunk-44, chunk-07] (mock) + + → summarize({'topic': 'policy enforcement'}) + ✅ ALLOWED — summary of 'policy enforcement': Agent Assembly governs agent tool calls... (mock) + + → run_shell_command({'command': 'rm -rf /var/data'}) + ❌ BLOCKED — [BLOCKED by governance policy] Tool 'run_shell_command' is blocked by policy rule 'deny_destructive_operations'. + +Assembly context shut down. +``` + +The two safe tools (`search_docs`, `summarize`) run their real bodies; the destructive `run_shell_command` is short-circuited by policy before its `forward()` executes. + +## Run the smoke tests + +The example ships an offline smoke suite that exercises the real adapter governing real `smolagents.Tool` instances: + +```bash +cd python/smolagents-tool-policy +uv run pytest tests/ -v +``` + +It asserts that an allowed tool runs its body, a denied tool returns the `[BLOCKED by governance policy]` marker (and `forward()` never executes), and that a negative-control case running the same tool with **no adapter applied** executes destructively — proving the governed cases are real interception, not a no-op. + +## Links + +- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/smolagents-tool-policy) +- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/smolagents-tool-policy/README.md) +- [Smolagents documentation](https://github.com/huggingface/smolagents) From 782a92f1471390e77616b83c9d41ded75aaa91cf Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20Agno=20examp?= =?UTF-8?q?le=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/agno.md | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/examples/agno.md diff --git a/docs/examples/agno.md b/docs/examples/agno.md new file mode 100644 index 00000000..2182f7ea --- /dev/null +++ b/docs/examples/agno.md @@ -0,0 +1,112 @@ +# Agno + +Integrates Agent Assembly with [Agno](https://docs.agno.com/) (formerly Phidata) to enforce governance policy on tool calls before they execute. + +## What this example demonstrates + +- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode. +- Installing Agno tool-level governance via the **native Agno adapter** — `AgnoPatch` patches `agno.tools.function.FunctionCall.execute`, the single chokepoint every Agno function-tool call runs through. +- Running an **allowed** tool call (`get_weather`), another **allowed** tool call (`summarize_docs`), and a **denied** tool call (`execute_sql`, blocked by policy rule `deny_arbitrary_execution`). +- How a denied tool short-circuits entirely — its body never runs — and returns a failure `FunctionExecutionResult` instead of a result. +- Driving genuine Agno `@tool` functions exactly as an Agno `Agent` does — `FunctionCall(...).execute()` — with no gateway, API key, or live LLM. + +## The framework / library + +[Agno](https://docs.agno.com/) (formerly Phidata) is the agent framework governed in this example. Agno has a **native Agent Assembly adapter** (`agent_assembly.adapters.agno`), so governance is wired by patching Agno's own tool-execution chokepoint rather than wrapping individual tools. + +Version pins (from `pyproject.toml`): + +| Dependency | Version | +|---|---| +| `agno` | `>=2.0.0` | +| `agent-assembly` | `>=0.0.1b4` | +| Python | `>=3.12` | + +## How it works + +`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `agno-demo-agent`: + +```python +with init_assembly( + gateway_url=gateway_url, + api_key=api_key, + agent_id="agno-demo-agent", + mode="sdk-only", +) as ctx: + ... +``` + +**Hook point.** The Agno adapter governs Agno by patching `agno.tools.function.FunctionCall.execute` (and its async counterpart `aexecute`) — the single chokepoint every Agno function-tool call runs through. Every tool an Agno agent invokes therefore passes through policy *before* its body executes. + +**Offline note.** In production, `init_assembly()` auto-detects Agno and wires the live runtime as the interceptor automatically. In this offline `sdk-only` demo there is no live runtime, so `init_assembly()` installs a no-op hook; the example reverts it and re-applies the hook wired to a local `LocalPolicyEngine` so the demo shows real allow/deny decisions without a gateway (the patch is idempotent, so the no-op hook is reverted first): + +```python +AgnoPatch(policy).revert() +patch = AgnoPatch(policy) +assert patch.apply() +``` + +The adapter calls `check_tool_start` on `LocalPolicyEngine` (`src/policy.py`), which returns a decision dict in the gateway wire format `{"status": "allow"}` or `{"status": "deny", "reason": ...}`. `execute_sql` and `run_shell_command` are denied (arbitrary execution); everything else is allowed. The engine runs fail-closed, so an unknown verdict denies rather than allows. + +**Deny behavior.** When `execute_sql` is denied, `FunctionCall.execute()` returns a `FunctionExecutionResult` with `status == "failure"` whose error carries the `[BLOCKED by governance policy]` marker and the `deny_arbitrary_execution` rule name. The tool's body never runs — the example proves this with a side-effect sink (`SQL_EXECUTIONS`) that stays empty for a denied call, the negative control that would fail if governance were a no-op. + +## Prerequisites & running it + +See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites. + +Then run the example (offline — no API key and no running gateway required): + +```bash +cd python/agno-tool-policy +uv sync --extra dev +uv run python src/main.py +``` + +### Expected output + +``` +============================================================== + Agent Assembly — Agno Tool Policy Demo +============================================================== + +Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode)... + Agent: agno-demo-agent + Gateway: http://localhost:8080 + Mode: sdk-only (offline demo) + +Policy rules (local simulation of gateway policy): + DENY — execute_sql, run_shell_command (arbitrary execution) + ALLOW — everything else + +Agno governance hook installed on FunctionCall.execute. +Tools governed: get_weather, summarize_docs, execute_sql + +Running governed tool calls: +-------------------------------------------- + → get_weather({'city': 'London'}) + ✅ ALLOWED — Weather in London: sunny, 22C (mock) + + → summarize_docs({'topic': 'policy enforcement'}) + ✅ ALLOWED — Summary for 'policy enforcement': Agent Assembly provides governance... (mock) + + → execute_sql({'sql': 'DROP TABLE users; --'}) + ❌ BLOCKED — [BLOCKED by governance policy] Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'.. Please choose a different approach to accomplish this task. + +Assembly context shut down. +``` + +## Run the smoke tests + +The example ships offline smoke tests that drive real Agno `@tool` functions through the real `AgnoPatch` and assert genuine governance — an allowed tool runs and returns its output, a denied tool's body is short-circuited before it runs (the negative control): + +```bash +cd python/agno-tool-policy +uv sync --extra dev +uv run pytest tests/ -v +``` + +## Links + +- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/agno-tool-policy) +- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/agno-tool-policy/README.md) +- [Agno documentation](https://docs.agno.com/) From 2419eab0f83248c86f79e5d0889f0e3ce61a84e4 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20Microsoft=20?= =?UTF-8?q?Agent=20Framework=20example=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/microsoft-agent-framework.md | 132 +++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/examples/microsoft-agent-framework.md diff --git a/docs/examples/microsoft-agent-framework.md b/docs/examples/microsoft-agent-framework.md new file mode 100644 index 00000000..d38ef481 --- /dev/null +++ b/docs/examples/microsoft-agent-framework.md @@ -0,0 +1,132 @@ +# Microsoft Agent Framework + +Integrates Agent Assembly with [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) to enforce governance policy on tool calls before they execute. + +## What this example demonstrates + +- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode. +- Installing Microsoft Agent Framework tool-level governance hooks — `MicrosoftAgentFrameworkAdapter` patches `agent_framework.FunctionTool.invoke`, the single async coroutine through which **every** function tool executes. +- Running an **allowed** tool call (`get_weather`), a **denied** tool call (`delete_records`), and a **pending** tool call (`send_email`, which requires approval and is auto-denied offline). +- How `PolicyViolationError` is raised when a tool is blocked or rejected during approval. +- Two run paths from one example: an **offline mock path** that replays the governed tool calls through the policy contract with no `agent_framework` install, and a **live path** that drives the real `FunctionTool.invoke` through the installed adapter. + +## The framework / library + +[Microsoft Agent Framework](https://github.com/microsoft/agent-framework) is Microsoft's unified agent framework, governed in this example. + +Version pins (from `pyproject.toml`): + +| Dependency | Version | +|---|---| +| `agent-framework` (the `live` extra) | `>=1.9,<2` | +| `agent-assembly` | `>=0.0.1b2` | +| Python | `>=3.12` | + +The adapter's `get_supported_versions()` reports `>=1.0.0,<2.0` — governance attaches across the 1.x line. + +**Install caveats — read these before installing the live extra:** + +- The PyPI distribution is named **`agent-framework`** but it **imports as the top-level module `agent_framework`**. The adapter detects the importable module, not the distribution name. +- `agent-framework` pulls **pre-release** sub-distributions, so `uv` refuses to resolve the `live` extra by default. This example sets `prerelease = "allow"` under `[tool.uv]`, so `uv sync --extra live` resolves without a per-command flag. (Outside that config you would add `--prerelease=allow`; `pip` resolves them without a flag.) +- Some of those sub-distributions are **platform-specific** — for example `github-copilot-sdk` ships **macOS-only wheels** and has no Linux wheel. The **live** run is therefore best on macOS (or any platform where the wheels exist). On Linux CI the `live` extra cannot install, which is exactly why the offline path exists. + +## How it works + +`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `microsoft-agent-framework-demo-agent`: + +```python +with init_assembly( + gateway_url=gateway_url, + api_key=api_key, + agent_id="microsoft-agent-framework-demo-agent", + mode="sdk-only", +) as ctx: + ... +``` + +**The hook point.** `MicrosoftAgentFrameworkAdapter` patches `agent_framework.FunctionTool.invoke` — the single async coroutine through which every function tool runs. Both the `@agent_framework.tool` decorator and direct `FunctionTool(...)` construction produce `FunctionTool` instances, and agents/workflows dispatch tool calls through `invoke`. Patching that one method governs all tool execution without requiring the user to register any framework middleware (which would be opt-in and bypassable). + +**Detection.** The framework name (`microsoft_agent_framework`) deliberately differs from the importable module (`agent_framework`), so the adapter overrides `is_available()` to probe the real module rather than the framework name. + +**Offline note.** Microsoft Agent Framework normally drives its agent loop against a chat model, which needs credentials and a live network call. To stay runnable with no secrets, the example does not start a live model. The mock path replays the three governed tool calls through the `LocalPolicyEngine` decision contract **without importing `agent_framework`**. The live path builds real `agent_framework.FunctionTool` instances (`src/tools.py`) and calls `tool.invoke(...)` directly — the exact surface the adapter patches — so the genuine governance code runs and only the model that would *choose* the tools is absent. + +In the live path, the adapter is registered *before* `init_assembly()`. Because the patch is idempotent, registering first makes `init_assembly()`'s auto-detection a no-op and keeps the offline `LocalPolicyEngine` wired as the interceptor. The adapter calls `check_tool_start` (and, for pending tools, `wait_for_tool_approval`) as `async` hooks on `LocalPolicyEngine` (`src/policy.py`). These return decision dicts in the gateway wire format `{"status": "allow" | "deny" | "pending"}`. `delete_records` and `write_file` are denied; `send_email` is pending and, with no approver available offline, is denied during approval. A `deny` verdict raises `PolicyViolationError` *before* the wrapped function body runs, so a denied tool never executes its side effects. + +## Prerequisites & running it + +See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites. + +### Offline / mock path (no `agent-framework` needed — what CI runs) + +```bash +cd python/microsoft-agent-framework-tool-policy +uv sync --extra dev +uv run python src/main.py --mock +``` + +### Live path (drives the real framework) + +The live path needs the `live` extra. Pre-releases are already allowed via `[tool.uv]`, so no extra flag is required (and it installs best on macOS, where the platform-specific wheels exist): + +```bash +cd python/microsoft-agent-framework-tool-policy +uv sync --extra live --extra dev +uv run python src/main.py +``` + +The live run prints the same allow/deny/pending outcomes as the mock run, but each line is the result of the real `FunctionTool.invoke` passing through the patched governance hook — a denied tool raises `PolicyViolationError` before its body runs. + +### Expected behavior + +Running `uv run python src/main.py --mock` produces: + +``` +============================================================== + Agent Assembly — Microsoft Agent Framework Governed Agent Demo +============================================================== + +Initializing Agent Assembly (gateway: http://localhost:8080, sdk-only mode, mock)... + Agent: microsoft-agent-framework-demo-agent + Gateway: http://localhost:8080 + Mode: sdk-only (offline demo) + +Policy rules (local simulation of gateway policy): + DENY — delete_records, write_file (destructive operations) + PENDING — send_email (requires human approval) + ALLOW — everything else + +Running governed tool calls (mock — policy contract, offline): +-------------------------------------------- + → invoke tool get_weather + ✅ ALLOWED — get_weather would execute + + → invoke tool delete_records + ❌ BLOCKED — Tool 'delete_records' is blocked by policy rule 'deny_destructive_operations'. + + → invoke tool send_email + ❌ BLOCKED — Tool 'send_email' requires approval, but no approver is available in offline mode. + +Assembly context shut down. +``` + +| Tool | Governance control | Outcome | +|---|---|---| +| `get_weather` | allow rule | **ALLOWED** — body runs | +| `delete_records` | deny (destructive) | **BLOCKED** before body runs | +| `send_email` | pending → no approver offline | **BLOCKED** during approval | + +### Smoke tests + +```bash +# The governance smoke tests import the real framework; install the live extra: +uv sync --extra live --extra dev +uv run pytest tests/ -v +``` + +The smoke tests `pytest.importorskip("agent_framework")`, so they **skip cleanly** (rather than fail) when only the `dev` extra is installed. A `tests/conftest.py` additionally coerces pytest's "no tests collected" exit code to success, so a fully-skipped suite is reported green in CI rather than as a failure. When `agent_framework` *is* installed, the tests run and assert that a denied tool's `FunctionTool.invoke` raises `PolicyViolationError` before its body runs, while an allowed tool's body executes (a negative control against a no-op). + +## Links + +- [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/microsoft-agent-framework-tool-policy) +- [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/microsoft-agent-framework-tool-policy/README.md) +- [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) From 0b7313519df26133c7918d22490b0c6e8e4421b1 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Replace=20LlamaInd?= =?UTF-8?q?ex=20manual=20page=20with=20native-adapter=20walkthrough?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/llamaindex-tool-policy.md | 151 +++++++++--------------- 1 file changed, 53 insertions(+), 98 deletions(-) diff --git a/docs/examples/llamaindex-tool-policy.md b/docs/examples/llamaindex-tool-policy.md index 50805911..ab37c989 100644 --- a/docs/examples/llamaindex-tool-policy.md +++ b/docs/examples/llamaindex-tool-policy.md @@ -1,27 +1,30 @@ -# LlamaIndex — manual tool policy +# LlamaIndex -Add Agent Assembly governance to [LlamaIndex](https://docs.llamaindex.ai/) tool calls by wrapping each `FunctionTool` with a `GovernedToolRunner`, since LlamaIndex has no native adapter yet. +Integrates Agent Assembly with [LlamaIndex](https://docs.llamaindex.ai/) using the **native `LlamaIndexAdapter`**, so every tool call a LlamaIndex agent makes is governed automatically — no per-tool wrapper. ## What this example demonstrates -Because LlamaIndex does not yet have a native Agent Assembly adapter, this example shows the **manual wrapper pattern**: each `FunctionTool` is wrapped with `GovernedToolRunner` so governance runs before every tool invocation. This pattern works for any Python callable. +- Initializing Agent Assembly with `init_assembly()` in offline `sdk-only` mode. +- Registering the native `LlamaIndexAdapter`, which patches the concrete `llama_index.core.tools.FunctionTool.call` / `acall` execution methods — the exact methods a `FunctionAgent` / `ReActAgent` invokes to run a tool. +- Running an **allowed** tool call (`query_index`) and another **allowed** call (`summarize_docs`) through the patched `FunctionTool.call`. +- Running a **denied** tool call (`execute_sql`, blocked by `deny_arbitrary_execution`) — its body never executes; the adapter returns a `ToolOutput` flagged `is_error=True` carrying a `[BLOCKED by governance policy]` message instead of raising, so the agent loop can react. +- A fully **offline** run — no API key, no running gateway, and no live LLM. -It walks through: +## The framework / library -- Initializing Agent Assembly with `init_assembly()`. -- Applying governance to `FunctionTool` calls using `GovernedToolRunner`. -- Running an **allowed** tool call (`query_index`). -- Running another **allowed** tool call (`summarize_docs`). -- Running a **denied** tool call (`execute_sql` — blocked by `deny_arbitrary_execution`). -- How to add governance to any framework that lacks a native adapter. +[LlamaIndex](https://docs.llamaindex.ai/) is the agent framework governed in this example. The package is imported as `llama_index.core`, and the adapter advertises support for `>=0.10.0` via `get_supported_versions()`. -## The framework / library +Version pins (from `pyproject.toml`): -[LlamaIndex](https://docs.llamaindex.ai/) — the example depends on `llama-index-core>=0.10.0` for its `FunctionTool` abstraction, and on the Agent Assembly Python SDK pinned at `agent-assembly>=0.0.1a2` (per `pyproject.toml`). Python `>=3.12` is required. +| Dependency | Version | +|---|---| +| `llama-index-core` | `>=0.14.22` | +| `agent-assembly` | `>=0.0.1b5` (the release that ships the LlamaIndex adapter) | +| Python | `>=3.12` | ## How it works -`main.py` opens an Agent Assembly context with `init_assembly()`, passing `agent_id="llamaindex-demo-agent"` and `mode="sdk-only"` so the demo runs offline: +`init_assembly()` is opened as a context manager in offline `sdk-only` mode with the agent id `llamaindex-demo-agent`: ```python with init_assembly( @@ -33,17 +36,31 @@ with init_assembly( ... ``` -The `gateway_url` defaults to `http://localhost:8080` (read from `AGENT_ASSEMBLY_GATEWAY_URL`), and `api_key` comes from `AGENT_ASSEMBLY_API_KEY`. Neither is required for the offline demo. +**The hook point.** LlamaIndex routes every tool invocation through a `BaseTool` subclass, but `BaseTool.call` / `acall` are *abstract* — the real bodies live on concrete classes such as `FunctionTool`. The adapter therefore patches the concrete `FunctionTool.call` (sync) and `acall` (async) methods directly: + +```python +from agent_assembly.adapters.llamaindex import LlamaIndexAdapter + +adapter = LlamaIndexAdapter() +adapter.register_hooks(interceptor) +# FunctionTool.call / acall are now governed. +... +adapter.unregister_hooks() # restores the original methods +``` + +Because both methods are patched, the modern agent stack is covered either way: `FunctionAgent` / `ReActAgent` (via `AgentWorkflow`) `await tool.acall(...)`, while the legacy / sync path calls `tool.call(...)`. -Each tool is then wrapped in a `GovernedToolRunner`, which holds the callable plus an `AssemblyCallbackHandler` configured with a `LocalPolicyEngine`. Calling `runner.run(**kwargs)` first fires `on_tool_start`, which routes through the policy engine before the underlying function executes. When the policy denies a tool (here, `execute_sql` and `run_shell_command`), the call surfaces as a `ToolExecutionBlockedError`, which `main.py` catches and prints as `❌ BLOCKED`. +**The `@dispatcher.span` descriptor nuance.** LlamaIndex wraps `call` / `acall` with an instrumentation decorator (`@dispatcher.span`), so `FunctionTool.call` yields a fresh bound wrapper on every attribute access. The adapter captures the stable original from the class `__dict__` (not the attribute) to restore on revert, and invokes the original through the descriptor protocol (`original_call.__get__(self, type(self))(...)`) so the instance binds correctly as `self` — a plain unbound call would lose `self`. This is handled inside the adapter; the example does not need to deal with it. -Unlike the native-adapter examples — where `init_assembly()` wires governance into the framework automatically — this is the fallback path for frameworks that lack a native adapter: you place the `GovernedToolRunner` in front of each callable yourself. As `main.py` notes, once a native LlamaIndex adapter exists, `GovernedToolRunner` will no longer be needed. +The adapter calls `interceptor.check_tool_start(...)` before a tool body runs. The example's `LocalPolicyEngine` (`src/policy.py`) is that interceptor: it returns a gateway-format `{"status": "allow"}` / `{"status": "deny", "reason": ...}` verdict, denying `execute_sql` and `run_shell_command` and allowing everything else. An unknown / malformed verdict denies rather than silently allowing. + +**Deny is non-raising.** On a `deny`, the patch does not raise — it returns a `ToolOutput(is_error=True)` whose `content` carries the `[BLOCKED by governance policy]` message. The security-relevant invariant is that the tool's underlying function never executes; returning a well-formed error result lets an agent loop observe the block and choose another approach instead of crashing. ## Prerequisites & running it See [Preparing the runtime environment](preparing-the-runtime-environment.md) for the shared prerequisites. -Then: +Then run the example (offline — no API key and no running gateway required): ```bash cd python/llamaindex-tool-policy @@ -51,83 +68,7 @@ uv sync --extra dev uv run python src/main.py ``` -The demo runs fully offline — no API key and no running gateway are required. - -## Code walkthrough - -`policy.py` defines the local policy engine and the runner that bridges any callable into governance. The denied tools are a static set, and `check_tool_start` returns an allow/deny verdict: - -```python -DENIED_TOOLS: frozenset[str] = frozenset({ - "execute_sql", - "run_shell_command", -}) - - -class LocalPolicyEngine: - """Simulates Agent Assembly gateway policy enforcement in offline mode.""" - - def check_tool_start(self, serialized, input_str, run_id=None, **kwargs): - tool_name = serialized.get("name", "") - if tool_name in DENIED_TOOLS: - return { - "status": "deny", - "reason": ( - f"Tool '{tool_name}' is blocked by policy rule " - "'deny_arbitrary_execution'." - ), - } - return {"status": "allow"} -``` - -`GovernedToolRunner.run()` serializes the kwargs, calls the handler's `on_tool_start` to enforce policy, then invokes the wrapped function: - -```python -def run(self, **kwargs: Any) -> Any: - import json - - input_str = json.dumps(kwargs) - self._handler.on_tool_start( - serialized={"name": self._tool_name, "type": "tool"}, - input_str=input_str, - run_id=uuid4(), - ) - return self._fn(**kwargs) -``` - -`tools.py` defines the three LlamaIndex `FunctionTool`s used by the demo: - -```python -query_index = FunctionTool.from_defaults( - fn=_query_index_fn, - name="query_index", - description="Query the document index for relevant information.", -) -``` - -`main.py` builds a runner per tool and executes the demo calls: - -```python -runners = { - name: GovernedToolRunner(name, fn, policy) - for name, fn, _ in _DEMO_CALLS -} -``` - -## Notes & caveats - -!!! note "Manual pattern for frameworks without a native adapter" - LlamaIndex does not yet have a native Agent Assembly adapter, so governance is applied explicitly via `GovernedToolRunner`. When a native adapter becomes available, `init_assembly()` will apply governance automatically and the manual runner will no longer be needed. - -Troubleshooting (from the README): - -| Problem | Fix | -|---|---| -| `ModuleNotFoundError: agent_assembly` | Run `uv sync` first | -| `ModuleNotFoundError: llama_index` | Run `uv sync` — `llama-index-core` is a required dependency | -| `ToolExecutionBlockedError` in tests | Expected — the deny policy rule for `execute_sql` is intentional | - -## Expected behavior +### Expected output ``` ============================================================== @@ -143,8 +84,8 @@ Policy rules (local simulation of gateway policy): DENY — execute_sql, run_shell_command (arbitrary execution) ALLOW — everything else -Wrapping LlamaIndex tools with GovernedToolRunner... - Tools wrapped: query_index, summarize_docs, execute_sql +Registering the native LlamaIndex governance adapter... + FunctionTool.call / acall are now governed by Agent Assembly. Running governed tool calls: -------------------------------------------- @@ -155,11 +96,25 @@ Running governed tool calls: ✅ ALLOWED — 📝 Summary for 'policy enforcement': Agent Assembly provides governance... → execute_sql({'sql': 'DROP TABLE users; --'}) - ❌ BLOCKED — Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'. + ❌ BLOCKED — [BLOCKED by governance policy] Tool 'execute_sql' is blocked by policy rule 'deny_arbitrary_execution'. ... +``` + +The two safe tools run and return their results; `execute_sql` is blocked before its body runs, surfacing the `[BLOCKED by governance policy]` message from the `ToolOutput` the adapter returns. + +### Switching to production mode + +In `sdk-only` mode the example reverts the auto-detected no-op patch and wires its own `LocalPolicyEngine` as the live interceptor. Against a real deployment you instead start an Agent Assembly gateway (or use your SaaS workspace URL), supply credentials via `.env`, and let `init_assembly()` auto-detect and register the LlamaIndex adapter against the live gateway interceptor — the tool-call code does not change; only the policy source does. + +## Run the smoke tests + +The example ships offline smoke tests that drive the **real** adapter — `LlamaIndexAdapter.register_hooks` patches `FunctionTool.call`, then an allowed tool runs and a denied tool's body never executes (the test asserts the tool function was never invoked and that the `[BLOCKED by governance policy]` marker is present). Each test reverts the patch so the global `FunctionTool` class is left clean. + +```bash +uv run pytest tests/ -v ``` ## Links - [Example directory](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/llamaindex-tool-policy) - [Example README](https://github.com/ai-agent-assembly/agent-assembly-examples/blob/master/python/llamaindex-tool-policy/README.md) -- [LlamaIndex docs](https://docs.llamaindex.ai/) +- [LlamaIndex documentation](https://docs.llamaindex.ai/) From b04467fa56a5da7e0a2aea72c429b9809a71a2b6 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Wire=20new=20frame?= =?UTF-8?q?work=20example=20pages=20into=20the=20Examples=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- mkdocs.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index d4dc5835..a896c2ee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -190,7 +190,11 @@ nav: - OpenAI Agents SDK: examples/openai-agents-sdk.md - Pydantic AI: examples/pydantic-ai.md - Google ADK: examples/google-adk.md - - LlamaIndex — manual tool policy: examples/llamaindex-tool-policy.md + - Haystack: examples/haystack.md + - LlamaIndex: examples/llamaindex-tool-policy.md + - Smolagents: examples/smolagents.md + - Agno: examples/agno.md + - Microsoft Agent Framework: examples/microsoft-agent-framework.md - Custom tool policy (no framework): examples/custom-tool-policy.md - Configuration: configuration.md - API Reference: From cc102f7f43de1de40e8f3143bc7804cca4d70e05 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20new=20framew?= =?UTF-8?q?orks=20to=20the=20framework-support=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/framework-support.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/examples/framework-support.md b/docs/examples/framework-support.md index faa1bf16..ed3524c3 100644 --- a/docs/examples/framework-support.md +++ b/docs/examples/framework-support.md @@ -47,8 +47,11 @@ repository. | OpenAI Agents | `agent_assembly.adapters.openai_agents` | ✅ Validated — see [OpenAI Agents SDK](openai-agents-sdk.md). | | Pydantic AI | `agent_assembly.adapters.pydantic_ai` | ✅ Validated — see [Pydantic AI](pydantic-ai.md). | | Google ADK | `agent_assembly.adapters.google_adk` | ✅ Validated — see [Google ADK](google-adk.md). | -| Microsoft Agent Framework | `agent_assembly.adapters.microsoft_agent_framework` | ✅ Validated — governs `agent_framework.FunctionTool.invoke`; see the [`microsoft-agent-framework-tool-policy`](https://github.com/ai-agent-assembly/agent-assembly-examples/tree/master/python/microsoft-agent-framework-tool-policy) example. | -| LlamaIndex | _no native adapter_ | ✅ Validated (manual wrapper) — see [LlamaIndex — manual tool policy](llamaindex-tool-policy.md); governs `FunctionTool` calls via `GovernedToolRunner`. | +| Haystack | `agent_assembly.adapters.haystack` | ✅ Validated — governs `haystack.tools.Tool.invoke` (Haystack 2.x); see [Haystack](haystack.md). | +| LlamaIndex | `agent_assembly.adapters.llamaindex` | ✅ Validated — governs the concrete `FunctionTool.call` / `acall`; see [LlamaIndex](llamaindex-tool-policy.md). | +| Smolagents | `agent_assembly.adapters.smolagents` | ✅ Validated — governs `smolagents.tools.Tool.__call__`; see [Smolagents](smolagents.md). | +| Agno | `agent_assembly.adapters.agno` | ✅ Validated — governs `agno.tools.function.FunctionCall.execute` / `aexecute`; see [Agno](agno.md). | +| Microsoft Agent Framework | `agent_assembly.adapters.microsoft_agent_framework` | ✅ Validated — governs `agent_framework.FunctionTool.invoke`; see [Microsoft Agent Framework](microsoft-agent-framework.md). | | MCP servers | `agent_assembly.adapters.mcp` | ⏳ Planned — adapter ships; a curated example is not yet vendored. | !!! note "Adapter present vs. example present" From a755c119edae70bc1ec38994ce86478f267bdd87 Mon Sep 17 00:00:00 2001 From: Bryant Date: Mon, 22 Jun 2026 20:01:42 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20List=20new=20frame?= =?UTF-8?q?work=20examples=20in=20the=20examples=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/examples/index.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/examples/index.md b/docs/examples/index.md index 3d97418e..600d1c13 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -30,16 +30,20 @@ adapter flow wires the framework, an annotated code walkthrough, and the expecte | [OpenAI Agents SDK](openai-agents-sdk.md) | OpenAI Agents SDK | Approval-gated and denied tool calls, intercepting `FunctionTool.__call__`. | | [Pydantic AI](pydantic-ai.md) | Pydantic AI | Tool-call governance driven offline by the built-in `TestModel`. | | [Google ADK](google-adk.md) | Google ADK | A scripted offline tool trajectory governing `BaseTool.run_async` — no cloud credentials. | -| [LlamaIndex — manual tool policy](llamaindex-tool-policy.md) | LlamaIndex | The manual wrapper pattern (`GovernedToolRunner`) for a framework with no native adapter. | +| [Haystack](haystack.md) | Haystack | Real `haystack.tools.Tool` instances run through a `ToolInvoker`, governing `Tool.invoke`. | +| [LlamaIndex](llamaindex-tool-policy.md) | LlamaIndex | Native adapter governing the concrete `FunctionTool.call` / `acall` — a denied tool returns a blocked `ToolOutput`. | +| [Smolagents](smolagents.md) | Smolagents | Governs `smolagents.tools.Tool.__call__`; a denied tool's `forward()` body never runs. | +| [Agno](agno.md) | Agno | Governs `agno.tools.function.FunctionCall.execute`; a denied tool returns a failure result. | +| [Microsoft Agent Framework](microsoft-agent-framework.md) | Microsoft Agent Framework | Governs the async `FunctionTool.invoke`; offline mock path + a live path. | | [Custom tool policy (no framework)](custom-tool-policy.md) | — | Govern plain Python functions with the minimal `governed()` helper — no AI framework required. | ## How the examples fit together - **No native adapter?** [Custom tool policy](custom-tool-policy.md) shows the minimal - `governed()` building block; [LlamaIndex — manual tool policy](llamaindex-tool-policy.md) - builds on it with `GovernedToolRunner`. Use these patterns for any framework Agent Assembly - does not hook automatically. -- **Native adapter?** The LangChain, LangGraph, CrewAI, OpenAI Agents, Pydantic AI, and Google - ADK examples each rely on `init_assembly()` detecting the framework and installing its - governance hooks for you — see [Framework support](framework-support.md) for the full - adapter list and priority order. + `governed()` building block. Use that pattern for any framework Agent Assembly does not + hook automatically. +- **Native adapter?** The LangChain, LangGraph, CrewAI, OpenAI Agents, Pydantic AI, Google + ADK, Haystack, LlamaIndex, Smolagents, Agno, and Microsoft Agent Framework examples each + rely on `init_assembly()` detecting the framework and installing its governance hooks for + you — see [Framework support](framework-support.md) for the full adapter list and priority + order.