AI Workbench & Forge Relay
AI coding workbench with 4 CLI tools, smart dispatcher, xterm.js terminals, Forge Relay API, and OpenClaw orchestration. Primary on Io CT 126, fallback on Titan 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.argobox.com)
▼
CF Tunnel (argobox-lite) ──── cloudflared
│
ttyd (7681) ──── forge-attach.sh ──── tmux session
│
Forge Relay v2 (7682) ──── tmux send-keys / capture-pane
│ (https://workbench-relay.argobox.com)
▼
Astro API (/api/admin/forge) ──── relayFetch()
│
OpenClaw orchestration ──── 8 Forge tools ──── agentic loop
Hosting
Primary: CT 126 workbench-io on Io (10.0.0.126)
- Alpine 3.23, 512 MB RAM, 2 cores
- On the local Jove network — always reachable from argobox-lite (CF tunnel host)
- Services: ttyd (7681), Forge Relay (7682)
- OpenRC init scripts:
workbench-ttyd,workbench-relay
Fallback: VM 160 on Titan (192.168.50.160)
- Full workbench with dispatcher, AI tool CLIs, git worktrees
- On the remote Kronos network — requires inter-site routing (can be flaky)
- To switch: update tunnel config on argobox-lite, 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 (Titan only) |
| tmux | 5 standard sessions (forge-*) + dynamic sessions (wb-*) |
Cloudflare Tunnels
| Hostname | Current Target | Purpose |
|---|---|---|
workbench-term.argobox.com |
http://10.0.0.126:7681 (Io) |
Browser WebSocket terminal |
workbench-relay.argobox.com |
http://10.0.0.126:7682 (Io) |
Forge Relay API |
Tunnel runs on argobox-lite (10.0.0.199), config: /home/argonaut/.cloudflared/config.yml
Switching Between Io and Titan
Edit /home/argonaut/.cloudflared/config.yml on argobox-lite:
# Io (primary — local network, reliable)
- hostname: workbench-term.argobox.com
service: http://10.0.0.126:7681
- hostname: workbench-relay.argobox.com
service: http://10.0.0.126:7682
# Titan (fallback — remote network, can be unreachable)
# service: http://192.168.50.160:7681
# service: http://192.168.50.160:7682
Then restart: ssh argonaut@10.0.0.199 "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.0.0.126:7681/ws?arg=forge-shell - Production:
wss://workbench-term.argobox.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)claude --agent build-goalie "instruction"(agent-routed, see below)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.
Telegram → OpenClaw → Agent Pipeline
The Forge system can be driven remotely via Telegram through OpenClaw. The full flow:
Telegram (@argobox_oc_bot)
→ OpenClaw Gateway (argobox-lite:18789)
→ LLM reads AGENTS.md (includes agent routing table)
→ LLM calls forge_dispatch(tool="claude", agent="build-goalie", instruction="...", mode="headless")
→ ArgoBox API POST /api/admin/forge (action: dispatch)
→ forge.ts: ensureSession() → buildCliCommand() → relayFetch() with retry
→ Forge Relay POST /sessions/forge-claude/command
→ tmux: claude --print --agent build-goalie "instruction"
→ Output captured via forge_capture("forge-claude")
→ Result returned to Telegram
Claude Code Agents
When dispatching to Claude Code (tool: "claude"), the agent parameter routes to specialized agents defined in .claude/agents/:
| Agent | Shorthand | Trigger Phrases | Model | Description |
|---|---|---|---|---|
build-goalie |
tcg | "check build", "build broken", "health check" | Haiku (cheap) | 3-level diagnostic: static scan → build check → runtime probe |
module-fixer |
tcf | "fix [X] module", "module broken" | Opus | Diagnose, fix, test, document one module at a time |
v2-architect |
tca | "work on v2", "port to v2" | Opus | Clean V2 rebuild |
Codex Approval Modes
When dispatching to Codex (tool: "codex"), the approval parameter controls autonomy:
| Mode | Description |
|---|---|
suggest (default) |
Suggests changes, waits for approval |
auto-edit |
Auto-applies edits, asks before running commands |
full-auto |
Fully autonomous, no approval needed |
Resilience
- Auto-session creation: If
forge-claudeorforge-codexsessions don't exist,ensureSession()creates them before dispatching - Retry with backoff: 3 attempts at 1s/2s/4s intervals for 5xx errors and network failures
- Actionable errors: Error responses include hints about which service is down
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.argobox.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) |
ArgoBox 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-io (/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/argobox-workbench/ → Gitea: ArgoBox/argobox-workbench
Troubleshooting
Terminal stuck on "Connecting to forge-shell..."
Quick checks (in order):
Is the container running?
ssh root@10.0.0.200 "pct exec 126 -- rc-service workbench-ttyd status"Are tmux sessions alive?
ssh root@10.0.0.200 "pct exec 126 -- tmux ls"If no sessions:
ssh root@10.0.0.200 "pct exec 126 -- bash /opt/workbench/init-sessions.sh"Can argobox-lite reach the container?
ssh argonaut@10.0.0.199 "curl -s -o /dev/null -w '%{http_code}\n' http://10.0.0.126:7681/"Should return
200.Is the CF tunnel routing correctly?
curl -s -o /dev/null -w '%{http_code}\n' "https://workbench-term.argobox.com/"Should return
200. If502, the tunnel can't reach the container.Does the WebSocket connect?
cd ~/Development/argobox && node -e " const ws = new (require('ws'))('wss://workbench-term.argobox.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 argobox.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 root@10.0.0.200 "pct exec 126 -- rc-service workbench-ttyd restart"
ssh root@10.0.0.200 "pct exec 126 -- rc-service workbench-relay restart"
Restart cloudflared
ssh argonaut@10.0.0.199 "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 argobox.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.argobox.com/
- Opaque response (status 0) = reachable
- Throws (AbortError/TypeError) = unreachable → show error overlay
2. connectTerminal(tab) — new WebSocket('wss://workbench-term.argobox.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