Skip to content

Make browser dev shareable over Tailscale#4556

Open
t3dotgg wants to merge 7 commits into
agent/dev-worktree-isolationfrom
agent/dev-server-sharing
Open

Make browser dev shareable over Tailscale#4556
t3dotgg wants to merge 7 commits into
agent/dev-worktree-isolationfrom
agent/dev-server-sharing

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member

What

  • Make browser development single-origin by proxying backend HTTP, WebSocket, OAuth, and well-known routes through Vite.
  • Add dev:share / --share support that publishes the web port with Tailscale and cleans up the mapping on exit.
  • Make pairing grants and cookies work correctly from the shared HTTPS origin.
  • Centralize dev proxy configuration and cover the runner, sharing, auth, and Tailscale behavior with focused tests.

Why

Remote browsers could load the frontend but then try to connect to their own localhost for backend traffic. Sharing also needed the startup pairing URL, cookie policy, allowed origins, and Tailscale lifecycle to agree on the same public origin.

Impact

This is PR 2 of 3 and is stacked on #4555. Its diff is limited to single-origin browser hosting and Tailscale sharing.

Verification

  • 64 focused tests across the dev runner, sharing helper, auth policy, and Tailscale package
  • targeted lint
  • typechecks for shared, Tailscale, server, and web packages
  • isolated dev-stack startup smoke test

Note

Add --share flag to expose the browser dev server over Tailscale

  • Adds a --share flag (and dev:share npm script) to the dev runner that publishes the Vite web server over Tailscale, registers cleanup on exit, and sets allowed CORS origins and VITE_DEV_SERVER_URL to the tailnet URL.
  • Introduces shareDevServer/unshareDevServer in dev-share.ts with structured error types for Tailscale unavailability, missing tailnet name, and serve failures.
  • Switches dev/dev:web modes to single-origin mode (T3CODE_SINGLE_ORIGIN_DEV=1), removing baked-in VITE_HTTP_URL/VITE_WS_URL and routing backend traffic through the Vite proxy instead.
  • Updates vite.config.ts to allow *.ts.net hosts, proxy a shared set of path prefixes from DEV_PROXIED_PATH_PREFIXES, and derive HMR connection from page origin when no explicit host is set.
  • Adds stderrDiagnostic classification to TailscaleCommandExitError and port-scoped session cookie names (t3_session_<port>) across all server modes.
  • Behavioral Change: resolveSessionCookieName now always returns a port-suffixed name; clients previously relying on t3_session in web mode will receive a different cookie name.

Macroscope summarized 2db512a.


Note

Medium Risk
Session cookie renaming invalidates existing dev sessions on upgrade; auth and Tailscale serve lifecycle changes affect how pairing and credentialed requests behave across origins.

Overview
Browser dev is now single-origin so the UI works from localhost, a tailnet hostname, or another device without baking in localhost backend URLs. dev / dev:web unset VITE_HTTP_URL and VITE_WS_URL, set T3CODE_SINGLE_ORIGIN_DEV=1, and Vite proxies /api, /oauth, /.well-known, and /ws to the backend via a shared DEV_PROXIED_PATH_PREFIXES list; the server 404s those paths in dev instead of redirecting to Vite.

bun run dev:share / --share publishes the web port with tailscale serve, wires VITE_DEV_SERVER_URL and T3CODE_DEV_ALLOWED_ORIGINS to the HTTPS tailnet origin, and removes the mapping on exit (skipped for dev:desktop and --dry-run). A new scripts/lib/dev-share layer clears stale mappings before serving and surfaces safe Tailscale failure diagnostics instead of raw stderr.

Auth and ports align with multi-instance and remote use: session cookies are always t3_session_<port> (web included), startup pairing tokens get a 24h TTL when a dev URL is configured, worktrees get stable hashed port offsets, and port probes focus on loopback (plus the backend bind host for server-role checks) so Tailscale-held ports do not spuriously shift assignments.

Reviewed by Cursor Bugbot for commit 2db512a. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added shared browser development via dev:share, publishing the web app over HTTPS on a tailnet.
    • Improved single-origin browser development with automatic proxying for backend and WebSocket routes.
    • Added support for configurable development hosts and allowed origins.
    • Startup pairing links now remain valid for up to 24 hours.
  • Bug Fixes

    • Prevented session collisions when running multiple development servers.
    • Improved port selection across worktrees and concurrent environments.
    • Replaced potentially sensitive Tailscale error output with safer diagnostic messages.
  • Documentation

    • Added guidance for shared development, browser proxying, port selection, and server startup output.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: making browser dev shareable over Tailscale.
Description check ✅ Passed The description covers the main change, motivation, impact, and verification, with only minor template deviations and a missing checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/dev-server-sharing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 26, 2026
@t3dotgg
t3dotgg marked this pull request as ready for review July 26, 2026 05:21
Comment thread packages/tailscale/src/tailscale.ts Outdated
Comment thread scripts/dev-runner.ts
Comment thread scripts/dev-runner.ts
@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

New feature introducing Tailscale dev sharing with auth-adjacent changes (port-scoped session cookies, extended token TTL) and CORS modifications across multiple files. The scope and nature of changes warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Took ownership of this PR, reviewed it, and pushed one fix.

Fixed: --share silently half-worked for dev:desktop (edc8e4f)

The share block only guarded dev:server, so dev:desktop --share fell through and shared. That's broken in a way the user can't see:

  • Desktop keeps VITE_HTTP_URL/VITE_WS_URL baked to 127.0.0.1 (it's deliberately not single-origin), so a tailnet visitor loads the UI and then watches it dial their own loopback for the backend.
  • Worse, the share block overwrites VITE_DEV_SERVER_URL when --dev-url is unset — and that's the origin Electron loads the renderer from (DesktopApp.ts:176, Option.getOrThrow(environment.devServerUrl)).

Now warns and carries on serving locally, matching the dev:server treatment. Added a test asserting it still spawns the stack.

Verified, not just read

  • Loopback-only port probing is safe. The switch from probing ["127.0.0.1","0.0.0.0","::1","::"] to ["127.0.0.1","::1"] looked like it could hand out an occupied port. Empirically it can't — a wildcard 0.0.0.0 listener still makes a subsequent 127.0.0.1 bind fail EADDRINUSE, so loopback alone is a sufficient test.
  • Single-origin proxying works end to end. Booted the stack and probed: /.well-known/t3/environment through Vite → 200 with a real descriptor; /200; /api/foo straight at the backend → 404 (not a redirect loop); /somepage at the backend → 302 to Vite. The DEV_PROXIED_PATH_PREFIXES contract holds on both sides.
  • Port stability holds. I saw ports climb 8579→8580→8581 mid-review and suspected a bug in the worktree-hash offset — it was my own overlapping dev servers. With a clean process table the offset is stable at server=2849,web=2849 across runs.
  • Dropping /attachments from the proxy list is dead-code removal, not a regression. There is no /attachments HTTP route on the server and no client that fetches one; attachmentsDir is filesystem-only, and attachments are served under /api/assets (ASSET_ROUTE_PREFIX), which /api already covers.

Deliberate behavior changes worth flagging for reviewers

Both are correct, but they're the kind of thing that generates a bug report:

  1. Every dev session's cookie is renamedt3_sessiont3_session_<port> in web mode too. Anyone with a live browser session gets logged out once and re-pairs. The reasoning (cookies are host-scoped but not port-scoped, so concurrent servers clobber each other) is sound and the alternative is the far worse "Invalid session token signature" loop.
  2. Startup pairing tokens now live 24h instead of 5m, gated on devUrl !== undefined && purpose === "startup" — so it's dev-only; user-issued links and real servers keep 5 minutes. Correct gating.

typecheck clean (0 errors), lint clean on changed files, 40/40 tests passing in dev-runner.test.ts + dev-share.test.ts.

I'm babysitting this PR and will pick up review comments as they land.

t3dotgg and others added 2 commits July 25, 2026 23:54
Desktop dev is not single-origin: the renderer gets VITE_HTTP_URL and
VITE_WS_URL baked to loopback, so a tailnet visitor would load the UI and
then watch it dial its own 127.0.0.1 for the backend. Sharing also
overwrote VITE_DEV_SERVER_URL, which is the origin Electron loads the
renderer from.

Warn and carry on serving locally instead of handing out a URL that is
broken in a way the user cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@t3dotgg
t3dotgg force-pushed the agent/dev-server-sharing branch from edc8e4f to dbad3bc Compare July 26, 2026 06:55
@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Rebased onto the updated base — #4555 picked up two new commits (e52d10409, 6404fc1a8) that touch scripts/dev-runner.ts in the same region as this PR, so GitHub flipped to CONFLICTING.

Resolution kept the base's improvements and adapted this PR to them:

  • Took HostProcessWorkingDirectory over process.cwd(), including for the new worktree port-offset hash this PR adds — it was the one remaining raw process.cwd() call, and using the service keeps it testable and consistent with the base.
  • Kept the base's blank---home-dir trim fix in the resolvedT3Home precedence chain.

45/45 tests passing (the base's 5 new tests merged in cleanly), typecheck 0 errors, and the runner still resolves the same stable offset (server=2849,web=2849) for both dev and dev:desktop.

Comment thread apps/web/vite.config.ts
Comment thread scripts/dev-runner.ts
Two findings from review.

stderrPreview carried the first 200 chars of raw CLI stderr into
DevShareError.detail and on into dev-runner's logs. tailscale prints auth
keys (tskey-...) and node names to stderr, and the package's own tests
already seed a token there and assert it does not leak — stderrPreview
walked straight past that invariant. Replace it with stderrDiagnostic, a
closed set of labels (no-existing-handler, not-logged-in,
permission-denied, unknown); callers match on the label and render our own
wording. Unrecognized stderr degrades to "unknown" rather than falling
back to text.

Port probing was narrowed to loopback, but --host/T3CODE_HOST moves the
backend onto another interface, and that interface decides whether the
bind succeeds. A port free on loopback and taken there was selected and
then failed to bind. Probe the configured host alongside loopback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ce4664d. Configure here.

Comment thread scripts/dev-runner.ts
t3dotgg and others added 2 commits July 26, 2026 00:05
The runner deleted VITE_HTTP_URL/VITE_WS_URL for dev and dev:web, but
vite.config.ts calls loadRepoEnv, which merges .env/.env.local underneath
the process env. A developer with either URL in their .env got it back,
and Vite baked it into the bundle — pinning the client to localhost and
breaking every non-localhost origin. Invisible, too: the page still loads,
then dials the visitor's own machine.

Absence is not a usable signal, so state the intent: the runner sets
T3CODE_SINGLE_ORIGIN_DEV=1, and Vite ignores both URLs when it is set.
VITE_HTTP_URL is now pinned in `define` as well, since Vite's automatic
VITE_ exposure would otherwise leak it past the check.

Verified by A/B with a .env setting both URLs: the served module carries
"http://localhost:13773" without this change and "" with it, proxying
still 200.

Also point the dev:desktop --share refusal at `dev` rather than `dev:web`,
which starts no backend of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--host/T3CODE_HOST configures the backend. Vite takes its bind address
from HOST, which the runner sets for desktop alone, so the web port stays
on loopback. Probing it against the backend's interface could reject a
port that was free where Vite would actually bind, shifting offsets or
failing startup for no reason.

The checker now takes the port's role. Passed explicitly rather than
inferred from the port number, which stops discriminating once a large
T3CODE_PORT_OFFSET pushes the web port past the server base.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Review round converged — CI green on 37e1c7978, mergeable, all six threads answered.

Fixed

dbad3bc9f --share silently half-worked for dev:desktop — shared a URL whose renderer dials the visitor's own loopback, and clobbered the VITE_DEV_SERVER_URL Electron loads from
ce4664d92 raw tailscale stderr (auth keys, node names) reached logs via stderrPreview; bind host was never probed after the loopback narrowing
2869a9e84 a repo .env revived VITE_HTTP_URL/VITE_WS_URL and silently defeated single-origin mode
37e1c7978 the web port was judged against the backend's bind interface (a flaw in my own earlier fix)

Declined, with reasoning

Refusing --share for dev:web. It's the documented "Vite only" mode, meant to pair with a separately-running dev:server, and that combination is legitimate to share; refusing it blocks a working setup to guard against a misconfiguration that announces itself the moment a request 502s. I did fix the half that was right — the dev:desktop refusal message pointed at dev:web, which was bad advice, and now points at dev. dev:desktop stays refused because it's broken even when everything runs correctly.

Things I checked empirically rather than by reading

  • Loopback-only probing is safe — a wildcard 0.0.0.0 listener still makes a later 127.0.0.1 bind fail EADDRINUSE, so loopback already catches wildcard squatters. This is why the narrowing was right and why I fixed the host gap by probing the specific configured host rather than restoring the wildcards.
  • The .env leak, by A/B — served module carried "http://localhost:13773" without the fix and "" with it, proxy still 200.
  • Single-origin proxying, end to end — descriptor through Vite 200, /api/foo at the backend 404 (no redirect loop), /somepage 302 to Vite.
  • Port stability — stable offset across restarts; earlier drift I saw was my own overlapping dev servers, not the worktree hash.

Still worth a human eye

Two intentional behavior changes that will generate reports if they surprise people: session cookies are renamed to t3_session_<port> in web mode, so every live dev session logs out once; and startup pairing tokens go to 24h (correctly gated to devUrl !== undefined && purpose === "startup", so real servers keep 5 minutes).

65 tests, typecheck 0 errors, lint clean on changed files. Still babysitting.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
scripts/dev-runner.ts (3)

639-639: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Recomputes the web port instead of reusing env.PORT.

createDevRunnerEnv already derived the same value into env.PORT; duplicating the BASE_WEB_PORT + webOffset arithmetic here is a second source of truth that will drift if web-port resolution ever changes.

♻️ Suggested change
-    const sharedWebPort = BASE_WEB_PORT + webOffset;
+    const sharedWebPort = Number(env.PORT);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev-runner.ts` at line 639, Update the code near sharedWebPort to
reuse the resolved env.PORT value produced by createDevRunnerEnv instead of
recomputing BASE_WEB_PORT + webOffset. Remove the duplicate arithmetic while
preserving the existing shared web-port behavior.

295-324: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

T3CODE_PORT is now set identically in both branches.

Lines 296 and 319 assign the same value; hoisting it above the isDesktopMode split would make the branch about the Vite URL/marker policy only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev-runner.ts` around lines 295 - 324, Move the shared T3CODE_PORT
assignment above the isDesktopMode conditional, then remove the duplicate
assignments from both branches. Keep the existing Vite URL and
T3CODE_SINGLE_ORIGIN_DEV handling unchanged so the split only controls URL and
marker policy.

206-216: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Numeric T3CODE_DEV_INSTANCE bypasses the bounds applied elsewhere.

Unlike T3CODE_PORT_OFFSET (negative-checked) and the hashed path (% MAX_HASH_OFFSET), a numeric seed is used verbatim, so T3CODE_DEV_INSTANCE=90000 resolves to an out-of-range port pair and surfaces as DevRunnerPortExhaustedError rather than a message naming the bad input. Consider validating it against the same offset range.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev-runner.ts` around lines 206 - 216, Update the numeric-seed branch
in the dev-runner offset resolution to validate Number(seed) against the same
allowed offset bounds used by the other paths, rejecting values outside that
range with an error that identifies the invalid T3CODE_DEV_INSTANCE input;
preserve the existing hashed behavior and valid numeric offsets.
scripts/lib/dev-share.test.ts (1)

86-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the no-tailnet-name / tailscale-unavailable branches.

Both produce user-facing hints and neither is exercised — a status payload without Self.DNSName and a non-zero status exit would close the gap cheaply given the existing spawnerLayer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/dev-share.test.ts` around lines 86 - 161, Add tests in the
shareDevServer suite for the no-tailnet-name and tailscale-unavailable branches:
configure spawnerLayer with a successful status response whose payload omits
Self.DNSName, and with a non-zero status exit respectively. Flip each effect and
assert the resulting DevShareError reason and user-facing hint message.
packages/tailscale/src/tailscale.ts (1)

252-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate stderrDiagnosticOf(stderr) evaluation in both error constructions. Both sites call the classifier once for the presence check and again for the value; binding the result once makes the intent clearer and avoids re-running the regex list.

  • packages/tailscale/src/tailscale.ts#L252-L254: hoist const stderrDiagnostic = stderrDiagnosticOf(stderr); before the TailscaleCommandExitError construction and spread ...(stderrDiagnostic ? { stderrDiagnostic } : {}).
  • packages/tailscale/src/tailscale.ts#L318-L320: apply the same hoist inside runTailscaleCommand.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tailscale/src/tailscale.ts` around lines 252 - 254, Hoist the result
of stderrDiagnosticOf(stderr) into a local stderrDiagnostic before each
TailscaleCommandExitError construction, then reuse it for the conditional
stderrDiagnostic property. Apply this at packages/tailscale/src/tailscale.ts
lines 252-254 and 318-320 within runTailscaleCommand, preserving the existing
behavior when no diagnostic is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/auth/EnvironmentAuth.test.ts`:
- Around line 18-26: The makeServerConfigLayer helper currently allows overrides
to replace TEST_SERVER_PORT, which can desynchronize the server port and
makeCookieRequest’s fixed cookie name. Apply ...overrides before assigning port:
TEST_SERVER_PORT so the test port is always pinned while preserving all other
overrides.

In `@docs/reference/scripts.md`:
- Line 4: Update the documented base and sharing commands in the web-stack
startup instructions to use the required Bun workflow: replace the base command
with “bun run dev” and the sharing variant with “bun run dev:share”. Preserve
the existing descriptions of sharing behavior.

In `@packages/tailscale/src/tailscale.test.ts`:
- Around line 29-40: Update assertCarriesNoSecret to recursively inspect strings
inside nested objects, arrays, and causes, while preserving the message check.
Track visited objects with cycle protection so cyclic error structures terminate
safely, and retain field-specific context in assertion failures where practical.

---

Nitpick comments:
In `@packages/tailscale/src/tailscale.ts`:
- Around line 252-254: Hoist the result of stderrDiagnosticOf(stderr) into a
local stderrDiagnostic before each TailscaleCommandExitError construction, then
reuse it for the conditional stderrDiagnostic property. Apply this at
packages/tailscale/src/tailscale.ts lines 252-254 and 318-320 within
runTailscaleCommand, preserving the existing behavior when no diagnostic is
available.

In `@scripts/dev-runner.ts`:
- Line 639: Update the code near sharedWebPort to reuse the resolved env.PORT
value produced by createDevRunnerEnv instead of recomputing BASE_WEB_PORT +
webOffset. Remove the duplicate arithmetic while preserving the existing shared
web-port behavior.
- Around line 295-324: Move the shared T3CODE_PORT assignment above the
isDesktopMode conditional, then remove the duplicate assignments from both
branches. Keep the existing Vite URL and T3CODE_SINGLE_ORIGIN_DEV handling
unchanged so the split only controls URL and marker policy.
- Around line 206-216: Update the numeric-seed branch in the dev-runner offset
resolution to validate Number(seed) against the same allowed offset bounds used
by the other paths, rejecting values outside that range with an error that
identifies the invalid T3CODE_DEV_INSTANCE input; preserve the existing hashed
behavior and valid numeric offsets.

In `@scripts/lib/dev-share.test.ts`:
- Around line 86-161: Add tests in the shareDevServer suite for the
no-tailnet-name and tailscale-unavailable branches: configure spawnerLayer with
a successful status response whose payload omits Self.DNSName, and with a
non-zero status exit respectively. Flip each effect and assert the resulting
DevShareError reason and user-facing hint message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6650ded2-f1b8-41f0-88dd-2bcf360de729

📥 Commits

Reviewing files that changed from the base of the PR and between 6404fc1 and 37e1c79.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .agents/skills/test-t3-app/SKILL.md
  • AGENTS.md
  • apps/server/src/auth/EnvironmentAuth.test.ts
  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.test.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.ts
  • apps/server/src/auth/PairingGrantStore.ts
  • apps/server/src/auth/SessionStore.ts
  • apps/server/src/auth/utils.ts
  • apps/server/src/http.ts
  • apps/web/vite.config.ts
  • docs/reference/scripts.md
  • package.json
  • packages/shared/package.json
  • packages/shared/src/devProxy.ts
  • packages/tailscale/src/tailscale.test.ts
  • packages/tailscale/src/tailscale.ts
  • scripts/dev-runner.test.ts
  • scripts/dev-runner.ts
  • scripts/lib/dev-share.test.ts
  • scripts/lib/dev-share.ts
  • scripts/package.json

Comment thread apps/server/src/auth/EnvironmentAuth.test.ts
Comment thread docs/reference/scripts.md Outdated
Comment thread packages/tailscale/src/tailscale.test.ts Outdated
Make assertCarriesNoSecret recurse. It checked only top-level strings, so
a leak inside a nested cause or an array would have passed while the
comment claimed coverage for fields added later. Verified the gap is real:
the shallow version passes on both a nested cause and an array carrying
the token, and the recursive one fails on each. Walks message/cause
explicitly (getters on Error subclasses are not own enumerable props) and
tracks visited objects so cyclic causes terminate.

Pin port after overrides in makeServerConfigLayer. makeCookieRequest
builds the cookie name from TEST_SERVER_PORT, so an override that changed
the port would leave the server reading one cookie name while requests
sent another.

Use bun run in docs/reference/scripts.md for the dev and dev:share lines,
matching AGENTS.md. Confirmed `bun run dev --share` forwards the flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
t3dotgg added a commit that referenced this pull request Jul 26, 2026
Rebasing onto the updated #4556 turned up two doc drifts: the base moved
the script list from `vp run` to `bun run`, and the state-directory
paragraph still described the fixed `userdata` preference that the mtime
tiebreak replaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Effect service conventions check was failing on this, correctly. The
single DevShareError violated four rules at once: a `reason` discriminator
chose both the user-facing message and the caller's remedy, the `message`
getter was a lookup table over it, `detail` copied `cause.message` and was
then used to build that message, and the underlying TailscaleCommandError
was discarded entirely so the error chain ended at the wrapper.

Now three classes, one per genuinely distinct failure:
TailscaleUnavailableError and TailnetNameMissingError each have one fixed
message and one remedy; DevServeFailedError keeps a `stage` discriminator,
which is legitimate — both stages share the same semantics (a serve
invocation failed for this port) and differ only in which one.

Each wrapping construction now preserves the immediate error as `cause`,
and every message derives solely from structural fields. unshareDevServer
returns the structured cause rather than a flattened string so its caller
can do the same. TailnetNameMissingError has no cause because none exists
— the status read succeeded and simply had no name.

Verified the chain end to end: the wrapped TailscaleCommandExitError is
reachable with exitCode and stderrDiagnostic intact, and the auth token in
stderr still never reaches the message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Fixed the failing Effect Service Conventions check in 2db512ad4.

The check was right, and its finding had been sitting in an orphaned inline comment from an earlier commit — the force-pushes marked it outdated, so it never showed up in my unreplied-thread sweep. My fault for only ever checking live threads.

DevShareError violated four rules of .macroscope/check-run-agents/effect-service-conventions.md at once:

  • a reason discriminator chose both the user-facing message and the caller's remedy (hint) — the convention says split those into separate classes
  • the message getter was a lookup table over that discriminator, which the convention names explicitly
  • detail copied cause.message and was then used to construct the wrapper message — also named explicitly
  • the underlying TailscaleCommandError was discarded entirely, so the error chain ended at the wrapper

Now three classes. TailscaleUnavailableError and TailnetNameMissingError each have one fixed message and one remedy, so the discriminator disappears. DevServeFailedError keeps a stage field — that one is legitimate under the convention, since both stages share the same semantics (a tailscale serve invocation failed for this port) and differ only in which one, which the message states plainly. The stage distinction matters operationally: clear-existing means the prior mapping survived, serve means it was already torn down.

Every wrapping construction now keeps the immediate error as cause, and messages derive solely from structural fields. unshareDevServer returns the structured cause instead of a flattened string so its caller can preserve the chain too. TailnetNameMissingError has no cause because there is no underlying failure — the status read succeeded and simply had no name, which is the case the convention describes as legitimately optional.

Verified the chain survives rather than assuming it: the wrapped TailscaleCommandExitError is reachable with exitCode and stderrDiagnostic intact, and the auth token seeded into stderr still never reaches the message — so this doesn't undo the earlier secret-leak fix.

204 tests passing across scripts/ and packages/tailscale/, typecheck 0 errors, lint clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant