Built-in tools
Tools are functions the model can decide to call while answering. Every agent on the platform can list the platform's built-ins — run_python, search_memory, remember and search_knowledge — and can bring its own. A built-in is present only when your agent lists it (the plumbing is always there; the tool is not added for you, except that the agent's memory policy adds remember and search_knowledge and withholds search_memory under off). This page covers what the built-ins do, how tool calls show up in responses, and where your tool code actually runs.
The built-in tools
| Tool | What it does | Limits |
|---|---|---|
run_python(code) | Runs a Python snippet in an isolated code sandbox — a throwaway container per call, or one interpreter per conversation with CODE_INTERPRETER_SESSION=true — and returns what it printed and the files it wrote | CODE_INTERPRETER_TIMEOUT_S seconds per run (default 20, max 120); a data-science image (numpy, pandas, matplotlib, scipy, scikit-learn, sympy, openpyxl, pillow — nothing else can be installed); no network access at all (not even DNS); nothing persists between calls; stdout and stderr come back, so the code has to print() what it wants seen, and files written to the working directory come back too (4 MB each, 16 MB per run) — the Console shows images inline and offers the rest as downloads, kept as long as the session |
search_memory(query) | Searches what this agent remembers about the current user (long-term memory) and returns the most relevant facts | Read-only; per user; withheld from the model when the agent's memory is off |
remember(fact) | Stores one fact about the current user, as the model wrote it | Offered only under memory mode explicit; refuses secrets and identity numbers |
search_knowledge(query) | Searches the agent's operator-written Knowledge store | Read-only; offered only when the store is enabled |
run_python is how an agent does real computation — arithmetic, parsing, reshaping data, drawing a chart — instead of guessing at an answer. Each snippet runs in a throwaway sandbox that is destroyed afterward and never reused. Code the model wrote therefore cannot touch your agent's credentials or reach the network. The model is told all of this in the tool's own description, so it does not try to pip install or fetch a URL. When the code saves a file (plt.savefig("chart.png"), df.to_csv("clean.csv")), the file comes back with the result: the model sees its name, type and size and can talk about it; the bytes are kept beside the session and shown in the Console — an image inline, anything else as a download. See security and limits for the isolation details.
Give your agent the tools
The built-ins ship in the platform SDK baked into every agent image, so there is nothing to add to requirements.txt. Each framework has its own package and its own shape for the same tools.
- ADK
- CrewAI
- LangGraph
The helpers live in crusoe_adk. The tools are plain functions — ADK introspects their type hints and docstrings — so pass them straight through:
from google.adk.agents import Agent
from crusoe_adk.foundry import foundry_model
from crusoe_adk.tools import run_python, search_memory
root_agent = Agent(
name="my_agent",
model=foundry_model(),
instruction="Use run_python for math and search_memory to recall facts.",
tools=[run_python, search_memory],
)
Your own ADK tools are plain functions too: define one at module level with a docstring and type hints, and add it to the list.
The helpers live in crusoe_crewai. Here the tools are classes, so note the parentheses — you pass instances:
from crewai import Agent, Crew, Process, Task
import crusoe_crewai as crusoe
research_buddy = Agent(
role="Research Buddy",
goal="Compute things and recall what you were told to remember.",
backstory="A concise assistant running on the Crusoe AI Platform.",
llm=crusoe.foundry_model(),
tools=[crusoe.RunPython(), crusoe.SearchMemory()],
verbose=False,
)
respond = Task(
description="Prior conversation:\n{history}\n\nRespond to:\n{message}",
expected_output="A helpful, concise answer.",
agent=research_buddy,
)
crew = Crew(agents=[research_buddy], tasks=[respond], process=Process.sequential)
Your own CrewAI tools must be @tool-decorated module-level functions, not BaseTool subclasses — see the table below for why the agent refuses to start otherwise.
The helpers live in crusoe_langchain. The tools are ready-made tool objects, so pass them by name with no parentheses:
from langgraph.prebuilt import create_react_agent
import crusoe_langchain as crusoe
graph = create_react_agent(
crusoe.foundry_model(),
tools=[crusoe.run_python, crusoe.search_memory],
prompt="Use run_python for math and search_memory to recall facts.",
)
Your own LangGraph tools must be @tool-decorated module-level functions with a synchronous body — see the table below.
See the framework guides for the full contract: ADK, CrewAI, LangGraph.
How tool calls appear in responses
A plain invoke response lists every tool the agent used in tool_calls:
{
"output": "2**32 is 4294967296.",
"tool_calls": [
{"name": "run_python", "summary": "called with args={'code': 'print(2**32)'}"}
]
}
A streaming invoke emits two dedicated line types, in order, as they happen:
{"type":"tool_call", "name":"run_python", "args":{"code":"print(2**32)"}}
{"type":"tool_result","name":"run_python", "result":"4294967296\n"}
The full detail — arguments and results as the model saw them — is also in the transcript: session events carry function_call and function_response parts. The CLI prints one tool_call: <name> called with args={...} line per call.
Your own tools run in a sandbox too
Agents can define their own tools — any function your framework registers. At startup, the platform moves the plain Python functions you registered out of the agent and into single-use sandboxes. This feature is called tool-call sandboxing, and the TOOL_SANDBOX environment variable controls it. It is on by default.
Tools written in other shapes are handled differently. Read the next section before you rely on this.
Here is how it works. When the model calls one of your tools, the platform starts a one-use sandbox from your agent's own image, so the same code and the same dependencies are there. What is not there is any platform credential: the sandbox's environment is empty. That empty environment is the security boundary. A buggy tool, or one steered by prompt injection — an attacker hiding instructions in text your agent reads — cannot get at your API keys or reach internal services.
What that means in practice:
- Tool calls default to a 30-second timeout, with a maximum of 120 seconds.
- Sandboxed tool calls can reach the public internet, so tools that call external APIs work. They cannot reach cloud metadata services or private network ranges.
- Tool calls have no warm pool waiting for them. Every sandboxed tool call pays for starting a sandbox and pulling the image, so the first call is slow. The sandbox gets up to 110 seconds to become ready.
- If the sandbox is unreachable, the harness fails closed: it returns an error instead of quietly running the tool in the agent instance.
- To opt out, store
TOOL_SANDBOX=falseas a per-agent secret:platformctl secrets set my-agent TOOL_SANDBOX=false. The env route refuses that name —PATCH /v1/agents/{name}/envanswers400naming the rule, becauseTOOL_SANDBOXis in the platform's reserved set — while the per-agent secret route accepts it and delivers it to the container.secrets setmerges, so it will not disturb an existingMODEL_API_KEY. Tool code then runs inside the agent instance, next to its credentials. Only do that for tools you fully trust.
A sandboxed tool call runs with an empty environment and no route to any platform service. os.environ holds nothing — not even the platform's own CAI_PROJECT_ID — and crusoe.secret() fails closed rather than fetching anything:
SecretError: crusoe.secret is not configured: missing env CAI_API_URL, CAI_PROJECT_ID, CAI_PROJECT_KEY. A deployed instance must carry a project cai_ service-account key, its project id and the platform API URL to mint a scoped read token.
The minting credential is not injected, and both the secrets API and the Secret Store behind it sit on private ranges the sandbox's egress policy excludes. A sandboxed tool reaches the public internet and nothing else, holding only the arguments the model passed it.
So a tool that needs a credential has two homes, and neither is a sandboxed agent tool:
- Publish it as an MCP server. That container is given
CAI_API_URL,CAI_PROJECT_IDandCAI_PROJECT_KEY, socrusoe.secret()works there, and the project's network policy admits it to the platform's services. - Or run the agent with
platformctl secrets set my-agent TOOL_SANDBOX=false, which puts your tool code next to your agent's credentials. Only for tools you fully trust.
What gets sandboxed, and what does not
Sandboxing works by swapping out a named, module-level Python function — one defined at the top level of a file, not inside another function. In its place the harness leaves a stub: a stand-in function with the same name and the same parameters, whose only job is to forward the call to the sandbox.
A tool that is not that shape has no single function to move. The three frameworks react to that differently:
| Tool shape | ADK | LangGraph | CrewAI |
|---|---|---|---|
Plain function, or a @tool-decorated function | Sandboxed | Sandboxed | Sandboxed |
Class-based tool — a BaseTool subclass, a FunctionTool(...) wrapper, any object that isn't a plain function | Runs in the agent instance. No error, no warning. | Startup fails | Startup fails |
Toolset — MCPToolset and friends | Left in place | Left in place | Left in place |
| Async-only tool (no synchronous body) | — | Startup fails | Startup fails |
run_python, search_memory | Never sandboxed, by design | Never | Never |
Three consequences worth internalizing:
ADK skips quietly. If an ADK tool is not a plain function, the harness leaves it exactly where it is: inside the agent instance, with MODEL_API_KEY in its environment and a network path to your data services. Nothing fails, and nothing is logged as a problem. LangGraph and CrewAI do the opposite. They refuse to start, with a message naming the tool:
tool 'lookup_customer' has no (module, function) body to sandbox; set TOOL_SANDBOX=false to run it in the agent process deliberately.
That difference is not a preference you can configure. If you are on ADK, the check has to be yours.
MCP toolsets are never relocated, and that is fine. An MCP server runs the tool on its own machine. What sits in your agent instance is only the client that calls it, so there is no tool body to move. That is why the MCP guide tells ADK users to put *mcp_toolsets() straight into tools=[...]: the sandbox skips it, and it does not need the sandbox.
Note the flip side. The toolset object, the code that carries the request, and the code that reads the reply all do run in your agent instance. A compromised MCP server is therefore talking to code that holds your credentials.
Platform tools are matched by name. run_python and search_memory are recognized by their function name, not their identity. So if you define your own function called run_python, the harness treats it as a platform tool and never sandboxes it. Pick a different name.
Decide where a tool lives before you write it
The empty environment and the closed network make this a design decision rather than a detail. Apply the rule before you write the code:
- A tool that only computes, or only calls a public API, belongs in the agent. Arguments in, JSON out, nothing else needed.
- A tool that must reach Vectors, Pub/Sub, the embedding endpoint, the secrets store, an MCP server, or any other address on the platform's private network cannot be an agent tool at all. It belongs in an MCP server, which is given
CAI_VECTORDB_URL,CAI_PUBSUB_URL,EMBED_BASE_URL,EMBED_MODEL,CAI_API_URL,CAI_PROJECT_IDandCAI_PROJECT_KEY, and the egress to use them.
Getting it wrong is quiet, and what you see depends on where the tool reads:
| What the tool does | What reaches the model |
|---|---|
Reads os.environ["VAR"] at module level | ERROR: could not import agent: ... KeyError: 'VAR' — the whole module failed to load, before your function was even looked up |
Reads os.environ["VAR"] inside the function | ERROR: 'VAR' |
| Calls an address on the platform's private network | A connection timeout, once the tool's own timeout expires |
The module name in the first row is the module your tool is defined in — agent on ADK, graph on LangGraph, crew on CrewAI. All three of these arrive as ordinary tool-result text. There is no failed invoke, no 500, and nothing in the agent's own logs.
Check what was actually sandboxed
Every harness prints one startup line naming the tools it relocated. Read it after a deploy:
platformctl logs my-agent --history | grep "tool sandbox"
You should see one of these:
tool sandbox ON: lookup_customer, fetch_invoice run in isolated sandboxes
tool sandbox ON: no user-defined tools to isolate
tool sandbox OFF (TOOL_SANDBOX=false): tool code runs in the agent process
If a tool you wrote is missing from the first line, it was not sandboxed. That one line is the only place the platform tells you, so check it whenever you add a tool.
Know what crosses the sandbox boundary
A sandboxed tool call is a network round trip. Arguments are converted to JSON on the way out, and results are converted back on the way in. Two things follow from that.
Arguments must be JSON-serializable. Strings, numbers, booleans, lists, and dicts are fine. Bytes, sets, dataclasses, model objects, file handles, and custom classes are not — the call is refused before it leaves the agent instance:
ToolSandboxError: arguments to render_report are not JSON-serialisable, so the call cannot cross the sandbox boundary: Object of type bytes is not JSON serializable
The fix is to change the tool's signature to take plain data, which is what the model can produce anyway.
A tool that raises does not raise. If your tool throws an exception inside the sandbox, that exception does not travel back into the agent. The platform hands the model an ordinary tool result whose text is:
ERROR: division by zero
The model then answers using that string. As far as it can tell the tool succeeded and returned some text, so the answer is usually plausible and wrong. There is no 500, no failed invoke, and nothing in the agent's own logs.
A tool raising an exception looks like a working agent giving bad answers. To find it, look for the ERROR: prefix in the tool result:
- In a streaming invoke, the
tool_resultline:{"type":"tool_result","name":"...","result":"ERROR: ..."} - In a session transcript, the
function_responsepart for that tool call
If you want a failed tool to be loud, catch the exception in your tool and return a value your agent's instructions know how to handle.
Tools from MCP servers
The platform also hosts MCP servers. MCP is a standard way to publish tools over the network, so agents and other clients — desktop AI apps, for instance — can call them.
There are two reasons to publish one. The organisational reason is sharing: one tool set across several agents, or offered to clients outside the platform, instead of baked into a single agent. The other reason is capability, and it is the one that decides most designs: an MCP server is the only place a tool of yours can hold a credential or reach a platform service. Retrieval over Vectors, a Pub/Sub publish, a secret read — none of those can work in a sandboxed agent tool, and all of them work in an MCP server. See connect agents and clients.
Choosing which servers an agent attaches
There is no attach flag. Every MCP server in the project that is ready at the moment you deploy is handed to the agent, and each framework's helper turns them into that framework's tools. names= narrows that down:
mcp_toolsets() # every attached server — the default
mcp_toolsets(names="doc-search-mcp") # just that one
mcp_toolsets(names=["a", "b"]) # those two
The same names= argument is on crusoe_langchain.mcp_tools() and crusoe_crewai.mcp_tools(). A bare string is one name, not an iterable of characters; an explicit empty list attaches nothing. Name servers when an agent should not see every tool in the project, or must not have its toolset widened the day somebody else deploys a server.
A name that is not attached raises crusoe_core.UnknownMCPServer, listing what is attached, rather than quietly attaching nothing:
MCP server(s) not attached to this project: doc-serch-mcp. Attached: doc-search-mcp, weather-tools
crusoe_core.mcp_server_names() lists what is attachable, if you want to check before you narrow. Attaching several servers at once is ordinary — each becomes its own toolset and one turn can call a tool from each — but tool names are not namespaced by server, so two servers publishing the same tool name collide.
Summary
| Question | Answer |
|---|---|
| What tools does every agent get? | run_python and search_memory |
Where does run_python code run? | A single-use sandbox, 20 s limit, a data-science image, no network; files it writes come back |
| Where do my own tools run? | Plain functions: single-use sandboxes from your agent's image, credential-free (TOOL_SANDBOX, on by default). Class-based tools and toolsets: in the agent instance |
| My tool needs a credential — where does it go? | An MCP server, or platformctl secrets set my-agent TOOL_SANDBOX=false. Not crusoe.secret() inside a sandboxed tool — it fails closed there |
| How do I confirm a tool was sandboxed? | platformctl logs my-agent --history | grep "tool sandbox" — the line names every tool it relocated |
| Why is my tool returning nonsense? | It may be raising. Look for a tool result starting ERROR: in the stream or the transcript |
| How do I see tool calls? | tool_calls in the invoke response, tool_call/tool_result stream lines, function_call parts in transcripts |
| Sharing tools across agents? | Publish an MCP server |
Next steps
- Code Sandbox security and limits — what the tool sandbox does and does not protect you from.
- Publish tools as an MCP server — share one tool set across several agents.
- Memory bank — what
search_memoryreads. - MCP weather tools tutorial — build and call a hosted tool end to end.