Add multi-terminal support with dynamic TTYD management - #5
Conversation
Enable creating multiple terminal instances via /ttyd1, /ttyd2, etc. endpoints. Each terminal runs its own TTYD process and tmux session, managed dynamically through a new TTYDManager class. Changes: - Add TTYDManager for spawning/killing TTYD processes with port reuse - Add REST API: GET/POST /terminals, DELETE /terminals/<id> with CSRF - Replace static Terminal button with dynamic JS-powered terminal list - Support terminal creation (opens in new tab) and deletion (× button) - Move TTYD lifecycle from entrypoint.sh to ttyd_proxy.py - Accept tmux session name as argument in tmux-wrapper.sh - Update CLAUDE.md with CI/CD, test structure, and template docs - Fix outdated test info in AGENTS.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add MAX_TERMINALS env var (default 100) with port range cap - Add CSP header to dashboard page (handle_menu) - Fix race condition: register terminal under lock before spawn - Non-blocking terminal creation (no 15s wait on HTTP request) - Reap zombie processes and clean tmux on dead terminal discovery - Add CSRF validation to POST/DELETE /terminals endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 2m 13s —— View job Code Review
Critical Issues1. Zombie process leak in
|
Triage of review commentsComment 1 — Zombie process leak in delete_terminalVerdict: Partially valid. Comment 2 — Lock held during blocking subprocess callsVerdict: FIX. Confirmed: Comment 3 — Terminal returned before TTYD is readyVerdict: Intentional design decision. Non-blocking creation was chosen deliberately to avoid blocking the single-threaded HTTP server for up to 15 seconds. TTYD typically starts in 1-2 seconds. The iframe/WebSocket will reconnect automatically. Comment 4 — No terminal ownership enforcementVerdict: By design. User explicitly specified "Общие для всех пользователей" (shared terminals for all users). This is not a bug — it's a requirement. Comment 5 — Session token port fieldVerdict: Irrelevant. Reviewer noted this is "not a new vulnerability." Already discussed in previous review cycle. Retained for backward token format compatibility. Minor: /health triggers cleanup under lockWill fix as part of Comment 2 — moving cleanup outside the lock. Minor: CSRF token expirySkip. CSRF token is re-issued on every dashboard page load. A user who leaves the page open >10min without action can simply refresh. Minor: /health breaking changeIntentional. Multi-terminal support requires updated health response format. |
- Move _cleanup_dead() calls outside self.lock in list_terminals() and get_terminal() to prevent 5s lock holds during subprocess.run() - Add process.wait() after process.kill() in delete_terminal() to properly reap zombie processes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 3m 4s —— View job Code Review
SummaryBoth critical fixes from the previous review were correctly applied. The remaining items are either acknowledged design decisions or minor issues. No new critical bugs were introduced. Previous Critical Issues — Status1. Zombie process leak ✅ Fixed —
|
- Redirect to /login on 401/403 auth errors in all fetch calls - Show error text on button when terminal create/delete fails - Handle CSRF expiry gracefully (redirect to login) - Surface API errors to user instead of silent console.log Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 3m 0s —— View job Code Review
Previous Critical Issues — Status1. Zombie process leak ✅ Fixed
2. Lock held during blocking subprocess ✅ FixedDead-terminal cleanup now runs outside the lock in both Remaining Critical Issue
|
- Return HTTP 419 for CSRF failures in terminal API (_check_csrf) - JS reloads page on 419 (re-issues CSRF token) instead of redirecting to login when session is still valid - Login form CSRF keeps 403 (handled by form, not JS) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 2m 24s —— View job Code Review
SummaryThe critical issue from cycle 3 ( Previous Critical Issues — Status1. Zombie process leak ✅ Fixed
2. Lock held during blocking subprocess ✅ FixedDead-terminal cleanup now runs outside the lock in both 3. CSRF/auth error conflation ✅ Fixed —
|
) Six P1 correctness/reliability fixes from the #101 code review. #3 (proxy.py): enable SO_KEEPALIVE on both tunnel sockets so a client that vanishes without a FIN (laptop sleep, NAT timeout) is detected by the OS and the idle tunnel thread + sockets are freed instead of leaking forever. #4 (ratelimit.py): use time.monotonic() instead of wall-clock time.time(), so a backward clock jump (NTP correction, suspend/resume, VM migration) cannot freeze the window and lock out legitimate users past 60s/300s. #5 (app.py): _check_auth now honours redirect=True in the removed-user branch — a browser navigation with a valid cookie whose account was deleted/renamed lands on /login (302) instead of a raw 403 JSON blob. #6 (proxy.py): add "cookie" to HOP_BY_HOP_HEADERS so the client's signed ttyd_session + csrf_token is not forwarded to the internal ttyd (defense depth). #10 (bin/clihost-sync.sh): the remote symlink guards were byte-identical triplicates (local pair + one copy in each of two `bash -s` heredocs), so hardening one silently bypassed the others — the class of bug caught 3× before (#88/#90/#95). Define them once in a shared emit_remote_prelude injected into both remote payloads. #11 (.env.example): document SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE (read by code + CLAUDE.md but missing from the example), plus the REQUEST_TIMEOUT slowloris knob for completeness. New/changed env vars documented in .env.example: SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE, REQUEST_TIMEOUT. No port or volume changes. Closes #101 findings #3, #4, #5, #6, #10, #11. Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
TTYDManagerclass to dynamically spawn/kill TTYD processes with automatic port reuseGET/POST /terminals,DELETE /terminals/<id>with CSRF protection/ttyd1,/ttyd2, etc.)entrypoint.shtottyd_proxy.pyCLAUDE.mdwith CI/CD, test structure, and HTML template documentationAGENTS.mdTest plan
python -m pytest tests/— all 93 tests passGET /healthreturns terminal count🤖 Generated with Claude Code