smolagents
Integrate Intutic governance with smolagents (smolagents, 1.26+) — Hugging Face's code-executing agent framework.
Read this first — CodeAgent gates code TEXT, not tool calls
smolagents ships two agent classes with fundamentally different governance shapes. ToolCallingAgent calls discrete, named tools — plain callables, governed by @guard/guard_tools exactly like every other framework on this site. CodeAgent is different in kind: a "tool call" in CodeAgent mode is a whole chunk of model-generated Python source handed to a PythonExecutor and run. There is no discrete tool call to intercept — the executor invocation itself is the only choke point smolagents publishes.
IntuticPythonExecutor gates the generated code string before it reaches the interpreter — the same {"command": ...} shape a shell command gets, so an existing SOP rule authored against kubectl apply already applies to a CodeAgent shelling out via subprocess/os.system, with no separate rule needed. What it does not see, precisely: control flow that only reaches a dangerous call after some computation the interpreter hasn't executed yet, runtime-constructed strings (os.system("ku" + "bectl apply " + rest) never contains the literal substring an argPattern matches, even though the executed command does), and tools invoked by the running code through send_tools — a real, in-process call once the code is already executing, invisible to this text-level pre-execution check the same way an already-running process is invisible to every other pre-exec check in this codebase. This closes real coverage CodeAgent had none of before, but it is narrower than the argument-level gating every other adapter on this site does — the same scope as openWebuiHooks.ts's inlet() filter, which governs prompt text rather than a tool call and says so explicitly in its own doc comment. See What the adapter does NOT do below.
How it works
The smolagents adapter is detected when pyproject.toml, requirements.txt, or uv.lock declares a smolagents dependency. Like LangGraph, it writes a .env.intutic file with proxy base-URL env vars plus a comment block pointing at the SDK gate.
smolagents is governed on two independent surfaces:
- LLM egress — point the model client's
base_urlat the Intutic proxy (or launch underintutic exec). - Local tool/code execution — split by agent class, per the warning above:
guard_tools/@guardforToolCallingAgent's discrete tools,IntuticPythonExecutorforCodeAgent's generated code.
Setup
1. Initialize Intutic
intutic init2. Route LLM traffic through the proxy
Source .env.intutic, launch under intutic exec, or set base_url explicitly on your model client — same as LangGraph.
3. Gate local execution (SDK) — pick the mechanism for your agent class
pip install intutic-clawde[smolagents]The smolagents extra requires Python 3.11 or later: it pulls in Pillow, and the patched Pillow (13.0.0) needs 3.11.
ToolCallingAgent — tools are plain callables, wrapped the same way as every other framework:
from intutic_clawde.gate import Gate, GateConfig, install, guard_tools
install(Gate(GateConfig()))
tools = guard_tools([shell_tool, write_file_tool, deploy_tool])
agent = ToolCallingAgent(tools=tools, model=model)guard_tools was extended specifically for smolagents: a smolagents Tool object is callable on its own __call__, so without duck-typing .forward too, guard_tools would silently replace it with a bare wrapper function and lose the .name/.inputs schema ToolCallingAgent needs to show the model. This is already handled — no extra step required.
CodeAgent — wrap the executor, not any individual tool:
from smolagents import CodeAgent, LocalPythonExecutor
from intutic_clawde.gate.adapters.smolagents import IntuticPythonExecutor
agent = CodeAgent(
tools=[...], model=model,
executor=IntuticPythonExecutor(LocalPythonExecutor(additional_authorized_imports=[])),
)send_tools/send_variables pass through untouched. On each step, __call__ evaluates the gate against {"command": code_action} before delegating to the wrapped executor. On deny it raises smolagents.local_python_executor.InterpreterError — smolagents' own "this code should not run" exception (the same one syntax errors and disallowed imports raise) — carrying the [Intutic Governance] BLOCKED: ... message. CodeAgent._step_stream catches this the same way it catches any other step failure: the error is fed back to the model, and the run is not crashed. This was driven live against smolagents==1.26.0, not just read from source.
Optional: intutic_step_callback. Gate enforcement and the audit trail (tool_blocked/tool_allowed hook events) already happen unconditionally inside IntuticPythonExecutor.__call__ — this callback is not required for either. It bridges into smolagents' own step_callbacks lifecycle (fires once per ActionStep, after the step completes) for building an observer around that lifecycle instead of around Gate directly:
agent = CodeAgent(
..., executor=IntuticPythonExecutor(...),
step_callbacks=[intutic_step_callback],
)It recognizes a block by the [Intutic Governance] BLOCKED: prefix in step.error, not by re-evaluating the gate — it logs via the standard logging module and deliberately does not call the control-plane client a second time, to avoid turning one real incident into two hook-events records.
4. Trace attribution
from smolagents import OpenAIServerModel
from intutic_clawde.gate import intutic_headers
model = OpenAIServerModel(
model_id="gpt-5-mini",
api_base="http://localhost:4000/v1",
client_kwargs={"default_headers": intutic_headers(session_id=run_id, harness="smolagents")},
)client_kwargs is passed straight to the openai.OpenAI client smolagents builds.
What gets written
Same shape as LangGraph's .env.intutic — proxy URLs plus a pointer at intutic_clawde.gate.adapters.smolagents.IntuticPythonExecutor.
To undo what intutic gates install writes here, run intutic disconnect --harness smolagents: each file goes back to what it held before Intutic first wrote it, or is deleted if Intutic created it, and edits you made since are kept. .env.intutic stays while another harness that writes it is still connected. See intutic disconnect.
What the adapter does NOT do
Same structural gaps as every SDK-gated framework — see LangGraph's "What the adapter does NOT do" — plus the CodeAgent-specific boundary from the warning above, an inherent property of gating generated code text, not a defect scheduled for a fix:
- Runtime-constructed strings evade
argPatternmatching. A rule authored against a literal substring (e.g.kubectl apply) does not match code that assembles the same command at runtime via string concatenation, even though the executed result is identical. - Conditional reach is not evaluated. A rule evaluates against the code as submitted; code whose dangerous branch is behind a condition the interpreter has not yet reached is judged the same as code that always takes that branch.
- Tools invoked by the executing code via
send_toolsare invisible to this check. smolagents lets generated code call tools passed tosend_tools— a real, in-process call once the code is already running.IntuticPythonExecutoronly sees the code text before execution starts, the same way any pre-exec check in this codebase cannot see into an already-running process. - No finer-grained veto point exists to target instead. Closing this gap would require smolagents publishing a per-statement or per-call hook inside
LocalPythonExecutoritself; nothing like that exists atsmolagents==1.26.0. Until it does, code-text-level gating is the only coverageCodeAgenthas — which is still strictly more than the zero coverage it had before this adapter existed.
Config details
| Property | Value |
|---|---|
| Harness type | smolagents |
| Config file | .env.intutic |
| Detection | smolagents in pyproject.toml, requirements.txt, or uv.lock |
| Format | Shell environment variables |
| Write strategy | Atomic (write to .intutic-tmp, then rename) |
| Tool gate | SDK-side, split by agent class: guard_tools/@guard for ToolCallingAgent's discrete tools; intutic_clawde.gate.adapters.smolagents.IntuticPythonExecutor (a PythonExecutor.__call__ wrapper raising InterpreterError) for CodeAgent's generated code text — see What the adapter does NOT do for what code-text-level gating does not cover; no sync-daemon hook file |