RunR v0.5.7

Documentation

Browser shell

Browser terminal access to the pods of an active deployment.

Overall flow#

Browser (xterm.js, ShellTerminalModal.vue)
       │
       │  WSS /ws/shell?token={uuid}
       │  (gateway Caddy / Vite dev proxy strips /ws → Reverb route /shell)
       ▼
   Reverb (Laravel, :8080)
   ┌──────────────────────────────────────────────────────┐
   │  ShellAwareFactory → ShellController                 │
   │                                                      │
   │  1. Redis GET exec:config:{token}  →  config         │
   │  2. Redis DEL exec:config:{token}  →  one-time use   │
   │  3. Wake phase: `php artisan shell:wake` subprocess  │
   │     finds a Running pod or wakes the Knative service │
   │     status frames → WS: searching, waking_up, waiting│
   │  4. PTY phase: kubeconfig → /tmp/runr-shell-{random} │
   │     (chmod 600), ShellPtyProcess: kubectl exec -it   │
   │  5. PTY ↔ WebSocket bridge                           │
   │     WS  → PTY : {input: base64}   → stdin            │
   │     PTY → WS  : stdout            → {output: base64} │
   │     WS  → PTY : {resize:{cols,rows}} → stty          │
   │  6. Keep-alive every 20s (prevents scale-to-zero)    │
   │  7. 5 min idle timeout (checked every 15s)           │
   │  8. Cleanup: kill process + delete kubeconfig file   │
   └──────────────────────────────────────────────────────┘
       │
       │  kubectl exec -it {pod} -n {ns} [-c {container}]
       │  --kubeconfig=/tmp/runr-shell-{random} -- /bin/sh
       ▼
  Kubernetes Pod
  (the user's deployment, /bin/sh)

Token flow#

1. The user clicks the "Shell" button

2. POST /api/projects/{p}/deployments/{d}/exec   { cols, rows }
   ├── auth:sanctum — authenticated users only
   ├── Deployment status must be active or running
   ├── Region must have a kubeconfig and must not be a demo region
   ├── Redis SETEX "exec:config:{uuid}" 120s  ← config JSON
   └── Response: { session_id: uuid }

3. The browser opens the WebSocket: /ws/shell?token={uuid}

4. ShellController accepts the connection:
   ├── Redis GET "exec:config:{uuid}"  → config found
   ├── Redis DEL "exec:config:{uuid}"  → token can no longer be used
   ├── shell:wake: finds a Running pod of the deployed version, or wakes the
   │   Knative service through the activator and waits for a pod
   ├── ShellPtyProcess writes the kubeconfig and spawns kubectl exec
   └── Sends { connected: true, pod }

5. Terminal session runs (bidirectional PTY ↔ WS)

6. Close: WS close / idle timeout / process exit
   └── process killed + kubeconfig file deleted

Errors at any step are sent as { error } and the connection is closed.

Security#

Layer Mechanism
API endpoint Laravel Sanctum auth — no token without a login
Token UUID v4 (122 bits of entropy), 120 second TTL
One-time use The Redis key is deleted on the first connection
Kubeconfig Written to /tmp with mode 600 on the Reverb host, never sent into the pod, deleted on close
kubectl exec Only the target pod + namespace
Reverb port Internal (8080); reached through the gateway's /ws/* route (port 8064 in the local Docker Compose setup)
Idle timeout Session closes after 5 minutes without input, resize or output

What the user can reach from inside the pod is governed by that pod's own Kubernetes service account and network policy, not by the shell session.

Why it runs inside Reverb#

The shell is embedded in the Reverb process instead of a separate service. Reverb's ReactPHP event loop handles the PTY streams without blocking. ShellPtyProcess allocates a real PTY with proc_open(), so terminal colors, Ctrl+C and tab completion work. One process, no extra container.

Files#

File Role
app/Services/Shell/ShellAwareFactory.php Reverb factory; registers the /shell route (and the /agent/{regionId} route for runr-agent)
app/Services/Shell/ShellController.php WebSocket handler: wake phase, PTY bridge, keep-alive, idle timeout
app/Services/Shell/ShellPtyProcess.php PTY spawn, kubectl exec, I/O, resize
app/Services/Shell/SafeRouter.php Reverb router fix (WebSocket upgrade detection, 400 for non-WS requests)
app/Console/Commands/ShellWakeCommand.php shell:wake — find or wake a pod, JSON-line status output
app/Console/Commands/StartReverbCommand.php reverb:start-shell artisan command
app/Http/Controllers/Api/ExecController.php Access checks, one-time token
vue/src/components/ShellTerminalModal.vue xterm.js terminal and WebSocket client

See also: websocket-events.md, development.md.

RunR v0.5.7 · MIT License