# feat\(server,web,mobile\): an agent sets up a link to another environment · PR \#17129 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/17129)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: t3code/peer/agent-link

Target branch: t3code/peer/remote-delegate

Head commit: 71bc8a87e79f3a40b808fe38dfe90a60caf556fb

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/17129)

[Commits](https://git.cafe/pingdotgg/t3code/pull/17129/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/17129/diffs?format=markdown)

## Description

---

Part of cross-environment orchestration. A user can now say "delegate this to my box" and the agent sets up the link itself: it asks with a card, the user picks the machine and the access, and the agent picks up from there. The user never types an address or reads out a pairing code.

**How it works**
- **The tool.** `t3_environment_link` takes no address in the common case: the agent passes a `hint` from the user's words ("my box"), an `environmentId` when it knows the machine (from an `@machine` chip, see the next PR), or a `url` only when the user gave one. Optional `requestedAccess` suggests the access level. It needs a full-access caller and is refused for work a link started (#16695). `t3_environment_unlink` undoes a link.
- **Returns at once.** The tool records a `link_request` card in the thread and returns `pending` straight away. Codex abandons a tool call after 300 s, so an approval that waited inside the call would vanish before the user got to it. A machine already linked here returns `linked` at once, with no card. A retry with the same `clientRequestId` returns the card's current outcome and records nothing new.
- **The card.**
  - **No target:** a **Choose machine…** button opens a list of the user's other machines from the client's catalog, with the machine icon, a **Linked** badge for machines already linked here, and **Not connected** for offline ones. The hint preselects the best match. **Another address…** reveals a URL field.
  - **A named machine:** **Review link…** skips the list.
  - Either way, the access picker follows (the same `LinkAccessPicker` as Settings), defaulting to supervised or the agent's suggestion.
- **The code.** If this client is connected to that machine with a session that may create pairing links (`access:write`), it mints a one-use code there (`POST /api/auth/pairing-token`, orchestration scopes only, labelled "T3 Code link"), so linking is one click. Otherwise the dialog asks the user to paste a code from that machine's Connections.
- **Which address.** The client sends the chosen machine's id and every address it knows for it: the addresses the machine itself reports (`serverConfig.directEndpoints`), then saved routes. `PeerLinks.link` with `expectedEnvironmentId` tries them in order. Before sending the code, it checks each address's descriptor answers as that machine, and keeps only the addresses that do. The address the browser uses isn't always one the server can reach (a phone through Connect, the laptop server over Tailscale), and this handles that case. If none answers: "This environment could not reach box at …". Picking a machine that is already linked answers **Use this link** and keeps its existing access.
- **The outcome reaches the agent** as a notification message in its thread (`message.dispatch`, `queue_after_active`): linked (environment id, label, access), declined, or failed with the reason. It steers a live turn or starts one when idle, so the agent continues on its own. The pairing code is never recorded: not in events, turn items, messages or logs.
- **Lifetime.** A pending card stays answerable after the asking turn ends and across restarts. Stopping or archiving the thread closes it as cancelled.
- **Discovery.** `t3_environment_links` tells the agent that a machine the user names but which isn't listed is not linked yet, and to call `t3_environment_link` rather than look elsewhere. Without that line, the agent in the real-client test went looking for an unrelated connector and gave up.
- **T3 Connect machines.** The list (here and in **Settings → Linked environments**) also offers the machines on the user's T3 Connect account that this client hasn't saved, marked **T3 Connect**, at their Connect address. The dialog asks Connect for that list when it opens, since the app only fetches it from Connections otherwise. A user who can't mint a code there still picks the machine and pastes a code, but never looks up the Connect URL.
- **Settings → Linked environments → Link** now opens on the same machine dropdown instead of an empty address field, and mints the code the same way when it can. **Another address…** keeps the old form.
- **Mobile** has the same flow in a native sheet.
- **Docs:** `docs/user/remote-access.md` adds a short paragraph.

**Verification**
- `peer/PeerLinkRequests.test.ts` (12 tests) runs the laptop's real orchestrator and toolkit against a served box behind real OAuth:
  - the agent asks without waiting, and the user links after the turn ended. Exactly one notification arrives, carrying the id and access, and the code appears nowhere in the database;
  - a retry while pending shows no second card;
  - decline links nothing and tells the agent;
  - Stop and archive close the card, and a late answer is refused;
  - a code the box refuses fails with its reason;
  - a non-full-access agent, and linked work, may not ask;
  - a target-less card is answered with a machine's addresses and links;
  - naming an already-linked machine returns linked with no card;
  - a card for a named machine can keep the existing link.
- `peer/PeerLinks.test.ts`: when the first address answers as a different environment and the second as the expected one, the link goes through the second, never signs in at the first, and stores only the second. When no address answers, the error names the machine and nothing is linked.
- Client-runtime `linkRequest.test.ts`: the machine list (reported addresses before saved ones, once each; Connect-only machines after the saved ones at their Connect address, never this environment or a saved machine twice); hint ranking, including the user's own words ("my box", "the hetzner vps box"); the answer payload; refusing a blank code, a blank address, or a machine with no address; the outcome text.
- Mutation-checked across the build rounds: the user's access choice, the code leak, answered-once, Stop and archive closing cards, no wake and a wake for an already-closed card, a retry recording a new card, recovery closing link cards, the full-access gate, the expected-environment check, and both hint rules. Each fails a test when removed.
- Per-layer check of the whole stack: this layer typechecks clean, and 380 server tests pass. Contracts, shared, client-runtime, web and mobile typecheck clean.

**Real client** (two dev servers, "laptop" and "box", Codex GPT 6.0 Astra on the laptop):

"Delegate some work to my box: add a TAX_RATE = 0.08 export to pricing.ts." The agent calls `t3_environment_link` with `hint: "box"` and ends its turn:

![The card in the thread: "Link a machine to this environment" with the agent's reason, Decline and Choose machine…](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/49dcf49621083f33/card.png)

**Choose machine…** opens with box preselected from the hint:

![The dialog lists box (preselected) and Another address…, then the access levels with Auto-accept edits suggested by the agent](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5823eddeb0e90564/10-hint-preselected.png)

One click on **Link** (this browser has an admin session on the box, so the code is minted there). The agent wakes on the notification, delegates, and reports back. `pricing.ts` on the box has the export:

![The card reads Linked box · Auto-accept edits; a Linked box notification starts a new turn, which delegates to the box and reports the task finished](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/611ef79234cb659f/6-delegated.png)

Without a session that may create links there, the dialog asks for a code instead:

![The dialog with a Pairing code field: "Create a link on box: Settings → Connections → Create link, then paste its code here"](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2b971dc900dbc8eb/modal-paste.png)

Opus 5.5 via Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/pingdotgg/t3code/pull/17129" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4" alt="Devin Review"></picture></a>
<!-- devin-review-badge-end -->

