# gptbridge

Give ChatGPT a terminal inside one project directory, over an OpenAI Secure MCP Tunnel.

ChatGPT chat draws on a different quota pool than Codex and Work, so this runs
high-end models against your machine without spending Codex limits. Quota and
model availability depend on your OpenAI account and can change; nothing here
grants extra quota.

```
ChatGPT → OpenAI Secure MCP Tunnel → tunnel-client → gptbridge MCP server → bubblewrap jail → your command
```

Needs `bubblewrap` (`apt install bubblewrap`). Without it the server refuses to start.

## Tools ChatGPT gets

| tool | what it does |
| --- | --- |
| `run_command` | run a command to completion, return exit code + output |
| `start_process` | start something long-running (dev server), return a process id |
| `read_output` | read what a background process printed since a byte offset |
| `send_input` | write to a background process's stdin |
| `stop_process` | terminate a background process and its children |
| `list_processes` | what is running |
| `workspace_info` | workspace root, sandbox mode, network access |

## Install

```bash
modules/gptbridge/bootstrap.sh
```

Creates the Python environment from `requirements.lock.txt` with
`--require-hashes`, downloads the pinned `openai/tunnel-client` release and
verifies its checksum on every run, and links `gptbridge` into `~/.local/bin`.

To move a dependency: edit `requirements.txt`, then regenerate the lock with the
command in its first line.

## Arm it

Three steps, all in OpenAI's own consoles — they cannot be automated from here.

1. **Create a tunnel and a runtime API key.**
   - Tunnels: <https://platform.openai.com/settings/organization/tunnels>
   - Runtime API keys: <https://platform.openai.com/settings/organization/api-keys>

2. **Store them locally.**
   ```bash
   mkdir -p ~/.gptbridge
   echo 'CONTROL_PLANE_TUNNEL_ID=tunnel_your_id' > ~/.gptbridge/env
   printf '%s' 'sk-your-runtime-api-key' > ~/.gptbridge/api-key
   chmod 600 ~/.gptbridge/api-key
   gptbridge doctor
   ```
   The key is read from the file by `tunnel-client` itself — it is never put in
   the environment, so it cannot leak through `systemctl show` or a process list.

3. **Create the ChatGPT app.** Enable developer mode, create a custom app,
   choose **Tunnel** as the connection type, paste the tunnel id, choose
   **No Auth**, save, enable, and set its permission to **Always Ask**. If the
   dialog insists on creating a new tunnel, click **Create** at the bottom right
   — the tunnel field then becomes a text box that accepts your existing id.

## Run

```bash
gptbridge run ~/Projects/my-project
```

Then in ChatGPT press **+** and pick the app by name.

Options:

- `--sandbox-mode read-only|workspace-write|full-access` (default `workspace-write`)
- `--network` — let commands reach the network. **Off by default**: without it a
  command that reads something has nowhere to send it. Turn it on when the model
  needs to install packages or hit an API.
- `--readable-root PATH` — make one more directory readable inside the jail
  (repeatable). Only the system directories are readable by default, so a
  toolchain living in `~/.nvm`, `~/.cargo` or `~/.pyenv` needs this. Name the
  toolchain directory itself: a root that is, contains, or sits inside a
  protected directory is refused, so `--readable-root ~` does not start.

`gptbridge serve <dir>` runs the MCP server alone on stdio, without the tunnel —
useful for debugging or for pointing a local MCP client at it.

Stop with Ctrl+C. Background processes are terminated with the server.

## What contains the blast radius

- **The workspace is an explicit argument**, never the current directory.
  `$HOME`, `/`, any ancestor of `$HOME`, and anything at, containing, or inside
  `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.config`, `~/.claude`, `~/.codex`,
  `~/.systray-ai`, `~/.gptbridge`, `~/.local/bin` or `~/.local/share/overdeck`
  are refused — by name and by what the name resolves to, so a symlinked
  credential directory is refused too. A `cwd` that escapes the workspace,
  including through a symlink, is refused.
- **Commands cannot read the rest of the machine.** The jail mounts the system
  directories read-only, the workspace read-write, and nothing else. `~/.ssh`,
  `~/.codex/auth.json` and every other file outside the workspace do not exist
  as far as a command is concerned. `--readable-root` is held to the same
  refusal list as the workspace, and every root it grants is included in the
  startup probes, so a widened jail is proved rather than assumed.
- **The network is off unless you ask for it**, so a command that reads
  something has no way to send it anywhere.
- **The session D-Bus socket is gone.** Left reachable it lets a command ask
  `systemd --user` to start a process outside the jail — that escape was
  reproduced against a write-only sandbox before this one replaced it.
- **The environment is not inherited.** Only `PATH`, `HOME`, `LANG`, `LC_ALL`,
  `TERM`, `USER`, `SHELL` and `TZ` reach a command, and `PATH` is a fixed system
  path so privileged helpers in `~/.local/bin` do not resolve. `bwrap` itself is
  launched with the same clean environment, because its own process lives inside
  the jail's pid namespace.
- **Commands are bounded**: 120s default timeout (3600s max), 8 MiB captured per
  command, 1 MiB of retained output per background process, 8 concurrent
  background processes, 60k characters per tool response.
- **Nothing listens on a socket.** The MCP server speaks stdio to
  `tunnel-client`; there is no inbound port.

Every one of these is proved at startup: the server runs the corresponding
attack against its own jail and refuses to serve if any of them succeeds.

Two behaviors worth knowing:

- **A write outside the workspace appears to succeed.** The jail gives each
  command a private root, so `touch /etc/foo` returns 0 and the file is visible
  for the rest of that command — and is discarded when it exits. Nothing reaches
  the host. Expect the model to be occasionally confused by this.
- **Writing inside the workspace is enough to run code later.** A model that
  edits `package.json`, `.githooks/`, `Makefile` or a test file changes what the
  *next* command does. The jail bounds where that code can reach, not whether it
  runs.

`full-access` disables the jail entirely. Use it only when you must.

Still true regardless: ChatGPT can run commands that modify files, install
software, and make network requests. Keep the workspace under version control.
Do not send passwords, API keys, or MFA codes through ChatGPT. Read commands
before approving them — that is what **Always Ask** is for. Stop the bridge when
you are not using it.

## Test

```bash
modules/gptbridge/health.sh
```

Runs the unit tests plus a live suite that drives the real MCP server over
stdio through the real jail — including the escapes above, run as attacks. No
tunnel and no OpenAI credentials needed.
