Skip to content

feat(web): reach daemons on other machines over ssh, installing hmz there - #384

Closed
futrime wants to merge 1 commit into
mainfrom
feat/web-remote-daemons
Closed

futrime wants to merge 1 commit into
mainfrom
feat/web-remote-daemons

Conversation

@futrime

@futrime futrime commented Oct 12, 2026

Copy link
Copy Markdown
Member

Unit 4 of the multi-daemon + notifications plan (contract C5).

What

  • src/hmz/web/remote.py: Remotes (held as site.remotes) with state, reached -> (local port, key), connect, close, plus asks -> (port, headers to be let in). It reads the C1 entries straight from coganchor.settings.read()['daemons'] and builds ssh from them through SSHRuntime and transport.Road, so the same options and ControlMaster reuse apply, with BatchMode=yes added.
    • It probes hmz --version and the remote daemon's daemon.json pid in every temp dir.
    • It installs with uv tool install --force --python '>=3.12' 'hmz==<ver>', first getting uv 0.12.24 through astral's pinned installer if the remote has none. While installing, the state is installing and detail is uv's last line.
    • It never replaces an hmz while the remote daemon is running, or when that can't be determined. It uses the hmz that is there and says why in detail. A 0.0.0 build installs nothing.
    • It runs remote hmz web --no-open --attached --port 0 and forwards a local port to it with its own ssh -L (no mux). The local port is the same number as the remote one where that is free.
    • Every child holds a stdin pipe, so remote processes go even if the local hmz web is SIGKILLed.
    • It reconnects with backoff (1 to 60 s). Connection is lazy, on the first /d/<name>/ request, or on connect. close stops and reaps every child.
    • A daemon removed from settings is dropped, and its processes are stopped.
  • src/hmz/web/proxy.py: streams the request body (refusing chunked bodies) and the response, SSE included.
    • It sends hmz-<remoteport>=<key> with Origin and Sec-Fetch-Site: same-origin.
    • It never forwards local cookies, and never returns Set-Cookie.
  • site.py: dispatches /d/<name>/api/.... local maps to the local handlers. Other names are proxied only after the existing key, origin and same-page-write checks.
  • Routes GET /api/daemons/{name}/state and POST /api/daemons/{name}/connect. GET /api/daemons is left to unit 3.

Tests

  • tests/integration/doubles_ssh.py provides a fake ssh that runs the command locally under a second HOME, HUMANIZE_HOME and TMPDIR and forwards -L over TCP. It also provides a fake remote hmz with a settable version, and a fake uv.
  • tests/integration/test_web_remote.py covers: connect, proxy GET, POST and an upload, SSE passthrough, the auth refusals, the failed state, close reaping its children, install of a missing or other version, the busy remote left alone, a 0.0.0 build, and a removed entry.
  • tests/unit/web/test_remote.py covers the pure helpers.
  • pre-commit is green apart from pyright errors on macOS that were already there, in Linux-only modules. pytest tests/unit and tests/integration/test_web_*.py pass.
  • Live check with the fake ssh: /d/box/api/held, /d/box/api/runs, /api/daemons/box/state and the SSE stream all answered. The remote hmz web was gone after a SIGTERM and after a SIGKILL of the local one.
  • No local sshd, so there was no real ssh localhost run.

Specs and user docs are units 1 and 12.

🤖 Generated with Claude Code

…here

Each daemon under `daemons` in settings.yaml is reached by running
`hmz web --attached` over ssh, with a loopback port forwarded to it, and
proxied at /d/<name>/api/... (SSE and uploads streamed) with its own key;
/d/local/api/... is this machine. A missing or other hmz there is installed
at this version with uv tool install, never while its daemon may hold
running epics. Adds /api/daemons/{name}/state and /connect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 15:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/hmz/web/proxy.py
handler.send_response(answer.status, answer.reason)
for name, value in answer.getheaders():
if name.lower() not in _KEPT:
handler.send_header(name, value)
Comment thread src/hmz/web/proxy.py
handler.send_response(answer.status, answer.reason)
for name, value in answer.getheaders():
if name.lower() not in _KEPT:
handler.send_header(name, value)
@futrime

futrime commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Landed together in #385.

@futrime futrime closed this Oct 12, 2026
@futrime
futrime deleted the feat/web-remote-daemons branch October 12, 2026 23:56
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.

3 participants