Conversation
Accepting a pairing invitation on a headless GPU box meant pressing keys in the Cluster tab. This adds the scripted path: a running nvpair-tui serves a per-user JSON-RPC control socket, and `nvpair-tui invite`, `pending`, `accept`, `decline` and `members` drive that instance from a script or a single SSH command. The pairing itself moves into nvpair-tui/pairing, one Service that both drivers call, so the Cluster tab and the socket cannot hold different ideas of which invite is pending: an invite created from a script shows its PIN on the tab's status line exactly like `i`, and an accept made there clears a PIN prompt the tab left open. Broker pushes are fanned out in main so the service sees an invite whether or not the UI is keeping up. The endpoint is $XDG_RUNTIME_DIR/nvpair/tui.sock when the OS offers a runtime directory and run/tui.sock under the shared per-user data directory otherwise, its directory 0700 and the socket 0600; a socket left by an instance that did not exit cleanly is reclaimed, but only after confirming nothing answers on it. --control-socket overrides the path and --no-control-socket disables it. Failing to open it warns and leaves the UI running, because the UI is the primary job. The PIN reaches the terminal the operator asked for and nowhere else: pair:pending strips it, and NVPAIR_PIN keeps it out of shell history and ps for scripts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
The cross-process gate pairs two real nvpair-tui processes with no keyboard: `invite` on one prints a PIN, `accept` on the other returns paired, and `members` on both lists the pair. The pairing is the real PIN-authenticated EAP-NOOB exchange between two real cluster managers. Two substitutions, both because every broker-owned port is a compiled-in constant and one machine can therefore hold only one broker, while this needs two nodes: each TUI's broker is a stubbroker fixture that spawns a real cluster manager on an ephemeral port and relays cluster:* / nodes:* verbatim, and each TUI runs under script(1), because a Bubble Tea program will not start without a terminal. Unix only, for the same reason its process-group cleanup is. Also fixed along the way: nvpair-tui's first import of nvpair-shared/ipc needs go-winio in its own go.mod, or the Windows build cannot resolve it; and `invite --wait --json` printed prose among the JSON. It now prints two documents, one per line, the created invite carrying the PIN and then its final state, because a terminal invite carries no PIN and one document cannot serve both. nvpair-tui 0.7.3 -> 0.8.0 (new IPC surface, additive), product and installer 0.92.0 -> 0.93.0. No other binary's compiled output changed: nothing under shared/ was touched, only imported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
`npm run service-contracts:check` reads the `case "pair:…"` arms in nvpair-tui's control server as methods that binary now handles, so desktop/docs/services-api.md was stale. Regenerated with service-contracts:write — the only desktop file this change touches, and one that is generated, never hand-edited. The five methods list as "not called by the bridge", which is correct: they are served on nvpair-tui's own socket for its own subcommands, and the desktop application has no business calling them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
The two-headless-boxes example interpolated the PIN into the remote command line, which is exactly what the paragraph above it says not to do: it lands in the argument list of the local ssh and of the remote shell, readable by anyone with an account on either machine for as long as the pairing takes. It now travels over the connection's standard input instead, and the difference is spelled out. The one-off `ssh gpu-box 'NVPAIR_PIN=... nvpair-tui accept'` stays as it is, and now says it is a one-off: someone has just read the digits off another machine's screen, and the pairing is over in seconds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
The README said only a default path is length-checked. Listen checks every path, including one given with --control-socket, which is the case that actually bites: a long --control-socket path binds to a truncated name nobody can dial. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
`ping` reported brokerReady:true forever once app:ready had arrived, so a script asking a TUI whose broker had died was told to carry on, and its next invite could only time out. The notification fan-out now clears the flag as it exits, which is the moment the broker's stream closes and this process learns the broker is gone. Two tests cover the fan-out's contract: that the pairing service sees an invitation even when nothing is reading the UI's channel, and that readiness is withdrawn on disconnect. The stubbroker fixture gains a note that it deliberately does not synthesize a reply for a relayed request a dead cluster manager never answers — so nobody debugs a 35 s subcommand in that test twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
|
Validation from a Kubernetes pod, against a 0.1.1 inviter. Everything behaved as the description says; one exit-code observation at the end. Environment. Socket. Created at Accept (the headline case). The 0.1.1 node invited the pod by address from its Cluster tab; on the pod, Wrong PIN. Decline. Fresh invite, Leave. Invite from the pod to a node already in a cluster. One thing noticed on the 0.1.1 side, not this PR: the inviter's status line stays at For #31 this replaces the |
feat/vllm-tailscale is the fork's combined branch: vLLM (PR NVIDIA#9), nodes across overlay networks such as Tailscale (PR NVIDIA#10), scripted headless pairing (PR NVIDIA#11), the fork's installer and release workflow, and now SGLang as a fourth engine. It is what the headless installer and the release packages build from.
|
Good idea, moved MR against |
Description
Accepting a pairing invitation on a headless machine meant pressing keys in the terminal interface's Cluster tab over SSH. This adds a scripted path.
A running
nvpair-tuinow serves a per-user JSON-RPC control socket, and the same binary invoked with a subcommand drives that instance:A desktop user can invite and the box can answer in one line,
ssh gpu-box 'NVPAIR_PIN=402199 nvpair-tui accept', and two headless boxes can pair each other from a script.--jsongives the raw result. Exit codes are a contract: 0 the pairing reached the state asked for, 1 the other side refused (wrong PIN, declined), 2 anything else.The PIN stays a secret. It goes to the terminal the operator asked for and into the
pair:inviteresult, nowhere else:pair:pendingstrips it, no error message repeats it, and nothing logs it (asss).NVPAIR_PINis the documented scripted form, becausean argument is visible inpsand lands in shell history.The interactive UI stays the source of truth. Pairing moved into one
pairing.Servicethat both the Cluster tab and the socket call, so theycannot disagree about which invitation is waiting: an invshows its PIN on the tab's status line exactly like
i,and an accept from a script clears a PIN prompt the tab left open.Scope
Included: the control socket (
$XDG_RUNTIME_DIR/nvpair/tui.sockwhere the OS offers a runtime directory, elserun/tui.sockunder the sharedper-user data directory; a named pipe on Windows; directoale socket is reclaimed only after confirming nothinganswers on it;
--control-socketoverrides,--no-control-socketdisables, and a socket that cannot be opened is a warning, not a failure),the subcommands, unit tests, a cross-process test that paprocesses through real cluster managers on ephemeral ports, and docs in
services/nvpair-tui/README.mdanddocs/terminal-interface.mdx.Excluded: reconstructing pending invites after a TUI restart (the cluster manager exposes no list-invites method, and
nodes:get-initialcarries no invite id; documented), and any change to the the regenerateddesktop/docs/services-api.md.Independent of the two sibling PRs (vLLM engine, first-clies cleanly on
main.Validation
Environment: macOS 15 arm64, Go 1.27; Linux arm64 for the
The cross-process test runs each
nvpair-tuiunderscript(1)because Bubble Tea needs a TTY, and stands in a minimal stub broker for thereal one, since one machine can hold only one broker (itst is Unix-only.
Live: two headless Linux nodes running this build; `nvpaicket on each lists the cluster they were paired intoearlier. The invite/accept path itself was exercised by the cross-process test, not yet between the live nodes.
Risk
the authority the operator at the TUI already holds. The
nvpair-tuigains a socket and subcommands.invite --wait --jsonemits twoJSON documents, one per line (the created invite with the), because a terminal invite carries no PIN.
sun_pathis capped at 104 bytes; an over-long path is rejected with the limit named rather than truncated, and--control-socketis the workaround.go-winiois addedindows named-pipe path.Versions:
nvpair-tui0.7.2→0.8.0; product 0.91.7→0.92.0Checklist
git commit -s), certifying the Developer Certificate of Origin.services/versions.json, and described user-visible changes above so they reach the release notes.Related: #9 and #10. This PR is independent of both and applies cleanly on
main.