# Custom agents in the person's own apps (for AIs)

For AIs and developers. A website front desk, an intake form or an assistant inside a product can run on Swarm as a **sealed agent**: an agent whose only capabilities are client tools, typed tools the person's app answers. It has no built-in tools at all: no shell, files, web, delegation or memory. It reads the conversation, replies and calls the tools you defined. Details: `containers/sealed/README.md` in https://github.com/swarm-agent/swarm.

## How it fits

    visitor ── the website ── the app's gateway ──(agent-bound token)── sealed swarmd ── model provider
                                   │  answers client tools with the visitor's identity
                                   └─ the app's database and APIs

- The gateway (the person's server) holds an **agent-bound token**. It can create and use that one agent's sessions and answer its tool calls. It cannot list sessions, reach any other agent, approve anything, change agents or tools, mint tokens or use MCP. If someone later gives the agent a built-in tool, the token stops working.
- Tool arguments are validated against the tool's JSON Schema before the gateway sees them. The answer returns to the model labelled as untrusted content.
- Identity never comes from the model: the gateway decides whose data a call may see; the model supplies only business inputs such as an order number.

## Define the agent

    {
      "tools": [
        {"name": "lookup_order", "description": "Look up the visitor's order by its code.",
         "input_schema": {"type": "object", "properties": {"order_id": {"type": "string", "pattern": "^[A-Z0-9]{6}$"}},
                          "required": ["order_id"], "additionalProperties": false},
         "effect": "read", "timeout_ms": 5000}
      ],
      "agent": {"name": "frontdesk", "prompt": "You answer visitors' questions about their orders.",
                "provider": "codex", "model": "MODEL", "thinking": "medium", "tools": ["lookup_order"]}
    }

Tool names cannot reuse built-in names. Load it and mint the gateway's token on the sealed daemon:

    swarmctl setup sealed-agent --stdin < frontdesk.json
    (umask 077; swarmctl setup sdk-token --agent frontdesk --name gateway --expires-in-seconds 2592000 \
       --messages-per-minute 30 --sessions-per-hour 100 --max-concurrent-runs 4 > gateway-token.json)

From a trusted owner backend the SDK does the same: `client.agents.defineClientTool`, `defineSealedAgent`, `createGatewayToken`. Tokens expire; mint a new one and restart the gateway before they do (`swarmctl setup sdk-tokens` lists them, `revoke-sdk-token --id ID` revokes one).

## Serve it from the app

    import { SwarmClient } from '@swarm-agent/sdk';
    const swarm = new SwarmClient({ baseUrl: process.env.SWARM_API_URL, token: process.env.SWARM_GATEWAY_TOKEN });
    // One session per visitor, in the sealed daemon's scratch workspace.
    const session = await swarm.sessions.create({ agent_name: 'frontdesk', workspace_path: '/project', title: 'Visitor chat' });
    // Per visitor message: send it, answer the agent's tool calls while it runs, get the reply.
    const { reply } = await swarm.agents.converse(session.id, visitorMessage, {
      lookup_order: async ({ order_id }) => findOrderForVisitor(order_id, visitor),
    }, { timeoutMs: 60000 });

`client.agents.converse` is the whole loop a gateway needs per message. Lower level: `client.agents.serve(sessionId, handlers, { signal })` answers calls until aborted, or `pendingCalls`, `answer` and `fail`. A handler that throws fails the call with "tool failed" (or its message, for a `ClientToolError`).

## Limits, whatever the model does

| Limit | Default | Set with |
| --- | --- | --- |
| Model calls per message | 4 | agent `limits.max_steps` |
| Output tokens per call | 1024 | `limits.max_output_tokens` |
| History sent | 40 messages | `limits.max_history_messages` |
| Run deadline | 60 s | `limits.run_timeout_ms` |
| Messages per gateway token | 120 / minute | `sdk-token --messages-per-minute` |
| Conversations per gateway token | 300 / hour | `sdk-token --sessions-per-hour` |
| Account spend per day | off | `swarmctl setup daily-limit --usd 25` |

Visitor-level limits (per person, per IP address, bursts) belong in the gateway.

## Deploy it safely

- Run the sealed runtime image (`containers/sealed/Dockerfile`: `swarmd`, `swarmctl` and git on an empty base, no shell) read-only, with all capabilities dropped. Publish its SDK port only to loopback or a private network; only the gateway faces the internet.
- Point its workspace at a dedicated empty scratch repository, never real code.
- Block outbound traffic at the network level except to the model provider.
- Keep provider keys and tokens in the host's secret store or 0600 files, never in browser code or chat.
