> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Running an Agent Loop on Sandcastles

> Connect your own agent loop to a TextQL Sandcastle — run the loop inside the sandbox with a local filesystem, or keep it in your own infra and drive the sandbox remotely.

A **Sandcastle** is a TextQL-managed, per-session Python/bash sandbox. It comes with a working filesystem, a stateful Python kernel, governed outbound network, your org's connectors, and — when context is enabled — a permissioned, RBAC-pruned mount of your org's Ontology (our persisted-memory layer) at `/sandbox/files/library`.

This guide shows how to wire **your own agent loop** into a Sandcastle. There are two ways to do it — pick the one that fits your architecture. The Sandcastle is the same either way; what changes is *where the loop runs*.

<Note>
  The Sandcastle is the execution environment — your agent never has to touch our infra directly. You either run your loop *inside* it and let the agent use the filesystem and tools natively, or run your loop in *your own* infra and execute commands inside the Sandcastle remotely over the [Sandcastle API](/api-reference/v2/sandbox/start-sandbox).
</Note>

## Prerequisites

| What               | Where                                                                                                                                                                                           |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **TextQL API key** | Settings → Developers → API Keys. Scopes the Sandcastle to your org and member, and gates the Ontology mount by your `OWNERS` permissions.                                                      |
| **Model API key**  | Whichever LLM provider drives your loop (e.g. Anthropic). For the *agent-inside* mode this must be reachable through the Sandcastle's egress proxy — see [Outbound network](#outbound-network). |
| **Base URL**       | `https://app.textql.com` (or your dedicated/self-hosted host).                                                                                                                                  |

All Sandcastle endpoints live under `/v2/sandcastles` and authenticate with `Authorization: Bearer <TEXTQL_API_KEY>`.

## Choose your topology

<CardGroup cols={2}>
  <Card title="Agent loop outside" icon="cloud-arrow-up">
    Your loop runs in **your** infra. You boot a Sandcastle and execute commands inside it remotely. Best when you already own the orchestration, want your loop next to your other services, or need to fan one loop across many Sandcastles. **Fully supported today.**
  </Card>

  <Card title="Agent loop inside" icon="box">
    Your loop runs **inside** the Sandcastle. Your orchestrating code just boots the Sandcastle, pushes the agent, and starts it. The agent then reads/writes the filesystem and calls tools natively — no round-trip per step. **Possible today via `exec`; not yet a packaged template.**
  </Card>
</CardGroup>

***

## Option A — Agent loop outside the Sandcastle

This is the "virtual bash tool" model: the loop lives in your orchestrating code and treats the Sandcastle as a remote execution + filesystem backend. Every tool call your model makes becomes an API call against the Sandcastle.

<Steps>
  <Step title="Start (or restart) a Sandcastle">
    ```bash theme={null}
    curl -X POST https://app.textql.com/v2/sandcastles \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -H "Content-Type: application/json" -d '{}'
    # → { "sandbox_id": "org123-<uuid>", "created_at": "..." }
    ```

    Pass back a `sandbox_id` to resume a specific Sandcastle; omit it for a fresh one. The Sandcastle is keyed to your member, and its `/sandbox/files/library` mount is pruned to what your `OWNERS` permissions allow.
  </Step>

  <Step title="Wire each tool the model can call to an endpoint">
    | Model tool                        | Sandcastle endpoint                          | Notes                                                                         |
    | --------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
    | `bash(cmd)`                       | `POST /v2/sandcastles/:id/exec`              | Fresh process per call (no shared shell state). `kind: "bash"` or `"python"`. |
    | `python(code)`                    | `POST /v2/sandcastles/:id/execute`           | **Stateful** kernel — variables and dataframes persist across calls.          |
    | `read_file` / `write_file` / `ls` | `GET/POST/DELETE /v2/sandcastles/:id/files…` | Upload, download, list, delete files in the working dir.                      |
    | `query_connector`                 | `POST /v2/sandcastles/:id/query`             | Runs SQL/TQL against an org connector and lands a dataframe in the kernel.    |
  </Step>

  <Step title="Run your loop">
    ```python theme={null}
    # your infra
    sandbox_id = start_sandcastle()
    messages = [{"role": "user", "content": task}]

    while True:
        resp = model.create(messages=messages, tools=TOOLS)
        if resp.stop_reason != "tool_use":
            break
        for call in resp.tool_calls:
            if call.name == "bash":
                out = http.post(f"/v2/sandcastles/{sandbox_id}/exec",
                                json={"command": call.input["cmd"]})
            elif call.name == "python":
                out = http.post(f"/v2/sandcastles/{sandbox_id}/execute",
                                json={"code": call.input["code"]})
            messages.append(tool_result(call.id, out))
    ```
  </Step>

  <Step title="Read the Ontology mount">
    The library is mounted read-only-ish at `/sandbox/files/library`. Your loop can `cat`/`grep` it like any directory:

    ```bash theme={null}
    curl -X POST https://app.textql.com/v2/sandcastles/$ID/exec \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -d '{"command": "grep -ri \"revenue definition\" /sandbox/files/library"}'
    ```
  </Step>
</Steps>

<Tip>
  Use `execute` (stateful kernel) for an iterative data-analysis loop where the model builds up dataframes across turns, and `exec` (one-shot) for filesystem/shell steps that don't need shared state.
</Tip>

***

## Option B — Agent loop inside the Sandcastle

Here the loop runs *in* the Sandcastle. Your orchestrating code's only job is to boot the Sandcastle, push your agent code, and start it. The agent then uses the local filesystem and `/sandbox/files/library` mount directly — no per-step round-trip — and makes model/tool calls out through the Sandcastle's egress proxy.

<Steps>
  <Step title="Start a Sandcastle and push your agent">
    ```bash theme={null}
    # 1. start
    ID=$(curl -s -X POST https://app.textql.com/v2/sandcastles \
      -H "Authorization: Bearer $TEXTQL_API_KEY" -d '{}' | jq -r .sandbox_id)

    # 2. push your agent code
    curl -X POST https://app.textql.com/v2/sandcastles/$ID/files \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -F "file=@agent.py"
    ```
  </Step>

  <Step title="Run the loop inside">
    ```bash theme={null}
    curl -X POST https://app.textql.com/v2/sandcastles/$ID/exec \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -d '{"kind":"bash",
           "command":"python3 /sandbox/files/agent.py",
           "env":{"ANTHROPIC_API_KEY":"'"$ANTHROPIC_API_KEY"'"}}'
    ```

    Inside `agent.py`, your tools are just local calls — `open()`, `subprocess`, `glob`, `grep` over `/sandbox/files/library`. The agent never leaves the box for a tool step; it only leaves the box to call the model.
  </Step>
</Steps>

### Outbound network

Sandcastle egress flows through a **forward proxy** that records every outbound call (host, status, bytes, duration) to an egress ledger and enforces an allowlist — you can see this in the **Network** tab of any Sandcastle. For the agent-inside topology this matters:

* The model endpoint your loop calls (e.g. `api.anthropic.com`) must be permitted by the egress policy, or the call shows up as `denied` in the ledger.
* In-cluster TextQL services are blocked from the Sandcastle except the sandbox proxy/query ports, so the agent reaches connectors and the Ontology through the provided mounts and the `/query` endpoint, not by calling internal services directly.

<Warning>
  The agent-inside topology is achievable today through `exec`, but it is **not yet a packaged template** (no first-class "agent image", no managed secret injection beyond per-call `env`). If you want fully managed in-sandbox agents, prefer Option A for now and talk to us about the roadmap.
</Warning>

***

## Persisting changes back to the Ontology (writeback)

Both topologies can **read** `/sandbox/files/library`, and both can **write back** to it. Edits an agent makes to that mount are diffed against a baseline snapshot (`/sandbox/files/.internal/library.snapshot`) and surfaced as a **change** for review/approval before they land in canonical git — the same pipeline whether the edit comes from the in-product chat tool or the Sandcastle API.

<Steps>
  <Step title="Stage edits in the mount">
    ```bash theme={null}
    curl -X POST https://app.textql.com/v2/sandcastles/$ID/exec \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -d '{"command":"echo \"## Revenue\nNet sales recognition policy...\" > /sandbox/files/library/metrics/revenue.md"}'
    ```
  </Step>

  <Step title="(Optional) preview what would be written back">
    ```bash theme={null}
    curl https://app.textql.com/v2/sandcastles/$ID/ontology/diff \
      -H "Authorization: Bearer $TEXTQL_API_KEY"
    # → { "has_changes": true, "diffs": [...], "raw_diff": "..." }
    ```
  </Step>

  <Step title="Author a change">
    ```bash theme={null}
    curl -X POST https://app.textql.com/v2/sandcastles/$ID/ontology/changes \
      -H "Authorization: Bearer $TEXTQL_API_KEY" \
      -d '{"title":"Add Q3 revenue definition","description":"Documents net-sales recognition under metrics/."}'
    # → { "change_number": 42, "status": "open", "has_conflicts": false, ... }
    ```

    The change is submitted for admin review (`open`), unless an auto-approve rule
    matches (`approved`, already live). See [Create Ontology
    Change](/api-reference/v2/sandbox/create-ontology-change).
  </Step>
</Steps>

<Warning>
  Writeback is an **explicit, reviewed** action — not a background auto-sync.
  Permissions are enforced twice (the mount is pruned to your `OWNERS` access,
  and every changed path is re-validated at merge), so a change can never widen
  access. If the library has drifted you'll get `has_conflicts: true`; resolve
  the `.rej` markers and re-submit with the same `change_number`.
</Warning>

## How this maps to the Ontology backend

When context is enabled, `/sandbox/files/library` is materialized per session from your org's canonical, git-versioned Ontology:

* On **FSx ONTAP** deployments, the worker mounts a writable **FlexClone** of the latest approved snapshot, pruned in-place to your `OWNERS` permissions — copy-on-write, so startup is fast and isolated per session.
* On the default **filestore** backend, permitted contents are copied into the session tree.

Either way the mount is RBAC-scoped at materialization time and `.git` / `OWNERS` metadata is stripped or locked, so a Sandcastle only ever sees the slice of org memory the caller is allowed to see.

## Related

* [Start a Sandcastle](/api-reference/v2/sandbox/start-sandbox)
* [Execute code](/api-reference/v2/sandbox/execute-code) · [Exec (bash/python)](/api-reference/v2/sandbox/list-executions)
* [Load connector data](/api-reference/v2/sandbox/load-connector-data)
* [Upload a file](/api-reference/v2/sandbox/upload-file)
