| name | deploy-relay |
| description | Deploy (or repair/verify) the zero-trust, end-to-end-encrypted AI relay end to end (serves both OpenAI Codex CLI and Claude Code): Taiwan VPS + sub2api behind a Cloudflare TCP tunnel wrapping mTLS. Use when the user wants to set up, finish, re-run, or verify the relay in this repo. Orchestrates all scripts, pausing only at the unavoidable human gates (VPS, domain+API token, ChatGPT/Claude OAuth, SSH). |
| allowed-tools | Read, Edit, Write, Bash, Agent, AskUserQuestion, TodoWrite |
Deploy the relay
You are orchestrating the full deployment described in CLAUDE.md. Read CLAUDE.md first and
honor every invariant. Drive it with a TodoWrite list. Be idempotent: every script is re-runnable,
so on a partial/failed prior run, re-run from the failed step rather than starting over.
Operating rules
- Never violate the invariants in CLAUDE.md (TCP-mode only, loopback-only, two gates, static IP,
secrets never committed). If a step would, stop and explain.
- Delegate all on-VPS work to the
relay-deployer subagent (it has SSH context). Do
Cloudflare API provisioning and the local stack yourself on the laptop.
- Pause at human gates with
AskUserQuestion — never fake or guess these values.
- After each phase, state what succeeded and the exact next action.
Phase 0 — Preflight & inputs (human gates G1–G2, G4)
Confirm prerequisites; collect with AskUserQuestion if missing:
- G1 VPS: a VPS exists with a reserved static IPv4, Ubuntu 24.04, ≥2 GB, and you have
SSH (user, host, key). If not, give the user the recommended spec (e.g. a 2 vCPU / 2 GB instance in a
region near the API you call; on GCP
gcloud compute addresses create zt-relay-ip --region=<region>)
and wait.
- G2 Cloudflare: the domain is on a Cloudflare zone, and the user has a scoped API token plus
Account ID and Zone ID. Token scopes: Account → Cloudflare Tunnel:Edit, Access: Apps and
Policies:Edit, Access: Service Tokens:Edit; Zone → DNS:Edit. Capture
DOMAIN (hostname will be
<domain>).
- Decide whether sensitive code will pass through (it shouldn't — design is non-sensitive) and
remind about the OpenAI ToS caveat in
docs/RISKS.md.
Phase 1 — Certificates (laptop, automatic)
- Run
bash certs/gen-certs.sh (set VPS_IP only if also using the Tier-2 fallback).
- Copy
certs/out/{server-ca.crt,client.crt,client.key} into local/certs/.
- Stage
certs/out/{client-ca.crt,server.crt,server.key} for the deployer to scp to the VPS.
Phase 2 — VPS base + sub2api (delegate to subagent)
Spawn relay-deployer with: SSH target, the desired non-root user, the SSH public key, and
the staged server certs. Instruct it to, idempotently:
01-harden.sh (keep the SSH safety net — do NOT close public SSH yet).
02-install-docker.sh (installs the DOCKER-USER drop).
SUB2API_ENV_READY=1 03-deploy-sub2api.sh and confirm the hard loopback assertion passes.
- Install ghostunnel server: scp certs, then
04-install-ghostunnel-server.sh.
Have it return: sub2api admin URL reachability, the loopback-assertion result, and any blockers.
Phase 3 — ChatGPT account + client key (human gate G3)
This is interactive. Tell the user to open an SSH local-forward
(ssh -L 8088:127.0.0.1:8080 user@vps) and in the sub2api admin UI: link the ChatGPT account via
OAuth (or import a Codex auth.json), then create a client API key and assign it to an "OpenAI"
group (critical — otherwise requests misroute). Collect the resulting SUB2API_KEY. Add it to
local/.env (the hook will allow writing, never committing).
Phase 4 — Cloudflare provisioning (laptop, automatic)
Run, with the Phase-0 values exported:
CLOUDFLARE_API_TOKEN=… CF_ACCOUNT_ID=… CF_ZONE_ID=… DOMAIN=… bash cloudflare/provision.sh
This creates the remote-managed tunnel, TCP ingress (tcp://127.0.0.1:9443), proxied DNS CNAME, the
Access app, a single Service-Auth (non_identity) policy, and the service token — writing
TUNNEL_HOSTNAME + CF_ACCESS_CLIENT_ID/SECRET into local/.env and printing TUNNEL_TOKEN.
Verify the script reported exactly one Service-Auth policy on the app.
Phase 5 — Connector on the VPS (delegate to subagent)
Hand the deployer the TUNNEL_TOKEN; have it run TUNNEL_TOKEN=… 05-install-cloudflared.sh and
confirm the connector shows HEALTHY. Optionally install sub2api-backup.timer.
Phase 6 — Local stack + clients (laptop, automatic)
- Ensure
local/.env has all four keys (TUNNEL_HOSTNAME, CF_ACCESS_CLIENT_ID/SECRET,
SUB2API_KEY) and local/certs/ has the client certs.
cd local && docker compose up -d. Confirm both containers are healthy (cf-access must NOT be
stuck retrying — that signals a non-Service-Auth policy).
- Copy
local/codex-config.toml → ~/.codex/config.toml; ensure SUB2API_KEY is exported.
- Claude Code (same stack — NO extra container; sub2api serves
/v1/messages natively):
- Copy
local/claude-settings.example.json → ~/.claude/settings.json (points at http://127.0.0.1:8443).
- Export
ANTHROPIC_AUTH_TOKEN = a sub2api key in an "Anthropic" group (link a Claude account in
sub2api for real Claude; an "OpenAI"-group key also works but answers come from GPT).
Phase 7 — Verify (definition of done)
Run bash local/verify.sh and require: token opens the edge without a browser; no-token is
refused; /v1/models returns 200 through the full mTLS path. Then have the deployer confirm
on the VPS: nmap/ss shows no off-loopback listener, and curl ifconfig.me equals the
reserved static IP. Finally run codex "summarize this repo" as a real smoke test.
- Optional Claude smoke test:
claude -p "summarize this repo".
- Offer
vps/close-public-ssh.sh only after confirming a working tunnel-SSH path.
On completion
Summarize: what's running, the four secrets' locations (and that they're gitignored), the verify
results, and the residual caveats (OpenAI ToS, keep the egress IP stable, Cloudflare sees ciphertext
only). If anything failed, report the exact failing step and command output — do not claim success.