Conversation
Services (Go): - stdio JSON-RPC read loops (broker + 12 modules): recoverable DecodeError frames now skip-and-continue; EOF stays clean; terminal scanner/transport errors stop instead of spinning (errors.As(nil) is false, so the success path must continue explicitly) - broker: bounded 4-goroutine dispatch pool so one slow worker relay cannot head-of-line block the control plane; producer goroutine classifies terminal reads (errTerminalRead) - relay: per-subscriber delivery pump goroutines (coalescing kick channel); Deliver is non-blocking so a stalled worker cannot stall the scanner read pump or other subscribers - cluster-manager: PBKDF2-HMAC-SHA256 PIN stretching (50k iterations, per-invite salt inside the EAP-MAC-covered ServerInfo); online Completion attempts capped at 5 with invite teardown; terminal pairing signals (cancel/decline/fail/expire) authenticated with an HMAC over the session's ephemeral Key Exchange secret - errors ingest: envelope nodeId must match the mTLS-authenticated caller UUID; 1 MiB body cap on the ingest endpoint - proxies: deny-by-default origin allowlist via NVPAIR_PROXY_ALLOWED_ORIGINS, 32 MiB inference body cap (413), CORS grants only for allowlisted origins, no arbitrary header echo - engine-manager: graceful stop then SIGKILL/pgid escalation after the manifest stop grace; unpinned manifest downloads fail closed unless NVPAIR_ALLOW_UNPINNED_DOWNLOADS=1 - broker: NVPAIR_SERVICE_*_PORT env overrides threaded as --port to every supervised worker so tests never skip on fixed-port collisions - workloadstore: Checkpoint re-marks dirty when the snapshot write fails - Makefile: test-services runs with -race - versions.json: bump all 13 changed components Desktop: - ipc/safe-handle: empty ELECTRON_RENDERER_URL no longer authorizes arbitrary origins - dead-code-omissions: drop stale @electron-toolkit/preload entry Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
- httpserver.go: remove explicit inviteMu/sess.mu unlocks in the over-completion-attempts branch; the deferred unlocks at the top of handlePairingCompletion unlock them again at return (double unlock of an unlocked mutex). The teardown helpers take only memMu/sessMu, so holding both locks through teardown is safe. - cancel.go: unlock sess.mu on the no-signal-key path. The fix that derived the signal key under the already-held lock left the early return without releasing it, deadlocking any goroutine that had fetched the session pointer (joiner Completion POST, respond). The terminal write and session delete now stay under sess.mu like the success path, preserving the serialization against Completion. - manifests: pin sha256 for every ollama.json and lmstudio.json fetch. The fail-closed default in install.go made every default engine install fail because no shipped manifest carried a digest. Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
3f600b2 to
820bcc6
Compare
Production fixes: - safe-handle.ts: require the dev URL to match exactly or with a trailing path separator so only the configured development origin and its paths pass the IPC sender check - ui-broker readLoop: skip recoverable per-frame decode errors instead of tearing the connection down; extract recoverableDecode so the producer and consumer predicates cannot drift - relay Directory.pump: prioritize done over a pending kick so a Deliver racing Unsubscribe cannot Send against a consumer that's gone Tests: - desktop: safeHandle sender authorization (dev URL exact/slash, file://, unset/empty env, lookalike host, userinfo, missing window) - ui-broker: readLoop terminal/EOF/decode-error contract, relay pump coalescing and post-unsubscribe silence, resolveServicePorts env overrides and invalid-value fallback - proxies (ollama + lmstudio): loopback cross-origin CORS gate and request-body limit / 413 path - cluster-manager: 401 signal gate (wrong phase/invite tags MACed with the live session key) and 429 completion rate limit - eap-noob: EphemeralKey lifecycle (pre-exchange errors, key agreement, copy semantics) Bump nvpair-ui-broker to 0.40.4. Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
CONTRIBUTING requires documentation updated in the same PR for behavior and networking changes. Three sections described the previous behavior: - ollama-proxy README (referenced by lmstudio-proxy's CORS section): documented the permissive wildcard CORS grant; the policy is now deny-by-default via NVPAIR_PROXY_ALLOWED_ORIGINS with a static preflight grant and the 403 origin-not-allowed gate - engine-manager README: install was 'verify-if-pinned with a loud warning' — now fail closed unless NVPAIR_ALLOW_UNPINNED_DOWNLOADS=1; stop was 'no timeout, no SIGKILL escalation' — now grace-then-forced-kill - cluster-manager README note: records the PIN stretching, authenticated terminal signals, and completion-attempt cap Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
Please note the above from the README. In addition, smaller PRs are easier to evaluate than large ones. |
e16bf21 to
2b0b375
Compare
|
Heads up — This PR edits a number of files across both areas, so it needs more than a Worth agreeing the new approach before reworking it. Happy to talk through where |
|
Please do not file security fixes like this (in the public). Follow the steps on https://github.com/NVIDIA/Personal-AI-Router/blob/develop/SECURITY.md I did review the changes here and none seem actionable given our threat model and the changes that have recently landed on the |
Description
PAIR routes independent inference requests across local nodes. This PR resolves
14 issues found in a whole-repo review plus later adversarial and regression
rounds across the service control plane, and adds coverage at each boundary the
fixes touch.
Rebased onto
developafter the "One engine proxy binary" change (#87 era):the two-proxy fixes are ported onto the unified
nvpair-proxy, and theper-engine CORS intersection is kept alongside the new request-entry allowlist
gate.
The 14 issues, by commit
Deliver.NVPAIR_PROXY_ALLOWED_ORIGINS) and no longer echo arbitrary headers. Ported onto the unified proxy as a request-entry gate layered over the per-engine CORS intersection.nvpair-proxy.NVPAIR_ALLOW_UNPINNED_DOWNLOADS=1.safeHandleno longer authorizes arbitrary renderer origins whenELECTRON_RENDERER_URLis empty.sess.muon the over-completion-attempts branch and no longer leaves it locked on the cancel path, so unauthenticated POSTs can no longer panic or deadlock the manager.Follow-ups close the remaining gaps the rounds surfaced: lookalike-host/userinfo
bypass of the dev-URL sender check, the same decode-error confusion in the
broker's consumer side, and the relay
Deliver-vs-Unsubscriberace.Intentionally in scope
Everything above, plus the regression tests listed under Validation. Version
bumps for every component whose compiled output changes are declared in the
release-intent block below (not hand-edited into
services/versions.json).Intentionally out of scope: the two-proxy port-override test harness
(
NVPAIR_SERVICE_*_PORT) that upstream's unified proxy superseded. It wasremoved rather than carried forward, so no compatibility shim remains.
Validation environment
go build ./...in all 15 services modules — passgo test -race -count=1 ./...in every services module — passservices/tests(go test -count=1 ./...) — pass, 0 skipsnpm run typecheck,npm run lint,npm run test:unit(238 tests),npm run dead-code:check,npm run service-contracts:check— all passnode scripts/spdx-headers.mjs— 1021 checked, 0 missingNew regression coverage at each fixed boundary:
desktop/tests/modular/safe-handle-sender.test.ts— sender authorization: dev-URL exact and path-suffix match,file://, unset and empty env, lookalike host, userinfo spoofing, missing window (red/green against the fix)services/nvpair-cluster-manager/pairing_signal_gate_test.go— 401 gate for cancel/decline/expire with wrong-phase and wrong-invite tags MACed under the live session key, state survival; 429 rate limit including failed/incorrect-pin attempts and teardown; cancel without a signal keyservices/nvpair-ui-broker/terminal_read_test.go— read-loop contract: transport error terminal, EOF clean, decode error recoverableservices/nvpair-ui-broker/relay/relay_test.go— trigger coalescing delivers latest state once; post-unsubscribeDeliveris silentservices/nvpair-proxy/ingress_test.go— request-entry allowlist gate: unlisted origin 403origin-not-allowed, allowlisted origin passes to routing, Origin-less caller unaffectedservices/nvpair-proxy/body_limit_test.go— body cap (over/at limit), model parse, end-to-end 413services/shared/cors/allowlist_test.go— exact-origin matching, empty-allowlist denial,403shapeservices/eap-noob/ephemeral_key_test.go— ephemeral key lifecycle: pre-exchange errors, server/peer agreement, copy semanticsCompatibility notes
safeHandledev-URL change only affects development-mode renderer IPC (ELECTRON_RENDERER_URL); packaged builds authorize viafile://and are unchanged.nvpair-proxykeeps the per-engine CORS intersection and adds a request-entry allowlist gate ahead of it: browser requests carrying anOriginare admitted only fromNVPAIR_PROXY_ALLOWED_ORIGINS; non-browser callers are unaffected.Documentation changes
Service READMEs updated to match the new behavior:
services/nvpair-proxy/README.md: records the request-entryNVPAIR_PROXY_ALLOWED_ORIGINSallowlist gate over the per-engine intersection policy, and the 32 MiB request-body cap with the413pathservices/nvpair-engine-manager/README.md: install "verify-if-pinned with a loud warning" → fail closed unlessNVPAIR_ALLOW_UNPINNED_DOWNLOADS=1; stop "no timeout, no SIGKILL escalation" → manifest grace then forced killservices/nvpair-cluster-manager/README.mdnote: records the PIN stretching, authenticated terminal signals, and completion-attempt cap it describesRelated issue
None — found and fixed through whole-repo review.
Release intent
Changelog title
Harden the service control plane: authenticated pairing, bounded queues, deny-by-default proxy origins
Changelog body
Bumps