An AI agent that writes and runs its own code is useful, and it is also a program you did not write, running on your infrastructure. In this tutorial you will build a small data-analysis agent for a fictional lender and run it in a sandbox. The agent answers questions about loan applications by writing Python, running it, and reading the results.

You will start with the agent unrestricted, then add one protection at a time and check that each one works. In this tutorial you will:

  • Store a Model Gateway API key as a Chalk secret
  • Run the agent with no restrictions
  • Allow the sandbox to reach only the Model Gateway
  • Keep the real API key out of the sandbox, so code in the sandbox never sees the secret’s value
  • Give the agent a snapshot of the data, with limits on CPU, memory, and running time
  • Clean up

The code is in the tutorials/locked-down-agent folder of the chalk-ai/examples repository. Each step is one script, and each script builds on the previous one, so you can read them side by side.


​
Before you start

You need:

  • A Chalk account and an environment to work in.
  • A role that can create sandboxes and run commands in them: the deploy.create and deploy.redeploy permissions. If you created your Chalk team yourself, you already have them. Otherwise, ask an admin for a role that includes them. See Permissions.
  • A Model Gateway with at least one provider connection that serves a chat model with tool calling. Run chalk router model list to see the models available to you.
  • uv and the Chalk CLI. Run chalk login from the folder you will work in.

The agent’s model calls go through the Model Gateway and are billed to your provider connection. The tutorial makes about a dozen short requests.

​
Get the code

git clone https://github.com/chalk-ai/examples.git
cd examples/tutorials/locked-down-agent

​
Create a gateway key and store it as a secret

The agent authenticates to the Model Gateway with an API key. Create one, then store it as a Chalk secret named AGENT_GATEWAY_KEY:

chalk router api-key create --description "Locked-down agent tutorial"
chalk secret set AGENT_GATEWAY_KEY

The first command prints the key once. The second command prompts for a value: paste the key.

​
Edit config.py

Open config.py and set three values for your environment:

API_HOST = "api.chalk.ai"
ENVIRONMENT_ID = "your-environment-id"
MODEL = "openai/gpt-4.1-mini"
  • API_HOST: the host from the API Server row of chalk config, without https://. The Model Gateway is served from this host, under /v1/router.
  • ENVIRONMENT_ID: the Env Id value from chalk whoami.
  • MODEL: a chat model your gateway serves that supports tool calling.

​
How the agent works

The agent is agent.py, and it runs inside the sandbox. It sends your question to the model along with one tool, run_python. When the model asks to run a script, the agent runs it, sends the output back, and repeats until the model replies with an answer:

for _ in range(8):
    response = client.chat.completions.create(
        model=os.environ["MODEL"], messages=messages, tools=TOOLS, temperature=0
    )
    message = response.choices[0].message
    messages.append(message)
    if not message.tool_calls:
        print(f"\nAnswer: {message.content}")
        return
    for call in message.tool_calls:
        code = json.loads(call.function.arguments)["code"]
        output = run_python(code)
        print(f"--- ran {len(code.splitlines())} lines of Python")
        print(output[:500])
        messages.append({"role": "tool", "tool_call_id": call.id, "content": output})

The client is the OpenAI Python client, pointed at the Model Gateway:

client = OpenAI(
    base_url=f"https://{os.environ['API_HOST']}/v1/router",
    api_key=os.environ[os.environ["KEY_SECRET"]],
    default_headers={"X-Chalk-Env-Id": os.environ["ENVIRONMENT_ID"]},
)

Two details in agent.py make the agent more reliable. The system prompt lists the columns of the data file, so the model does not guess them. And run_python returns a message when a script prints nothing, so the model knows to add print() calls rather than assuming the script failed.

common.py holds what every step shares: the sandbox image, which installs openai and pandas and adds agent.py; the upload of data/loans.csv to a volume named loan-agent-data; and the short checks each step prints after the agent answers.


​
Step 1: Run the agent with no restrictions

1_unrestricted.py uploads the data, starts a sandbox with the data mounted at /data, and passes the gateway key to the sandbox as an environment variable:

import chalkcompute

import common
import config

common.upload_data()

sandbox = chalkcompute.Sandbox(
    name="loan-agent",
    image=common.IMAGE,
    volumes=[(common.VOLUME, "/data")],
    env={
        **common.AGENT_ENV,
        # The real gateway key, as an environment variable the agent can read.
        config.KEY_SECRET: chalkcompute.Secret.from_chalk_env(config.KEY_SECRET),
    },
).run()

try:
    common.ask(sandbox, "Which loan purpose has the highest default rate?")
    common.check_internet(sandbox)
    common.check_key(sandbox)
finally:
    sandbox.terminate()

Run it:

uv run 1_unrestricted.py

The first run builds the sandbox image, which takes a minute or two. Then the agent answers:

Question: Which loan purpose has the highest default rate?

--- ran 13 lines of Python
small_business 0.30386740331491713

Answer: The loan purpose with the highest default rate is "small_business" with a default rate of approximately 30.39%.

Request to pypi.org: 200
Key inside the sandbox: the real key (35 characters)

The agent’s wording can differ slightly from run to run. The number comes from the data, so it is the same each time.

The answer is right, and the last two lines show what is wrong with this setup. The sandbox can reach any site on the internet, so code the model writes could download anything or send your data anywhere. And the real gateway key is in the sandbox, where any script the agent runs can read it.


​
Step 2: Allow only the Model Gateway

A network policy lists the hosts a sandbox can reach, and blocks every other host. 2_network.py is Step 1 with one addition:

    network_policy=chalkcompute.NetworkPolicy(allowed_hosts=[config.API_HOST]),

Run it:

uv run 2_network.py
--- ran 13 lines of Python
small_business
0.30386740331491713

Answer: The loan purpose with the highest default rate is "small_business," with a default rate of approximately 30.39%.

Request to pypi.org: URLError <urlopen error [Errno 104] Connection reset by peer>
Key inside the sandbox: the real key (35 characters)

The agent still works, because its model calls go to the one host the policy allows. The request to pypi.org is now refused. The image installed pandas and openai while it was built, before any policy applied, so the agent has the packages it needs without network access.

The key is still exposed. A script could not send it anywhere except the gateway, but it is still readable inside the sandbox, which the next step fixes.


​
Step 3: Keep the real key out of the sandbox

add_credentials gives a secret to requests bound for specific hosts, without giving it to the sandbox. The sandbox receives a placeholder in the secret’s environment variable. When a request to an allowed host carries that placeholder in a header, Chalk replaces it with the real value on the way out.

3_key.py removes the key from env and adds this instead:

    .add_credentials(config.API_HOST, config.KEY_SECRET)

The agent does not change. It still sends the value of AGENT_GATEWAY_KEY as its API key. That value is now the placeholder, and Chalk swaps in the real key between the sandbox and the gateway.

Run it:

uv run 3_key.py
--- ran 13 lines of Python
small_business 0.30386740331491713

Answer: The loan purpose with the highest default rate is "small_business" with a default rate of approximately 30.39%.

Request to pypi.org: URLError <urlopen error [Errno 104] Connection reset by peer>
Key inside the sandbox: a placeholder: __chalk-secret-d7d3326206db40fa93f5862d77a6abb3

Your placeholder has different characters at the end. If code in the sandbox prints or sends the placeholder, nothing is exposed: the swap happens only on requests to the gateway host.


​
Step 4: Use a snapshot of the data, and set limits

The agent reads the data from a volume. In the earlier steps, the sandbox mounted the volume’s latest version, and anything the agent wrote there would be saved to the volume when the sandbox terminated. In 4_limits.py the sandbox mounts one specific version instead, which is an immutable snapshot:

volume = common.upload_data()
# A specific version of a volume is an immutable snapshot.
snapshot = volume.at_version(volume.latest_version().id)
        volumes=[snapshot.mount("/data")],
        cpu="1",
        memory="2Gi",
        lifetime="900s",  # Chalk terminates the sandbox after 15 minutes.

The step also sets resource limits. cpu and memory cap what the agent’s scripts can use, and lifetime ends the sandbox after 15 minutes even if a script never finishes. The sandbox runs under gVisor, which separates it from the host and from other sandboxes; see Isolation.

After the agent answers, the script tries to add a row to the data file from inside the sandbox. After terminating the sandbox, it reads the saved data back.

Run it:

uv run 4_limits.py
--- ran 14 lines of Python
small_business
0.30386740331491713

Answer: The loan purpose with the highest default rate is "small_business" with a default rate of approximately 30.39%.

Adding a row to /data/loans.csv inside the sandbox: done
Request to pypi.org: URLError <urlopen error [Errno 104] Connection reset by peer>
Key inside the sandbox: a placeholder: __chalk-secret-470434cdb5f04772890e4fedaff8ceec
Row A99999 in the saved data: False

The write inside the sandbox succeeds, because the agent works on its own copy of the files. The change is never saved: a snapshot mount does not commit writes back to the volume, so the data the next sandbox reads is the data you uploaded.


​
Step 5: Clean up

Each step terminated its sandbox when it finished. 5_cleanup.py deletes the data volume:

uv run 5_cleanup.py
Deleted volume loan-agent-data.

The gateway key and the AGENT_GATEWAY_KEY secret remain. To remove them, run chalk secret delete --name AGENT_GATEWAY_KEY, and revoke the key with chalk router api-key revoke, using the key hash that chalk router api-key list shows.


​
What you built

The agent now runs with:

ProtectionSetting
Network access only to the Model Gatewaynetwork_policy=NetworkPolicy(allowed_hosts=[...])
No real credentials inside the sandboxadd_credentials(host, secret)
Data it can read but not changeA volume version, mounted with .mount(...)
Limits on resources and running timecpu, memory, lifetime

Each protection is a setting on the sandbox, so the agent code is the same in every step.

​
Next step

To give the agent approved tools, such as a search index or an internal API, without giving it the credentials for them, connect them through the MCP Gateway. See MCP Server Access.