diff --git a/examples/sandboxy/README.md b/examples/sandboxy/README.md index 3246c2e8f..f1837b951 100644 --- a/examples/sandboxy/README.md +++ b/examples/sandboxy/README.md @@ -100,6 +100,7 @@ On first run, `sandboxy` downloads a kernel, pulls a base image, and installs th ## Supported Agents - **Claude Code** - built-in +- **Pi** - built-in; PTY mode is enabled automatically Additional agents can be added via JSON config files. See [Adding a New Agent](#adding-a-new-agent). @@ -148,6 +149,9 @@ sandboxy run --rm claude # Pass flags through to the agent sandboxy run claude -- --model foobar + +# Run Pi's interactive terminal UI (PTY is automatic for Pi) +sandboxy run pi ``` **Options:** @@ -267,7 +271,7 @@ Every session automatically saves its rootfs when it exits. The instance appears ```bash # First run -- auto-named instance sandboxy run claude -# => Instance claude-20260328-091522 saved. Resume with: sandboxy run claude --name claude-20260328-091522 +# => Instance claude-20260328-091522 saved. Resume with: sandboxy run --name claude-20260328-091522 claude # Resume it sandboxy run --name claude-20260328-091522 claude diff --git a/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift b/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift index 38b8f4c87..987001729 100644 --- a/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift +++ b/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift @@ -108,7 +108,8 @@ struct AgentMount: Codable, Sendable { extension AgentDefinition { /// Built-in agent definitions, keyed by their CLI name. static let builtIn: [String: AgentDefinition] = [ - "claude": .claude + "claude": .claude, + "pi": .pi, ] /// Returns all available agents: built-in definitions merged with any @@ -186,6 +187,38 @@ extension AgentDefinition { "*.pythonhosted.org", ] ) + + static let pi = AgentDefinition( + displayName: "Pi", + baseImage: "docker.io/library/node:22", + installCommands: [ + "apt-get update && apt-get install -y --no-install-recommends git ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*", + "npm install -g --ignore-scripts @earendil-works/pi-coding-agent", + ], + launchCommand: ["pi"], + environmentVariables: [ + "ANTHROPIC_API_KEY", + "ANTHROPIC_OAUTH_TOKEN", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "OPENROUTER_API_KEY", + "XAI_API_KEY", + ], + mounts: [ + AgentMount(hostPath: "~/.pi/agent", containerPath: "/root/.pi/agent") + ], + allowedHosts: [ + "*.anthropic.com", + "api.openai.com", + "generativelanguage.googleapis.com", + "openrouter.ai", + "*.openrouter.ai", + "api.x.ai", + "npm.org", + "*.npmjs.org", + ] + ) } /// All-optional mirror of `AgentDefinition` used when loading user override files. diff --git a/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift b/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift index cdad0d318..447831d53 100644 --- a/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift +++ b/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift @@ -654,7 +654,10 @@ private func runContainerSession( let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH]) let current = try Terminal.current try current.setraw() - defer { current.tryReset() } + defer { + sigwinchStream.cancel() + current.tryReset() + } // Build environment for the agent process. var envVarsBuilder = [ @@ -684,17 +687,17 @@ private func runContainerSession( envVarsBuilder.append("https_proxy=\(proxyURL)") envVarsBuilder.append("NO_PROXY=localhost,127.0.0.1") envVarsBuilder.append("no_proxy=localhost,127.0.0.1") - envVarsBuilder.append("GLOBAL_AGENT_HTTP_PROXY=\(proxyURL)") - envVarsBuilder.append("GLOBAL_AGENT_HTTPS_PROXY=\(proxyURL)") - envVarsBuilder.append("GLOBAL_AGENT_NO_PROXY=localhost,127.0.0.1") - - // Prepend global-agent bootstrap to NODE_OPTIONS so Node.js http/https - // modules respect the proxy environment variables. - if let idx = envVarsBuilder.firstIndex(where: { $0.hasPrefix("NODE_OPTIONS=") }) { - let existing = String(envVarsBuilder[idx].dropFirst("NODE_OPTIONS=".count)) - envVarsBuilder[idx] = "NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js \(existing)" - } else { - envVarsBuilder.append("NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js") + if definition.installCommands.contains(where: { $0.contains("global-agent") }) { + envVarsBuilder.append("GLOBAL_AGENT_HTTP_PROXY=\(proxyURL)") + envVarsBuilder.append("GLOBAL_AGENT_HTTPS_PROXY=\(proxyURL)") + envVarsBuilder.append("GLOBAL_AGENT_NO_PROXY=localhost,127.0.0.1") + + if let idx = envVarsBuilder.firstIndex(where: { $0.hasPrefix("NODE_OPTIONS=") }) { + let existing = String(envVarsBuilder[idx].dropFirst("NODE_OPTIONS=".count)) + envVarsBuilder[idx] = "NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js \(existing)" + } else { + envVarsBuilder.append("NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js") + } } } @@ -725,6 +728,7 @@ private func runContainerSession( let status = try await agentProcess.wait() group.cancelAll() + sigwinchStream.cancel() try await agentProcess.delete() @@ -754,7 +758,9 @@ private func runContainerSession( ) try stopped.save(appRoot: Sandboxy.appRoot) - ProgressUI.printStatus("Instance \u{1b}[1m\(instanceName)\u{1b}[0m saved. Resume with: sandboxy run \(agentName) --name \(instanceName)") + ProgressUI.printStatus( + "Instance \u{1b}[1m\(instanceName)\u{1b}[0m saved. Resume with: sandboxy run --name \(instanceName) \(agentName)" + ) } if status.exitCode != 0 {