# Terminal

Browser-based shell into any computer you own. The terminal is a **human debug window**, you use it to inspect what your agent did, tail logs, verify state, and diagnose failures. It's not how your agent talks to the computer (the agent is already inside the sandbox as a normal process), and it's not how you configure or set up your computer (that's strictly declarative via `orb.toml`).

When to use it:
- Tail a log the agent wrote: `tail -f /agent/code/logs/agent.log`
- Verify agent env vars: `env | grep ANTHROPIC`
- Check package state: `pip list | grep claude-agent-sdk`
- Run the agent binary manually to see fresh stderr: `python agent.py`
- Poke at `/agent/code/findings/` after an overnight run
- Test LLM connectivity: `curl $ANTHROPIC_BASE_URL/v1/models -H "x-api-key: $ANTHROPIC_API_KEY"`

### The terminal is **not** for setup

ORB is declarative. Every piece of setup you need your computer to have must live in `orb.toml` or your cron config, so that tearing down and redeploying the computer reproduces the same environment byte-for-byte. The terminal does not survive a rebuild.

- Install packages → `orb.toml` `[build].steps` + `POST /v1/computers/{id}/build`
- Place files → your git source; ORB clones it into the sandbox on build
- Schedule tasks → `/agent/.orb/cron.json` (or `orb.toml` `[build]` step that writes it)
- Provision secrets → `org_secrets` at deploy time, referenced via `${VAR}` in `[agent.env]`
- Write startup config → `[build].steps` or baked into your agent's source

If you find yourself `apt install`-ing something in the terminal, stop, add it to `orb.toml` instead. The next rebuild will destroy that state and you'll be confused when it breaks.

The terminal is strictly for **inspection** and **debugging**. Treat it like `kubectl exec` into a production pod: useful for seeing what's happening, never for defining what happens.

---

## Open a terminal

```
https://api.orbcloud.dev/terminal/{computer-id}
```

The page prompts for an API key. Paste any `orb_…` key belonging to the org that owns the computer, click **Connect**. The key is saved to browser localStorage; subsequent visits skip the prompt.

Example (replace with your own computer id):

```
https://api.orbcloud.dev/terminal/080767ba-9c07-411b-a13a-87e788acd255
```

Direct WebSocket endpoint (if you're building your own client):

```
wss://api.orbcloud.dev/v1/computers/{computer-id}/terminal?key=orb_YOUR_KEY
```

---

## What you get

A `bash --login` shell as `root` inside the sandbox, starting in `/root`, with a real PTY (prompt, line editing, colors, TUI apps all work).

**Exported environment mirrors your running agent:**

- Everything in your `orb.toml` `[agent.env]` (literals; `${VAR}` references resolve empty, the terminal doesn't carry your org secrets by design)
- `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `LLM_BASE_URL`, `GOOGLE_API_BASE_URL`, `ORB_PROXY_URL` all point at your per-computer LLM proxy
- `PATH` includes `/root/.npm-global/bin` so globally-installed CLI tools work (`claude`, `npx …` etc.)
- `HOME=/root`, `TERM=xterm-256color`

The shell is in the **same network namespace** as your agent. LLM requests from the terminal go through the same proxy and count as your usage, exactly like your agent's calls.

---

## What works

- Interactive prompt with line editing (`↑`/`↓` history, `Tab` completion, `Ctrl+R` reverse search)
- Full-screen TUI apps: `vim`, `htop`, `less`, `nano`, `tmux`, `top`
- ANSI colors (`ls --color`, `git` colored output, `python -m rich` etc.)
- Window resize, `tput cols; tput lines` tracks the browser window; apps like `vim` reflow correctly
- UTF-8 input and output
- `Ctrl+C` to interrupt, `Ctrl+D` to exit, `Ctrl+Z` to suspend
- Copy: select with mouse → `Ctrl+Shift+C`. Paste: `Ctrl+Shift+V`
- Keepalive, the server pings every 25 seconds; an idle prompt stays connected indefinitely (sessions can last hours, days, overnight, no cap)
- Works while the agent is asleep, the terminal spawns its own bash, never wakes the agent
- **Clickable URLs**, bare `http(s)://…` in output is underlined; `Cmd/Ctrl+click` opens in a new tab
- **Search scrollback**, `Ctrl+F` opens a search bar, `Enter` = next, `Shift+Enter` = previous, `Esc` = close; 10 000-line scrollback
- **Drag-drop file upload**, drag a file from your desktop onto the terminal window; it lands at `/root/<filename>` inside the sandbox. Or click the **Upload** button in the toolbar. 200 MB per file. Also callable from the CLI via `PUT /v1/computers/{id}/files/{path}`, see [API Reference](api-reference.md#files).
- **File download**, click **Download** in the toolbar and type a path (e.g. `agent/code/findings/candidates.json`). Browser saves it. Same API under the hood: `GET /v1/computers/{id}/files/{path}`.
- **Mobile keyboard toolbar**, on touch devices, a bottom strip exposes `Esc`, `Tab`, sticky `Ctrl`, sticky `Alt`, arrow keys, `^C`/`^D`/`^Z`/`^L`, and common shell characters (`| / ~ - >`). Sticky modifier = tap `Ctrl`, then tap `c` → sends `^C`, modifier releases.

---

## What doesn't work yet

- **Session persistence across page reload.** Refreshing the browser ends the session. Work around it with `tmux` inside the shell: `tmux new -s work` first time, `tmux attach -t work` on reconnect.
- **Multiple tabs share one API key.** Each browser tab opens its own independent shell; there's no shared session between them.
- **Secrets injected at deploy time (`${VAR}` in `agent.env`) are empty in the terminal.** By design: the runtime doesn't persist your org secrets. Export them manually if you need to exercise something that depends on them.
- **No audit log of commands.** Session start and stop are logged (computer id + pid); keystrokes are not recorded. If you need compliance-grade recording, ask, it's straightforward to add.
- **No per-session share links.** Designed but deferred, if you want to send someone a temporary shell without handing out an API key, flag it and we'll ship.

---

## Security model

The terminal shell is sandboxed exactly like your agent:

- Runs inside the computer's **pivot_root**, your shell's `/` is the sandbox's rootfs, not the host's
- Runs inside the computer's **network namespace**, `/etc/hosts`, routing, firewall, and outbound NAT are all the per-tenant setup; you can't see peer tenants or the host's network
- Runs inside a fresh **PID namespace**, only sees its own process tree
- Same **Landlock** and **seccomp** filters as the agent; same `cgroup` limits (can't starve neighbors)

You can't escape your computer through the terminal. The terminal is convenient *because* it's inside the same sandbox as the agent, and constrained for the same reason.

**Authorization check on every connect:**

1. API key is verified against `org_api_keys` (or legacy `tenants` if you're on an older key)
2. Org ownership of the requested computer is verified
3. Only if both pass does the WebSocket upgrade complete

A leaked key lets the holder into every computer the org owns. Rotate via `/v1/orgs/{id}/keys` if you suspect a key is compromised.

---

## Common patterns

### Tail the agent's stdout

```
tail -f /agent/code/logs/agent.log
```

(Path depends on your agent, check your `orb.toml` `working_dir` and where your entry writes logs.)

### Run your agent manually for one iteration

```
cd /agent/code
python agent.py
# or: node index.js, cargo run --release, whatever your entry is
```

Useful for seeing a real stderr trace the scheduler's captured log would have truncated.

### Verify LLM proxy is reachable

```
curl -s "$ANTHROPIC_BASE_URL/v1/messages" \
  -H "x-api-key: dummy" -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"claude-opus-4-7","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'
```

401 = proxy is up, key is wrong (expected for `dummy`). Connection refused = proxy is down.

### Inspect cached packages your agent installed

```
pip list
ls /root/.cache/pip/http-v2/  # your org's wheel cache
```

### Check cron job state

```
cat /agent/.orb/cron.json          # your declared cron jobs
cat /agent/.orb/cron-history.json  # recent fire history (exit codes, duration, stderr_preview)
```

---

## Troubleshooting

### "session ended" immediately after entering the key

Your API key is wrong or revoked. Check [org_api_keys], most common cause is an old key stuck in browser localStorage. Open DevTools → Application → Local Storage → `api.orbcloud.dev` → delete `orb_key`, or use a private window.

### Connected but can't type

Hard-refresh the page (`Cmd/Ctrl + Shift + R`), you may have the pre-PTY cached JS.

### "WebSocket connection failed" with no other info

Your network is blocking outbound WebSocket traffic. Try from a different network; corporate proxies sometimes strip the `Upgrade: websocket` header.

### Session drops after a few minutes of no input

Should not happen anymore, the server sends keepalive pings every 25 seconds. If it does, your local network might be aggressively killing idle connections. Type something every minute, or wrap your work in `tmux` so the session can be reattached.

### Terminal opens, prompt shows, but all commands say "command not found"

Your sandbox's `$PATH` is set from the shell's inherited env + your `[agent.env]`. If you overrode `PATH` in `orb.toml` without including system paths, you've shot yourself in the foot. Fix the config and redeploy, or `export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH` in the session.

### "WebSocket is already in CLOSING or CLOSED state" after the agent demotes

The terminal is independent of the agent lifecycle, demoting the agent does not end your session. If you see this, your browser/network dropped the WebSocket; reconnect.

---

## For agents

Agents don't use the terminal. The terminal exists for humans debugging agents. An agent that wants to run something inside its own sandbox just… runs it, because the agent is already inside the sandbox. There's no "connect to yourself" operation.

If you're writing an agent that needs to shell out, use Python's `subprocess` or Node's `child_process` directly. The LLM proxy URLs and tokens are already in your environment.
