From b25c682a8fbb1e9446639cfa5b060e0832e3c685 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 01:45:38 -0700 Subject: [PATCH 1/2] fix(dev): agents get --share right on the first try Session-log audit of 66 agent runs showed three recurring traps: an inherited T3_SERVICE_LAUNCHER_CONTEXT killing the dev server after 'shared on tailnet' already printed, contradictory guidance on whether to hand over the pairing URL, and a stale multi-flag pairing-recovery incantation that predates 't3 pair'. - dev-runner strips T3_SERVICE_LAUNCHER_CONTEXT and T3_BOOT_SERVICE_UNIT from the child env; a dev server is never launcher-managed - AGENTS.md states the share path as three steps and points recovery at 'bin.ts pair' - test-t3-app skill: pairing URL is the deliverable when the user asked for a shared environment; recovery section rewritten around 'pair' with the scope caveat Co-Authored-By: Claude Fable 5 --- .agents/skills/test-t3-app/SKILL.md | 19 ++++--------------- AGENTS.md | 4 ++-- scripts/dev-runner.test.ts | 24 ++++++++++++++++++++++++ scripts/dev-runner.ts | 8 ++++++++ 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md index 45524f6fcd3..0e11b50e1c8 100644 --- a/.agents/skills/test-t3-app/SKILL.md +++ b/.agents/skills/test-t3-app/SKILL.md @@ -40,7 +40,7 @@ Treat the overall testing or implementation loop—not an assistant turn or one - Do not stop the server merely because one verification pass completed or because you are yielding a response to the user. - Before starting another environment, check whether the existing process and browser tab still serve the task. Reuse them when healthy instead of discarding useful state. - On a later turn, verify that the existing process is alive and reuse its printed ports and base directory. If it exited, restart with the same base directory; create a new pairing token only when the browser session is no longer valid. -- Tell the user when a test environment remains available, including its non-secret web URL when useful. Never include a pairing token. +- Tell the user when a test environment remains available, including its non-secret web URL when useful. Include a pairing token only when the user still needs to pair (see below). ## Authenticate the browser on the first navigation @@ -50,24 +50,13 @@ Treat the overall testing or implementation loop—not an assistant turn or one 4. Wait for the pairing exchange and redirect to finish before navigating elsewhere. 5. Continue in the same browser context so its stored bearer session remains available. -Treat pairing URLs as secrets. Do not copy them into final responses, screenshots, committed files, or durable logs. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it. +Keep pairing URLs out of screenshots, committed files, and durable logs. When the user asked for a shared environment, the deliverable IS the full pairing URL — paste it in your reply, token and all; a bare origin is useless to them. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it, so never open a URL you handed to the user. ## Recover a consumed or expired pairing token -Create another token against the same database and web URL as the running dev server: +Run `node apps/server/src/bin.ts pair` from the repository root. It discovers the running dev server (worktree `.t3` first, same precedence as the dev runner) and prints a fresh `Pair URL` against the server's current web origin, including a `--share` tailnet origin. Pass `--base-dir ` only when the server was started with `--home-dir`, using the identical path. -```bash -T3CODE_PORT= node apps/server/src/bin.ts auth pairing create \ - --base-dir \ - --dev-url \ - --base-url \ - --ttl 15m \ - --label agent-ui-test -``` - -Use the `Pair URL` from this command once. Derive `` and `` from the current dev-runner output, including any automatically selected port offset. Setting `T3CODE_PORT` keeps the administrative CLI from probing for an unrelated free port. - -Always pass `--dev-url` for a dev-runner environment so the generated pairing URL uses the current web origin. An explicit base directory stores runtime state in `/userdata`; the `/dev` fallback is only used by an implicit dev home. A worktree-local `.t3` counts as explicit, so its state lives in `/.t3/userdata`. Use `auth pairing list` to inspect active token metadata; it intentionally cannot reveal token secrets. +Tokens from `pair` carry standard client scopes. The startup pairing URL carries admin scopes; if the user needs Settings → Connections management (`access:write`), restart the server and hand over the new startup URL instead. ## Inspect or seed SQLite state diff --git a/AGENTS.md b/AGENTS.md index c3a7fe92bf4..2f7eb129cca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,8 +79,8 @@ The most common defect in this repo is a change that works on the path you teste - `vp i` installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run. - `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins. - Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift. -- `--share` publishes over the tailnet. Do not open the URL when you use this, just send it to the user with the pairing code included in url -- The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. +- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not use tsdev or raw `tailscale serve` for this, and do not open the URL yourself. +- The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. If the token got consumed, mint a fresh one with `node apps/server/src/bin.ts pair` — note it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management). - Stop what you started, by the PID you tracked. See rule 1. ## Test data diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 2ea3064c2a4..ac3c89b8575 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -227,6 +227,30 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }), ); + it.effect("strips inherited service-launcher context", () => + Effect.gen(function* () { + const env = yield* createDevRunnerEnv({ + mode: "dev", + baseEnv: { + T3_SERVICE_LAUNCHER_CONTEXT: '{"childVersion":"9.9.9"}', + T3_BOOT_SERVICE_UNIT: "t3code.service", + }, + serverOffset: 0, + webOffset: 0, + t3Home: undefined, + browser: undefined, + autoBootstrapProjectFromCwd: undefined, + logWebSocketEvents: undefined, + host: undefined, + port: undefined, + devUrl: undefined, + }); + + assert.equal(env.T3_SERVICE_LAUNCHER_CONTEXT, undefined); + assert.equal(env.T3_BOOT_SERVICE_UNIT, undefined); + }), + ); + it.effect("does not force websocket logging on in dev mode when unset", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index cb4e3f74c4f..a7d90d66da8 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -340,6 +340,14 @@ export function createDevRunnerEnv({ delete output.T3CODE_HOME; } + // A dev-runner server is never launcher-managed. When the shell that runs + // this script was itself spawned by the machine's managed t3 service (an + // agent working inside T3 Code), these leak through and the child server + // fails startup with "The service launcher started a different t3 version" + // (serviceLauncherClient.ts resolveStartup). + delete output.T3_SERVICE_LAUNCHER_CONTEXT; + delete output.T3_BOOT_SERVICE_UNIT; + if (!isDesktopMode) { output.T3CODE_PORT = String(serverPort); // HOST is Vite's own bind address, and the desktop branch below is the From 7410b3420d9e3e63085878a14e960c197b333464 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 02:24:48 -0700 Subject: [PATCH 2/2] Drop machine-specific tsdev mention from the share guidance Co-Authored-By: Claude Fable 5 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 2f7eb129cca..1b41f833ce5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ The most common defect in this repo is a change that works on the path you teste - `vp i` installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run. - `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins. - Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift. -- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not use tsdev or raw `tailscale serve` for this, and do not open the URL yourself. +- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not wire up `tailscale serve` by hand for this, and do not open the URL yourself. - The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. If the token got consumed, mint a fresh one with `node apps/server/src/bin.ts pair` — note it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management). - Stop what you started, by the PID you tracked. See rule 1.