Repository navigation
Description
Problem
Upstream's linked environments (Labs beta, in v0.1.7) let agents on one machine use T3 tools on another: thread tools, queues, projects, scheduled tasks, pull-request tools and delegate_task, 38 tools plus delegate_task. A link is a synchronous call to the other machine's /mcp, so it only works towards a machine that can be reached.
In the corporate setup (an always-on work VM, and a laptop behind a firewall that refuses every inbound connection, reaching the VM over Zscaler Private Access), the laptop can link to the VM but the VM can never link to the laptop. Adopting upstream's link features as shipped would leave every one of them one-way there. That's a gap that depends on how the two devices connect, which is a downgrade from J5's peering today, where poll mode makes everything J5 offers work both ways. Hiding the features instead would withhold upstream's work.
Alternatives considered and set aside:
- Holding upstream's tools on the polling rail. Every held tool's result would need a "pending" variant on result types upstream owns. Reads would need snapshots, and control calls (interrupt, steer, answering approvals) would need to be tied to their target and expire.
- T3 Connect. Upstream's hosted Connect can't be used: one account per server, and J5 points at none of upstream's infrastructure. Running J5's own (Should J5 run its own T3 Connect? #725) is operations work on the scale of v3, and
cloudflaredconnects out on port 7844 rather than through the corporate network, which security teams commonly flag. - Polling as a give-back. Upstream declined a poll mode for the first version of links (pingdotgg/t3code#16655 reply).
Proposal: a reverse route
The laptop opens a connection to the VM over ordinary HTTPS, through ZPA, and keeps it open. The VM exposes that connection on its own loopback address, such as 127.0.0.1:<port>, so anything on the VM that calls that address is talking to the laptop. Upstream's link from the VM to the laptop uses that address. Upstream accepts plain http on loopback (peerOrigin in apps/server/src/peer/PeerLinks.ts) and checks that the answer reports the laptop's environment id.
Upstream's link tools then work both ways and synchronously, with no edit to upstream's code. J5's direct mode can use the same route. It's what Connect does, with your own reachable server in Cloudflare's place.
Evidence that it will hold: the J5 web app on the laptop already keeps a WebSocket to the VM's J5 open through ZPA all day, and Zscaler's client signs in silently through Jamf SSO, so redialling never waits on the person.
Design questions
- The line says which machine it carries. The VM needs to know which laptop is on which loopback address. The laptop stating its environment id when it opens the line is enough. Forgery and impersonation aren't design considerations on J5's closed, trusted network (Jackson, 2026-11-07), so the line needs no sign-in of its own beyond what reaching the VM already requires.
- Only the VM uses the route. The address stays on the VM's own loopback. Reaching the laptop from a phone through the VM is out of scope.
- Poll mode's storage stays. A sleeping laptop has no line, and J5 messages still need holding until it wakes.
- A distinct failure code when the machine can't be reached, and a notice when the line comes back. Upstream turns an unreachable machine into a generic
orchestration_error(PeerForwarding.ts). Keep a distinct code (such aspeer_unreachable), record which thread hit it with which tools, and send each such thread one platform notice when the line is back. The agent decides whether to retry; no automatic retry, and no instruction to end its turn. This needs a small hook in upstream's forwarding, which would be a FORK.md entry. J5's cross-machinej5_spawn_agentfails with the same code when the line is down.- A delegated task already running when the line drops. Upstream's follower (
RemoteDelegation.ts) treats every failure as retryable exceptcapability_denied,thread_not_found,run_not_foundand aninvalid_requestfor a forgotten link, and backs off from 2 seconds up to 5 minutes. A new code is outside that list, so the follower keeps retrying; the new code must not reuse one of those four. - Where the hook sits. On
PeerForwarding.call, which carries agents' tool calls and the launch step ofdelegate_task, not onreadThreadandwaitForThread, which the follower uses. A launch that fails gets the notice; a follower's retries don't produce one. - No notice for a delegate's parent. The parent isn't waiting on a call; the task's completion wakes it. The follower's own retry is enough, at the cost of up to 5 minutes after the line returns before it notices.
- A delegated task already running when the line drops. Upstream's follower (
- No nginx changes. The route is a WebSocket on a path the VM's J5 already serves, so the existing nginx block forwards it. Keepalives every few seconds, like the web app's (it treats 15 s of silence as unresponsive,
PING_TIMEOUT), keep it inside nginx's and ZPA's idle timeouts; poll mode's 25-second held requests already pass the same block. nginx's 1 MB body limit covers request bodies, not WebSocket messages. The VM-side hop to127.0.0.1:<port>never touches nginx. - Docs. The October poll-mode session listed "relay, tunnel and client forwarding" as out of scope. The peering definition's rule that "a relay or tunnel is never a required part of it" still holds, because the route is optional.
Line test
Skipped (Jackson, 2026-11-10). With keepalives on the web app's rhythm, ZPA's idle timeout no longer matters: the web app's WebSocket already survives all day through ZPA and nginx. The two things the test would still have shown are measured on the real route during the live test instead:
- a 5 MB message from the laptop to the VM over the WebSocket, through ZPA;
- how fast the line comes back after sleep and network changes.
The test script stays in https://github.com/Jacksondr5/j5-line-test (private) if a question comes up later.
Related
#725 (J5's own Connect), #772 (v0.1.7 advance: linked environments), #399 (poll mode).
Drafted with Claude Opus 5.5.
Activity
Jacksondr5 commentedon Nov 7, 2026
Sequencing (Jackson, 2026-11-07): cross-machine work starts after the upstream v0.1.7 merge. This issue comes first, then #956 (
j5_spawn_agenton another J5 machine, which needs this for the VM-to-laptop direction) and the remote persona delegate, then #953 (Crews across machines). The line test can run any time; its results tune this design and don't gate it.