Quick Start
1
Enable Tool Bridge on an Agent
Register tools on the agent and enable
code_tools in ExecutionConfig.2
Low-Level: Call the Executor Directly
Use
execute_code_with_tools when you need to run a script outside an agent.Why Use Code Mode?
Before code mode, fetching 20 URLs required 20 LLM round-trips and 20 page bodies entering the context window. After code mode, the model emits one short script —best = min(extract_price(fetch(u)) for u in urls) — executed in a single turn. Only the final value returns to the model.
Three wins:
- Fewer LLM round-trips — one turn instead of N
- Less context-window pollution — intermediate results never enter the model
- Lower cost and latency — especially on tool-heavy pipelines
How It Works
Intermediate tool results stay inside the executor and never re-enter the model’s context. ThePolicyEngine + guardrails step runs the same policy/guardrail chain as the normal tool-call loop — see Same gates as direct tool calls below.
Same gates as direct tool calls
A code-mode tool call now goes through the exact same policy/guardrail chain as the normal tool-call loop. Before dispatch, the agent threads apolicy_hook into every code-mode call that runs, in order:
_check_permission_manager_deny— anyPermissionManager/PolicyEngine.check_tool(...)deny-rule._check_tool_policy_and_guardrails— agent-wideguardrails=onAgent(...)(the_tool_call_guardrailslist) and per-tool@tool(input_guardrails=[...])/@tool(output_guardrails=[...]).- Approval gate —
require_approval, allow-lists, andPRAISON_AUTO_APPROVEon each call.
code_mode="unsafe" / "isolated" snippet and bypass a guardrail that blocks the same call on the direct path.
Before / after: a guardrail is no longer bypassable
Guardrail rewrite is honoured
A sanitisinginput_guardrail that rewrites arguments is applied to the actual tool call — the rewritten args are dispatched, not discarded. This works for both positional (fetch("a")) and keyword (fetch(url="a")) call forms, because positional args are bound to parameter names via inspect.signature.bind_partial before the hook runs.
Approval rewrite is re-authorised
If the approval backend rewrites arguments (decision.modified_args), the policy/guardrail hook is re-run on the final args before dispatch — an approval rewrite cannot smuggle a call past an argument-scoped policy. This mirrors the direct path.
BYPASS mode carve-out. In
Agent._is_bypass_mode() the permission-deny gate is skipped (matching the direct path), so a call permitted directly is not rejected only in code mode. The tool-policy / guardrails gate still runs.Backward compatibility. Third-party
CodeToolBridge transports whose run_code() predates the new policy_hook= kwarg keep working — the kwarg is only forwarded when set. Custom bridges gain the same gating if they thread the kwarg into their own serve_tool_call(...) calls.Tools must also be granted to the agent
code_tools_allow is a per-run filter, not a grant. Every name it lists must also be present in Agent(tools=[...]). Code mode is scoped to a private registry built from the agent’s own tools, so a globally-registered plugin or entry-point tool the agent was never given cannot be reached — the call fails.
Every code-mode tool proxy passes the approval gate on each call, not once per run. Approving
fetch once inside a script does not unlock later fetch(...) calls with different arguments — each is gated independently (YAML / env auto-approve still apply as configured).When to Use Code Mode
Configuration Options
Both options live on
ExecutionConfig alongside code_execution and code_mode.
Safety
- Opt-in only —
code_tools=Falseby default; no tools are exposed unless you setcode_tools=True. - Explicit allow-list required —
code_tools_allow=None(the default) exposes zero tools. You must name each tool. - Same gates as direct tool calls — every code-mode call runs the full policy/guardrail chain (
_check_permission_manager_deny→_check_tool_policy_and_guardrails→ approval), identical to the normal tool-call loop. Agent-wideguardrails=, per-tool@tool(input_guardrails=/output_guardrails=),PolicyEngine, andPermissionManagerdeny-rules all apply. See Same gates as direct tool calls. - Every tool-proxy call passes the approval gate — the gate fires on each invocation, not once per run.
require_approval, allow-lists, andPRAISON_AUTO_APPROVEenv var all apply on every call the model’s code makes. An approval that rewrites arguments is re-authorised through the policy/guardrail hook before dispatch. - Approval is not sticky — a single approval does not silently unlock later calls in the same script.
- Disallowed tool →
PermissionError— attempting to call a tool not on the allow-list raisesPermissionError. - Unregistered tool →
NameError— calling a name that isn’t registered raisesNameError. - Reserved name
"tools"→ValueError— you cannot add"tools"to the allow-list; it is always the namespace object. - Imports and dangerous builtins remain blocked — AST + blocklist checks from
execute_codestill apply in code mode. - Two execution paths — code mode runs either in-process or isolated:
- In-process (default) — no
bridge=, same as today. TheToolProxystores its registry and allow-list in a closure that in-process code cannot reach. - Isolated + tool-capable (opt-in) — supply a
CodeToolBridge; the script runs under isolation and tool calls are serviced in the parent viaserve_tool_call, gated by the same allow-list and approval gate. The parent never trusts the child — the allow-list, registry, timeout, and max output size are all forwarded to the bridge, so the transport is gated by the caller’s policy, not its own defaults.
- In-process (default) — no
Isolated Code with Tools (Bridge)
Run code mode inside a sandbox while still calling registered tools — same allow-list, same approval gate, no in-process execution. Supply abridge= object that implements run_code. The transport calls serve_tool_call in the parent for every tool request marshalled from the isolated child — never a weaker path than the in-process proxy.
Bridge Contract
Therun_code method receives the caller’s policy as keyword-only params and returns a result dict.
bridge=None (the default) is fully backward-compatible — omit it to keep today’s in-process behaviour. Bridged calls pass through the same allow-list and require_approval gate, so isolation is never a weaker path. Disallowed tools raise PermissionError, unregistered tools raise NameError, and a denied approval raises PermissionError — exactly as in-process.Common Patterns
Map/Reduce Over a URL List
Pipeline: Fetch → Parse → Filter → Summarize
Bare-Name vs Namespaced Calls
Inside the script, tools are callable two ways:Best Practices
Keep the allow-list as small as possible
Keep the allow-list as small as possible
Only list the tools that the script genuinely needs. A smaller allow-list limits the blast radius if the model generates unexpected code.
Don't allow-list tools that mutate external state without approval
Don't allow-list tools that mutate external state without approval
Tools that write to databases, send emails, or delete records should require explicit approval. Configure
require_approval=True or use a webhook approval backend before allow-listing them.Prefer code mode only when intermediate results are bulky or numerous
Prefer code mode only when intermediate results are bulky or numerous
For two or three lightweight tool calls where the model needs to reason about each result, plain tool calling is simpler and easier to debug.
Register helper tools instead of importing libraries in the script
Register helper tools instead of importing libraries in the script
Imports in model-generated code are blocked by AST checks. Pre-register Python functions as tools so the model can call them instead of importing.
Related
Sandbox
Secure isolated environments for code execution — pair with a
CodeToolBridge to service tool calls from an isolated run.Approval
Configure tool approval gates that apply on every call in code mode.
Allowed Tools
Environment-level and agent-level tool allow-lists.
Code Agent
AI agents that write and execute Python code using external interpreters.

