| 1 | # `codewhale remote-setup` - Tailscale-first design |
| 2 | |
| 3 | Status: **design / revision**. This RFC revises the earlier cloud-first |
| 4 | `remote-setup` plan. Keep the accurate implementation work already present: |
| 5 | `codewhale remote-setup` exists today as a generate-only bundle wizard for |
| 6 | cloud plus chat bridge deployments, and `--apply` is still not implemented. |
| 7 | |
| 8 | ## Goal |
| 9 | |
| 10 | Give users a guided, education-forward way to reach a local-first CodeWhale |
| 11 | runtime from another surface without accidentally publishing their agent. |
| 12 | |
| 13 | Default posture: |
| 14 | |
| 15 | 1. **Local-first by default.** |
| 16 | 2. **Tailnet-private when remote.** |
| 17 | 3. **Public only when explicitly chosen.** |
| 18 | |
| 19 | The wizard should ask: |
| 20 | |
| 21 | > How do you want to reach CodeWhale? |
| 22 | |
| 23 | and offer these paths, in this order: |
| 24 | |
| 25 | 1. This machine only (localhost) |
| 26 | 2. Private devices with Tailscale (**Recommended**) |
| 27 | 3. Telegram bot |
| 28 | 4. Feishu/Lark bot |
| 29 | 5. Weixin personal bridge |
| 30 | 6. Public webhook / Funnel (**Advanced**) |
| 31 | |
| 32 | The recommended remote answer is Tailscale Serve with the backend still bound |
| 33 | to `127.0.0.1`. Tailscale supplies device identity and encrypted transport. |
| 34 | Tailscale Funnel is public internet exposure and must stay advanced. |
| 35 | |
| 36 | ## Current implementation checkpoint |
| 37 | |
| 38 | ### `/setup` → Remote runtime step (#3409) |
| 39 | |
| 40 | The setup wizard's `RemoteRuntime` card presents exactly four modes, each with a |
| 41 | status derived from observable state — never from intent: |
| 42 | |
| 43 | | mode | status source | |
| 44 | |------|---------------| |
| 45 | | this machine only | always `ready`; nothing is exposed and nothing to configure | |
| 46 | | runtime API | presence (never value) of `CODEWHALE_RUNTIME_TOKEN` / `DEEPSEEK_RUNTIME_TOKEN` | |
| 47 | | phone on your network | `disabled` unless `CODEWHALE_RUNTIME_HOST` is set, because the shipped unit binds loopback | |
| 48 | | chat app | per-bridge `secret_keys` presence from `remote_setup::registry` | |
| 49 | |
| 50 | Contract for that step: |
| 51 | |
| 52 | - **Secret values are never read or rendered.** Only variable *names* and |
| 53 | set/unset are used, so a hostile token cannot reach the UI or the persisted |
| 54 | step result. |
| 55 | - **`R` renders a plan preview in memory** through `remote_setup::bundle::render_bundle` |
| 56 | with every secret replaced by `<redacted>`. It never calls `write_bundle`, |
| 57 | never runs a provisioning command, and creates no files. |
| 58 | - **Missing tokens/config are `NeedsAction`, never blocking.** Local-only is the |
| 59 | default and settles the step in one key, so a user who wants no remote access |
| 60 | is finished immediately. `/setup`'s report and `doctor` inherit the recorded |
| 61 | status verbatim. |
| 62 | |
| 63 | Verified against the codebase: |
| 64 | |
| 65 | - `codewhale app-server --http` is the canonical HTTP/SSE runtime API entrypoint. |
| 66 | It delegates to the mature `serve --http` implementation. |
| 67 | - `codewhale app-server --mobile` is real and serves the phone control page at |
| 68 | `/mobile`. |
| 69 | - `--host`, `--port`, `--workers`, `--auth-token`, `--insecure-no-auth`, and |
| 70 | repeatable `--cors-origin` exist on `app-server --http` / `--mobile`. |
| 71 | - `--mobile` without `--host` binds to `0.0.0.0` by design. Use |
| 72 | `--host 127.0.0.1` when putting Tailscale in front of the runtime. |
| 73 | - `/health` and `/v1/runtime/info` are public bootstrap/supervision endpoints. |
| 74 | `/v1/*` control routes require the runtime bearer token unless auth is |
| 75 | explicitly disabled on a trusted loopback bind. |
| 76 | - `codewhale doctor --json` exists as the machine-readable local diagnostic. |
| 77 | - `codewhale remote-setup` exists, but today it is generate-only. Its current |
| 78 | matrix is cloud target (`lighthouse`, `azure`, `digitalocean`) x bridge |
| 79 | (`feishu`, `telegram`) x provider registry. It does **not** yet model |
| 80 | localhost, Tailscale, Weixin, or Funnel as first-class choices. |
| 81 | - Telegram and Feishu bridge validators exist as `npm run validate:config`. |
| 82 | Weixin currently has `npm run check`, but no validate-config script. |
| 83 | |
| 84 | Accuracy note for the Tailscale recommendation: the requested setup uses |
| 85 | `app-server --http`, but the current runtime serves `/mobile` only in mobile |
| 86 | mode. This RFC keeps the target command shape for the recommended loopback |
| 87 | runtime, and documents the verified current-binary variant when the mobile page |
| 88 | is required: |
| 89 | |
| 90 | ```bash |
| 91 | # Runtime API only, verified: |
| 92 | codewhale app-server --http --host 127.0.0.1 --port 7878 --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 93 | |
| 94 | # Runtime API plus /mobile, verified: |
| 95 | codewhale app-server --mobile --host 127.0.0.1 --port 7878 --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 96 | ``` |
| 97 | |
| 98 | ## Common runtime base |
| 99 | |
| 100 | Every path starts from the same local runtime trust boundary. |
| 101 | |
| 102 | ```bash |
| 103 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 104 | export CODEWHALE_RUNTIME_TOKEN |
| 105 | |
| 106 | codewhale app-server --http \ |
| 107 | --host 127.0.0.1 \ |
| 108 | --port 7878 \ |
| 109 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 110 | ``` |
| 111 | |
| 112 | For the current binary, use `--mobile --host 127.0.0.1` instead of `--http` if |
| 113 | the path needs the built-in `/mobile` page. |
| 114 | |
| 115 | Doctor-style local validation: |
| 116 | |
| 117 | ```bash |
| 118 | codewhale doctor --json |
| 119 | curl -fsS http://127.0.0.1:7878/health |
| 120 | curl -fsS \ |
| 121 | -H "Authorization: Bearer $CODEWHALE_RUNTIME_TOKEN" \ |
| 122 | http://127.0.0.1:7878/v1/runtime/info |
| 123 | ``` |
| 124 | |
| 125 | Runtime mental model: |
| 126 | |
| 127 | - Exposed by CodeWhale: only the address it binds. The recommended bind is |
| 128 | `127.0.0.1:7878`. |
| 129 | - Auth token: `CODEWHALE_RUNTIME_TOKEN`, passed as `Authorization: Bearer ...` |
| 130 | by clients and bridges. Legacy `DEEPSEEK_RUNTIME_TOKEN` remains a fallback. |
| 131 | - Provider secrets: stay in runtime configuration, not in bridge env files. |
| 132 | - Bridge secrets: stay in transport-specific env files. |
| 133 | |
| 134 | ## Guided flow |
| 135 | |
| 136 | ### 1. This machine only (localhost) |
| 137 | |
| 138 | Use this when the TUI, SDK, browser, or local script runs on the same machine as |
| 139 | CodeWhale. |
| 140 | |
| 141 | Setup: |
| 142 | |
| 143 | ```bash |
| 144 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 145 | export CODEWHALE_RUNTIME_TOKEN |
| 146 | |
| 147 | codewhale app-server --http \ |
| 148 | --host 127.0.0.1 \ |
| 149 | --port 7878 \ |
| 150 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 151 | ``` |
| 152 | |
| 153 | Env template: |
| 154 | |
| 155 | ```env |
| 156 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 157 | CODEWHALE_RUNTIME_TOKEN=<same value used to start app-server> |
| 158 | ``` |
| 159 | |
| 160 | Validation: |
| 161 | |
| 162 | ```bash |
| 163 | codewhale doctor --json |
| 164 | curl -fsS http://127.0.0.1:7878/health |
| 165 | curl -fsS \ |
| 166 | -H "Authorization: Bearer $CODEWHALE_RUNTIME_TOKEN" \ |
| 167 | http://127.0.0.1:7878/v1/runtime/info |
| 168 | ``` |
| 169 | |
| 170 | Trust boundary: |
| 171 | |
| 172 | - Exposed: loopback only. |
| 173 | - Not exposed: LAN, tailnet, or public internet. |
| 174 | - Token used: `CODEWHALE_RUNTIME_TOKEN` for control routes; local `/health` and |
| 175 | `/v1/runtime/info` are public bootstrap endpoints. |
| 176 | |
| 177 | ### 2. Private devices with Tailscale (Recommended) |
| 178 | |
| 179 | Use this to reach CodeWhale from your phone or laptop without opening a LAN or |
| 180 | public port. Tailscale authenticates devices in your tailnet; CodeWhale still |
| 181 | binds to localhost. |
| 182 | |
| 183 | Target setup to feature in the wizard: |
| 184 | |
| 185 | ```bash |
| 186 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 187 | export CODEWHALE_RUNTIME_TOKEN |
| 188 | |
| 189 | codewhale app-server --http \ |
| 190 | --host 127.0.0.1 \ |
| 191 | --port 7878 \ |
| 192 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 193 | |
| 194 | tailscale serve --bg --https=443 localhost:7878 |
| 195 | ``` |
| 196 | |
| 197 | Then open the Tailscale Serve URL from a phone or laptop in the same tailnet. |
| 198 | For the current binary's mobile page, start CodeWhale with the verified mobile |
| 199 | variant: |
| 200 | |
| 201 | ```bash |
| 202 | codewhale app-server --mobile \ |
| 203 | --host 127.0.0.1 \ |
| 204 | --port 7878 \ |
| 205 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 206 | ``` |
| 207 | |
| 208 | Then open (put the token in the URL **fragment**, not a query param — the |
| 209 | `/mobile` page reads it from `location.hash`, and a fragment is never sent to |
| 210 | the Tailscale serving layer or to any proxy log): |
| 211 | |
| 212 | ```text |
| 213 | https://<machine>.<tailnet>.ts.net/mobile#token=<CODEWHALE_RUNTIME_TOKEN> |
| 214 | ``` |
| 215 | |
| 216 | Env template: |
| 217 | |
| 218 | ```env |
| 219 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 220 | CODEWHALE_RUNTIME_TOKEN=<openssl-rand-hex-32> |
| 221 | TAILSCALE_SERVE_TARGET=localhost:7878 |
| 222 | TAILSCALE_SERVE_URL=https://<machine>.<tailnet>.ts.net |
| 223 | ``` |
| 224 | |
| 225 | Validation: |
| 226 | |
| 227 | ```bash |
| 228 | codewhale doctor --json |
| 229 | curl -fsS http://127.0.0.1:7878/health |
| 230 | curl -fsS https://<machine>.<tailnet>.ts.net/health |
| 231 | curl -fsS \ |
| 232 | -H "Authorization: Bearer $CODEWHALE_RUNTIME_TOKEN" \ |
| 233 | https://<machine>.<tailnet>.ts.net/v1/runtime/info |
| 234 | tailscale serve status |
| 235 | ``` |
| 236 | |
| 237 | Trust boundary: |
| 238 | |
| 239 | - Exposed: an HTTPS endpoint reachable by devices authorized in your tailnet. |
| 240 | - Not exposed: the raw CodeWhale listener; it stays on `127.0.0.1`. |
| 241 | - Token used: Tailscale identity gates network reachability; CodeWhale still |
| 242 | uses `CODEWHALE_RUNTIME_TOKEN` for runtime control. |
| 243 | - Caveat: Tailscale Serve is private to the tailnet. Tailscale Funnel is public |
| 244 | internet exposure and belongs only in the advanced path below. |
| 245 | |
| 246 | ### 3. Telegram bot |
| 247 | |
| 248 | Use this when a Telegram DM should control a local CodeWhale runtime. The bridge |
| 249 | uses Telegram Bot API long polling, so it does not require a public webhook URL |
| 250 | or inbound port. |
| 251 | |
| 252 | Setup: |
| 253 | |
| 254 | ```bash |
| 255 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 256 | export CODEWHALE_RUNTIME_TOKEN |
| 257 | |
| 258 | codewhale app-server --http \ |
| 259 | --host 127.0.0.1 \ |
| 260 | --port 7878 \ |
| 261 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 262 | |
| 263 | cd integrations/telegram-bridge |
| 264 | npm install --omit=dev |
| 265 | cp .env.example .env |
| 266 | $EDITOR .env |
| 267 | npm run validate:config -- \ |
| 268 | --env .env \ |
| 269 | --workspace-root "$PWD/../.." \ |
| 270 | --check-filesystem |
| 271 | npm start |
| 272 | ``` |
| 273 | |
| 274 | Env template: |
| 275 | |
| 276 | ```env |
| 277 | TELEGRAM_BOT_TOKEN=replace-with-botfather-token |
| 278 | |
| 279 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 280 | CODEWHALE_RUNTIME_TOKEN=<same value used to start app-server> |
| 281 | CODEWHALE_WORKSPACE=/path/to/workspace |
| 282 | # Optional override; leave blank to inherit the runtime's configured provider/model. |
| 283 | CODEWHALE_MODEL= |
| 284 | CODEWHALE_MODE=agent |
| 285 | CODEWHALE_ALLOW_SHELL=true # grants shell execution from the bridge; set false for text-only chat |
| 286 | CODEWHALE_TRUST_MODE=false |
| 287 | CODEWHALE_AUTO_APPROVE=false |
| 288 | |
| 289 | TELEGRAM_CHAT_ALLOWLIST= |
| 290 | TELEGRAM_ALLOW_UNLISTED=false |
| 291 | TELEGRAM_ALLOW_GROUPS=false |
| 292 | ``` |
| 293 | |
| 294 | First pairing: |
| 295 | |
| 296 | ```bash |
| 297 | # Temporarily in .env: |
| 298 | TELEGRAM_ALLOW_UNLISTED=true |
| 299 | ``` |
| 300 | |
| 301 | DM the bot `/status`, copy the returned `chat_id` or `user_id` into |
| 302 | `TELEGRAM_CHAT_ALLOWLIST`, then set `TELEGRAM_ALLOW_UNLISTED=false` and restart |
| 303 | the bridge. |
| 304 | |
| 305 | Validation: |
| 306 | |
| 307 | ```bash |
| 308 | codewhale doctor --json |
| 309 | curl -fsS http://127.0.0.1:7878/health |
| 310 | npm run validate:config -- \ |
| 311 | --env .env \ |
| 312 | --workspace-root "$PWD/../.." \ |
| 313 | --check-filesystem |
| 314 | ``` |
| 315 | |
| 316 | Trust boundary: |
| 317 | |
| 318 | - Exposed: no inbound CodeWhale port. Telegram sees messages sent to the bot. |
| 319 | - Not exposed: CodeWhale remains on `127.0.0.1`; provider keys stay in the |
| 320 | runtime env, not the Telegram env. |
| 321 | - Tokens used: `TELEGRAM_BOT_TOKEN` for Telegram, `CODEWHALE_RUNTIME_TOKEN` for |
| 322 | bridge-to-runtime calls, and `TELEGRAM_CHAT_ALLOWLIST` for user/chat gating. |
| 323 | - Caveat: direct messages are the intended MVP control surface. Group control is |
| 324 | off unless `TELEGRAM_ALLOW_GROUPS=true`. |
| 325 | |
| 326 | ### 4. Feishu/Lark bot |
| 327 | |
| 328 | Use this when a Feishu or Lark chat should control the local runtime. The bridge |
| 329 | uses the Lark/Feishu long-connection SDK, so the first version does not need a |
| 330 | public webhook URL. |
| 331 | |
| 332 | Setup: |
| 333 | |
| 334 | ```bash |
| 335 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 336 | export CODEWHALE_RUNTIME_TOKEN |
| 337 | |
| 338 | codewhale app-server --http \ |
| 339 | --host 127.0.0.1 \ |
| 340 | --port 7878 \ |
| 341 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 342 | |
| 343 | cd integrations/feishu-bridge |
| 344 | npm install --omit=dev |
| 345 | cp .env.example .env |
| 346 | $EDITOR .env |
| 347 | npm run validate:config -- \ |
| 348 | --env .env \ |
| 349 | --workspace-root "$PWD/../.." \ |
| 350 | --check-filesystem |
| 351 | npm start |
| 352 | ``` |
| 353 | |
| 354 | Env template: |
| 355 | |
| 356 | ```env |
| 357 | FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx |
| 358 | FEISHU_APP_SECRET=replace-with-app-secret |
| 359 | FEISHU_DOMAIN=feishu # international Lark users: set to "lark" |
| 360 | |
| 361 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 362 | CODEWHALE_RUNTIME_TOKEN=<same value used to start app-server> |
| 363 | CODEWHALE_WORKSPACE=/path/to/workspace |
| 364 | # Optional override; leave blank to inherit the runtime's configured provider/model. |
| 365 | CODEWHALE_MODEL= |
| 366 | CODEWHALE_MODE=agent |
| 367 | CODEWHALE_ALLOW_SHELL=true # grants shell execution from the bridge; set false for text-only chat |
| 368 | CODEWHALE_TRUST_MODE=false |
| 369 | CODEWHALE_AUTO_APPROVE=false |
| 370 | |
| 371 | CODEWHALE_CHAT_ALLOWLIST= |
| 372 | CODEWHALE_ALLOW_UNLISTED=false |
| 373 | FEISHU_ALLOW_GROUPS=false |
| 374 | ``` |
| 375 | |
| 376 | First pairing: |
| 377 | |
| 378 | Temporarily set `CODEWHALE_ALLOW_UNLISTED=true`, message the app once, copy the |
| 379 | logged open id into `CODEWHALE_CHAT_ALLOWLIST`, then set |
| 380 | `CODEWHALE_ALLOW_UNLISTED=false` and restart the bridge. |
| 381 | |
| 382 | Validation: |
| 383 | |
| 384 | ```bash |
| 385 | codewhale doctor --json |
| 386 | curl -fsS http://127.0.0.1:7878/health |
| 387 | npm run validate:config -- \ |
| 388 | --env .env \ |
| 389 | --workspace-root "$PWD/../.." \ |
| 390 | --check-filesystem |
| 391 | ``` |
| 392 | |
| 393 | Trust boundary: |
| 394 | |
| 395 | - Exposed: no inbound CodeWhale port. Feishu/Lark sees messages sent to the app. |
| 396 | - Not exposed: CodeWhale remains on `127.0.0.1`; provider keys stay in runtime |
| 397 | config. |
| 398 | - Tokens used: `FEISHU_APP_ID` / `FEISHU_APP_SECRET` for the platform, |
| 399 | `CODEWHALE_RUNTIME_TOKEN` for bridge-to-runtime calls, and |
| 400 | `CODEWHALE_CHAT_ALLOWLIST` for chat gating. |
| 401 | - Caveat: group control is off unless explicitly enabled. |
| 402 | |
| 403 | ### 5. Weixin personal bridge |
| 404 | |
| 405 | Use this when a personal Weixin account should control the local runtime by QR |
| 406 | login. This is not a public account webhook. The bridge initiates long polling |
| 407 | and does not need a public port. |
| 408 | |
| 409 | Setup: |
| 410 | |
| 411 | ```bash |
| 412 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 413 | export CODEWHALE_RUNTIME_TOKEN |
| 414 | |
| 415 | codewhale app-server --http \ |
| 416 | --host 127.0.0.1 \ |
| 417 | --port 7878 \ |
| 418 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 419 | |
| 420 | cd integrations/weixin-bridge |
| 421 | npm install --omit=dev |
| 422 | cp .env.example .env |
| 423 | $EDITOR .env |
| 424 | npm run check |
| 425 | npm start |
| 426 | ``` |
| 427 | |
| 428 | Env template: |
| 429 | |
| 430 | ```env |
| 431 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 432 | CODEWHALE_RUNTIME_TOKEN=<same value used to start app-server> |
| 433 | CODEWHALE_WORKSPACE=/path/to/workspace |
| 434 | # Optional override; leave blank to inherit the runtime's configured provider/model. |
| 435 | CODEWHALE_MODEL= |
| 436 | CODEWHALE_MODE=agent |
| 437 | CODEWHALE_ALLOW_SHELL=true # grants shell execution from the bridge; set false for text-only chat |
| 438 | CODEWHALE_TRUST_MODE=false |
| 439 | CODEWHALE_AUTO_APPROVE=false |
| 440 | |
| 441 | WEXIN_CHAT_ALLOWLIST= |
| 442 | WEXIN_ALLOW_UNLISTED=false |
| 443 | WEXIN_STATE_DIR=/var/lib/codewhale-weixin-bot-bridge |
| 444 | ``` |
| 445 | |
| 446 | First pairing: |
| 447 | |
| 448 | Set `WEXIN_ALLOW_UNLISTED=true`, start the bridge, scan the QR code, send |
| 449 | `/status`, copy the returned `user_id` into `WEXIN_CHAT_ALLOWLIST`, then set |
| 450 | `WEXIN_ALLOW_UNLISTED=false` and restart the bridge. |
| 451 | |
| 452 | Validation: |
| 453 | |
| 454 | ```bash |
| 455 | codewhale doctor --json |
| 456 | curl -fsS http://127.0.0.1:7878/health |
| 457 | npm run check |
| 458 | ``` |
| 459 | |
| 460 | Trust boundary: |
| 461 | |
| 462 | - Exposed: no inbound CodeWhale port. The personal Weixin session and the |
| 463 | bridge state directory become sensitive local state. |
| 464 | - Not exposed: CodeWhale remains on `127.0.0.1`; provider keys stay in runtime |
| 465 | config. |
| 466 | - Tokens used: the scanned Weixin login/session state for platform access, |
| 467 | `CODEWHALE_RUNTIME_TOKEN` for bridge-to-runtime calls, and |
| 468 | `WEXIN_CHAT_ALLOWLIST` for user gating. |
| 469 | - Caveat: this is a personal-account bridge. Treat the host and state directory |
| 470 | like a logged-in phone session. |
| 471 | |
| 472 | ### 6. Public webhook / Funnel (Advanced) |
| 473 | |
| 474 | Use this only when the user explicitly chooses public internet reachability, |
| 475 | understands that the URL can be reached outside the tailnet, and has a reason |
| 476 | that Tailscale Serve or long polling cannot satisfy. |
| 477 | |
| 478 | Preferred advanced pattern: |
| 479 | |
| 480 | ```bash |
| 481 | CODEWHALE_RUNTIME_TOKEN="$(openssl rand -hex 32)" |
| 482 | export CODEWHALE_RUNTIME_TOKEN |
| 483 | |
| 484 | codewhale app-server --mobile \ |
| 485 | --host 127.0.0.1 \ |
| 486 | --port 7878 \ |
| 487 | --auth-token "$CODEWHALE_RUNTIME_TOKEN" |
| 488 | |
| 489 | tailscale funnel --bg --https=443 localhost:7878 |
| 490 | ``` |
| 491 | |
| 492 | Env template: |
| 493 | |
| 494 | ```env |
| 495 | CODEWHALE_RUNTIME_URL=https://<public-name> |
| 496 | CODEWHALE_RUNTIME_TOKEN=<openssl-rand-hex-32> |
| 497 | PUBLIC_EXPOSURE_ACK=true |
| 498 | ``` |
| 499 | |
| 500 | Validation: |
| 501 | |
| 502 | ```bash |
| 503 | codewhale doctor --json |
| 504 | curl -fsS http://127.0.0.1:7878/health |
| 505 | curl -fsS https://<public-name>/health |
| 506 | curl -fsS \ |
| 507 | -H "Authorization: Bearer $CODEWHALE_RUNTIME_TOKEN" \ |
| 508 | https://<public-name>/v1/runtime/info |
| 509 | tailscale funnel status |
| 510 | ``` |
| 511 | |
| 512 | Trust boundary: |
| 513 | |
| 514 | - Exposed: a public HTTPS endpoint, not just your tailnet. |
| 515 | - Not exposed by CodeWhale directly: the backend still binds to `127.0.0.1`, |
| 516 | but the fronting layer makes selected routes reachable from the internet. |
| 517 | - Token used: `CODEWHALE_RUNTIME_TOKEN` remains mandatory for control routes. |
| 518 | - Caveat: public does not mean safe. Do not use `--insecure-no-auth`, do not bind |
| 519 | CodeWhale to `0.0.0.0`, and do not call this the default. |
| 520 | |
| 521 | ## Cloud/VPS posture |
| 522 | |
| 523 | Cloud/VPS is a placement choice, not a trust model. The old RFC's cloud work is |
| 524 | still useful, but it should sit behind the same reachability choices: |
| 525 | |
| 526 | - A VPS can run the runtime bound to `127.0.0.1`. |
| 527 | - Recommended remote access from personal devices is still Tailscale Serve. |
| 528 | - Bot bridges should use long polling / long connection where available, keeping |
| 529 | the runtime localhost-only on the host. |
| 530 | - SSH tunnels remain acceptable for ad hoc validation: |
| 531 | |
| 532 | ```bash |
| 533 | ssh -L 7878:127.0.0.1:7878 <host> |
| 534 | ``` |
| 535 | |
| 536 | Public inbound listeners, public webhooks, and Tailscale Funnel are advanced |
| 537 | choices, not the default cloud path. |
| 538 | |
| 539 | ## Prior art: Hermes Agent (reference only - do not copy) |
| 540 | |
| 541 | Nous Research's Hermes Agent validates the table-driven part of this design. |
| 542 | Use it for ideas; keep CodeWhale's style: Rust core, local runtime, zero-dep |
| 543 | Node bridges where possible, and plain-text replies. |
| 544 | |
| 545 | - `gateway/platform_registry.py` maps to our `BridgeSpec` / access-path |
| 546 | registry: one row per platform, with setup hints, required env, validation, |
| 547 | and adapter factory. |
| 548 | - `gateway/pairing.py` maps to our allowlist / first-pairing flow. |
| 549 | |
| 550 | Telegram hardening carried forward from the original RFC: |
| 551 | |
| 552 | | Edge case | In Hermes | In our Telegram bridge | |
| 553 | |---|---|---| |
| 554 | | 409 polling conflict | `_looks_like_polling_conflict` | done - poll loop backs off and warns | |
| 555 | | 429 `retry_after` | rate-limit handling | done - `telegramApi` honors `parameters.retry_after` | |
| 556 | | Forum General topic id handling | send/typing split | done - omit `message_thread_id` when id is 1 on send | |
| 557 | | Stale reply anchor after restart | retry without anchor | sidestepped - no `reply_to_message_id` | |
| 558 | | Network/connect timeout retry | network error detection | partial - generic poll-loop backoff | |
| 559 | | Text batching / progress edit | progress-edit tests | deferred - plain periodic chunks | |
| 560 | | MarkdownV2 escaping | escaping helpers | deferred - plain text | |
| 561 | | Webhook mode | webhook adapter | out of default scope - long polling first | |
| 562 | |
| 563 | ## Design principle: table-driven, like `ProviderSpec` |
| 564 | |
| 565 | The provider registry is the model to preserve: adding a provider is one row. |
| 566 | Apply the same idea to access paths, bridges, and cloud placements so the matrix |
| 567 | grows by data. |
| 568 | |
| 569 | ```text |
| 570 | AccessPath x Placement x BridgeSpec + ProviderSpec |
| 571 | ---------- --------- ---------- ------------ |
| 572 | localhost local none deepseek / openai / ... |
| 573 | tailscale local/vps none provider lives in runtime.env |
| 574 | telegram local/vps telegram bridge is pure transport |
| 575 | feishu local/vps feishu bridge is pure transport |
| 576 | weixin local/vps weixin bridge is pure transport |
| 577 | funnel local/vps optional explicit public exposure |
| 578 | ``` |
| 579 | |
| 580 | Clean separation: |
| 581 | |
| 582 | - **Provider = runtime env.** The runtime resolves provider/model/API key from |
| 583 | `CODEWHALE_PROVIDER`, provider key vars, and the provider registry. Bridges do |
| 584 | not need provider keys. |
| 585 | - **Access path = reachability.** Localhost, Tailscale Serve, chat long polling, |
| 586 | and Funnel are separate choices with different trust boundaries. |
| 587 | - **Bridge = transport.** A chat bridge forwards allowed chat messages to |
| 588 | `http://127.0.0.1:7878` with `CODEWHALE_RUNTIME_TOKEN`. |
| 589 | - **Cloud = where it runs and where secrets live.** It is not permission to |
| 590 | open port 7878. |
| 591 | |
| 592 | ## Proposed command surface |
| 593 | |
| 594 | Current flags are verified for the generate-only cloud/bridge wizard: |
| 595 | |
| 596 | | Flag | Current status | |
| 597 | |---|---| |
| 598 | | `--cloud <lighthouse|azure|digitalocean>` | verified | |
| 599 | | `--bridge <telegram|feishu>` | verified | |
| 600 | | `--provider <slug>` | verified, provider registry-backed | |
| 601 | | `--out <dir>` | verified | |
| 602 | | `--generate-only` | verified | |
| 603 | | `--apply` | verified flag, but not implemented | |
| 604 | | `--yes` | verified flag | |
| 605 | | `--non-interactive` | verified flag | |
| 606 | |
| 607 | Proposed Tailscale-first revision: |
| 608 | |
| 609 | | Flag | Meaning | |
| 610 | |---|---| |
| 611 | | `--access <localhost|tailscale|telegram|feishu|weixin|funnel>` | Skip the reachability prompt. | |
| 612 | | `--placement <local|vps|lighthouse|azure|digitalocean>` | Where the runtime runs; default local. | |
| 613 | | `--bridge <telegram|feishu|weixin>` | Optional when `--access` implies a bridge. | |
| 614 | | `--provider <slug>` | Provider slug; validated against the existing provider registry. | |
| 615 | | `--out <dir>` | Bundle output dir. | |
| 616 | | `--generate-only` | Emit commands/env/runbook, do not provision. Default. | |
| 617 | | `--apply` | Future cloud CLI provisioning, behind confirmation. Still not implemented. | |
| 618 | | `--yes` | Skip final confirmation gates where safe for CI/non-interactive use. | |
| 619 | | `--non-interactive` | Fail instead of prompting for missing required values. | |
| 620 | |
| 621 | The first prompt should be the reachability question, not the cloud question. |
| 622 | Tailscale should be visually marked as recommended. |
| 623 | |
| 624 | ## Generated bundle |
| 625 | |
| 626 | The current bundle model stays useful. Extend it so the generated runbook is |
| 627 | access-path-first. |
| 628 | |
| 629 | Files: |
| 630 | |
| 631 | - `runtime.env` - provider and runtime config: |
| 632 | |
| 633 | ```env |
| 634 | CODEWHALE_PROVIDER=openai |
| 635 | OPENAI_API_KEY=replace-with-provider-key |
| 636 | # Optional override; leave blank to inherit the runtime's configured provider/model. |
| 637 | CODEWHALE_MODEL= |
| 638 | CODEWHALE_RUNTIME_TOKEN=<random> |
| 639 | CODEWHALE_RUNTIME_PORT=7878 |
| 640 | CODEWHALE_RUNTIME_WORKERS=2 |
| 641 | RUST_LOG=info |
| 642 | ``` |
| 643 | |
| 644 | - `<bridge>.env` - transport only when a bridge is selected: |
| 645 | |
| 646 | ```env |
| 647 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 648 | CODEWHALE_RUNTIME_TOKEN=<same random token> |
| 649 | CODEWHALE_WORKSPACE=/opt/whalebro |
| 650 | # Optional override; leave blank to inherit the runtime's configured provider/model. |
| 651 | CODEWHALE_MODEL= |
| 652 | CODEWHALE_MODE=agent |
| 653 | CODEWHALE_ALLOW_SHELL=true # grants shell execution from the bridge; set false for text-only chat |
| 654 | CODEWHALE_TRUST_MODE=false |
| 655 | CODEWHALE_AUTO_APPROVE=false |
| 656 | ``` |
| 657 | |
| 658 | - `codewhale-runtime.service` |
| 659 | - optional `codewhale-<bridge>.service` |
| 660 | - optional cloud artifacts: `cloud-init.yaml`, `provision.sh`, `cnb.yml`, or |
| 661 | cloud-specific runbook steps |
| 662 | - `RUNBOOK.md` with: |
| 663 | - exact setup commands |
| 664 | - env template |
| 665 | - doctor-style validation |
| 666 | - first-pairing steps for bridges |
| 667 | - trust-boundary summary |
| 668 | - explicit "public exposure acknowledged" section for Funnel/webhook modes |
| 669 | |
| 670 | ## Auto-provision |
| 671 | |
| 672 | Preserve the original safety model: |
| 673 | |
| 674 | - `--generate-only` is the default. |
| 675 | - `--apply` is explicit and is not implemented today. |
| 676 | - Every command is rendered before execution. |
| 677 | - Secrets are not passed through shell history or argv. |
| 678 | - Cloud CLIs are placement helpers, not permission to open runtime ports. |
| 679 | |
| 680 | Existing cloud target design remains accurate: |
| 681 | |
| 682 | - Tencent Lighthouse: native plus systemd, env-file secrets, CNB-oriented plan. |
| 683 | - Azure VM: Docker image plus Key Vault, managed identity at boot. |
| 684 | - DigitalOcean Droplet: native plus systemd, env-file secrets, `doctl` plan. |
| 685 | |
| 686 | All cloud plans should bind CodeWhale to `127.0.0.1` and then layer one of the |
| 687 | reachability paths above. |
| 688 | |
| 689 | ## Namespace migration: `DEEPSEEK_*` to `CODEWHALE_*` |
| 690 | |
| 691 | Carry forward the convention already used in code: read `CODEWHALE_X` first, |
| 692 | fall back to `DEEPSEEK_X` where compatibility is needed. |
| 693 | |
| 694 | Touch list from the original RFC remains valid: |
| 695 | |
| 696 | 1. Bridges: read `CODEWHALE_X ?? DEEPSEEK_X` for runtime URL/token, workspace, |
| 697 | model, mode, shell/trust/approval flags, allowlists, and timeouts. Templates |
| 698 | should emit `CODEWHALE_*`. |
| 699 | 2. Deploy units: prefer `/etc/codewhale/*.env`; keep legacy path reads only for |
| 700 | compatibility where needed. |
| 701 | 3. `.env.example` files and `config.example.toml`: lead with `CODEWHALE_*`, |
| 702 | document legacy aliases. |
| 703 | 4. Drop DeepSeek-shaped defaults in bridge templates except where DeepSeek is |
| 704 | explicitly the chosen provider. Provider choice belongs in `runtime.env`. |
| 705 | |
| 706 | ## Tests |
| 707 | |
| 708 | Existing bundle tests should stay: |
| 709 | |
| 710 | - Every cloud / bridge / provider triple renders. |
| 711 | - Runtime and bridge env files share the same `CODEWHALE_RUNTIME_TOKEN`. |
| 712 | - Env files lead with `CODEWHALE_*`. |
| 713 | - Generated runbooks are non-empty and list the provision plan. |
| 714 | - Provision plans are command data and are not executed in tests. |
| 715 | |
| 716 | New tests for this revision: |
| 717 | |
| 718 | - Every `AccessPath` row has setup commands, env template, validation commands, |
| 719 | and trust-boundary copy. |
| 720 | - Tailscale is the recommended remote path in prompt ordering. |
| 721 | - Funnel/webhook mode requires an explicit advanced/public acknowledgement. |
| 722 | - `/mobile` docs use `app-server --mobile --host 127.0.0.1` for current binary |
| 723 | behavior, or clearly mark any `--http` plus `/mobile` path as proposed. |
| 724 | - Weixin can be documented before it is in the `remote-setup` registry, but the |
| 725 | wizard must mark it proposed until a `BridgeSpec` row and validation story |
| 726 | exist. |
| 727 | |
| 728 | ## Suggested sequencing |
| 729 | |
| 730 | 1. Revise the RFC and runbook copy to be Tailscale-first. |
| 731 | 2. Add an access-path registry above the existing cloud/bridge/provider tables. |
| 732 | 3. Add localhost and Tailscale generate-only bundles. |
| 733 | 4. Add Weixin as a `BridgeSpec` row or explicitly hide it behind "proposed" in |
| 734 | the wizard until registry and validation support land. |
| 735 | 5. Rework cloud bundles so placement is second and reachability is first. |
| 736 | 6. Add Funnel/webhook only as an advanced path with explicit public-exposure |
| 737 | acknowledgement. |
| 738 | 7. Implement `--apply` last, after generate-only output is reviewed. |
| 739 | |
| 740 | ## Command verification ledger |
| 741 | |
| 742 | Verified against CodeWhale code/docs in this worktree: |
| 743 | |
| 744 | - `codewhale app-server --http --host 127.0.0.1 --port 7878 --auth-token TOKEN` |
| 745 | - `codewhale app-server --mobile --host 127.0.0.1 --port 7878 --auth-token TOKEN` |
| 746 | - `codewhale doctor --json` |
| 747 | - `curl /health` and authenticated `curl /v1/runtime/info` |
| 748 | - `npm run validate:config` for Telegram and Feishu bridges |
| 749 | - `npm run check` for the Weixin bridge |
| 750 | - Existing `remote-setup` generate-only flags listed above |
| 751 | |
| 752 | Marked proposed or external: |
| 753 | |
| 754 | - `codewhale remote-setup --access ...` and access-path registry |
| 755 | - first-class Tailscale, localhost, Weixin, and Funnel choices in the wizard |
| 756 | - `--apply` execution |
| 757 | - Tailscale CLI commands (`tailscale serve ...`, `tailscale funnel ...`) are |
| 758 | external Tailscale commands. They are the intended RFC examples, but they are |
| 759 | not CodeWhale CLI flags. |
| 760 |