Skip to content

refactor: ttyd reaper, argv daemon launch, url dedup, keep-alive, retries (#101 P2) - #104

Merged
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-24/pr-c-p2-refactor
Jul 9, 2026
Merged

refactor: ttyd reaper, argv daemon launch, url dedup, keep-alive, retries (#101 P2)#104
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-24/pr-c-p2-refactor

Conversation

@axisrow

@axisrow axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Six P2 refactoring-debt fixes from the multi-agent code review in #101, plus NOTE comments for the deferred TOCTOU finding (#8).

  • Fix CSP blocking eval in TTYD and add debug logging #7 — dead ttyd processes not reaped without traffic (manager.py, app.py). Added a background daemon reaper thread; zombies were only reaped lazily inside list_terminals()/get_terminal(), so with no list/get/health traffic a defunct process pinned its PID slot forever. Start/stop wired into main() + the signal handler.
  • CSRF token: investigate suspected additional bug #9run_as_hapi interpolated args into sh -c (entrypoint.sh). Routed droid/ao/hapi startup through the argv-safe run_as_hapi_argv (a latent root-context command-injection surface). | tee → redirect (argv can't express a pipe, matching the tunnel helpers), inline env via env NAME=VAL; the unused string run_as_hapi is removed.
  • Document design decisions from PR review cycles #12 — relay-URL built by a drifted bash reimplementation (entrypoint.sh). Delegated the legacy /home/hapi/url writer to the same ttydproxy.views.build_hapi_url_from_runtime the dashboard uses (bash took the FIRST relay URL + left the token un-encoded; views takes the LAST + quote()s both).
  • Sync ~/.claude config and sessions to container #13 — per-request allocation in the HTTP proxy (proxy.py). HOP_BY_HOP_HEADERS is now a frozenset, the HTML skip-set is precomputed, and resp.getheaders() is read once (was twice per response). A keep-alive connection pool was intentionally not added — a shared HTTPConnection across ThreadingHTTPServer workers would introduce a concurrency bug.
  • Sync GitHub CLI auth (~/.config/gh) to container #14 — duplicated JS helpers across assets (app/assets/*.html). Exported window.__containsFiles / window.__sendToTTYD from the injected tab_fix_script and delegated to them from the parent page + virtual keyboard, removing byte-identical containsFiles copies and three hard-coded socket-discovery + '0' ttyd-prefix sites.
  • Sync SSH keys to container #15 — network fetches with no retry (Dockerfile). Wrapped the Node/ttyd/cloudflared/chisel curl downloads and the ao git-fetch + Hermes git-clone/pip steps in the CLAUDE.md retry loop, previously applied only to npm.

#101 findings closed

Deferred: #8 (port-reuse TOCTOU) — narrow window (usually ECONNREFUSED→502), not a security boundary. NOTE comments added in app.py + manager.py pointing at a per-terminal lease/refcount as the proper fix, tracked separately.

New/changed env vars

None.

Ports / volumes

No changes.

Tests

New: reaper tests in test_ttyd_manager.py, TestResponseHeaderEfficiency in test_proxy_http.py, TestDockerNetworkRetries + a droid-daemon injection test + relay-URL delegation tests in test_shell_scripts.py, __sendToTTYD delegation in the vkbd JS tests. Full pytest tests/ and npm test green; bash -n entrypoint.sh clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn

@axisrow

axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1)

Reviewed locally (Claude subagent + Codex companion), no bots pinged. Both reviewers ran; findings triaged against the real code + git show main:entrypoint.sh. bash -n entrypoint.sh clean; 176 Python + 4 JS tests pass.

Verdict Reviewer Finding Location
IRRELEVANT codex Root opens hapi-controlled log path (>>) before privilege drop entrypoint.sh:497
SKIP claude | tee>> changes truncate to append (stale fallback URL + log growth) entrypoint.sh:493-497

Both findings stem from the \| tee "$LOG">>"$LOG" 2>&1 change in the argv refactor; neither is a required FIX:

  • C1 (IRRELEVANT — pre-existing, no incremental exposure): True that cmd >>"$F" & opens $F in the root outer shell before runuser drops privileges. But every daemon that gained a root >> redirect already has a root touch "$LOG" + chown on the exact same path one line above — and those lines are unchanged from main (hapi server 493-494, droid 298-299, ao 337-338). Root already creates/opens and re-owns the target through any hapi-planted symlink there; the >> follows the identical symlink root already dereferenced. The root-context run_as_hapi_argv … >>"$LOG" pattern itself pre-exists in main (the SSH-tunnel launch). No new attacker-reachable target introduced by this PR. (The absence of a symlink guard on these log paths is a real but pre-existing gap, out of scope here.)

  • C2 (SKIP — real mechanism, negligible severity + optional hardening): tee (no -a) truncated server.log at startup; >> appends, and nothing else truncates it, so on the persistent volume old relay URLs persist. cleanup_runner_state removes settings.json (token stays fresh) but not server.log. However the dashboard link is built live on every request via views.build_hapi_url_from_runtime (last-wins over the trailing 8 KiB), which self-heals to the new URL+token within seconds of hapi appending; only the static /home/hapi/url fallback file can go briefly stale, and only in a degraded state (live files missing). Unbounded server.log growth is minor (reads are tail-bounded). Optional one-char hardening: replace touch "${HAPI_SERVER_LOG}" with : > "${HAPI_SERVER_LOG}" to restore the old truncate-at-startup semantics and cap growth — I'll apply this in the cleanup pass.

Verified correct (both reviewers): reaper thread (no double-clean race — removal happens under lock; daemon thread catches exceptions; start_reaper idempotent; stop_reaper joins; correctly wired in main/signal_handler), argv env-passing (nested env AO_PORT=…/HAPI_RELAY_FORCE_TCP=… reaches the child; stdbuf/&/$! preserved), python relay delegation (PYTHONPATH=/app matches COPY app/ /app/; -f-gated; None-safe; captured + chown'd), proxy.py frozenset + single getheaders() (byte-equivalent), JS __containsFiles/__sendToTTYD dedup (identical behavior + pre-load fallback), and the 7 Dockerfile retry loops (break-on-success, hermes clone rm -rf before retry, syntactically valid in case arms).

Applying the optional C2 truncate hardening in the cleanup pass; merge is left to the maintainer.

axisrow added a commit that referenced this pull request Jul 8, 2026
Non-blocking finding from the local review of #104: the argv refactor changed
the daemon launches from `| tee "$LOG"` (which truncates the log on open) to
`>>"$LOG"` (append). Without a matching truncate, server.log / droid /ao logs
on the persistent /home/hapi volume would keep stale content across restarts —
for server.log that means a brief window where the /home/hapi/url fallback pairs
a fresh token with an old relay URL (the live dashboard builder self-heals, but
the fallback file can go stale) — and grow unbounded.

Replace `touch "$LOG"` with `: > "$LOG"` (truncate-or-create) for the three
daemon logs, restoring the old tee truncate-at-startup semantics. Update the
server-log pre-create test accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn
@axisrow

axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Applied the C2 cleanup in 1ad695c: the three daemon logs (server/droid/ao) are now : > "$LOG" (truncate-or-create) instead of touch, restoring the old | tee truncate-at-startup semantics now that the launches append via >>. This closes the narrow stale-fallback-URL window on server.log and caps unbounded log growth on the persistent volume. Full pytest tests/ green. Local review cycle for #104 is complete.

axisrow and others added 2 commits July 8, 2026 23:13
…ries (P2)

Six P2 refactoring-debt fixes from the #101 code review, plus NOTE comments for
the deferred TOCTOU finding (#8).

processes were only reaped lazily inside list_terminals()/get_terminal(), so
with no list/get/health traffic a defunct zombie pinned its PID slot forever.
The reaper polls periodically (start/stop wired into main + signal handler).

of the string-interpolating run_as_hapi (`sh -c "... ${command}"`) — a latent
root-context command-injection surface. `| tee` is replaced with a redirect
(argv can't express a pipe, matching the tunnel helpers) and inline env vars go
through `env NAME=VAL`. The unused string run_as_hapi is removed.

ttydproxy.views.build_hapi_url_from_runtime the dashboard uses, replacing a
drifted bash reimplementation (bash took the FIRST relay URL + left the token
un-encoded; views takes the LAST + quote()s both).

skip-set, and read resp.getheaders() once (was called twice per response). A
keep-alive connection pool was intentionally NOT added — a shared HTTPConnection
across ThreadingHTTPServer worker threads would introduce a concurrency bug.

tab_fix_script and delegate to them from the parent page + virtual keyboard,
removing byte-identical containsFiles copies and three hard-coded socket-
discovery + '0' ttyd-prefix sites (protocol change now lives in one place).

ao git-fetch + Hermes git-clone/pip network steps in the CLAUDE.md retry loop
(`for i in 1 2 3 4 5; ... && break || sleep 10`), previously applied only to npm.

(narrow window, not a security boundary) and point at a per-terminal lease as the
proper fix, tracked separately.

No new env vars. No port or volume changes.

Closes #101 findings #7, #9, #12, #13, #14, #15 (documents deferred #8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn
Non-blocking finding from the local review of #104: the argv refactor changed
the daemon launches from `| tee "$LOG"` (which truncates the log on open) to
`>>"$LOG"` (append). Without a matching truncate, server.log / droid /ao logs
on the persistent /home/hapi volume would keep stale content across restarts —
for server.log that means a brief window where the /home/hapi/url fallback pairs
a fresh token with an old relay URL (the live dashboard builder self-heals, but
the fallback file can go stale) — and grow unbounded.

Replace `touch "$LOG"` with `: > "$LOG"` (truncate-or-create) for the three
daemon logs, restoring the old tee truncate-at-startup semantics. Update the
server-log pre-create test accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn
@axisrow
axisrow force-pushed the ao/clihost_af28bd122d-24/pr-c-p2-refactor branch from 1ad695c to 7c97c65 Compare July 8, 2026 15:20
@axisrow

axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto merged main (3552a8b, with #102 P0 + #103 P1 squash-merged). Now MERGEABLE.

Conflicts (2, both resolved by union — kept the P1 change from main AND layered the P2 change on top, nothing reverted):

Auto-merged cleanly (verified, not silently mismerged):

Verification (all green): pytest tests/ → 441 passed; npm test → 15 passed (assets touched); bash -n entrypoint.sh + bash -n bin/clihost-sync.sh clean.

Not merging — leaving that to the maintainer once CI shows green.

@axisrow
axisrow merged commit 2c86459 into main Jul 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant