Skip to content

Let clients without a managed backend manage pairing - #4496

Closed
colonelpanic8 wants to merge 7 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/env-scoped-pairing-access
Closed

Let clients without a managed backend manage pairing#4496
colonelpanic8 wants to merge 7 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/env-scoped-pairing-access

Conversation

@colonelpanic8

@colonelpanic8 colonelpanic8 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

What Changed

Access management (pairing links, authorized clients, and the pairing QR code) is now scoped to whichever server the Connections page is administering, instead of always the primary environment.

  • resolveAccessEnvironment() administers the primary when one exists, and the active environment otherwise. A desktop that manages its own backend is unaffected.
  • New packages/client-runtime/src/state/authHttp.ts routes /api/auth/session, /api/auth/pairing-token, /api/auth/pairing-links/revoke, /api/auth/clients/revoke, and /api/auth/clients/revoke-others through the environment supervisor's prepared connection.
  • environmentPairingBaseUrl() produces a shareable pairing URL only for a directly addressable HTTP origin. Relay and SSH environments fall back to showing the bare pairing code.
  • Pairing no longer requests an explicit scope set, so a client keeps exactly what its credential carries.
  • t3 pair prints the same scannable QR code that t3 serve already prints at startup.

Why

platformConnectionSourceLayer returns no registrations when isHostedStaticApp(), so a hosted-app client has no primary environment at all. canManageLocalBackend reads scopes from the primary session, which is null in that case, so the entire "Authorized clients" section was hidden. A user who had paired the hosted app to their own server could not create a pairing link, revoke a client, or display a pairing QR code for that server.

Worse, the page rendered a contradictory "Administrative access — pairing links and client-session management require the access:write scope for this backend" row even when the client had paired using the server's own administrative startup token.

That second part had its own cause: onboarding always requested the standard client scopes, and the server rejects a wider request rather than clamping it, so a client bootstrapped from a server's startup token was silently downgraded to a standard session and could not manage the access it had just been handed. Requesting no scopes and accepting what the credential carries fixes it.

The access-management endpoints were already per-environment; only the client bound them to the primary.

UI Changes

Hosted web client (no primary environment, served from a non-loopback origin on :53595) paired to a
standalone t3 serve on :53141, at Settings → Connections. Same viewport, theme, and backend for
every shot; only the apps/web build was swapped between commits.

Note: these captures were taken at b38c6b5. The branch has since been rebased onto upstream main as f14509692ccb; the rebase changed no file content, so every image below still reflects the current head exactly.

Before (38cfc25, main) — no "Authorized clients" section at all, so there is no way to create a
pairing link or revoke a client, plus a misleading scope message despite having paired with the
server's administrative startup token:

Before — no access management on main

After (b38c6b5) — the section renders against the saved environment, with Create link, revoke
controls, and the pairing link's QR affordance:

After — Authorized clients renders for the saved environment

After (b38c6b5) — the pairing link and its QR resolve to the administered backend's own origin
(:53141), not to the client app's origin (:53595). The token is redacted and the QR blurred; the
link was left to expire and the disposable backend was destroyed after capture:

After — pairing link resolves to the administered backend

Control for the resolveShareablePairingUrl review fix (unchanged rendering)

The review fix only changes the case where the administered environment has no directly
addressable HTTP origin — a relay environment, an SSH environment, or a bearer entry with no profile.
Those states are not reachable from a locally runnable web client (relay needs managed-relay
infrastructure to connect; SSH needs window.desktopBridge; a connected bearer environment always has
a base URL, since the resolver reads it from the same profile environmentPairingBaseUrl() reads), so
that branch is covered by the unit tests in ConnectionsSettings.logic.test.ts rather than by a
screenshot. For the case that is reachable here — an environment with a usable HTTP base URL — the
rendering is unchanged, as it should be:

Before the fix (dcd149d):

Control before — dcd149d

After the fix (b38c6b5):

Control after — b38c6b5

Only the expiry countdown differs.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Validation

vp test run apps/server/src/cliAuthFormat.test.ts \
  apps/web/src/components/settings/ConnectionsSettings.logic.test.ts \
  packages/client-runtime/src/connection/onboarding.test.ts   # 20 passed
vp run typecheck   # no errors
vp run lint        # no new warnings
vp run fmt:check   # clean

Integrated verification: hosted-mode web client (VITE_HOSTED_APP_CHANNEL set, no VITE_HTTP_URL/VITE_WS_URL) paired to an isolated t3 serve, then created a pairing link, opened its QR popover, and confirmed the link resolves to the environment's own base URL. Also confirmed a client holding only standard scopes still sees no access-management section.

Note for reviewers

The section's visibility is gated on access:write while its data stream requires access:read, so a credential granted one but not the other renders an error state. That mismatch already exists on main for the primary path, so it is left unchanged here.


Note

Medium Risk
Touches auth pairing, session scopes, and shareable URL selection across web and client-runtime; mistakes could pair the wrong device or hide admin capabilities, but logic is heavily unit-tested.

Overview
Connections now administers pairing links and authorized clients on whichever server the page is targeting—primary when the desktop manages a backend, otherwise the active saved environment—so hosted and client-only apps can mint links and revoke sessions without a primary.

New client-runtime HTTP helpers route /api/auth/* mutations through each environment’s prepared connection, with session-scope queries gating UI on the credential for that server. Shareable pairing URLs are chosen via resolveShareablePairingUrl (loopback and wrong-origin links filtered); relay/SSH entries show the bare code only.

Pairing onboarding no longer sends an explicit OAuth scope list, so sessions inherit exactly what the pairing credential grants (fixing silent downgrade when bootstrapping from an admin startup token). CLI human output adds a terminal QR when a pair URL exists; JSON stays machine-readable. Chat and root bootstrap sync active environment id with the current route without overwriting an existing selection.

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

Note

Let clients without a managed backend manage pairing for saved environments

  • Adds per-environment HTTP helpers in authHttp.ts for reading session state and mutating access (pairing links, client sessions) using the active environment's prepared connection.
  • Extends createAuthEnvironmentAtoms in auth.ts to expose these per-environment access-management queries and commands.
  • Updates the Connections settings page to administer either the managed backend or the selected saved environment, routing all access mutations and pairing link creation to the chosen environment.
  • Pairing link rows now filter out loopback and non-actionable URLs via new resolveShareablePairingUrl logic in ConnectionsSettings.logic.ts; human-readable CLI output gains a terminal QR code when a shareable URL is available.
  • Pairing registration no longer forwards explicit scopes, so the server grants exactly the scopes carried by the pairing credential (including admin scopes).

Macroscope summarized 3496404.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d9ca262-d93f-44c9-bee9-b4956186a9cc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 25, 2026
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new authentication and access management capabilities, enabling clients without managed backends to manage pairing links and sessions on external servers. Changes to auth/session handling and the introduction of new user-facing features warrant careful human review.

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

@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 25, 2026
Comment thread apps/web/src/components/settings/ConnectionsSettings.logic.ts
@colonelpanic8
colonelpanic8 force-pushed the t3code/env-scoped-pairing-access branch from b38c6b5 to f145096 Compare July 25, 2026 03:23
Comment thread apps/web/src/components/settings/ConnectionsSettings.logic.ts Outdated
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 25, 2026
…nage pairing)

# Conflicts:
#	apps/web/src/components/settings/ConnectionsSettings.logic.test.ts
#	apps/web/src/components/settings/ConnectionsSettings.logic.ts
#	apps/web/src/components/settings/ConnectionsSettings.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 25, 2026
…nage pairing)

# Conflicts:
#	apps/web/src/components/settings/ConnectionsSettings.logic.test.ts
#	apps/web/src/components/settings/ConnectionsSettings.logic.ts
#	apps/web/src/components/settings/ConnectionsSettings.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 25, 2026
…nage pairing)

# Conflicts:
#	apps/web/src/components/settings/ConnectionsSettings.logic.test.ts
#	apps/web/src/components/settings/ConnectionsSettings.logic.ts
#	apps/web/src/components/settings/ConnectionsSettings.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
…nage pairing)

# Conflicts:
#	apps/web/src/components/settings/ConnectionsSettings.logic.test.ts
#	apps/web/src/components/settings/ConnectionsSettings.logic.ts
#	apps/web/src/components/settings/ConnectionsSettings.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
…nage pairing)

# Conflicts:
#	apps/web/src/components/settings/ConnectionsSettings.logic.test.ts
#	apps/web/src/components/settings/ConnectionsSettings.logic.ts
#	apps/web/src/components/settings/ConnectionsSettings.tsx
@colonelpanic8
colonelpanic8 force-pushed the t3code/env-scoped-pairing-access branch from b824828 to c2b774f Compare July 28, 2026 02:08
colonelpanic8 and others added 5 commits July 27, 2026 19:47
Access management was bound to the primary environment, so it was
reachable only from a client that owns a local backend. A hosted-app
client has no primary at all — `platformConnectionSourceLayer` emits no
registrations for `isHostedStaticApp()` — so the Connections page hid
the whole "Authorized clients" section. A user who had paired the
hosted app to their own server could not mint a pairing link, revoke a
client, or see a pairing QR code for that server.

Route the access-management endpoints through the environment
supervisor's prepared connection instead of the primary, and administer
the active environment when there is no primary. A desktop that manages
its own backend keeps administering that backend, so managed mode is
unchanged.

Pairing also silently lost administrative scope: onboarding always
requested the standard client scopes, and the server rejects a wider
request rather than clamping it, so a client bootstrapped from a
server's own startup token was downgraded to a standard session and
could not manage the access it had just been handed. Request no scopes
and take exactly what the credential carries.

Only a directly addressable HTTP origin yields a shareable pairing URL;
relay and SSH environments are reached through a broker or a local
tunnel whose address means nothing on another device, so those fall
back to showing the bare pairing code.
`t3 serve` already prints a QR code next to its startup pairing URL, but
a link minted later with `t3 pair` printed only text, so pairing a phone
meant transcribing a URL by hand.

Encode the pair URL when there is one. A bare token is not actionable on
the device that scans it, and JSON output stays machine-readable.
When the Connections page administers an environment it is not served by
— a relay or SSH environment, or a bearer environment whose profile is
missing — that environment has no addressable base URL, and the pairing
row is meant to fall back to the bare pairing code. It instead fell
through to the current page's `/pair` URL whenever this page was itself
on a non-loopback host, so the QR code and the copy button handed out a
link to this client app rather than to the server the link belongs to.

The page's own origin only reaches the administered server when that
server is the one that served the page, which is the managed-backend
case. Make that a stated input of the fallback rather than an accident
of the page's hostname, and move the choice into a tested helper.
@colonelpanic8
colonelpanic8 force-pushed the t3code/env-scoped-pairing-access branch from c2b774f to 9f1395b Compare July 28, 2026 02:51
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx

@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 fc202bc. Configure here.

Comment thread apps/web/src/components/ChatView.tsx
@colonelpanic8

Copy link
Copy Markdown
Contributor Author

Folded into #9361, which carries this change on a current main base.

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:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant