Skip to content

Conversation

Copy link

Copilot AI commented Nov 27, 2025

  • Create sandbox filesystem module (src/sandbox/filesystem.ts)
  • Create process isolation module (src/sandbox/process.ts)
  • Create command validation module (src/sandbox/commands.ts)
  • Create gym configuration schema (src/sandbox/config.ts)
  • Create sandbox instance manager (src/sandbox/instance.ts)
  • Create main sandbox module (src/sandbox/index.ts)
  • Create gym CLI command (src/cli/cmd/gym.ts)
  • Add tests for filesystem module
  • Add tests for commands module
  • Fix TypeScript errors in config.ts (restructured Zod schema to avoid .default({}) type issues)
  • Restore bun.lock file
  • Improve defaults() return type accuracy
Original prompt

Summary

Implement a sandbox/gym environment for OpenCode that allows safe development and testing without risking accidental file system damage. This is the local-first implementation focusing on directory isolation and process-level sandboxing.

Goals

  • Create a safe execution environment where the AI agent can operate without affecting the real filesystem
  • Implement a hybrid approach starting with local isolation (WASM preparation for future, process isolation now)
  • Ensure all file operations are confined to an isolated temporary directory
  • Block dangerous shell commands
  • Provide easy CLI access via opencode gym command

Architecture

1. Sandbox Filesystem (packages/opencode/src/sandbox/filesystem.ts)

  • Copy source directory to temp location (/tmp/opencode-sandbox-*)
  • Track original ↔ sandbox path mappings
  • Auto-cleanup on dispose (configurable persistence for debugging)
  • Validate all paths stay within sandbox boundaries
export namespace SandboxFilesystem {
  export interface Options {
    source: string        // Original directory to mirror
    persist?: boolean     // Keep sandbox after dispose
  }

  export interface Sandbox {
    root: string          // Sandbox temp directory
    original: string      // Original source directory  
    dispose: () => Promise<void>
  }

  export async function create(opts: Options): Promise<Sandbox>
  export function contains(sandbox: Sandbox, filepath: string): boolean
  export function rewritePath(sandbox: Sandbox, filepath: string): string
}

2. Process Isolation (packages/opencode/src/sandbox/process.ts)

  • Linux: Use bubblewrap (bwrap) for namespace isolation
  • macOS: Use sandbox-exec profiles
  • Windows/fallback: Directory-only isolation
  • Control network access (default: disabled)
export namespace ProcessSandbox {
  export interface Options {
    cwd: string
    allowNetwork?: boolean
    allowedPaths?: string[]
  }

  export function getSpawnOptions(opts: Options): SpawnOptions
  export function wrapWithBwrap(cmd: string[], opts: Options): string[]  // Linux
  export function getSandboxProfile(opts: Options): string  // macOS
}

3. Command Validation (packages/opencode/src/sandbox/commands.ts)

Block dangerous commands by default:

  • rm -rf /, rm -rf ~, rm -rf *
  • sudo *
  • chmod 777 /
  • mkfs.*
  • dd if=* of=/dev/*
  • curl * | sh, wget * | sh
  • > /dev/sda, etc.
export namespace CommandValidator {
  export interface ValidationResult {
    allowed: boolean
    reason?: string
    blockedPattern?: string
  }

  export function validate(command: string, config?: Config): ValidationResult
  export const DEFAULT_BLOCKED_PATTERNS: RegExp[]
}

4. Gym Configuration (packages/opencode/src/sandbox/config.ts)

export const GymConfig = z.object({
  sandbox: z.object({
    enabled: z.boolean().default(true),
    persist: z.boolean().default(false),
    mode: z.enum(["docker", "process", "directory", "none"]).default("directory"),
  }),
  
  tools: z.object({
    bash: z.object({
      enabled: z.boolean().default(true),
      blockedCommands: z.string().array().default([...]),
      timeout: z.number().default(30000),
    }),
    write: z.object({
      enabled: z.boolean().default(true),
      maxFileSize: z.number().default(10 * 1024 * 1024), // 10MB
    }),
    read: z.object({
      enabled: z.boolean().default(true),
      allowOutsideSandbox: z.boolean().default(false),
    }),
  }),
  
  limits: z.object({
    maxDiskUsage: z.number().default(500 * 1024 * 1024), // 500MB
    timeout: z.number().default(3600000), // 1 hour session max
  }),
  
  network: z.object({
    enabled: z.boolean().default(false),
  }),
})

5. Sandbox Instance (packages/opencode/src/sandbox/instance.ts)

Wrap the existing Instance system to provide sandbox context:

export namespace SandboxInstance {
  export interface Options {
    directory: string
    mode: "docker" | "process" | "directory" | "none"
    persist?: boolean
    allowNetwork?: boolean
  }

  export async function provide<R>(opts: Options, fn: () => R): Promise<R>
  export function isActive(): boolean
  export function getConfig(): GymConfig | null
}

6. Main Sandbox Module (packages/opencode/src/sandbox/index.ts)

Export unified interface:

export type SandboxMode = "docker" | "process" | "directory" | "none"

export interface SandboxConfig {
  mode: SandboxMode
  source: string
  allowNetwork?: boolean
  persist?: boolean
  timeout?: number
}

export interface Sandbox {
  type: SandboxMode
  root: string
  dispose: () => Promise<void>
}

export async function createSandbox(config: SandboxConfig): Promise<Sandbox>

7. CLI Command (packages/opencode/src/cli/cmd/gym.ts)

export const GymCommand = cmd({
  command: "gym [directory]",
  describe: "Run OpenCode in sandbox/gym mode for safe development and testing",
  builder: (yar...

</details>

*This pull request was created as a result of the following prompt from Copilot chat.*
> ## Summary
> Implement a sandbox/gym environment for OpenCode that allows safe development and testing without risking accidental file system damage. This is the **local-first implementation** focusing on directory isolation and process-level sandboxing.
> 
> ## Goals
> - Create a safe execution environment where the AI agent can operate without affecting the real filesystem
> - Implement a hybrid approach starting with local isolation (WASM preparation for future, process isolation now)
> - Ensure all file operations are confined to an isolated temporary directory
> - Block dangerous shell commands
> - Provide easy CLI access via `opencode gym` command
> 
> ## Architecture
> 
> ### 1. Sandbox Filesystem (`packages/opencode/src/sandbox/filesystem.ts`)
> - Copy source directory to temp location (`/tmp/opencode-sandbox-*`)
> - Track original  sandbox path mappings
> - Auto-cleanup on dispose (configurable persistence for debugging)
> - Validate all paths stay within sandbox boundaries
> 
> ```typescript
> export namespace SandboxFilesystem {
>   export interface Options {
>     source: string        // Original directory to mirror
>     persist?: boolean     // Keep sandbox after dispose
>   }
> 
>   export interface Sandbox {
>     root: string          // Sandbox temp directory
>     original: string      // Original source directory  
>     dispose: () => Promise<void>
>   }
> 
>   export async function create(opts: Options): Promise<Sandbox>
>   export function contains(sandbox: Sandbox, filepath: string): boolean
>   export function rewritePath(sandbox: Sandbox, filepath: string): string
> }
> ```
> 
> ### 2. Process Isolation (`packages/opencode/src/sandbox/process.ts`)
> - Linux: Use bubblewrap (bwrap) for namespace isolation
> - macOS: Use sandbox-exec profiles
> - Windows/fallback: Directory-only isolation
> - Control network access (default: disabled)
> 
> ```typescript
> export namespace ProcessSandbox {
>   export interface Options {
>     cwd: string
>     allowNetwork?: boolean
>     allowedPaths?: string[]
>   }
> 
>   export function getSpawnOptions(opts: Options): SpawnOptions
>   export function wrapWithBwrap(cmd: string[], opts: Options): string[]  // Linux
>   export function getSandboxProfile(opts: Options): string  // macOS
> }
> ```
> 
> ### 3. Command Validation (`packages/opencode/src/sandbox/commands.ts`)
> Block dangerous commands by default:
> - `rm -rf /`, `rm -rf ~`, `rm -rf *`
> - `sudo *`
> - `chmod 777 /`
> - `mkfs.*`
> - `dd if=* of=/dev/*`
> - `curl * | sh`, `wget * | sh`
> - `> /dev/sda`, etc.
> 
> ```typescript
> export namespace CommandValidator {
>   export interface ValidationResult {
>     allowed: boolean
>     reason?: string
>     blockedPattern?: string
>   }
> 
>   export function validate(command: string, config?: Config): ValidationResult
>   export const DEFAULT_BLOCKED_PATTERNS: RegExp[]
> }
> ```
> 
> ### 4. Gym Configuration (`packages/opencode/src/sandbox/config.ts`)
> ```typescript
> export const GymConfig = z.object({
>   sandbox: z.object({
>     enabled: z.boolean().default(true),
>     persist: z.boolean().default(false),
>     mode: z.enum(["docker", "process", "directory", "none"]).default("directory"),
>   }),
>   
>   tools: z.object({
>     bash: z.object({
>       enabled: z.boolean().default(true),
>       blockedCommands: z.string().array().default([...]),
>       timeout: z.number().default(30000),
>     }),
>     write: z.object({
>       enabled: z.boolean().default(true),
>       maxFileSize: z.number().default(10 * 1024 * 1024), // 10MB
>     }),
>     read: z.object({
>       enabled: z.boolean().default(true),
>       allowOutsideSandbox: z.boolean().default(false),
>     }),
>   }),
>   
>   limits: z.object({
>     maxDiskUsage: z.number().default(500 * 1024 * 1024), // 500MB
>     timeout: z.number().default(3600000), // 1 hour session max
>   }),
>   
>   network: z.object({
>     enabled: z.boolean().default(false),
>   }),
> })
> ```
> 
> ### 5. Sandbox Instance (`packages/opencode/src/sandbox/instance.ts`)
> Wrap the existing `Instance` system to provide sandbox context:
> 
> ```typescript
> export namespace SandboxInstance {
>   export interface Options {
>     directory: string
>     mode: "docker" | "process" | "directory" | "none"
>     persist?: boolean
>     allowNetwork?: boolean
>   }
> 
>   export async function provide<R>(opts: Options, fn: () => R): Promise<R>
>   export function isActive(): boolean
>   export function getConfig(): GymConfig | null
> }
> ```
> 
> ### 6. Main Sandbox Module (`packages/opencode/src/sandbox/index.ts`)
> Export unified interface:
> 
> ```typescript
> export type SandboxMode = "docker" | "process" | "directory" | "none"
> 
> export interface SandboxConfig {
>   mode: SandboxMode
>   source: string
>   allowNetwork?: boolean
>   persist?: boolean
>   timeout?: number
> }
> 
> export interface Sandbox {
>   type: SandboxMode
>   root: string
>   dispose: () => Promise<void>
> }
> 
> export async function createSandbox(config: SandboxConfig): Promise<Sandbox>
> ```
> 
> ### 7. CLI Command (`packages/opencode/src/cli/cmd/gym.ts`)
> ```typescript
> export const GymCommand = cmd({
>   command: "gym [directory]",
>   describe: "Run OpenCode in sandbox/gym mode for safe development and testing",
>   builder: (yargs) =>
>     yargs
>       .positional("directory", {
>         type: "string",
>         default: process.cwd(),
>         describe: "Directory to sandbox",
>       })
>       .option("mode", {
>         choices: ["docker", "process", "directory", "none"] as const,
>         default: "directory" as const,
>         describe: "Isolation mode (directory is safest without Docker)",
>       })
>       .option("network", {
>         type: "boolean",
>         default: false,
>         describe: "Allow network access in sandbox",
>       })
>       .option("persist", {
>         type: "boolean",
>         default: false,
>         describe: "Keep sandbox directory after exit (for debugging)",
>       })
>       .option("timeout", {
>         type: "number",
>         default: 3600000,
>         describe: "Session timeout in milliseconds",
>       }),
>   async handler(args) {
>     // Create sandbox and launch TUI
>   },
> })
> ```
> 
> ### 8. Tool Modifications
> 
> **Modify `packages/opencode/src/tool/bash.ts`:**
> - Add command validation before execution
> - Respect sandbox boundaries for cwd
> - Apply blocked command patterns
> 
> **Modify `packages/opencode/src/tool/write.ts`:**
> - Check if in sandbox mode
> - Rewrite paths to sandbox directory
> - Enforce file size limits
> 
> **Modify `packages/opencode/src/tool/read.ts`:**
> - Check if in sandbox mode  
> - Block reads outside sandbox (configurable)
> - Rewrite paths transparently
> 
> **Modify `packages/opencode/src/tool/patch.ts`:**
> - Same path rewriting as write tool
> 
> ### 9. Integration with Existing Systems
> 
> - **Snapshot system**: Works normally within sandbox (tracks sandbox directory)
> - **Permission system**: Sandbox adds additional layer on top of existing permissions
> - **LSP**: Operates within sandbox directory
> - **Git operations**: Work within sandbox (isolated .git)
> 
> ## File Structure
> ```
> packages/opencode/src/sandbox/
> ├── index.ts           # Main exports
> ├── filesystem.ts      # Temp directory isolation
> ├── process.ts         # OS-level process isolation  
> ├── commands.ts        # Command validation/blocking
> ├── config.ts          # Zod schema for gym config
> └── instance.ts        # SandboxInstance provider
> 
> packages/opencode/src/cli/cmd/
> └── gym.ts             # CLI command
> ```
> 
> ## Testing
> Create test files in `packages/opencode/test/sandbox/`:
> - `filesystem.test.ts` - Test directory isolation
> - `commands.test.ts` - Test command validation
> - `integration.test.ts` - End-to-end sandbox tests
> 
> ## Usage Examples
> 
> ```bash
> # Run in sandbox mode (default: directory isolation)
> opencode gym ./my-project
> 
> # Run with process isolation (Linux with bwrap)
> opencode gym ./my-project --mode process
> 
> # Allow network access
> opencode gym ./my-project --network
> 
> # Keep sandbox for debugging
> opencode gym ./my-project --persist
> 
> # With timeout
> opencode gym ./my-project --timeout 1800000
> ```
> 
> ## Future Enhancements (Not in this PR)
> - Docker container isolation mode
> - WASM-based tool execution for sub-10ms operations
> - Firecracker microVM support for hosted gym
> - E2B integration for cloud sandboxes
> - Pre-warmed sandbox pools

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

opencode and others added 30 commits November 22, 2025 05:02
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
…if not current day (anomalyco#4503)

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: opencode <opencode@sst.dev>
Copilot AI self-assigned this Nov 27, 2025
Copilot AI and others added 3 commits November 27, 2025 06:20
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement sandbox environment for OpenCode development Add sandbox/gym environment for safe AI development Nov 27, 2025
Copilot AI requested a review from v1truv1us November 27, 2025 06:29
@v1truv1us
Copy link
Owner

@copilot review the failing checks and address the problems

Copilot AI and others added 3 commits November 27, 2025 15:25
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Co-authored-by: ferg-cod3s <28580642+ferg-cod3s@users.noreply.github.com>
Copy link
Author

Copilot AI commented Nov 27, 2025

@copilot review the failing checks and address the problems

Fixed the TypeScript typecheck failures in commit 36a8b85. The issue was with Zod's .default({}) on nested object schemas causing type mismatches. Restructured the schema to use named sub-schemas with .optional() at the top level, and the defaults() function now uses .parse({}) on each sub-schema to apply the defaults.

@v1truv1us v1truv1us force-pushed the dev branch 2 times, most recently from 4b65759 to d7c4c3b Compare December 29, 2025 23:31
@v1truv1us v1truv1us force-pushed the dev branch 2 times, most recently from 4b65759 to 8ed4027 Compare January 4, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.