Skip to content

pi-mono Deep Dive 15: Containerization, Sandbox, Permission Model — Gondolin, Docker, OpenShell, Security Boundaries

Aug 31, 20261 min
TL;DRWhy Pi has no built-in permission system, Gondolin Extension (micro-VM), Docker mode, OpenShell policy-controlled sandbox, permission model philosophy, three containerization patterns, security boundary comparison, micro-VM vs container vs process isolation.

🌏 中文版

TL;DR

  • 設計哲學:Pi 不內建 Permission System,理由是「一種規則不適合所有環境」,交由 Extension/容器實作
  • 三種容器化模式:
    1. Gondolin Extension:微 VM(KVM/QEMU)、Host 保留 Auth、工具路由到 VM
    2. Docker:整個 Pi 跑在 Container、簡單隔離
    3. OpenShell:Policy-controlled Sandbox、細粒度權限
  • Gondolin 細節:packages/coding-agent/src/extensions/gondolin/、KVM/QEMU、9pfs 檔案共享、Tool Proxy
  • OpenShell:Policy Engine、System Call Interception、Capability-based Security
  • Extension 實作沙盒:onBeforeToolCall Hook 攔截、路由到沙盒執行器

為什麼 Pi 不內建 Permission System?

設計決策

「Pi 不包含內建權限系統來限制檔案系統、進程、網路或憑證存取。預設情況下,它以啟動使用者和進程的權限運行。」

理由

考量說明
環境多樣性本機開發、CI/CD、生產伺服器、Kubernetes、無伺服器——每個環境需不同權限模型
使用者自主開發者最了解自己的風險承受度、專案敏感度
擴充性Permission System 很難設計成通用、Extension 更靈活
安全邊界Process-level isolation 不夠、需要容器/VM 級別
維護負擔內建權限系統會成為維護負債、且永遠無法滿足所有人

替代方案

需求Pi 官方建議
檔案系統限制Gondolin Extension(微 VM)、Docker、OpenShell
進程隔離Docker、Podman、systemd-nspawn
網路控制容器網路命名空間、防火牆規則、OpenShell Policy
憑證保護容器內運行、秘密管理系統(Vault、Sealed Secrets)
審計日誌Extension Hooks 記錄所有工具調用

三種容器化部署模式對比

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Pi Containerization Modes                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐           │
│  │   Gondolin       │  │     Docker       │  │    OpenShell     │           │
│  │   (Micro-VM)     │  │   (Container)    │  │   (Sandbox)      │           │
│  ├──────────────────┤  ├──────────────────┤  ├──────────────────┤           │
│  │ 隔離等級: ★★★★★  │  │ 隔離等級: ★★★★☆  │  │ 隔離等級: ★★★★☆  │           │
│  │ 啟動時間: ~秒     │  │ 啟動時間: ~毫秒   │  │ 啟動時間: ~毫秒   │           │
│  │ 資源開銷: 高      │  │ 資源開銷: 低      │  │ 資源開銷: 中      │           │
│  │ 宿主機需求: KVM   │  │ 宿主機需求: Docker│  │ 宿主機需求: Linux │           │
│  │ 檔案共享: 9pfs    │  │ 檔案共享: Volume  │  │ 檔案共享: Bind   │           │
│  │ Auth 保留 Host    │  │ Auth 在 Container │  │ Auth 在 Sandbox  │           │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Gondolin Extension:微 VM 隔離

架構

┌─────────────────────────────────────────────────────────────────┐
│                        Gondolin Architecture                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Host (Pi Process)                    Guest (Micro-VM)          │
│  ┌─────────────────────┐           ┌─────────────────────┐      │
│  │ Pi Agent            │           │ Linux Kernel        │      │
│  │ ├─ Auth (Keys)      │           │ ├─ Tool Executors   │      │
│  │ ├─ LLM Calls        │    ◄───►  │ ├─ File System      │      │
│  │ ├─ UI/TUI           │   9pfs    │ ├─ Process Manager  │      │
│  │ └─ Gondolin Ext     │           │ └─ Network Stack    │      │
│  └─────────────────────┘           └─────────────────────┘      │
│         │                                    ▲                   │
│         │         QEMU/KVM + virtio         │                   │
│         └────────────────────────────────────┘                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

核心組件

// packages/coding-agent/src/extensions/gondolin/client.ts
export class GondolinClient {
  private vm: MicroVM;
  private fileServer: NinePServer;
  
  async start(config: GondolinConfig): Promise<void> {
    // 1. 啟動 Micro-VM (QEMU/KVM)
    this.vm = await MicroVM.create({
      kernel: config.kernelPath,
      initrd: config.initrdPath,
      memory: config.memory ?? "512M",
      cpus: config.cpus ?? 2,
      // 9pfs 共享宿主機檔案系統
      fs: { type: "9p", tag: "hostfs", path: config.workspacePath },
    });
    
    // 2. 等待 Guest Agent 就緒
    await this.waitForGuestAgent();
    
    // 3. 建立 Tool Proxy
    this.toolProxy = new ToolProxy(this.vm);
  }
  
  async executeTool(toolName: string, args: unknown): Promise<ToolResult> {
    // 將工具調用路由到 VM 內執行
    return this.toolProxy.execute(toolName, args);
  }
}

9pfs 檔案共享

// 宿主機掛載到 Guest
const qemuArgs = [
  "-virtfs", "local,path=/host/workspace,mount_tag=hostfs,security_model=mapped-xattr",
  // Guest 內: mount -t 9p -o trans=virtio hostfs /workspace
];

Tool Proxy:路由工具執行

// packages/coding-agent/src/extensions/gondolin/tool-proxy.ts
export class ToolProxy {
  constructor(private vm: MicroVM) {}
  
  async execute(toolName: string, args: unknown): Promise<ToolResult> {
    const requestId = uuidv7();
    
    // 1. 發送請求到 Guest Agent
    await this.vm.send({
      type: "tool_execute",
      requestId,
      tool: toolName,
      args,
    });
    
    // 2. 等待回應(支援串流更新)
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => reject(new Error("Timeout")), 120000);
      
      this.vm.onMessage((msg) => {
        if (msg.requestId === requestId) {
          clearTimeout(timeout);
          if (msg.type === "tool_result") resolve(msg.result);
          else if (msg.type === "tool_error") reject(new Error(msg.error));
        }
      });
    });
  }
}

Guest Agent(VM 內)

// Gondolin Guest 內部運行的 Agent
class GuestAgent {
  private tools: Map<string, AgentTool> = new Map();
  
  async handleExecute(request: ToolExecuteRequest): Promise<void> {
    const tool = this.tools.get(request.tool);
    if (!tool) {
      this.send({ type: "tool_error", requestId: request.requestId, error: "Tool not found" });
      return;
    }
    
    try {
      const result = await tool.execute(request.requestId, request.args, 
        new AbortSignal(), (partial) => {
          // 串流更新回 Host
          this.send({ type: "tool_update", requestId: request.requestId, partial });
        }
      );
      this.send({ type: "tool_result", requestId: request.requestId, result });
    } catch (e) {
      this.send({ type: "tool_error", requestId: request.requestId, error: e.message });
    }
  }
}

Docker 模式:簡單隔離

Dockerfile

# Dockerfile.pi
FROM node:22-alpine

# 安裝 Pi
RUN npm install -g @earendil-works/pi-coding-agent

# 建立非 root 使用者
RUN adduser -D -s /bin/sh pi
USER pi
WORKDIR /workspace

# 進入點
ENTRYPOINT ["pi"]

Docker Compose

# docker-compose.yml
version: '3.8'
services:
  pi:
    build: .
    volumes:
      - ./workspace:/workspace
      - ~/.pi:/home/pi/.pi  # 持久化設定
      - ~/.ssh:/home/pi/.ssh:ro  # SSH Keys (唯讀)
    environment:
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    # 安全選項
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
      - /home/pi/.cache

執行模式

# 1. 互動模式
docker compose run --rm pi

# 2. 單次指令
docker compose run --rm pi -p "Read package.json"

# 3. 持久化 Session
docker compose run --rm -v pi-sessions:/home/pi/.pi/agent/sessions pi

OpenShell:Policy-controlled Sandbox

架構

┌─────────────────────────────────────────────────────────────────┐
│                        OpenShell Architecture                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────┐     ┌─────────────────┐                    │
│  │ Pi Process      │     │ OpenShell       │                    │
│  │                 │     │ Sandbox         │                    │
│  │ Tool Call ──────┼────►│ Policy Engine   │                    │
│  │                 │     │ ├─ Allow/Deny   │                    │
│  │ Result ◄────────┼────┤ ├─ Resource     │                    │
│  │                 │     │ │   Limits      │                    │
│  │                 │     │ └─ Syscall      │                    │
│  │                 │     │     Intercept   │                    │
│  └─────────────────┘     └─────────────────┘                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Policy 定義

# openshell-policy.yaml
version: "1.0"
policies:
  - name: "default-deny"
    default: deny
    rules:
      - action: allow
        tool: "read"
        paths:
          - "/workspace/**"
        conditions:
          - "file.size < 10MB"
  
  - name: "write-restricted"
    default: deny
    rules:
      - action: allow
        tool: "write"
        paths:
          - "/workspace/src/**"
          - "/workspace/tests/**"
        conditions:
          - "file.extension in ['.ts', '.js', '.json', '.md']"
  
  - name: "bash-limited"
    default: deny
    rules:
      - action: allow
        tool: "bash"
        commands:
          - "npm test"
          - "npm run build"
          - "git status"
          - "git diff"
        denied:
          - "rm -rf"
          - "sudo"
          - "chmod 777"

Policy Engine 實作

// openshell/src/policy-engine.ts
export class PolicyEngine {
  private policies: Policy[];
  
  evaluate(tool: string, args: unknown): PolicyDecision {
    for (const policy of this.policies) {
      const rule = policy.rules.find(r => r.tool === tool);
      if (!rule) continue;
      
      // 條件匹配
      if (this.matchConditions(rule.conditions, args)) {
        return { allowed: rule.action === "allow", reason: rule.reason };
      }
    }
    return { allowed: false, reason: "No matching policy" };
  }
  
  private matchConditions(conditions: Condition[], args: unknown): boolean {
    return conditions.every(c => this.evaluateCondition(c, args));
  }
}

System Call Intercept (Linux)

// openshell/src/syscall-intercept.c
// 使用 seccomp-bpf 或 ptrace 攔截系統調用
static int seccomp_filter(struct seccomp_data *data) {
    // 允許的 syscall 白名單
    switch (data->nr) {
        case __NR_read:
        case __NR_write:
        case __NR_openat:
        case __NR_close:
            return SECCOMP_RET_ALLOW;
        case __NR_execve:
        case __NR_execveat:
            // 檢查命令是否在允許清單
            return check_execve_allowed(data) ? SECCOMP_RET_ALLOW : SECCOMP_RET_KILL;
        default:
            return SECCOMP_RET_KILL;
    }
}

Extension 實作沙盒:Hook Points

onBeforeToolCall 攔截

// 任意 Extension 可實作沙盒路由
const sandboxExtension: Extension = {
  name: "custom-sandbox",
  version: "1.0.0",
  
  onBeforeToolCall: async (ctx, signal) => {
    const { toolCall, args } = ctx;
    
    // 1. 檢查是否需要沙盒
    if (requiresSandbox(toolCall.name, args)) {
      // 2. 路由到沙盒執行器
      const result = await sandboxExecutor.execute(toolCall.name, args);
      
      // 3. 返回結果,阻擋原生執行
      return {
        block: true,
        reason: "Executed in sandbox",
        // 自訂結果
        customResult: result,
      };
    }
    
    return undefined; // 繼續原生執行
  },
};

function requiresSandbox(toolName: string, args: unknown): boolean {
  // 高風險工具
  if (["bash", "write", "edit"].includes(toolName)) return true;
  
  // 敏感路徑
  if (args && typeof args === "object" && "path" in args) {
    const path = (args as any).path;
    if (path.startsWith("/etc/") || path.startsWith("/root/")) return true;
  }
  
  return false;
}

Sandbox Executor 抽象

interface SandboxExecutor {
  execute(toolName: string, args: unknown): Promise<ToolResult>;
}

class DockerSandboxExecutor implements SandboxExecutor {
  async execute(toolName: string, args: unknown): Promise<ToolResult> {
    // 在臨時 Container 執行
    const container = await docker.createContainer({
      Image: "pi-sandbox:latest",
      Cmd: [toolName, JSON.stringify(args)],
      HostConfig: {
        NetworkMode: "none",
        ReadonlyRootfs: true,
        Memory: 512 * 1024 * 1024,
        CpuQuota: 50000,
      },
    });
    
    await container.start();
    const result = await container.wait();
    const logs = await container.logs({ stdout: true, stderr: true });
    await container.remove();
    
    return { content: [{ type: "text", text: logs }], details: { exitCode: result.StatusCode } };
  }
}

class GondolinSandboxExecutor implements SandboxExecutor {
  async execute(toolName: string, args: unknown): Promise<ToolResult> {
    // 路由到 Gondolin Micro-VM
    return gondolinClient.executeTool(toolName, args);
  }
}

class OpenShellSandboxExecutor implements SandboxExecutor {
  async execute(toolName: string, args: unknown): Promise<ToolResult> {
    // 經過 Policy Engine 評估
    const decision = policyEngine.evaluate(toolName, args);
    if (!decision.allowed) {
      throw new Error(`Policy denied: ${decision.reason}`);
    }
    // 執行(可能在 seccomp 沙盒內)
    return nativeExecute(toolName, args);
  }
}

安全邊界總結

攻擊向量GondolinDockerOpenShell原生 Pi
惡意檔案寫入✅ VM 隔離✅ Volume 權限✅ Policy Deny❌ 無防護
惡意指令執行✅ VM 隔離✅ Capability Drop✅ Seccomp/Seccomp❌ 無防護
網路洩漏✅ VM 網路隔離✅ Network Namespace✅ Policy Control❌ 無防護
憑證竊取✅ Auth 留 Host⚠️ 需 Volume 掛載⚠️ 需 Policy 保護❌ 無防護
逃逸風險低中低高

選擇指南

場景推薦模式
本機開發、高安全需求Gondolin (Micro-VM)
CI/CD Pipeline、快速啟動Docker
生產環境、細粒度控制OpenShell
團隊共享、標準化Docker + 共享 Policy
臨時實驗、一次性任務原生 Pi + Extension Hook

參考資料


下一篇預告

第 16 篇:Release 流程、Lockstep Versioning、Binary Build、Trusted Publishing

Lockstep Versioning 所有套件同版本、Release Script 流程、Local Smoke Test、Binary Build (Bun/Node)、npm-shrinkwrap.json、GitHub Actions OIDC Trusted Publishing、R2 Release Marker、pi.dev/api/latest-version、Announcement Verification。