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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,27 @@ working_dir = "/tmp"
env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" }
```

### SSH Sandbox (Local Deployments)

For local deployments, you can run the agent inside an isolated VM or container using SSH as a transparent stdio transport β€” no changes to OpenAB are needed. SSH is a byte pipe over stdin/stdout, which is exactly how `AcpConnection` communicates with agents.

```toml
[agent]
command = "ssh"
args = [
"-T", # no PTY β€” required, PTY corrupts JSON-RPC
"-o", "BatchMode=yes", # fail-fast, no interactive prompts
"-o", "ServerAliveInterval=30", # keep-alive for long sessions
"-o", "ServerAliveCountMax=3",
"-o", "StrictHostKeyChecking=accept-new", # daemon has no terminal for prompts
"user@sandbox-host",
"claude", "--acp"
]
working_dir = "/tmp"
```

See [docs/ssh-sandbox.md](docs/ssh-sandbox.md) for setup details, MCP server access patterns, and known limitations.

## Configuration Reference

```toml
Expand Down
16 changes: 16 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ working_dir = "/home/agent"
# working_dir = "/home/agent"
# env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" }

# SSH sandbox β€” run agent inside an isolated VM or container (local deployments)
# SSH is a transparent byte pipe over stdio; no changes to ACP protocol needed.
# See docs/ssh-sandbox.md for setup guide and known limitations.
# [agent]
# command = "ssh"
# args = [
# "-T", # no PTY β€” required, PTY corrupts JSON-RPC
# "-o", "BatchMode=yes", # fail-fast, no interactive prompts
# "-o", "ServerAliveInterval=30", # keep-alive for long sessions
# "-o", "ServerAliveCountMax=3",
# "-o", "StrictHostKeyChecking=accept-new", # daemon has no terminal for prompts
# "user@sandbox-host",
# "claude", "--acp"
# ]
# working_dir = "/tmp"

[pool]
max_sessions = 10
session_ttl_hours = 24
Expand Down
114 changes: 114 additions & 0 deletions docs/ssh-sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# SSH Sandbox for Local Deployments

OpenAB targets k3s on cloud, where Kubernetes NetworkPolicy and Pod isolation handle security. For local deployments (developer laptop, home server), the default config runs the agent with full host permissions:

```toml
[agent]
command = "claude"
args = ["--acp"]
```

The Claude subprocess inherits the host's full filesystem and network access. For a Discord bot accepting messages from arbitrary users, this is a meaningful attack surface.

## SSH as a Zero-Code-Change Transport

`AcpConnection::spawn()` treats the agent as a stdio JSON-RPC process. SSH is a transparent byte pipe over that same stdio β€” no changes to the ACP protocol, `SessionPool`, or `AcpConnection` internals are needed.

```
Current Proposed
─────────────────────── ──────────────────────────────
OpenAB OpenAB
β”‚ spawn β”‚ spawn
β–Ό β–Ό
claude (host permissions) ssh -T user@sandbox
β”œβ”€ reads ~/.ssh βœ— β”‚ encrypted stdio pipe
β”œβ”€ reads ~/Documents βœ— β–Ό
└─ unrestricted network βœ— claude (inside sandbox)
β”œβ”€ restricted filesystem βœ“
β”œβ”€ network allowlist βœ“
└─ MCP via host proxy βœ“
```

## Configuration

```toml
[agent]
command = "ssh"
args = [
"-T", # no PTY β€” required (see below)
"-o", "BatchMode=yes", # fail-fast, no interactive prompts
"-o", "ServerAliveInterval=30", # keep-alive for long sessions
"-o", "ServerAliveCountMax=3",
"-o", "StrictHostKeyChecking=accept-new", # daemon has no terminal for prompts
"user@sandbox-host",
"claude", "--acp"
]
working_dir = "/tmp"
```

### Why `-T` Is Required

| Flag | Behavior | JSON-RPC safe? |
|------|----------|----------------|
| `-T` | Clean byte pipe, stderr separated | Yes |
| `-t` | Warns "PTY not allocated", stderr leaks into stdout | No β€” corrupts JSON stream |
| `-tt` | Forced PTY + piped stdin β†’ hangs indefinitely | No β€” deadlock |

PTY inserts CR/LF conversion (`\n` β†’ `\r\n`), merges stderr into stdout, and enables echo mode β€” all of which break JSON-RPC parsing. **`-T` is mandatory, not optional.**

### Why `BatchMode=yes`

OpenAB runs as a daemon without a terminal. Interactive password prompts will hang the process. `BatchMode=yes` forces fail-fast behavior. SSH key-based auth must be configured beforehand.

## Sandbox Options

The SSH target is your choice β€” OpenAB does not care what is behind the SSH connection:

| Environment | SSH target | Notes |
|-------------|-----------|-------|
| Mac (OrbStack) | `vm-name@orb` | Via `~/.orbstack/ssh/config` ProxyCommand |
| Linux | `user@nspawn-container` | systemd-nspawn with SSH |
| Remote machine | `user@10.0.0.5` | Any Linux server |
| Docker | wrapper script using `docker exec` | Alternative to SSH |

## MCP Server Access from Sandbox

If MCP servers run on the host, the sandbox cannot reach them via `localhost` (which resolves to the sandbox's own loopback). Options:

**Option A: Host DNS alias (OrbStack)**
```
claude (VM) ──http://host.internal:PORT──► MCP server (host)
```

**Option B: SSH port forwarding (universal)**
```bash
# add to ssh args:
"-L", "8080:localhost:8080"
```
```
claude (VM) ──http://localhost:8080──► [tunnel] ──► MCP server (host)
```

**Option C: Network bridge (Docker `--network host`)**
```
claude (container) ──http://localhost:PORT──► MCP server (host)
```

## Known Limitations

### `kill_on_drop` does not reliably terminate remote processes

Killing the local SSH client process leaves the remote subprocess running. The SSH server sends SIGHUP to the remote shell, but the agent may survive (especially with `nohup` or ControlMaster active).

Mitigations:
- Do **not** use SSH ControlMaster for agent connections
- Ensure the SSH server has `ClientAliveInterval` set to detect dead clients
- Session pool TTL cleanup (`session_ttl_hours`) will eventually reclaim idle sessions

### SSH connection startup latency

Each `AcpConnection::spawn()` incurs an SSH handshake (~50–200 ms). This is negligible for long-lived sessions (default pool TTL = 24 h), but noticeable if sessions are frequently recycled.

### SSH key auth is required

OpenAB runs as a daemon without a terminal. Configure SSH key-based authentication on the sandbox host before using this transport.