diff --git a/agent/assistant/sandbox_integration_test.go b/agent/assistant/sandbox_integration_test.go index cbad24f6..c4214e92 100644 --- a/agent/assistant/sandbox_integration_test.go +++ b/agent/assistant/sandbox_integration_test.go @@ -111,10 +111,14 @@ func TestClaudeCCRConfigBuilding(t *testing.T) { t.Logf("CCR config: %s", string(configJSON)) - // Verify the JSON contains expected fields - assert.Contains(t, string(configJSON), "baseUrl") - assert.Contains(t, string(configJSON), "apiKey") - assert.Contains(t, string(configJSON), "model") + // Verify the JSON contains expected fields (CCR uses snake_case) + assert.Contains(t, string(configJSON), "api_base_url") + assert.Contains(t, string(configJSON), "api_key") + assert.Contains(t, string(configJSON), "models") + // Verify CCR format fields + assert.Contains(t, string(configJSON), "Providers") + assert.Contains(t, string(configJSON), "Router") + assert.Contains(t, string(configJSON), "volcengine") } // TestDefaultImageSelection tests that default images are correctly selected diff --git a/agent/context/JSAPI.md b/agent/context/JSAPI.md index 4103b244..2963e132 100644 --- a/agent/context/JSAPI.md +++ b/agent/context/JSAPI.md @@ -40,6 +40,7 @@ interface Context { mcp: MCP; // MCP object for external tool/resource access agent: Agent; // Agent-to-Agent calls (A2A) llm: LLM; // Direct LLM connector calls + sandbox?: Sandbox; // Sandbox operations (only when sandbox configured) } ``` @@ -2211,6 +2212,251 @@ function Next(ctx, payload) { } ``` +## Sandbox API + +The `ctx.sandbox` object provides access to sandbox operations when the assistant is configured with a sandbox executor (e.g., Claude CLI, Cursor CLI). The sandbox allows hooks to interact with an isolated Docker container environment for file operations and command execution. + +> **Note:** `ctx.sandbox` is only available when the assistant has `sandbox` configuration in `package.yao`. If no sandbox is configured, `ctx.sandbox` will be `null`. + +### Properties + +- `ctx.sandbox.workdir`: String - The workspace directory path inside the container (e.g., `/workspace`) + +### Methods Summary + +| Method | Description | +| ----------------------------- | ---------------------------------------- | +| `ReadFile(path)` | Read a file from the container | +| `WriteFile(path, content)` | Write content to a file in the container | +| `ListDir(path)` | List directory contents | +| `Exec(command)` | Execute a command in the container | + +### File Operations + +#### `ctx.sandbox.ReadFile(path): string` + +Reads a file from the sandbox container. + +**Parameters:** + +- `path`: String - File path (relative to workdir or absolute) + +**Returns:** + +- `string`: File contents as string + +**Example:** + +```javascript +// Read a file from workspace +const content = ctx.sandbox.ReadFile("config.json"); +console.log(content); + +// Read with absolute path +const readme = ctx.sandbox.ReadFile("/workspace/README.md"); +``` + +#### `ctx.sandbox.WriteFile(path, content): void` + +Writes content to a file in the sandbox container. + +**Parameters:** + +- `path`: String - File path (relative to workdir or absolute) +- `content`: String - Content to write + +**Example:** + +```javascript +// Write a configuration file +ctx.sandbox.WriteFile("config.json", JSON.stringify({ debug: true })); + +// Write a script +ctx.sandbox.WriteFile("script.sh", "#!/bin/bash\necho 'Hello'"); +``` + +#### `ctx.sandbox.ListDir(path): FileInfo[]` + +Lists the contents of a directory in the sandbox container. + +**Parameters:** + +- `path`: String - Directory path (relative to workdir or absolute) + +**Returns:** + +- `FileInfo[]`: Array of file information objects + +**FileInfo Structure:** + +```typescript +interface FileInfo { + name: string; // File or directory name + size: number; // Size in bytes + is_dir: boolean; // True if directory +} +``` + +**Example:** + +```javascript +// List workspace contents +const files = ctx.sandbox.ListDir("."); +files.forEach(f => { + console.log(`${f.is_dir ? "DIR" : "FILE"} ${f.name} (${f.size} bytes)`); +}); + +// List specific directory +const srcFiles = ctx.sandbox.ListDir("src"); +``` + +### Command Execution + +#### `ctx.sandbox.Exec(command): string` + +Executes a command in the sandbox container and returns the output. + +**Parameters:** + +- `command`: String[] - Command and arguments as an array + +**Returns:** + +- `string`: Command stdout output + +**Throws:** + +- Error if command exits with non-zero code (includes stderr in error message) + +**Example:** + +```javascript +// Run a simple command +const output = ctx.sandbox.Exec(["echo", "Hello, World!"]); +console.log(output); // "Hello, World!\n" + +// Run git commands +const status = ctx.sandbox.Exec(["git", "status"]); +console.log(status); + +// Run npm install +try { + const result = ctx.sandbox.Exec(["npm", "install"]); + console.log("Install complete:", result); +} catch (e) { + console.error("Install failed:", e.message); +} + +// Run shell script +ctx.sandbox.WriteFile("test.sh", "#!/bin/bash\necho 'Running script'\nls -la"); +ctx.sandbox.Exec(["chmod", "+x", "test.sh"]); +const scriptOutput = ctx.sandbox.Exec(["./test.sh"]); +``` + +### Use Cases + +```javascript +// Use case 1: Prepare workspace before Claude CLI execution +function Create(ctx, messages) { + if (ctx.sandbox) { + // Create project structure + ctx.sandbox.WriteFile("package.json", JSON.stringify({ + name: "project", + version: "1.0.0" + }, null, 2)); + + // Write initial code + ctx.sandbox.WriteFile("src/index.ts", "console.log('Hello');"); + + ctx.trace.Info("Workspace prepared"); + } + return { messages }; +} + +// Use case 2: Post-process sandbox results +function Next(ctx, payload) { + if (ctx.sandbox && !payload.error) { + // Read generated files + try { + const files = ctx.sandbox.ListDir("output"); + const results = files.map(f => ({ + name: f.name, + content: ctx.sandbox.ReadFile(`output/${f.name}`) + })); + + return { + data: { + status: "success", + generated_files: results + } + }; + } catch (e) { + ctx.trace.Warn("No output directory found"); + } + } + return null; +} + +// Use case 3: Run tests after code generation +function Next(ctx, payload) { + if (ctx.sandbox && payload.completion) { + try { + // Run tests + const testOutput = ctx.sandbox.Exec(["npm", "test"]); + ctx.trace.Info("Tests passed"); + + return { + data: { + status: "success", + test_output: testOutput + } + }; + } catch (e) { + ctx.trace.Error("Tests failed: " + e.message); + return { + data: { + status: "test_failed", + error: e.message + } + }; + } + } + return null; +} +``` + +### Sandbox Configuration + +The sandbox is configured in the assistant's `package.yao`: + +```jsonc +{ + "name": "Coder Assistant", + "connector": "deepseek.v3", + "sandbox": { + "command": "claude", // claude | cursor (future) + "image": "yaoapp/sandbox-claude:latest", // Optional, auto-selected by command + "max_memory": "4g", // Memory limit (optional) + "max_cpu": 2.0, // CPU limit (optional) + "timeout": "10m", // Execution timeout + "arguments": { // Command-specific arguments + "max_turns": 20, + "permission_mode": "acceptEdits" + } + } +} +``` + +### Notes + +- Sandbox operations are **synchronous** - they block until complete +- File paths can be relative (to workdir) or absolute +- Relative paths are resolved against the `workdir` directory +- The sandbox container is created at the start of the request and removed when the request completes +- Commands are executed with the sandbox user's permissions +- Errors throw JavaScript exceptions - use try/catch for error handling +- Large file operations may timeout - use appropriate timeout settings + ## LLM API The `ctx.llm` object provides direct access to LLM connectors for streaming completions. This allows calling LLM models directly without going through the full agent pipeline, useful for quick completions, model comparisons, or building custom workflows. diff --git a/agent/docs/context-api.md b/agent/docs/context-api.md index b3a9a49e..380fac42 100644 --- a/agent/docs/context-api.md +++ b/agent/docs/context-api.md @@ -21,6 +21,7 @@ interface Context { search: Search; // Search API agent: Agent; // Agent-to-Agent calls (A2A) llm: LLM; // Direct LLM calls + sandbox?: Sandbox; // Sandbox operations (optional) } ``` @@ -446,6 +447,75 @@ interface Message { } ``` +## Sandbox API + +The `ctx.sandbox` object provides access to sandbox operations when the assistant is configured with a sandbox executor (e.g., Claude CLI). Only available when `sandbox` is configured in `package.yao`. + +### Properties + +```typescript +ctx.sandbox.workdir // Workspace directory path (e.g., "/workspace") +``` + +### File Operations + +```typescript +// Read file +const content = ctx.sandbox.ReadFile("config.json"); + +// Write file +ctx.sandbox.WriteFile("output.txt", "Hello World"); + +// List directory +const files = ctx.sandbox.ListDir("src"); +files.forEach(f => console.log(f.name, f.is_dir, f.size)); +``` + +### Command Execution + +```typescript +// Execute command (returns stdout) +const output = ctx.sandbox.Exec(["npm", "test"]); + +// Handle errors +try { + ctx.sandbox.Exec(["git", "commit", "-m", "fix"]); +} catch (e) { + console.error("Command failed:", e.message); +} +``` + +### FileInfo Structure + +```typescript +interface FileInfo { + name: string; // File/directory name + size: number; // Size in bytes + is_dir: boolean; // True if directory +} +``` + +### Use Cases + +```typescript +// Prepare workspace before execution +function Create(ctx, messages) { + if (ctx.sandbox) { + ctx.sandbox.WriteFile("config.json", JSON.stringify({ debug: true })); + } + return { messages }; +} + +// Post-process results +function Next(ctx, payload) { + if (ctx.sandbox && !payload.error) { + const files = ctx.sandbox.ListDir("output"); + return { data: { generated: files.map(f => f.name) } }; + } + return null; +} +``` + ## LLM API The `ctx.llm` object provides direct access to LLM connectors for streaming completions.