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 uses Cloudflare Sandboxes (container-based Durable Objects) to provide isolated, secure command execution environments. Each room gets its own dedicated sandbox instance that persists for the lifetime of the room.
Sandbox Architecture
Sandboxes are implemented as container-based Durable Objects:
[durable_objects]
bindings = [
{ name = "DUET_AGENT", class_name = "DuetAgent" },
{ name = "Sandbox", class_name = "Sandbox" },
]
[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"
[[migrations]]
tag = "v2"
new_sqlite_classes = ["Sandbox"]
Source: ~/workspace/source/cf-worker/wrangler.toml:6-21
Each sandbox:
- Runs in an isolated container environment
- Has its own filesystem state
- Is bound to a specific room via naming convention
- Persists across multiple command executions
Sandbox Naming
Sandboxes are named using the pattern sandbox-{roomId}:
const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
Source: ~/workspace/source/cf-worker/index.ts:196
This ensures:
- Each room has its own isolated sandbox
- Multiple users in the same room share the same sandbox
- Different rooms cannot access each other’s sandboxes
Command Execution
Direct Execution Endpoint
The /sandbox/exec endpoint allows direct command execution:
private async handleSandboxExec(roomId: string, rawBody: unknown): Promise<Response> {
const parseResult = SandboxExecRequestSchema.safeParse(rawBody);
if (!parseResult.success) {
return Response.json(
{
error: "invalid request",
details: z.flattenError(parseResult.error).fieldErrors,
},
{ status: 400 }
);
}
const data = parseResult.data;
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 instanceof Error ? error.message : "unknown error"}`,
},
{ status: 500 }
);
}
}
Source: ~/workspace/source/cf-worker/index.ts:210-242
Request Validation
Commands are validated using Zod schema:
const SandboxExecRequestSchema = z.object({
cmd: z.string().min(1, "Command cannot be empty"),
});
Source: ~/workspace/source/cf-worker/index.ts:14-16
Execution Result
Sandbox execution returns stdout and stderr:
const { stderr, stdout } = await sandbox.exec(cmd);
The result structure:
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"`
}
Source: ~/workspace/source/internal/ai/client.go:54-65
AI-Triggered Execution
The AI can trigger sandbox commands automatically using <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) {
const msg = e instanceof Error ? e.message : String(e);
result += `\n\nError (${cmd}):\n${msg}`;
}
}
return result.replace(/<run>[\s\S]*?<\/run>/g, "").trim() || "";
}
Source: ~/workspace/source/cf-worker/index.ts:185-208
The execution flow:
- Extract commands - Regex finds all
<run>...</run> tags
- Execute each command - Run in the room’s sandbox
- Capture output - Get first 500 chars of stdout/stderr
- Append to response - Add output after AI’s explanation
- Remove tags - Strip
<run> tags from final response
Output Truncation
To prevent response bloat, output is limited:
const summary = stdout.slice(0, 500) || stderr.slice(0, 500) || "[no output]";
Source: ~/workspace/source/cf-worker/index.ts:199-200
This ensures:
- Responses remain reasonably sized
- Users see immediate feedback
- Long outputs don’t overwhelm the UI
Error Handling
Execution errors are caught and included in the response:
try {
const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
const { stderr, stdout } = await sandbox.exec(cmd);
// ...
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
result += `\n\nError (${cmd}):\n${msg}`;
}
Source: ~/workspace/source/cf-worker/index.ts:195-205
Errors are displayed to the user rather than failing silently.
Sandbox Lifecycle
Creation
Sandboxes are created on-demand when first accessed:
const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
The getSandbox function from @cloudflare/sandbox handles lazy initialization.
Persistence
Sandboxes persist across multiple command executions, maintaining:
- Filesystem state
- Working directory
- Installed packages or files
This allows multi-step workflows like:
- Create a file
- Modify it
- Run it
Cleanup
When a room is deleted, its sandbox is 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 instanceof Error ? e.message : String(e)}`);
}
if (errors.length > 0) {
return Response.json({ cleaned: true, errors }, { status: 207 });
}
return Response.json({ cleaned: true, roomId });
}
Source: ~/workspace/source/cf-worker/index.ts:244-266
Cleanup errors are collected and returned with a 207 Multi-Status response.
Client API
The Go client provides a method to execute commands:
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)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
defer resp.Body.Close()
var result ExecResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
if result.Error != "" {
return nil, fmt.Errorf("sandbox error: %s", result.Error)
}
return &result, nil
}
Source: ~/workspace/source/internal/ai/client.go:128-162
Example Usage
Direct Command Execution
POST /api/rooms/room-123/sandbox/exec
Content-Type: application/json
{
"cmd": "echo 'Hello from sandbox'"
}
Response:
{
"result": {
"stdout": "Hello from sandbox\n",
"stderr": ""
},
"sandboxName": "sandbox-room-123"
}
AI-Triggered Execution
User: “Create a hello.txt file with ‘Hello World’”
AI Response:
I'll create the file for you.
<run>echo 'Hello World' > hello.txt</run>
Final Response to User:
I'll create the file for you.
Output (echo 'Hello World' > hello.txt):
[no output]
Next Steps