yao/agent/sandbox/PLAN.md
Max c1e92b726d Implement MCP Configuration and Tool Integration for Sandbox
- Added functionality to build and manage MCP configuration for sandbox environments, allowing for dynamic tool execution.
- Enhanced the Assistant's Stream method to skip MCP tool calls in sandbox mode, with internal handling by Claude CLI.
- Introduced unit tests for MCP configuration building and skills directory resolution, ensuring robust integration.
- Updated sandbox manager to create IPC sessions and manage tool exposure dynamically, improving interaction with external agents.
- Enhanced documentation to reflect new features and integration points for MCP and skills within the sandbox.
2026-01-30 19:57:31 +08:00

12 KiB

Agent Sandbox Implementation Plan

Overview

This plan covers the implementation of the agent sandbox integration layer (agent/sandbox/), which enables coding agents (Claude CLI, Cursor CLI) to run in isolated Docker containers with Yao's LLM pipeline.

Test Environment

Environment Configuration

Tests should run with the local development environment:

# Source environment variables
source /Users/max/Yao/yao/env.local.sh

# Key variables used:
# YAO_TEST_APPLICATION=/Users/max/Yao/yao-dev-app
# YAO_ROOT=$YAO_TEST_APPLICATION
# DEEPSEEK_API_KEY, DEEPSEEK_API_PROXY, DEEPSEEK_MODELS_V3

Test Application

Test assistants at yao-dev-app/assistants/tests/sandbox/:

yao-dev-app/assistants/tests/
└── sandbox/
    ├── basic/                    # Basic sandbox execution test
    │   ├── package.yao           # uses.search: disabled
    │   └── prompts.yml
    ├── hooks/                    # Hook integration test
    │   ├── package.yao           # uses.search: disabled
    │   ├── prompts.yml
    │   └── src/index.ts
    └── full/                     # Full test with MCPs, Skills, Hooks
        ├── package.yao           # uses.search: disabled, mcp: {servers: [...]}
        ├── prompts.yml
        ├── src/index.ts
        └── skills/echo-test/     # Agent Skills standard
            ├── SKILL.md
            └── scripts/echo.sh

Connector Configuration

Use deepseek.v3 as the default connector (via Volcengine API).

Implementation Status

Phase 1: Core Types and Interfaces COMPLETED

  • Define Executor interface with all methods
  • Define Options struct with JSON tags
  • Define FileInfo alias to infrastructure sandbox
  • Add DefaultImage() and IsValidCommand() helpers

Phase 2: Claude Executor Implementation COMPLETED

  • Implement Executor struct
  • Implement NewExecutor() constructor with container reuse
  • Implement Stream() method with CCR config writing
  • Implement Execute() method (wrapper)
  • Implement Close() method (removes container)
  • Implement filesystem methods: ReadFile, WriteFile, ListDir
  • Implement Exec() method
  • Implement GetWorkDir() method

Phase 3: CCR Configuration COMPLETED

  • Implement BuildCCRConfig() with correct CCR format
  • Auto-detect provider type (volcengine, deepseek, openai, claude)
  • Add transformer for DeepSeek/Volcengine (maxtoken)
  • Generate Router configuration
  • Write config to container before execution

Phase 4: Assistant Integration COMPLETED

  • Implement GetSandboxManager() singleton
  • Implement HasSandbox() method
  • Implement initSandbox() with cleanup function
  • Implement executeSandboxStream() method
  • Build executor options from assistant config
  • Resolve connector settings (host, key, model)
  • Add trace logging for sandbox creation
  • Send loading message during sandbox init
  • Expose executor to hooks via ctx.SetSandboxExecutor()
  • Handle sandbox lifecycle (create → hooks → execute → cleanup)

Phase 5: JSAPI Integration COMPLETED

  • Define SandboxExecutor interface
  • Implement JS bindings for ReadFile, WriteFile, ListDir, Exec
  • Expose workdir property
  • Register in context's NewObject method

Phase 6: Concurrency & Resource Management COMPLETED

  • Container creation uses Double-Check Locking (in manager.GetOrCreate)
  • Same chatID reuses container (by design)
  • Container cleanup on request completion (defer sandboxCleanup())
  • Unique chatID in tests to avoid conflicts

Phase 7: MCP & Skills Integration COMPLETED

  • Build MCP config from assistant's mcp.servers configuration
  • Write MCP config to container workspace (.mcp.json)
  • Resolve skills directory from assistants/{name}/skills/
  • Copy skills to container (/workspace/.claude/skills/)
  • Skip MCP tool execution in agent.go for sandbox mode (Claude CLI handles internally)
  • Add unit tests for MCP config building (TestBuildMCPConfigForSandbox)
  • Add unit tests for skills directory resolution (TestSandboxMCPAndSkillsOptions)

Phase 8: MCP IPC Bridge COMPLETED

  • Modify BuildMCPConfigForSandbox to use yao-bridge command for IPC
  • Create IPC session in sandbox/manager.createContainer() (socket created before container)
  • Bind mount IPC socket to container at /tmp/yao.sock
  • Add SetMCPTools() method to ipc.Session for runtime tool configuration
  • Set MCP tools dynamically in claude.Executor.Stream() before execution
  • IPC session lifecycle managed by sandbox.Manager (create on container create, close on remove)
  • Load MCP tool definitions from gou/mcp and pass to IPC session
  • Add TestClaudeExecutorIPCSocketMount to verify socket bind mount
  • Verify E2E test shows "Loaded X MCP tools for IPC"

Phase 9: Workspace Management PENDING

  • Implement workspace cleanup configuration
  • Implement stale workspace detection
  • Implement cleanup scheduler

Phase 9: Cursor Placeholder PENDING

  • Create cursor/README.md placeholder

Testing Status

Unit Tests

Package Test File Status
agent/sandbox types_test.go PASS
agent/sandbox executor_test.go PASS
agent/sandbox/claude command_test.go PASS
agent/sandbox/claude executor_test.go PASS

Integration Tests

Package Test File Status
agent/sandbox integration_test.go PASS

JSAPI Tests

Package Test File Status
agent/context jsapi_sandbox_test.go PASS

Assistant Loading Tests

Package Test File Status
agent/assistant sandbox_test.go PASS
agent/assistant sandbox_integration_test.go PASS

E2E Tests

Package Test Case Status
agent/assistant TestSandboxBasicE2E PASS
agent/assistant TestSandboxHooksE2E PASS
agent/assistant TestSandboxFullE2E PASS
agent/assistant TestSandboxContextAccess PASS
agent/assistant TestSandboxLoadConfiguration PASS
agent/assistant TestSandboxMCPToolCall PASS
agent/assistant TestSandboxMCPEchoTool PASS

Running Tests

# Source environment
source /Users/max/Yao/yao/env.local.sh

# Run all sandbox tests
go test -v ./agent/sandbox/...

# Run assistant sandbox tests
go test -v ./agent/assistant -run "Sandbox"

# Run E2E tests (requires Docker)
go test -v ./agent/assistant -run "TestSandbox.*E2E" -timeout 300s

File Structure

yao/agent/sandbox/                    # Executor layer
├── DESIGN.md                         # ✅ Design document
├── PLAN.md                           # ✅ This file
├── types.go                          # ✅ Common types and interfaces
├── types_test.go                     # ✅ Types tests
├── executor.go                       # ✅ Factory function
├── executor_test.go                  # ✅ Factory tests
├── integration_test.go               # ✅ Integration tests
├── claude/
│   ├── types.go                      # ✅ Claude-specific types
│   ├── executor.go                   # ✅ Executor implementation
│   ├── executor_test.go              # ✅ Executor tests
│   ├── command.go                    # ✅ Command builder + CCR config
│   └── command_test.go               # ✅ Command tests
└── cursor/
    └── README.md                     # ⏳ Placeholder (pending)

yao/agent/assistant/                  # Integration layer
├── sandbox.go                        # ✅ Sandbox handler
├── sandbox_test.go                   # ✅ Loading tests
├── sandbox_integration_test.go       # ✅ Integration tests
├── sandbox_e2e_test.go               # ✅ E2E tests
├── sandbox_debug_test.go             # ✅ Debug tests
└── agent.go                          # ✅ Modified: sandbox detection in Stream()

yao/agent/context/                    # Context layer
├── jsapi_sandbox.go                  # ✅ Sandbox JSAPI bindings
└── jsapi_sandbox_test.go             # ✅ Sandbox JSAPI tests

yao-dev-app/assistants/tests/sandbox/ # Test assistants
├── basic/                            # ✅ Basic sandbox test
├── hooks/                            # ✅ Hooks test
└── full/                             # ✅ Full test with MCPs and Skills

Key Design Decisions

1. Container Reuse

Same userID + chatID reuses the same container:

  • Workspace directory persists across requests
  • CCR config is written on each request (same content, safe to overwrite)
  • Container is removed when request completes

2. Concurrency

  • Container creation: Protected by mutex + double-check locking
  • Container execution: Multiple requests can run concurrently in same container
  • Claude CLI: Supports concurrent execution

3. CCR Configuration

CCR requires specific JSON format:

{
  "Providers": [{"name": "volcengine", "api_base_url": "...", ...}],
  "Router": {"default": "volcengine,model", ...}
}

Auto-detection of provider type based on host URL.

4. Resource Cleanup

  • executor.Close() removes the container and closes IPC session
  • defer sandboxCleanup() in agent.go ensures cleanup
  • Tests use unique chatID (timestamp) to avoid conflicts

5. MCP IPC Architecture

Host (Yao)                              Container (Claude CLI)
┌────────────────────────┐              ┌────────────────────────┐
│ IPC Manager            │              │ yao-bridge             │
│   └─ Session           │◄─────────────│   (stdio ↔ socket)     │
│       └─ MCPTools      │  Unix Socket │                        │
│           └─ Process   │   (/tmp/     │ Claude CLI reads       │
│              executor  │    yao.sock) │ .mcp.json and calls    │
└────────────────────────┘              │ yao-bridge for tools   │
                                        └────────────────────────┘
  • .mcp.json points to single "yao" server using yao-bridge /tmp/yao.sock
  • IPC session created with authorized MCP tools from assistant config
  • Tools executed via process.New() in IPC session handler

Known Issues

macOS Docker Desktop Socket Permissions

On macOS with Docker Desktop (gRPC-FUSE), Unix socket permissions are not properly preserved when bind mounting from the host. The IPC socket created on the host with 0666 permissions appears as 0660 inside the container.

Solution: After container start, we execute chmod 666 /tmp/yao.sock as root inside the container to fix permissions. This is handled automatically by sandbox.Manager.fixIPCSocketPermissions().

Notes

  • All tests validate return values (use require/assert)
  • Docker must be available for integration and E2E tests
  • Tests automatically clean up containers after completion
  • Use uses.search: disabled in test assistants to avoid auto-search LLM calls