Commit Graph
4 Commits
Author SHA1 Message Date
Palash Debnath 5a615d2c66 feat(workers): package headless GPU nodes (#1638) (#1648)
Closes #1638.\n\nPackages headless GPU workers with durable enrollment, bounded artifact handling, cross-platform lifecycle cleanup, and regression coverage. Incorporates CodeRabbit, Greptile, CodeQL, and platform-CI findings before merge.
2026-08-24 16:32:56 +05:30
Codex 80eca8cc6f fix(workers): harden inbound panel boundaries 2026-08-11 23:50:30 +00:00
velixio b718b2be46 fix(workers): accept the nested input ids that staging actually produces
Found by clicking Synthesize in the desktop UI — the one path nothing had
exercised.

task_store.stage_input mints inputs/<digest><ext>, a path rather than a bare
name. The node ran safe_filename over it, which rejects anything nested, so
every real clone input was refused, the dispatch failed, and the scheduler
retried about eighteen times a second while the 4090 sat idle and the user
watched a spinner.

The wire id is now hashed into a directory name rather than used as one. That
accepts any id the protocol allows while leaving placement entirely ours to
decide, which is the property the check was really buying. The declared
filename is still required to be a bare name, and a hostile one is still
refused outright — covered by its own test so the containment cannot be traded
away later to fix some future rejection.

Every earlier test used a flat id like 'ref-1' and so never met the shape
production emits.
2026-08-12 00:14:08 +05:30
velixio ef671de36e feat(workers): let a panel dial the GPU machine, so more than one person can use it
Remote workers connect outbound: the node dials the control plane, spends an
enrollment token, pins a certificate. That stays the default and is unchanged.

It is also structurally 1:1 — a worker process holds one endpoint, one pinned
certificate and one worker id — so a second person wanting the same GPU box has
to get shell access to it, repoint the start script at their own address and
restart, which disconnects whoever was using it. Sharing a GPU requires root on
it and evicts the incumbent, and no amount of UI work fixes that, because the
constraint is the shape of the connection.

This adds the other arrangement: the node listens, and any panel holding a key
connects to it, concurrently, with no shell access to the machine.

  * NodeService mirrors WorkerService. Transport roles invert; message roles do
    not — the node still sends WorkerMessage and the panel still sends
    ServerMessage, so every state machine on both sides is untouched. Register
    folds into the stream as the first exchange and reuses the existing
    request/response messages rather than growing parallel ones.
  * Keys are per panel, not per node. Revoking one person leaves everyone else
    connected; a shared key would be revoked by nobody and leave no record of
    who used it. Stored hashed, compared in constant time against every key so
    the reply time is not an oracle, and the plaintext exists exactly once.
  * Failed authentication is throttled per source address, so one stale
    bookmark cannot lock out a different panel.
  * A connection log records every attach, refusal and disconnect, and any
    session can be kicked. That is what replaces per-job approval, which would
    make a shared GPU unusable and train people to click yes.
  * Artifacts invert too: the panel pushes inputs before assigning, and fetches
    results after. The node stages both under one contained directory and
    trusts no id or filename off the wire.

Runs in plaintext by deliberate decision, recorded with its accepted risk in
docs/adr/inbound-node-mode.md, and scoped there to LAN and self-hosted use —
never a fleet transport, which goal_v2 B2/B5.2 still require to dial out.

Off by default, and bound to 127.0.0.1 until someone explicitly widens it.
2026-08-11 22:06:12 +05:30