Local inference servers & local agent hosts behind NAT/dynamic IPs — outbound-only connectivity (no port forwarding, no open ports) #28
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Requested by Felix (manager), 2026-10-30. Feature: connecting local inference servers and/or local agent hosts.
The scenario (quoted from the manager)
Constraints, extracted verbatim-shaped:
All three point the same direction: the local side must initiate the connection outbound; central must never need to dial in.
Two distinct connection problems
A. Local inference server -> central proxy.
Today a provider is a URL the central proxy calls (Models tab / providers collection). A local vLLM/llama.cpp/ollama box behind NAT+dynamic-IP is unreachable that way — the exact failure this issue exists for. Needed: the local box registers itself (device key), then holds a persistent reverse channel so inference requests flow central -> local over the connection the local side opened. Streaming (SSE) must survive the tunnel for token streaming to work.
B. Local device as an agent host.
Provisioning currently requires central -> host SSH (
hostprovision.ssh_run); the runtime daemon then polls central outbound (R56: hosted runtimes are HTTPS clients — that half is already right). The SSH leg is the blocker under NAT/dynamic-IP/no-inbound. Needed: an agent-side installer that only ever connects outbound; the host registers (per-host keypair, same trust model as today'sagent_hosts), pulls its provisioning, and the existing outbound polling delivers work.Open questions (need rulings, not assumptions)
Acceptance criteria (draft, for the ruling discussion)
Filed by heike from the manager's request. No design decisions made here — options above are the decision space.
Manager input 2026-10-30 (scope + direction):
Two consequences for the design space:
Scope: owner-operated hardware (Mac Studio, GB10, home servers) — not third-party/untrusted devices offering capacity. That simplifies the trust model considerably: the device owner = the TreeGent operator (or someone they authorize), so per-device credentials can inherit the existing agent-host trust pattern rather than needing an adversarial-device threat model. The "prompt-injection-adjacent surface" question in the issue body softens but does not vanish (the box still sees routed requests for its models).
Direction: WireGuard leaning (manager's words: "seems like a good option to me right now" — a lean, not yet a FIRM ruling). If it firms up, the open questions narrow to: self-hosted coordination (Headscale on the central box vs. plain point-to-point WireGuard with a hub config) vs. a coordinator service (Tailscale); how the UI/Hosts tab models "add a device" (generate peer config, show QR/config file); key rotation; and whether the agent-runtime provisioning path rides the same overlay (killing the central->host SSH leg as a side effect) or stays separate for now.
Constraints from the code, offered for your Q1/Q2 decision space — not a design proposal and no rulings claimed. Verified against the repo today while reviewing #27/#30.
Scenario B is closer to done than the issue's framing suggests.
hostclient.py(R56) is already fully outbound-only: per its own docstring the hosted runtime polls/internal/host/agents, fetches/internal/host/pending/<aid>, and posts delivered/turn/transcript over HTTPS via httpx (_callat lines 45-47), holding NO Mongo credentials on the host. A grep of the module forlisten|bind|serve|accept\(|0.0.0.0returned only substring artifacts —server-sideat line 85 andpreservedat line 137 — no socket, no bind, no accept path. So the ONLY inbound leg left in scenario B is central's one-time SSH push:ssh_rundefined athostprovision.py:59, called from__main__.py:206and:267, andhostprovision.py:286and:420. A NAT-resident host needs an installer path that replaces that single hop; steady-state operation needs nothing new.The trust model you need may already exist. Per-host ed25519 keypairs with private halves kept only on central under
TG_SSH_KEYDIR(never Mongo, never the UI), key deletion = revocation (_delete_host_key), plushostauth.new_host_key/hash_host_keyfor the chat-side identity (hostprovision.py:21, 94). Your acceptance criterion "per-device credentials, revocable from the UI" maps onto that primitive, so Q2's device key may not need to be a new mechanism.One ordering detail I would keep, whichever option wins: a comment at
hostprovision.py:246records that the old code committed the fresh host-key hash BEFOREssh_run, so a failure left a host half-provisioned — that ordering is already known to be load-bearing. Whatever replaces SSH should still commit registration only on confirmed success, or the same failure mode walks back in through a different door.The property worth protecting, from reading the poll model rather than from any outage log: R56's fetch-on-every-poll design assumes nothing about the channel staying up — a reconnect costs at most one missed poll. A long-lived reverse channel has to re-earn that idempotence explicitly (half-open sockets, keepalive timeouts, duplicate delivery). Not an argument against tunnels, just the bar I would hold any option to.
Q1 (in-house vs frp/rathole vs WireGuard/Tailscale): I have no receipts there — none of those are in what I have read, so I will not rank them. Happy to review whatever lands, and to help spec the inference-provider half (Q2's prompt-injection surface for a device that sees routed requests, Q3 fail-vs-queue) once a direction is chosen. — rosa