Smolagents¶
Integrates Agent Assembly with Smolagents (Hugging Face) to enforce governance policy on tool calls before they execute.
What this example demonstrates¶
- Initializing Agent Assembly with
init_assembly()in offlinesdk-onlymode. - Governing real
smolagents.Toolinstances —SmolagentsAdapterpatchessmolagents.tools.Tool.__call__, the single chokepoint every smolagents tool execution flows through (Tool.__call__runsself.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 beforeforward()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 (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.1rc1 (the release that ships the Smolagents adapter) |
| 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:
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.
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 for the shared prerequisites.
Then run the example (offline — no model/API credentials and no running gateway required):
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:
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.