intutic_clawde.gate.adapters.smolagents
smolagents adapter: IntuticPythonExecutor, plus intutic_step_callback.
smolagents is different in kind from the other three Wave-2 adapters. Its ToolCallingAgent calls discrete, named tools — those are plain callables (smolagents Tool objects, .forward-dispatched — see framework.py's guard_tools, extended in this same change to duck-type .forward; a smolagents Tool is callable on its own __call__, so without that extension guard_tools silently replaced it with a bare function and lost its .name/.inputs schema, which ToolCallingAgent needs to show the model — see framework.py's doc for the full finding). But its OTHER agent class, CodeAgent, does not call tools at all in that sense: a "tool call" in CodeAgent mode is a whole chunk of model-generated Python source being handed to a PythonExecutor (LocalPythonExecutor and friends) and run. That executor call — not any individual function inside the generated code — is the actual choke point, and it is a different SHAPE of governance decision than every other adapter in this package makes: the "tool input" being judged is a source-code string, not a {name: value} argument dict for a pre-declared tool.
Verified live against smolagents==1.26.0 (installed in a scratch venv, read directly, then driven end to end — not just imported):
PythonExecutoris an ABC:send_tools(tools),send_variables(vars),__call__(code_action: str) -> CodeOutput.IntuticPythonExecutorbelow wraps any instance of it (LocalPythonExecutor, or a sandboxed executor such as smolagents' Docker/E2B ones — this adapter does not care which, since it only touches__call__) and forwardssend_tools/send_variablesuntouched.CodeAgent._step_stream(smolagents/agents.py) callsself.python_executor(code_action)inside atry/except Exception. ANY exception — not a special governance type — is caught and re-raised asAgentExecutionError(error_msg, self.logger), smolagents' own "this step failed, tell the model and let it try again" path (the same path a syntax error or an unauthorized-import error already takes). This was driven live: raisingInterpreterErrorfrom a wrapped executor's__call__resulted in exactly this normal step-failure handling, not a crashed run.InterpreterError(smolagents.local_python_executor.InterpreterError) — NOT exported from thesmolagentstop-level package, only fromsmolagents.local_python_executor— is a plainValueErrorsubclass with no required constructor arguments (unlikesmolagents.utils.AgentError, which needs alogger). It is smolagents' own documented "this code should not run" exception (syntax errors, unsupported operations, disallowed imports all raise it), so raising it here — rather thanIntuticGateRefusal— means smolagents' own retry/error-surfacing machinery treats a governance refusal exactly like any other reason the interpreter refused to run a snippet, per this codebase's existing "raise the target's own veto exception, not ours" precedent (seegate.py'sIntuticGateRefusaldoc: even elsewhere, refusals aim to speak the host's own error language when the host does not recognise Intutic's).
Honesty note — "code-execution-genre coverage", matched to how services/sync-daemon/src/harness/openWebuiHooks.ts's inlet() states its own scope (the closest existing precedent found for "this governs TEXT, not a named tool call, and is narrower than the other harnesses on purpose" — no "n8n code-as-command" precedent under that literal name was found; openWebuiHooks.ts is the real one, governing prompt text rather than generated code, with the same honest framing this file follows):
IntuticPythonExecutor governs the code STRING before it is handed to the interpreter — the same tool_input["command"] shape and the same SOP argPattern/snapshot rules a shell command gets (Gate.guard() reads tool_input.get("command") for both is_deploy classification and Tier A3 argument matching; passing the code text as {"command": code_action} here means a rule authored against kubectl apply or similar already applies to a CodeAgent shelling out via subprocess/os.system, with no separate rule needed). It says nothing about what the code does once it starts running: control flow that only reaches a dangerous call after some computation, runtime-constructed strings (os.system("ku" + "bectl apply ...")), or a tool invoked BY the executed code through send_tools (smolagents lets code call tools passed to send_tools — a real, in-process call, invisible to this text-level check the same way an already-executing process is invisible to a pre-exec check everywhere else in this codebase). Governing the generated code text closes the CodeAgent gap that plain guard_tools() cannot reach at all (there are no discrete tool calls to wrap in CodeAgent mode); it does not make CodeAgent as tightly governed as ToolCallingAgent, where every argument to every call is inspected individually.
intutic_step_callback is a smaller, deliberately optional addition. Gate enforcement and the real audit trail (tool_blocked/tool_allowed via hook events) already happen unconditionally inside IntuticPythonExecutor.__call__ — wiring step_callbacks is NOT required for either. What it adds: a bridge into smolagents' OWN step lifecycle (step_callbacks fires once per ActionStep, after the step — success or failure — completes, receiving the step and the agent) for a caller building an observer around that lifecycle rather than around intutic_clawde.gate.Gate directly, so a CodeAgent run and a ToolCallingAgent run (its tools gated via guard_tools, which also already emits through Gate.guard() independently) can be watched through one consistent, smolagents-native channel that speaks the same tool_blocked/tool_allowed vocabulary. It logs via the standard logging module (intutic_clawde.gate.adapters.smolagents logger) — it deliberately does NOT call Gate._emit/the control-plane client a second time, to avoid turning one real incident into two hook-events records.
Optional import: importing this module never fails even without smolagents installed. Only instantiating IntuticPythonExecutor requires it — pip install intutic-clawde[smolagents].
Classes
IntuticPythonExecutor
class IntuticPythonExecutor(
wrapped: Any,
*,
gate: Optional[Gate] = None,
tool_name: str = 'python_exec',
)Wraps a smolagents PythonExecutor, gating the code string before delegating to it.
Usage:
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 to the wrapped executor untouched. __call__ evaluates the gate against {"command": code_action} (see this module's doc for why "command") BEFORE calling the wrapped executor; on deny it raises smolagents.local_python_executor.InterpreterError with the [Intutic Governance] BLOCKED: ... message — smolagents' own "this code should not run" exception, so CodeAgent's existing step-failure handling (feed the error back to the model, do not crash the run) applies exactly as it would to a syntax error.
Methods
IntuticPythonExecutor.send_tools()
def send_tools(tools: Dict[str, Any]) -> NoneIntuticPythonExecutor.send_variables()
def send_variables(variables: Dict[str, Any]) -> NoneFunctions
intutic_step_callback()
def intutic_step_callback(step: Any, agent: Any = None) -> NoneOptional step_callbacks entry — logs a step's governance verdict through smolagents' own step lifecycle. See this module's doc for what this does and does not add over IntuticPythonExecutor's own (unconditional, already-real) enforcement and audit trail.
Usage:
agent = CodeAgent(
..., executor=IntuticPythonExecutor(...),
step_callbacks=[intutic_step_callback],
)Recognises a block by the [Intutic Governance] BLOCKED: prefix IntuticPythonExecutor raises (wrapped by smolagents into step.error, an AgentError), and a hold by [Intutic Governance] HELD: — not by re-evaluating the gate.
Constants
CODE_EXEC_TOOL_NAME
CODE_EXEC_TOOL_NAME = 'python_exec'The tool name reported to Gate.guard() / hook-events for a CodeAgent step. Not a real smolagents tool — chosen to be recognisable in the dashboard and in SOP rules authored against "code execution" broadly.