anyagent-bridge 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +81 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/anyagent-bridge.js +127 -0
- package/client/index.html +525 -0
- package/config.example.json +69 -0
- package/docs/INSTALL.md +138 -0
- package/docs/ROADMAP.md +168 -0
- package/docs/SECURITY.md +85 -0
- package/docs/WALKTHROUGH.md +82 -0
- package/docs/screenshots/.gitkeep +3 -0
- package/docs/screenshots/01-startup-banner.png +0 -0
- package/docs/screenshots/02-terminal-view.png +0 -0
- package/docs/screenshots/03-agent-running.png +0 -0
- package/docs/screenshots/04-mobile.png +0 -0
- package/package.json +57 -0
- package/server/auth/index.js +20 -0
- package/server/auth/manager.js +448 -0
- package/server/auth/oauth.js +154 -0
- package/server/auth/providers/github.js +59 -0
- package/server/auth/providers/google.js +44 -0
- package/server/auth/sessions.js +160 -0
- package/server/auth/store.js +135 -0
- package/server/auth/totp.js +140 -0
- package/server/index.js +1779 -0
- package/server/safety/audit.js +139 -0
- package/server/safety/clientip.js +73 -0
- package/server/safety/index.js +17 -0
- package/server/safety/manager.js +507 -0
- package/server/safety/redact.js +153 -0
- package/server/safety/sandbox.js +130 -0
- package/server/tunnel/adapters/cloudflare-quick.js +40 -0
- package/server/tunnel/adapters/cloudflared-named.js +49 -0
- package/server/tunnel/adapters/devtunnel.js +54 -0
- package/server/tunnel/adapters/tailscale.js +42 -0
- package/server/tunnel/base-adapter.js +185 -0
- package/server/tunnel/detect.js +65 -0
- package/server/tunnel/index.js +15 -0
- package/server/tunnel/manager.js +321 -0
- package/server/tunnel/registry.js +31 -0
- package/test/stage4-boot.js +98 -0
- package/test/stage4-smoke.js +267 -0
package/.env.example
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Optional environment overrides for anyagent-bridge.
|
|
2
|
+
# Values set here take precedence over config.json where supported.
|
|
3
|
+
|
|
4
|
+
# Port the bridge server listens on (default: 3001).
|
|
5
|
+
PORT=3001
|
|
6
|
+
|
|
7
|
+
# Host/interface to bind. Default 127.0.0.1 (localhost only, safe).
|
|
8
|
+
# Set to 0.0.0.0 to expose on your network (opt-in; the token is the only gate).
|
|
9
|
+
HOST=127.0.0.1
|
|
10
|
+
|
|
11
|
+
# Optional: pin the access token instead of auto-generating one on first boot.
|
|
12
|
+
# Leave unset to let the server generate and persist a token in .data/auth.json.
|
|
13
|
+
# BRIDGE_AUTH_TOKEN=
|
|
14
|
+
|
|
15
|
+
# Optional: an extra allowed CORS origin (e.g. an external proxy URL). localhost
|
|
16
|
+
# origins are always allowed; set this only for a custom front-end host.
|
|
17
|
+
# ALLOWED_ORIGIN=
|
|
18
|
+
|
|
19
|
+
# ── Stage 2: remote access via a free tunnel (all optional) ──────────────────
|
|
20
|
+
# These override the `tunnel` block in config.json.
|
|
21
|
+
# Enable/disable the tunnel ("true"/"1" enables; anything else disables).
|
|
22
|
+
# BRIDGE_TUNNEL_ENABLED=false
|
|
23
|
+
# Which adapter to use: devtunnel | cloudflare-quick | tailscale | cloudflared-named
|
|
24
|
+
# BRIDGE_TUNNEL_PROVIDER=devtunnel
|
|
25
|
+
# Public hostname for the cloudflared-named provider (the URL shown to you).
|
|
26
|
+
# BRIDGE_TUNNEL_HOSTNAME=
|
|
27
|
+
|
|
28
|
+
# ── Stage 3: authentication (sessions, 2FA, OAuth — all optional) ────────────
|
|
29
|
+
# These override the `auth` block in config.json. Defaults keep Stage 2 behavior
|
|
30
|
+
# (the static token works directly; OAuth off; 2FA only once you enroll it).
|
|
31
|
+
|
|
32
|
+
# Require a login (the static token becomes login-only, cannot be used directly).
|
|
33
|
+
# BRIDGE_REQUIRE_LOGIN=false
|
|
34
|
+
# Pin the HMAC secret used to sign sessions (else generated + persisted in .data).
|
|
35
|
+
# BRIDGE_SESSION_SECRET=
|
|
36
|
+
# Session lifetime in hours (default 12).
|
|
37
|
+
# BRIDGE_SESSION_TTL_HOURS=12
|
|
38
|
+
# Allow TOTP enrollment in the UI (default true). Enrolling 2FA makes the static
|
|
39
|
+
# token login-only and requires the code on every token login.
|
|
40
|
+
# BRIDGE_TOTP_ENABLED=true
|
|
41
|
+
|
|
42
|
+
# OAuth (Google / GitHub). Keep client secrets in env, NOT in config.json.
|
|
43
|
+
# BRIDGE_OAUTH_ENABLED=false
|
|
44
|
+
# Pin the public base URL so the OAuth redirect_uri is stable behind a tunnel
|
|
45
|
+
# (recommended whenever OAuth is on), e.g. https://your-tunnel.example
|
|
46
|
+
# BRIDGE_OAUTH_CALLBACK_URL=
|
|
47
|
+
# BRIDGE_GOOGLE_CLIENT_ID=
|
|
48
|
+
# BRIDGE_GOOGLE_CLIENT_SECRET=
|
|
49
|
+
# BRIDGE_GITHUB_CLIENT_ID=
|
|
50
|
+
# BRIDGE_GITHUB_CLIENT_SECRET=
|
|
51
|
+
|
|
52
|
+
# ── Stage 4: sandboxing & safety (all optional, all default-off) ─────────────
|
|
53
|
+
# These override the `safety` block in config.json. With safety off (the default)
|
|
54
|
+
# the server is byte-identical to Stage 3.
|
|
55
|
+
|
|
56
|
+
# Master switch for the safety subsystem. Also makes trustProxy (below) take effect.
|
|
57
|
+
# BRIDGE_SAFETY_ENABLED=false
|
|
58
|
+
|
|
59
|
+
# Trust X-Forwarded-For for the client IP (rate limiting + audit). Default ignores
|
|
60
|
+
# XFF and uses the direct socket peer (closes the Stage-3 spoofing residual). Set to
|
|
61
|
+
# "true" to trust ONE proxy hop (the nearest, rightmost XFF entry), or a number N to
|
|
62
|
+
# trust N hops. Behind a tunnel/reverse proxy, set this so per-client IPs are correct.
|
|
63
|
+
# BRIDGE_TRUST_PROXY=false
|
|
64
|
+
|
|
65
|
+
# Docker sandbox: run each session's shell (and its agent) inside a container.
|
|
66
|
+
# Requires the `docker` CLI and an image that already contains your agent CLI.
|
|
67
|
+
# BRIDGE_SANDBOX_ENABLED=false
|
|
68
|
+
# Image to run (must contain claude/codex on PATH). No default — you build/pull it.
|
|
69
|
+
# BRIDGE_SANDBOX_IMAGE=
|
|
70
|
+
# Container network: bridge (default; agents can reach their API) | none (offline) | <name>
|
|
71
|
+
# BRIDGE_SANDBOX_NETWORK=bridge
|
|
72
|
+
# When docker is not found: host (fall back to a host shell) | refuse (don't start)
|
|
73
|
+
# BRIDGE_SANDBOX_ON_DOCKER_MISSING=host
|
|
74
|
+
|
|
75
|
+
# Audit log of REST mutations + agent commands (JSONL under .data/audit, redacted).
|
|
76
|
+
# BRIDGE_AUDIT_ENABLED=false
|
|
77
|
+
# BRIDGE_AUDIT_DIR=
|
|
78
|
+
|
|
79
|
+
# Live PTY-stream secret redaction (best-effort; the audit log is always redacted).
|
|
80
|
+
# Off by default to keep the terminal stream byte-identical.
|
|
81
|
+
# BRIDGE_REDACT_LIVE=false
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 elon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# anyagent-bridge
|
|
2
|
+
|
|
3
|
+
Control your local computer's terminal — and **any** CLI AI coding agent you've registered (Claude Code, Codex, aider, …) — from a browser on your phone or another PC, anywhere.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Fastest — run it with one command (Node 18+):
|
|
11
|
+
npx anyagent-bridge
|
|
12
|
+
|
|
13
|
+
# Or self-host with Docker:
|
|
14
|
+
docker compose up -d --build && docker compose logs bridge
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open the printed URL, paste the access token from the banner, and you're in. Full
|
|
18
|
+
install paths (npx · from source · Docker) and per-OS notes are in
|
|
19
|
+
**[docs/INSTALL.md](docs/INSTALL.md)**; read **[docs/SECURITY.md](docs/SECURITY.md)**
|
|
20
|
+
before exposing the bridge beyond localhost, and
|
|
21
|
+
**[docs/WALKTHROUGH.md](docs/WALKTHROUGH.md)** for a guided tour.
|
|
22
|
+
|
|
23
|
+
## Goals
|
|
24
|
+
|
|
25
|
+
- **Open** — fully open source, no lock-in, bring whatever agent you like.
|
|
26
|
+
- **Cross-platform** — runs on macOS, Windows, and Linux.
|
|
27
|
+
- **Free & secure** — no accounts, no cloud middleman; localhost-only by default with a single-token gate.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Drive a real terminal (PTY) from the browser, with full scrollback (10,000 lines).
|
|
32
|
+
- Register **any** command as an "agent" and launch it with one click — not hardcoded to one tool.
|
|
33
|
+
- Multiple browser viewers can watch/control the same session at once (live broadcast).
|
|
34
|
+
- Persistent sessions that survive reconnects, with automatic PTY respawn and backoff.
|
|
35
|
+
- Heartbeat + dead-connection detection so stale viewers get cleaned up.
|
|
36
|
+
- File management API: browse, read, write, rename, move, delete, upload, download — all behind a path whitelist.
|
|
37
|
+
- Crash guards (uncaught exceptions, signals) so the server stays up.
|
|
38
|
+
- Constant-time token comparison and basic rate limiting.
|
|
39
|
+
- Optional **login**: Google/GitHub OAuth, TOTP 2FA, and signed expiring sessions on top of the token (Stage 3).
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- **Node.js 18+**
|
|
44
|
+
- Your own AI CLI, already installed and logged in. anyagent-bridge **never injects or stores any credentials** — it just runs the command you registered (e.g. `claude`, `codex`), and that CLI uses its own existing authentication.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`node-pty` is a native module, so `npm install` compiles it on first run. If the build fails you likely need standard build tools:
|
|
53
|
+
|
|
54
|
+
- **macOS** — Xcode Command Line Tools: `xcode-select --install`
|
|
55
|
+
- **Linux** — `build-essential` (or your distro's gcc/g++/make) and Python 3
|
|
56
|
+
- **Windows** — the Visual Studio Build Tools (C++ workload)
|
|
57
|
+
|
|
58
|
+
For one-command **`npx`** and **Docker / docker-compose** installs, plus updating, uninstalling, and per-OS troubleshooting, see **[docs/INSTALL.md](docs/INSTALL.md)**.
|
|
59
|
+
|
|
60
|
+
## Configure
|
|
61
|
+
|
|
62
|
+
Copy the example config and edit it:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cp config.example.json config.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"host": "127.0.0.1",
|
|
71
|
+
"port": 3001,
|
|
72
|
+
"shell": null,
|
|
73
|
+
"auth": { "token": null },
|
|
74
|
+
"agents": [
|
|
75
|
+
{ "id": "claude", "name": "Claude Code", "command": "claude" },
|
|
76
|
+
{ "id": "codex", "name": "Codex", "command": "codex" }
|
|
77
|
+
],
|
|
78
|
+
"projects": [],
|
|
79
|
+
"allowedPaths": [],
|
|
80
|
+
"sessionTimeoutDays": 7
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- **`host`** — `127.0.0.1` (default) keeps the bridge on localhost only. Set `0.0.0.0` to expose it on your network (opt-in; you'll see a warning at boot).
|
|
85
|
+
- **`port`** — the port to listen on.
|
|
86
|
+
- **`shell`** — `null` auto-picks per OS (`$SHELL` or `/bin/bash`; on Windows `%COMSPEC%` or `powershell.exe`). Set a path to override.
|
|
87
|
+
- **`auth.token`** — `null` generates a random token on first boot and saves it to `.data/auth.json`. There is no default password or default token.
|
|
88
|
+
- **`agents`** — register **any** command. Each entry is `{ "id", "name", "command" }`; `command` can be literally any executable on your PATH. Add your own (aider, custom scripts, etc.) by adding more entries.
|
|
89
|
+
- **`projects`** — optional `[{ "name", "path" }]` shortcuts for quick directory switching.
|
|
90
|
+
- **`allowedPaths`** — file-API whitelist. `[]` defaults to your home directory.
|
|
91
|
+
- **`sessionTimeoutDays`** — how long idle sessions persist.
|
|
92
|
+
|
|
93
|
+
## Run
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm start
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Connect
|
|
100
|
+
|
|
101
|
+
On first boot the server prints your **access token** and the local URL in a
|
|
102
|
+
banner like this:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
===============================================================
|
|
106
|
+
AnyAgent Bridge — server running
|
|
107
|
+
===============================================================
|
|
108
|
+
URL: http://127.0.0.1:3001?token=9f3c...
|
|
109
|
+
Host: 127.0.0.1
|
|
110
|
+
Shell: /bin/zsh
|
|
111
|
+
Agents: claude, codex
|
|
112
|
+
...
|
|
113
|
+
Access token (generated): 9f3c...
|
|
114
|
+
===============================================================
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `URL` already includes the token, so opening it logs you in; otherwise visit
|
|
118
|
+
the address and paste the token when prompted. The token is saved to
|
|
119
|
+
`.data/auth.json` and reused on the next boot.
|
|
120
|
+
|
|
121
|
+
## Remote access (Stage 2)
|
|
122
|
+
|
|
123
|
+
To reach the bridge from your phone or another machine, enable a **tunnel**. Each
|
|
124
|
+
provider is just an external CLI you install yourself — anyagent-bridge spawns it,
|
|
125
|
+
no new npm dependencies and no credentials stored. Tunnels are **off by default**;
|
|
126
|
+
when disabled, missing, or misconfigured the server runs exactly as before
|
|
127
|
+
(localhost-only) and never crashes.
|
|
128
|
+
|
|
129
|
+
| Provider (`provider` id) | CLI | Account? | URL | One-time setup |
|
|
130
|
+
|---|---|---|---|---|
|
|
131
|
+
| Microsoft Dev Tunnels (`devtunnel`, default) | `devtunnel` | yes | rotates per run (stable with a pre-created `tunnelId`) | `devtunnel user login` |
|
|
132
|
+
| Cloudflare Quick (`cloudflare-quick`) | `cloudflared` | no | ephemeral `*.trycloudflare.com` | none — testing-grade (≈200 req cap, no SSE) |
|
|
133
|
+
| Tailscale Funnel (`tailscale`) | `tailscale` | yes | stable `*.ts.net` | `tailscale up` + enable Funnel in the tailnet ACL |
|
|
134
|
+
| cloudflared named (`cloudflared-named`) | `cloudflared` | yes | stable custom hostname | `cloudflared tunnel login` → `tunnel create` → `tunnel route dns` |
|
|
135
|
+
|
|
136
|
+
Enable it in `config.json`:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"tunnel": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"provider": "devtunnel",
|
|
143
|
+
"cloudflared-named": { "tunnelName": "my-tunnel", "hostname": "bridge.example.com" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or via environment variables (override `config.json`):
|
|
149
|
+
|
|
150
|
+
- `BRIDGE_TUNNEL_ENABLED` — `true`/`1` to enable.
|
|
151
|
+
- `BRIDGE_TUNNEL_PROVIDER` — one of the ids above.
|
|
152
|
+
- `BRIDGE_TUNNEL_HOSTNAME` — public hostname for the `cloudflared-named` provider.
|
|
153
|
+
|
|
154
|
+
Inspect and control the tunnel at runtime (all require the access token):
|
|
155
|
+
|
|
156
|
+
- `GET /api/tunnel/status` — current state, provider, and public URL.
|
|
157
|
+
- `POST /api/tunnel/start` · `POST /api/tunnel/stop` · `POST /api/tunnel/restart`.
|
|
158
|
+
|
|
159
|
+
The boot banner prints the public URL once the tunnel is ready. **A public tunnel
|
|
160
|
+
makes your access token the only thing between the internet and your terminal** —
|
|
161
|
+
keep it secret, or add a login (next section).
|
|
162
|
+
|
|
163
|
+
## Login & accounts (Stage 3)
|
|
164
|
+
|
|
165
|
+
On top of the static token you can require a real login. Everything here is
|
|
166
|
+
**opt-in**: with OAuth off, no 2FA enrolled, and `requireLogin` false, the bridge
|
|
167
|
+
behaves exactly as before (the token works directly). No new npm dependencies.
|
|
168
|
+
|
|
169
|
+
**The one rule:** the static token is a *direct* credential **unless** you set
|
|
170
|
+
`requireLogin` or enroll 2FA — then it becomes *login-only* (you exchange it, with
|
|
171
|
+
your 2FA code, for a session). That is what makes 2FA actually protective. Logins
|
|
172
|
+
mint a signed, expiring session delivered as an httpOnly cookie.
|
|
173
|
+
|
|
174
|
+
**Two-factor (TOTP).** Once authenticated, enroll from the API: `POST /api/auth/totp/setup`
|
|
175
|
+
returns an `otpauth://` URI to scan with Google Authenticator / 1Password / Authy,
|
|
176
|
+
then `POST /api/auth/totp/confirm` with a code activates it and returns one-time
|
|
177
|
+
recovery codes (store them). Enrolling 2FA flips the token to login-only.
|
|
178
|
+
|
|
179
|
+
**OAuth (Google / GitHub).** Register an OAuth app with each provider, then set
|
|
180
|
+
the client id/secret (in env, not committed config) and enable it:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"auth": {
|
|
185
|
+
"requireLogin": false,
|
|
186
|
+
"oauth": {
|
|
187
|
+
"enabled": true,
|
|
188
|
+
"callbackBaseUrl": "https://your-tunnel.example",
|
|
189
|
+
"claimFirstUser": true,
|
|
190
|
+
"google": { "clientId": "…", "clientSecret": "…", "allowedEmails": ["you@gmail.com"] },
|
|
191
|
+
"github": { "clientId": "…", "clientSecret": "…", "allowedLogins": ["yourlogin"] }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Set each provider's callback to `<callbackBaseUrl>/api/auth/oauth/<provider>/callback`.
|
|
198
|
+
The allowlist (`allowedEmails` / `allowedLogins`) is **fail-closed** — an empty
|
|
199
|
+
allowlist admits no one, unless `claimFirstUser` is on, in which case the **first**
|
|
200
|
+
successful login claims the bridge and everyone else is denied (so don't expose a
|
|
201
|
+
fresh, unclaimed bridge publicly). Always set `callbackBaseUrl` when OAuth is
|
|
202
|
+
reachable, so the redirect URI is not derived from request headers.
|
|
203
|
+
|
|
204
|
+
Login endpoints (all under `/api/auth`): `POST /login` · `POST /logout` ·
|
|
205
|
+
`GET /config` · `GET /me` · `GET /sessions` · `DELETE /sessions/:id` ·
|
|
206
|
+
`GET /oauth/:provider/{start,callback}` · `POST /totp/{setup,confirm,disable}`.
|
|
207
|
+
Configure it all via the `auth` block in `config.json` or `BRIDGE_*` env vars
|
|
208
|
+
(see `.env.example`).
|
|
209
|
+
|
|
210
|
+
## Sandboxing & safety (Stage 4)
|
|
211
|
+
|
|
212
|
+
Four opt-in safety layers, all **off by default**. With `safety.enabled` false the
|
|
213
|
+
server is byte-identical to Stage 3. No new npm dependencies — the Docker layer
|
|
214
|
+
spawns the `docker` CLI; everything else is Node core. Configure via the `safety`
|
|
215
|
+
block in `config.json` or `BRIDGE_*` env vars (see `.env.example`).
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"safety": {
|
|
220
|
+
"enabled": true,
|
|
221
|
+
"trustProxy": false,
|
|
222
|
+
"sandbox": { "enabled": true, "image": "your-image-with-claude:latest", "network": "bridge", "onDockerMissing": "host" },
|
|
223
|
+
"killSwitch": { "enabled": true },
|
|
224
|
+
"audit": { "enabled": true },
|
|
225
|
+
"redaction": { "liveStream": false }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Docker sandbox** — runs each session's shell (and therefore its agent) inside a
|
|
231
|
+
container instead of on the host. Only sessions with a real **project folder** are
|
|
232
|
+
sandboxed (it never bind-mounts your whole home directory); the project is mounted
|
|
233
|
+
at `/workspace`. You supply an **`image` that already contains your agent CLI** (we
|
|
234
|
+
can't ship one). `network` defaults to `bridge` so the agent can reach its API;
|
|
235
|
+
set `none` for offline file-only work. Resource limits (`memory`/`cpus`/`pidsLimit`)
|
|
236
|
+
and `no-new-privileges` are applied by default; `readOnlyRootfs` / `dropAllCaps` /
|
|
237
|
+
`runAsHostUser` are opt-in hardening. Secrets reach the container via an
|
|
238
|
+
**`envPassthrough` allowlist** as `-e NAME` (values never appear in the process
|
|
239
|
+
table; `BRIDGE_*`/token are always denied). If `docker` is missing, `onDockerMissing`
|
|
240
|
+
decides: `host` (fall back to a host shell, the default) or `refuse` (don't start).
|
|
241
|
+
- **Kill-switch** — `POST /api/safety/kill/:sessionId` hard-kills one session
|
|
242
|
+
(SIGKILL + container removal); `POST /api/safety/panic` kills **all** sessions,
|
|
243
|
+
sweeps stray containers, optionally stops the tunnel, and **locks** the bridge so
|
|
244
|
+
no new agents launch until `POST /api/safety/unlock`. The lock never strands your
|
|
245
|
+
shell (only agent launches are refused) and survives a restart. All operator-only;
|
|
246
|
+
also reachable over WebSocket (`{type:"panic"}` / `{type:"kill"}`).
|
|
247
|
+
- **Audit log** — append-only JSONL under `.data/audit/` of REST mutations and the
|
|
248
|
+
semantic agent commands (`agent.start` / `agent.send`); raw keystrokes are not
|
|
249
|
+
logged (they can't be reconstructed into commands). Date + size rotation with
|
|
250
|
+
retention pruning. Every field is redacted before write.
|
|
251
|
+
- **Secret redaction** — the audit log is **always** scrubbed (AWS / OpenAI / GitHub /
|
|
252
|
+
Slack / Google keys, JWTs, PEM private keys, plus the bridge's own token). Live
|
|
253
|
+
PTY-stream redaction is **opt-in** (`redaction.liveStream`) and best-effort — it
|
|
254
|
+
holds back partial tokens across chunk boundaries but is kept off by default so the
|
|
255
|
+
terminal stream stays byte-identical.
|
|
256
|
+
|
|
257
|
+
**Proxy trust (`trustProxy`).** Closes a Stage-3 residual: by default the client IP
|
|
258
|
+
for rate limiting and the audit log now comes from the direct socket peer, ignoring a
|
|
259
|
+
spoofable `X-Forwarded-For`. Behind a tunnel or reverse proxy, set `trustProxy` to
|
|
260
|
+
`true` (trust the nearest hop) or a number `N` (trust `N` hops) so per-client IPs are
|
|
261
|
+
correct. It only takes effect once you opt into the safety subsystem, so an upgraded
|
|
262
|
+
install with no `safety` config is unchanged.
|
|
263
|
+
|
|
264
|
+
Status at `GET /api/safety/status` (and folded into `GET /api/system/status`).
|
|
265
|
+
|
|
266
|
+
> **Note:** the Docker sandbox is implemented and unit-tested for argv/secret
|
|
267
|
+
> correctness and graceful degradation, but live container spawning was not exercised
|
|
268
|
+
> on the build machine (no daemon). Test your `image` end-to-end before relying on it.
|
|
269
|
+
|
|
270
|
+
## Security notes
|
|
271
|
+
|
|
272
|
+
- **Localhost by default.** Out of the box the server binds `127.0.0.1`, so only your own machine can reach it.
|
|
273
|
+
- **The token is the gate.** There is no default password and no default token — one is generated on first boot and persisted to `.data/auth.json`. Keep it secret. If you set `host` to `0.0.0.0`, the token is the *only* thing standing between the internet (or your LAN) and your terminal — the server warns you about this at boot.
|
|
274
|
+
- **No credential injection.** The bridge runs your registered command and nothing more; your AI CLI's own login is used as-is.
|
|
275
|
+
- **Add a login before exposing it.** Free tunnels (Stage 2), OAuth + 2FA login (Stage 3), and the Docker sandbox / kill-switch / audit / redaction (Stage 4, see [Sandboxing & safety](#sandboxing--safety-stage-4)) are all shipped. If you expose the bridge, require a login, set `callbackBaseUrl`, and set `trustProxy` for your proxy; prefer localhost or a trusted network otherwise. The full security model and disclaimers are in **[docs/SECURITY.md](docs/SECURITY.md)**.
|
|
276
|
+
|
|
277
|
+
## Roadmap
|
|
278
|
+
|
|
279
|
+
- **Stage 1 (this release)** — portable, cross-platform core: terminal + any-agent control over WebSocket, file API, persistent sessions, token auth.
|
|
280
|
+
- **Stage 2 (this release)** — free tunnel adapters (Dev Tunnels, Cloudflare, Tailscale, cloudflared) for zero-config remote access. See [Remote access](#remote-access-stage-2).
|
|
281
|
+
- **Stage 3 (this release)** — OAuth (Google/GitHub) + 2FA + real session management. See [Login & accounts](#login--accounts-stage-3).
|
|
282
|
+
- **Stage 4 (this release)** — Docker sandboxing, kill-switch, audit logging, secret redaction. See [Sandboxing & safety](#sandboxing--safety-stage-4).
|
|
283
|
+
- **Stage 5 (this release)** — packaging: a `bin` launcher for **npx**, a **Dockerfile** + **docker-compose**, cross-platform install docs ([INSTALL](docs/INSTALL.md)), a security guide ([SECURITY](docs/SECURITY.md)), and a [walkthrough](docs/WALKTHROUGH.md).
|
|
284
|
+
|
|
285
|
+
Full detail in [docs/ROADMAP.md](docs/ROADMAP.md).
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* anyagent-bridge — CLI launcher (Stage 5: packaging)
|
|
4
|
+
*
|
|
5
|
+
* A thin wrapper over `server/index.js`. It parses a few friendly flags, maps
|
|
6
|
+
* them onto the environment variables the server already reads, and then boots
|
|
7
|
+
* the server in-process. It sets NOTHING the server doesn't already understand,
|
|
8
|
+
* so running `node server/index.js` directly is always equivalent. CLI flags are
|
|
9
|
+
* applied to process.env before the server loads; dotenv does not override an
|
|
10
|
+
* already-set variable, so the precedence is: CLI flag > .env > config.json.
|
|
11
|
+
*
|
|
12
|
+
* npx anyagent-bridge --port 8080 --tunnel devtunnel
|
|
13
|
+
* anyagent-bridge --host 0.0.0.0 --token "$(openssl rand -hex 32)"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const pkg = require('../package.json');
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
const lines = [
|
|
25
|
+
`anyagent-bridge v${pkg.version}`,
|
|
26
|
+
'',
|
|
27
|
+
'Control your local terminal and any CLI AI coding agent from a browser.',
|
|
28
|
+
'',
|
|
29
|
+
'Usage:',
|
|
30
|
+
' anyagent-bridge [options]',
|
|
31
|
+
'',
|
|
32
|
+
'Options:',
|
|
33
|
+
' -p, --port <n> Port to listen on (default 3001).',
|
|
34
|
+
' -H, --host <addr> Interface to bind (default 127.0.0.1; 0.0.0.0 to expose).',
|
|
35
|
+
' -t, --token <value> Pin the access token (default: generated + saved in .data).',
|
|
36
|
+
' --tunnel [provider] Enable a remote tunnel. provider = devtunnel (default) |',
|
|
37
|
+
' cloudflare-quick | tailscale | cloudflared-named.',
|
|
38
|
+
' --no-tunnel Force the tunnel off (overrides config).',
|
|
39
|
+
' -h, --help Show this help and exit.',
|
|
40
|
+
' -v, --version Print the version and exit.',
|
|
41
|
+
'',
|
|
42
|
+
"Long options also accept --flag=value (use it for a value starting with '-',",
|
|
43
|
+
'e.g. --token=-secret). Every option just sets the matching PORT/HOST/BRIDGE_*',
|
|
44
|
+
'environment variable, so anything here also works via env vars + `npm start`.',
|
|
45
|
+
'Full configuration: config.json (see config.example.json) and docs/INSTALL.md.',
|
|
46
|
+
];
|
|
47
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fail(msg) {
|
|
51
|
+
process.stderr.write(`anyagent-bridge: ${msg}\n`);
|
|
52
|
+
process.stderr.write("Run 'anyagent-bridge --help' for usage.\n");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
let arg = argv[i];
|
|
58
|
+
// Support the `--flag=value` form for long options. Splitting it out here also
|
|
59
|
+
// lets values that legitimately start with '-' be passed (e.g. --token=-abc),
|
|
60
|
+
// which the space form rejects to avoid swallowing the next flag.
|
|
61
|
+
let inlineValue;
|
|
62
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
63
|
+
const eq = arg.indexOf('=');
|
|
64
|
+
inlineValue = arg.slice(eq + 1);
|
|
65
|
+
arg = arg.slice(0, eq);
|
|
66
|
+
}
|
|
67
|
+
// Resolve this option's value: an inline `=value` wins; otherwise consume the
|
|
68
|
+
// next token, erroring if it is missing or looks like another flag.
|
|
69
|
+
const value = () => {
|
|
70
|
+
if (inlineValue !== undefined) return inlineValue;
|
|
71
|
+
const v = argv[i + 1];
|
|
72
|
+
if (v === undefined || v.startsWith('-')) fail(`option ${arg} requires a value`);
|
|
73
|
+
i++;
|
|
74
|
+
return v;
|
|
75
|
+
};
|
|
76
|
+
switch (arg) {
|
|
77
|
+
case '-h':
|
|
78
|
+
case '--help':
|
|
79
|
+
printHelp();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
break;
|
|
82
|
+
case '-v':
|
|
83
|
+
case '--version':
|
|
84
|
+
process.stdout.write(pkg.version + '\n');
|
|
85
|
+
process.exit(0);
|
|
86
|
+
break;
|
|
87
|
+
case '-p':
|
|
88
|
+
case '--port': {
|
|
89
|
+
const v = value();
|
|
90
|
+
if (!/^\d+$/.test(v)) fail(`--port expects a number, got "${v}"`);
|
|
91
|
+
process.env.PORT = v;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case '-H':
|
|
95
|
+
case '--host':
|
|
96
|
+
process.env.HOST = value();
|
|
97
|
+
break;
|
|
98
|
+
case '-t':
|
|
99
|
+
case '--token':
|
|
100
|
+
process.env.BRIDGE_AUTH_TOKEN = value();
|
|
101
|
+
break;
|
|
102
|
+
case '--tunnel': {
|
|
103
|
+
process.env.BRIDGE_TUNNEL_ENABLED = 'true';
|
|
104
|
+
if (inlineValue !== undefined) {
|
|
105
|
+
// --tunnel=<provider>
|
|
106
|
+
if (inlineValue) process.env.BRIDGE_TUNNEL_PROVIDER = inlineValue;
|
|
107
|
+
} else {
|
|
108
|
+
// Optional provider as the next token, only if it is not another flag.
|
|
109
|
+
const peek = argv[i + 1];
|
|
110
|
+
if (peek !== undefined && !peek.startsWith('-')) {
|
|
111
|
+
process.env.BRIDGE_TUNNEL_PROVIDER = peek;
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case '--no-tunnel':
|
|
118
|
+
process.env.BRIDGE_TUNNEL_ENABLED = 'false';
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
fail(`unknown option "${arg}"`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Boot the server in-process. ROOT inside the server resolves relative to its own
|
|
126
|
+
// __dirname, so config.json and .data live alongside the installed package.
|
|
127
|
+
require(path.join(__dirname, '..', 'server', 'index.js'));
|