Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/sandboxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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:**
Expand Down Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion examples/sandboxy/Sources/sandboxy/AgentDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 19 additions & 13 deletions examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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") }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AntApper I don't have a deep understanding of what's necessary when embedding agents in containers but having all this mostly-nodes-related stuff conditionalized here feels somewhat arbitrary.

Can you think of any way to do this declaratively as part of the agent definition, such that it's secure (for example, doesn't allow an agent setup to perform arbitrary operations on the host or access user data)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the conversation here to discuss approaches before making any code changes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completely agree, sniffing the install commands was a bit of a hack and having node paths hardcoded in the runner is messy.

the only reason it's there is claude code doesn't respect standard HTTP_PROXY env vars without global-agent, whereas pi doesn't need it (and node crashes if it tries to require a file that isn't installed).

to make it declarative and safe from arbitrary user JSONs, we could do a simple enum like proxyBootstrap: .standard | .nodeGlobalAgent so it's strictly bounded, or something like a proxyEnv dictionary on the agent definition with a {PROXY_URL} token so sandboxy doesn't need any node-specific logic at all. since it's just setting container env vars either way, host files/execution wouldn't be exposed.

let me know if either of those sounds good to you or if you were picturing something different!

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")
}
}
}

Expand Down Expand Up @@ -725,6 +728,7 @@ private func runContainerSession(

let status = try await agentProcess.wait()
group.cancelAll()
sigwinchStream.cancel()

try await agentProcess.delete()

Expand Down Expand Up @@ -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 {
Expand Down
Loading