AI Workbench & Forge Relay
AI coding workbench with 4 CLI tools, smart dispatcher, xterm.js terminals, Forge Relay API, and OpenClaw orchestration. Primary on Izar-Host CT 126, fallback on Tarn-Host VM 160.
AI Workbench & Forge Relay
The Workbench is the admin coding environment at /admin/workbench. It provides browser-based access to 4 AI coding CLIs, a smart dispatcher that routes tasks based on subscription capacity, and OpenClaw agentic orchestration.
Architecture
Browser (xterm.js)
│ WebSocket (wss://workbench-term.Arcturus-Prime.com)
▼
CF Tunnel (Altair-Link) ──── cloudflared
│
ttyd (7681) ──── forge-attach.sh ──── tmux session
│
Forge Relay v2 (7682) ──── tmux send-keys / capture-pane
│ (https://workbench-relay.Arcturus-Prime.com)
▼
Astro API (/api/admin/forge) ──── relayFetch()
│
OpenClaw orchestration ──── 8 Forge tools ──── agentic loop
Hosting
Primary: CT 126 workbench-Izar-Host on Izar-Host (10.42.0.126)
- Alpine 3.23, 512 MB RAM, 2 cores
- On the local Milky Way — always reachable from Altair-Link (CF tunnel host)
- Services: ttyd (7681), Forge Relay (7682)
- OpenRC init scripts:
workbench-ttyd,workbench-relay
Fallback: VM 160 on Tarn-Host (192.168.20.160)
- Full workbench with dispatcher, AI tool CLIs, git worktrees
- On the remote Andromeda — requires inter-site routing (can be flaky)
- To switch: update tunnel config on Altair-Link, restart cloudflared
| Component | Purpose |
|---|---|
| ttyd (7681) | WebSocket terminal — bridges browser xterm.js to tmux via forge-attach.sh |
| Forge Relay v2 (7682) | Hono.js REST API wrapping tmux programmatic control (Bearer auth) |
| Dispatcher (8096) | FastAPI smart router, usage tracking (SQLite), git worktree management (Tarn-Host only) |
| tmux | 5 standard sessions (forge-*) + dynamic sessions (wb-*) |
Cloudflare Tunnels
| Hostname | Current Target | Purpose |
|---|---|---|
workbench-term.Arcturus-Prime.com | http://10.42.0.126:7681 (Izar-Host) | Browser WebSocket terminal |
workbench-relay.Arcturus-Prime.com | http://10.42.0.126:7682 (Izar-Host) | Forge Relay API |
Tunnel runs on Altair-Link (10.42.0.199), config: /home/argonaut/.cloudflared/config.yml
Switching Between Izar-Host and Tarn-Host
Edit /home/argonaut/.cloudflared/config.yml on Altair-Link:
# Izar-Host (primary — local network, reliable)
- hostname: workbench-term.Arcturus-Prime.com
service: http://10.42.0.126:7681
- hostname: workbench-relay.Arcturus-Prime.com
service: http://10.42.0.126:7682
# Tarn-Host (fallback — remote network, can be unreachable)
# service: http://192.168.20.160:7681
# service: http://192.168.20.160:7682
Then restart: ssh [email protected] "sudo systemctl restart cloudflared"
AI Coding CLIs
| Tool | Binary | Version | Auth | Rate Limit |
|---|---|---|---|---|
| Claude Code | claude | 2.1.63 | OAuth (copied from workstation) | ~900 msgs/5hr (Max) or ~45/5hr (Pro) |
| Codex CLI | codex | 0.106.0 | Needs codex login | ~150 msgs/5hr (Plus) |
| OpenCode | opencode | 1.2.15 | BYOK (API keys) | Unlimited |
| Kilo Code | kilocode | 7.0.33 | BYOK (API keys) | Unlimited |
tmux Sessions
| Session | Purpose |
|---|---|
forge-shell | General bash shell |
forge-claude | Claude Code CLI |
forge-codex | Codex CLI |
forge-opencode | OpenCode CLI |
forge-kilocode | Kilo Code CLI |
Sessions auto-created by init-sessions.sh at ttyd startup and healthcheck. Dynamic wb-* sessions created by the dispatcher for automated tasks.
Workbench Modes
Terminal Mode
5 xterm.js terminals in browser tabs, each connecting to a tmux session via ttyd WebSocket. Lazy-connect on tab switch (only the active tab opens a WebSocket). Reconnect button for dropped connections.
WebSocket URL pattern:
- Local dev:
ws://10.42.0.126:7681/ws?arg=forge-shell - Production:
wss://workbench-term.Arcturus-Prime.com/ws?arg=forge-shell
Agent Mode (Forge Dispatch)
Dispatch form sends tasks to AI tools. The buildCliCommand() function maps tool names to CLI invocations:
claude "instruction"(interactive) orclaude --print "instruction"(headless)codex --approval-mode suggest "instruction"(interactive)opencode run "instruction"(headless) oropencode(interactive TUI)kilocode run "instruction"(headless) orkilocode(interactive TUI)
Session grid shows status of tmux sessions. Activity log tracks dispatched commands.
Chat Mode
Streaming chat via the unified-chat endpoint. Conversation management stored in localStorage.
Smart Dispatcher (port 8096)
When tool: "auto", the dispatcher scores tools:
score = (remaining_capacity / rate_limit) * (1 / priority)
| Priority | Tool | Strengths |
|---|---|---|
| 1 (highest) | Claude Code | Architecture, refactoring, complex, debugging |
| 2 | Codex CLI | Quick fixes, tests, simple features |
| 3 | OpenCode | Multi-model, batch |
| 4 (fallback) | Kilo Code | Automation, batch |
Subscription tools first, BYOK fallback. Usage tracked in SQLite with 5-hour rolling windows.
API Routes
/api/admin/forge (forge.ts)
Proxies to Forge Relay via FORGE_RELAY_URL (through Cloudflare tunnel in production).
| Action | Description |
|---|---|
dispatch | Send CLI command to a tool’s tmux session |
send | Send raw keystrokes to a session |
interrupt | Send Ctrl+C to a session |
capture | Read terminal output from a session |
sessions | List all tmux sessions |
/api/admin/forge-git (forge-git.ts)
| Action | Description |
|---|---|
create-branch | Create branch via Gitea API |
create-pr | Create pull request via Gitea API |
commit-via-relay | Stage + commit via relay |
push-via-relay | Push to remote via relay |
pipeline | Full auto: branch + push + PR |
/api/admin/openclaw (openclaw.ts)
OpenClaw agentic orchestration with 8 Forge tools (dispatch, status, capture, interrupt, git branch/commit/push/pr). Creates a ForgeExecutor that calls Gitea and Forge Relay APIs directly.
Dispatcher API (port 8096)
| Method | Path | Purpose |
|---|---|---|
POST | /api/task | Submit task (repo, prompt, tool, commit, push, create_pr, timeout) |
GET | /api/task/{id} | Task status + output |
GET | /api/tools | Tool capacity + availability |
GET | /api/repos | List cloned repos |
POST | /api/repos | Clone new repo from Gitea |
Environment Variables
| Variable | Location | Purpose |
|---|---|---|
FORGE_RELAY_URL | CF Pages env, local .env | https://workbench-relay.Arcturus-Prime.com |
FORGE_RELAY_SECRET | CF Pages env, relay OpenRC service | Bearer token for relay auth |
WORKBENCH_API_TOKEN | VM dispatcher service, OpenClaw .env | Dispatcher API auth |
GITEA_API_TOKEN | CF Pages env | Gitea API for git operations |
OPENCLAW_API_TOKEN | CF Pages env | OpenClaw gateway auth |
GITEA_TOKEN | VM dispatcher service | Git push auth |
Files
VM 160 (/opt/workbench/)
| File | Purpose |
|---|---|
forge-relay/server.js | Hono.js REST API for tmux control (v2) |
dispatcher/main.py | FastAPI smart router, usage tracking, git worktrees |
forge-attach.sh | ttyd session attachment (reads ?arg= URL param) |
init-sessions.sh | Creates forge-* tmux sessions if missing |
healthcheck.sh | Auto-heal script (root cron every 2min) |
status.sh | Diagnostic dashboard (wb status) |
repos/*.git | Bare git clones (auto-fetch every 5min) |
Arcturus-Prime Astro
| File | Purpose |
|---|---|
src/pages/admin/workbench.astro | Full workbench UI (5000+ lines) |
src/pages/api/admin/forge.ts | Forge API route + relayFetch() proxy |
src/pages/api/admin/forge-git.ts | Git pipeline API route |
src/pages/api/admin/openclaw.ts | OpenClaw orchestration API |
src/lib/forge-client.ts | Browser-side Forge API client |
src/lib/tool-router.ts | Heuristic tool selection engine |
src/config/modules/workbench.ts | Module manifest |
CT 126 workbench-Izar-Host (/opt/workbench/)
| File | Purpose |
|---|---|
forge-attach.sh | tmux session attachment script for ttyd |
init-sessions.sh | Creates the 5 standard forge-* sessions |
forge-relay/server.js | Hono.js REST API (285 lines) |
forge-relay/package.json | Dependencies: hono, @hono/node-server |
OpenRC services: /etc/init.d/workbench-ttyd, /etc/init.d/workbench-relay
Logs: /var/log/workbench/ttyd.log, /var/log/workbench/relay.log
Source Repository
~/Development/Arcturus-Prime-workbench/ → Gitea: Arcturus-Prime/Arcturus-Prime-workbench
Troubleshooting
Terminal stuck on “Connecting to forge-shell…”
Quick checks (in order):
-
Is the container running?
ssh [email protected] "pct exec 126 -- rc-service workbench-ttyd status" -
Are tmux sessions alive?
ssh [email protected] "pct exec 126 -- tmux ls"If no sessions:
ssh [email protected] "pct exec 126 -- bash /opt/workbench/init-sessions.sh" -
Can Altair-Link reach the container?
ssh [email protected] "curl -s -o /dev/null -w '%{http_code}\n' http://10.42.0.126:7681/"Should return
200. -
Is the CF tunnel routing correctly?
curl -s -o /dev/null -w '%{http_code}\n' "https://workbench-term.Arcturus-Prime.com/"Should return
200. If502, the tunnel can’t reach the container. -
Does the WebSocket connect?
cd ~/Development/Arcturus-Prime && node -e " const ws = new (require('ws'))('wss://workbench-term.Arcturus-Prime.com/ws?arg=forge-shell', {handshakeTimeout:5000}); ws.on('open', () => { console.log('OK'); ws.close(); process.exit(0); }); ws.on('error', (e) => { console.log('FAIL:', e.message); process.exit(1); }); setTimeout(() => { console.log('TIMEOUT'); process.exit(1); }, 6000); " -
Browser-specific checks:
- Hard refresh: Ctrl+Shift+R (bypass cache after CF Pages deploy)
- Disable ad blocker for Arcturus-Prime.com (can block WebSocket subdomains)
- Check browser console for WebSocket errors (filter “ws” or “websocket”)
- Try incognito/private window
Restart services on CT 126
ssh [email protected] "pct exec 126 -- rc-service workbench-ttyd restart"
ssh [email protected] "pct exec 126 -- rc-service workbench-relay restart"
Restart cloudflared
ssh [email protected] "sudo systemctl restart cloudflared"
Known issues
| Issue | Cause | Fix |
|---|---|---|
CORS error in console from checkTermHost() | ttyd doesn’t send CORS headers | Code uses no-cors fetch — opaque response is expected. Console error is cosmetic if using old cached code. Hard refresh. |
ERR_BLOCKED_BY_CLIENT | Ad blocker blocking a CF-hashed JS chunk | Disable ad blocker or add exception for Arcturus-Prime.com |
| WebSocket works from CLI but not browser | Browser cache serving old code before CORS fix | Hard refresh (Ctrl+Shift+R), wait for CF Pages deploy to complete |
| 502 from CF tunnel | Container down or tunnel misconfigured | Check steps 1-4 above |
Connection flow (browser)
1. checkTermHost() — no-cors fetch to https://workbench-term.Arcturus-Prime.com/
- Opaque response (status 0) = reachable
- Throws (AbortError/TypeError) = unreachable → show error overlay
2. connectTerminal(tab) — new WebSocket('wss://workbench-term.Arcturus-Prime.com/ws?arg=forge-shell')
- 8-second connection timeout
- On open: send resize, show terminal
- On message: ttyd binary protocol (type byte + payload)
- On close/error: show reconnect overlay, reset reachability cache