Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Duet provides two execution environments: the shared terminal workspace and isolated Cloudflare Sandboxes. Sandboxes allow you to run commands in a secure, ephemeral container without affecting your main workspace.

Why sandboxes?

Sandboxes are useful for:

Testing dangerous commands

Try commands like rm -rf without risking your workspace

AI command execution

Let the AI run commands in isolation and show you the output

Quick experiments

Test shell scripts or one-liners without cluttering your workspace

Parallel execution

Run commands concurrently while continuing work in the main terminal

Running commands in a sandbox

Press Ctrl+R to execute a command in the sandbox:
1

Open sandbox input

Press Ctrl+R in the terminal. You’ll see:
Command to run... |
2

Enter your command

Type any shell command:
ls -la && whoami
3

View the result

A toast notification appears with the output:
$ ls -la && whoami → total 8 -rw-r--r-- 1 nobody nogroup...
Press Esc to cancel without executing.
Sandbox execution requires a Cloudflare Worker URL, just like the AI assistant:
duet --worker https://duet-cf-worker.your-subdomain.workers.dev

Architecture

Sandboxes are powered by Cloudflare’s Browser Rendering service:
import { getSandbox } from "@cloudflare/sandbox";

private async handleSandboxExec(
  roomId: string,
  rawBody: unknown
): Promise<Response> {
  const sandboxName = `sandbox-${roomId}`;

  try {
    const sandbox = getSandbox(this.env.Sandbox, sandboxName);
    const result = await sandbox.exec(data.cmd);

    return Response.json({ result, sandboxName });
  } catch (error) {
    return Response.json(
      { error: `sandbox execution failed: ${error.message}` },
      { status: 500 }
    );
  }
}

Per-room sandboxes

Each room gets its own persistent sandbox instance:
const sandboxName = `sandbox-${roomId}`;
const sandbox = getSandbox(this.env.Sandbox, sandboxName);
This means:
  • Files created in the sandbox persist across commands (within the same session)
  • Each room’s sandbox is completely isolated from others
  • When the room ends, the sandbox is destroyed

Command execution flow

1

Client sends request

The Go client makes an HTTP POST to the Worker:
func (c *Client) ExecCommand(ctx context.Context, roomID, cmd string) (*ExecResponse, error) {
    url := fmt.Sprintf("%s/api/rooms/%s/sandbox/exec", c.baseURL, roomID)

    body := ExecRequest{
        Cmd: cmd,
    }

    jsonBody, err := json.Marshal(body)
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.http.Do(req)
    // ... handle response
}
2

Worker validates input

The Worker uses Zod for schema validation:
const SandboxExecRequestSchema = z.object({
  cmd: z.string().min(1, "Command cannot be empty"),
});

const parseResult = SandboxExecRequestSchema.safeParse(rawBody);
if (!parseResult.success) {
  return Response.json(
    {
      error: "invalid request",
      details: z.flattenError(parseResult.error).fieldErrors,
    },
    { status: 400 }
  );
}
3

Sandbox executes command

Cloudflare runs the command in an isolated container and captures stdout/stderr.
4

Result returned to client

{
  "result": {
    "stdout": "total 8\ndrwxr-xr-x 2 nobody nogroup 4096...",
    "stderr": ""
  },
  "sandboxName": "sandbox-a3f8e9d2-4c1b-4f3a-9e2b-8d7c6b5a4e3f"
}

Response format

Sandbox execution returns both stdout and stderr:
type ExecResult struct {
    Stdout string `json:"stdout"`
    Stderr string `json:"stderr"`
}

type ExecResponse struct {
    Result      ExecResult `json:"result"`
    SandboxName string     `json:"sandboxName"`
    Error       string     `json:"error,omitempty"`
}
The client displays whichever is available:
output := resp.Result.Stdout
if output == "" {
    output = resp.Result.Stderr
}

return SandboxResultMsg{Output: output, Cmd: cmd}

AI integration

The AI assistant automatically uses sandboxes when it includes <run> tags:
private async executeCommands(text: string, roomId: string): Promise<string> {
  const matches = Array.from(text.matchAll(/<run>([\s\S]*?)<\/run>/g));
  let result = text;

  for (const match of matches) {
    const cmd = match[1]?.trim();
    if (!cmd) continue;

    try {
      const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
      const { stderr, stdout } = await sandbox.exec(cmd);

      const summary = stdout.slice(0, 500) || stderr.slice(0, 500) || "[no output]";
      result += `\n\nOutput (${cmd}):\n${summary}`;
    } catch (e) {
      result += `\n\nError (${cmd}):\n${e.message}`;
    }
  }
  return result.replace(/<run>[\s\S]*?<\/run>/g, "").trim();
}

Example

When you ask the AI:
You: Create a file called hello.txt with "Hello world"
The AI responds:
AI: I'll create the file for you:

<run>echo "Hello world" > hello.txt</run>

Output (echo "Hello world" > hello.txt):
[no output]
The command runs in the sandbox, and you can verify it worked:
You: Show me the contents of hello.txt

AI: <run>cat hello.txt</run>

Output (cat hello.txt):
Hello world
Files created in the sandbox are NOT accessible from your shared terminal. Sandboxes and the terminal workspace are completely separate environments.

API endpoint

POST /api/rooms/:roomId/sandbox/exec
endpoint
Execute a command in the room’s sandboxRequest body:
{
  "cmd": "ls -la && whoami"
}
Success response:
{
  "result": {
    "stdout": "total 8\ndrwxr-xr-x 2 nobody nogroup 4096...\nnobody",
    "stderr": ""
  },
  "sandboxName": "sandbox-a3f8e9d2-4c1b-4f3a-9e2b-8d7c6b5a4e3f"
}
Error response:
{
  "error": "sandbox execution failed: command not found"
}

Cleanup

When a room ends, the sandbox is automatically destroyed:
private async handleCleanup(roomId: string): Promise<Response> {
  const errors: string[] = [];

  // Reset agent state
  this.setState({ messages: [] });

  // Terminate sandbox
  try {
    const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
    await sandbox.destroy();
  } catch (e) {
    errors.push(`sandbox: ${e.message}`);
  }

  if (errors.length > 0) {
    return Response.json({ cleaned: true, errors }, { status: 207 });
  }

  return Response.json({ cleaned: true, roomId });
}
This happens when:
  1. The last participant leaves the room
  2. The Go server calls DELETE /api/rooms/:roomId
  3. The Worker destroys the sandbox and clears AI state
Cleanup is best-effort. If the Worker is unreachable, Cloudflare will eventually garbage-collect idle sandboxes.

Limitations

Sandboxes are designed for short commands. Long-running processes may be terminated:
# This will likely fail
sleep 3600
Sandboxes have a restricted filesystem with minimal tools. Advanced utilities may not be available.
Sandboxes cannot make outbound network requests:
# This will fail
curl https://example.com
CPU and memory are constrained. Intensive operations may be throttled or killed.

Error handling

{
  "error": "sandbox execution failed: command not found: invalid_command"
}
The command doesn’t exist in the sandbox environment.

Comparison: Sandbox vs Terminal

FeatureShared TerminalSandbox
PersistencePermanent (until room ends)Ephemeral (per-room)
VisibilityAll participants see outputOnly command initiator
FilesystemShared workspaceIsolated per room
ToolsFull shell with installed packagesMinimal environment
NetworkFull accessNo outbound connections
Use casePrimary development workTesting, AI experiments

Best practices

Use for experiments

Test unfamiliar commands in the sandbox before running in the terminal

Check output length

Sandbox output is truncated to 500 characters. For long output, use the terminal.

Don't rely on state

Sandboxes are destroyed when the room ends. Use the terminal for persistent work.

Verify AI commands

Always review AI-generated commands before manually running them in your terminal.

Next steps

AI assistant

Learn how the AI uses sandboxes for command execution

Deploy a Worker

Set up Cloudflare Worker and Sandbox bindings

Build docs developers (and LLMs) love