Skip to content

fix(web): show saved service access with Local environment off - #26464

Open
shivamhwp wants to merge 2 commits into
mainfrom
issue-watch/26420
Open

shivamhwp wants to merge 2 commits into
mainfrom
issue-watch/26420

Conversation

@shivamhwp

@shivamhwp shivamhwp commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

With Local environment off, the Linux desktop client showed "No environment selected" in Sharing & access despite being connected to a background service. The page now selects a connected service and offers a picker for multiple services. Access and T3 Connect actions use the selected service's credentials, permissions and organization policy; desktop network controls still belong only to the backend the app manages.

Closes #26420

Evidence

Verified head d2bc956caf92ebc74dbe49e2b5952af2e16c1c55. No code changed during this verification.

Unverified live path: production Clerk account authentication and live relay provisioning/revocation, managed connector startup, tunnel reachability, and guest links through that tunnel. The recording uses fixture Clerk/relay responses and real current-head desktop/server code. Initial linking keeps the existing loopback-origin restriction.

Linux desktop

Current-head Electron 44.10.0 on physical corex, with two separate current-head bundled servers at 127.0.0.1:27731 and 127.0.0.1:27732, isolated data and Local environment off. Both services were enrolled through the normal Add environment UI. The service launcher uses the same serve command in apps/server/src/serviceLauncher.ts.

The app used Electron's production gnome_libsecret backend through a task-isolated GNOME secret-service session collection. Its persisted connection catalog is encrypted. This pass did not use Electron's basic store or a mock keychain.

Linux Electron: Local environment off and both saved services connected

Linux Electron: Labs points to Sharing & access

Linux Electron: selected service A exposes access controls and creates pairing links

Linux Electron: selecting service B shows its own pairing link

Linux Electron: revoking B's link removes it from B

Linux Electron: returning to A after revoking B preserves A's links and clears the copyable credential

Linux Electron: revoking an A link removes that row

Linux Electron: actual credential-backend startup output and persisted catalog shape

The screenshots above came from a build without cloud public configuration. The cloud-enabled recording below covers the Connect controls and selected-service publish-only linking/unlinking. Web and macOS/Windows desktop use the same ConnectionsSettings, AuthorizedClientsSettings and executeConnectedEnvironmentHttpRequest path exercised here. No native mobile UI changes are included.

The earlier before build at 48a292364ab37574c67fb377b5227e5d281d9203 reproduces the connected-service empty page:

linux-before.mp4

Linux desktop before: a connected saved service has no access target

Cloud-enabled Linux desktop and real server proof

Recorded on physical corex at d2bc956caf92ebc74dbe49e2b5952af2e16c1c55, using the production Electron renderer and two real bundled servers. Local environment is off. Both saved services are connected through normal pairing and Electron's production gnome_libsecret credential store. No app source, hooks, controller, environment-auth helper, or /api/connect server handler was replaced.

The 56-second recording shows Local environment off, both connected services, Labs to Sharing & access, the enabled T3 Connect controls, then publish-only cloud linking for A and B. Unlinking A leaves B enabled; returning to B shows that state before unlinking B. These actions use production useCloudLinkController and linkEnvironmentToCloud/unlinkEnvironmentFromCloud. They do not start a managed tunnel. Idle pauses were shortened, with a fixture caption added below the actual Electron viewport.

cloud-selected-service.mp4

B remains linked after A is unlinked

Only unavailable external boundaries were faked: Clerk FAPI session/token responses and its account widget, plus a task-local HTTPS relay. The real Clerk SDK reads those responses. The relay fixture verifies the actual server's Ed25519 proof before returning an invented per-environment credential and a valid mint public key. The task certificate uses Electron's --ignore-certificate-errors; this is not production TLS or live account verification. The managed T3 Connect switch is shown but stays off.

After every recorded publish toggle, authenticated reads of both real servers and SHA-256 comparisons of their relay secret files verify the selected service changed and the other service did not. All five checkpoints passed, including the final both-unlinked state.

Separate commands against those same real current-head servers prove manual and managed link-proof signatures, exact claims and loopback ports, unauthorized and wrong-port rejection, and relay-configuration persistence. Both processes were restarted with their original isolated homes and ports. Exact secret bytes, 0600 modes, preferences and signing keys survived. A conflicting account was rejected without modifying files; A's bearer was rejected by B; unlinking A left B's state and files unchanged.

Real current-head server commands and successful results

Build provenance and real-server commands/results
Head: d2bc956caf92ebc74dbe49e2b5952af2e16c1c55
Cloud-enabled desktop build: j_5bv56wx34domznzjbrkh / cloud-config-desktop-evidence
Boat worker bx_2r283mwc / large / attempt 1 / exit 0 / vp run 5/5 successful
Public fixture configuration:
  T3CODE_CLERK_PUBLISHABLE_KEY=pk_test_Y2xlcmsuZml4dHVyZS50ZXN0JA
  T3CODE_CLERK_JWT_TEMPLATE=fixture-relay
  T3CODE_RELAY_URL=https://127.0.0.1:27740
Command: vp i --frozen-lockfile; vp run build:desktop
runtime.tar.gz SHA256: 99fce0d64188e2ac28db26226262918241d7f8f9374d6cfe60e9eb09a249fe36
Real server bundle: j_wwsrbchxxukfzvbqw3to / bx_evarxx7s / attempt 1 / exit 0
Server command, separately for service-a:27731 and service-b:27732:
  node runtime/apps/server/dist/bin.mjs serve --base-dir /tmp/t3code-26464-cloud/service-a --port 27731 --host 127.0.0.1 --no-browser
Desktop: Electron 44.10.0, production gnome_libsecret backend, isolated desktop settings and keyring.
PR #26464 | real current-head servers on corex
Head d2bc956caf92ebc74dbe49e2b5952af2e16c1c55
Production bundled servers at 127.0.0.1:27731 and 127.0.0.1:27732
Command: node real-server-proof.mjs prepare proof-input.private.json
Restart both real server processes, preserving isolated homes and ports.
Command: node real-server-proof.mjs verify-after-restart proof-input.private.json
Both phases exited 0. Private input and bearer tokens omitted.

service-a: POST /oauth/token -> 200
service-b: POST /oauth/token -> 200
service-a: GET /api/connect/link-state -> 200
service-a: POST /api/connect/link-proof -> 200
service-a: verified manual link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-a: POST /api/connect/link-proof -> 200
service-a: verified cloudflare_tunnel link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-a: POST /api/connect/link-proof -> 400
service-a: POST /api/connect/link-proof -> 401
service-a: POST /api/connect/relay-config -> 200
service-a: POST /api/connect/preferences -> 200
service-a: GET /api/connect/link-state -> 200
service-a: POST /api/connect/relay-config -> 409
service-a: real relay-config and preference handlers persisted six private secret files; exact values and 0600 modes verified; account mismatch rejected without mutation
service-b: GET /api/connect/link-state -> 200
service-b: POST /api/connect/link-proof -> 200
service-b: verified manual link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-b: POST /api/connect/link-proof -> 200
service-b: verified cloudflare_tunnel link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-b: POST /api/connect/link-proof -> 400
service-b: POST /api/connect/link-proof -> 401
service-b: POST /api/connect/relay-config -> 200
service-b: POST /api/connect/preferences -> 200
service-b: GET /api/connect/link-state -> 200
service-b: POST /api/connect/relay-config -> 409
service-b: real relay-config and preference handlers persisted six private secret files; exact values and 0600 modes verified; account mismatch rejected without mutation
service-a: GET /api/connect/link-state -> 401
service-b: GET /api/connect/link-state -> 401
PREPARE PASS. Stop/restart both isolated servers using their original base directories and ports, then run verify-after-restart with this same input file. No server, relay or process was mocked by this script.

service-a: GET /api/connect/link-state -> 200
service-a: POST /api/connect/link-proof -> 200
service-a: verified manual link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-a: restart retained linked state, publishing preference, exact relay-config secret bytes and signing key
service-b: GET /api/connect/link-state -> 200
service-b: POST /api/connect/link-proof -> 200
service-b: verified manual link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match
service-b: restart retained linked state, publishing preference, exact relay-config secret bytes and signing key
service-b: GET /api/connect/link-state -> 200
service-a: POST /api/connect/unlink -> 200
service-a: GET /api/connect/link-state -> 200
service-b: GET /api/connect/link-state -> 200
service-a: unlink removed only A's relay secrets; B's real link state and persisted bytes are unchanged
service-b: POST /api/connect/unlink -> 200
service-b: GET /api/connect/link-state -> 200
service-b: unlink removed B's relay secrets
VERIFY-AFTER-RESTART PASS. Real production HTTP auth, link-proof signing, configuration persistence, restart reads and selected-server unlink isolation verified. External Clerk/relay/tunnel/guest-link behavior remains unverified.

UI checks after each recorded publish toggle:
baseline PASS: http://127.0.0.1:27731 linked=false publishing=false; http://127.0.0.1:27732 linked=false publishing=false; other service state and persisted file hashes unchanged.
link-a PASS: http://127.0.0.1:27731 linked=true publishing=true; http://127.0.0.1:27732 linked=false publishing=false; other service state and persisted file hashes unchanged.
link-b PASS: http://127.0.0.1:27731 linked=true publishing=true; http://127.0.0.1:27732 linked=true publishing=true; other service state and persisted file hashes unchanged.
unlink-a PASS: http://127.0.0.1:27731 linked=false publishing=false; http://127.0.0.1:27732 linked=true publishing=true; other service state and persisted file hashes unchanged.
unlink-b PASS: http://127.0.0.1:27731 linked=false publishing=false; http://127.0.0.1:27732 linked=false publishing=false; other service state and persisted file hashes unchanged.
Reproduce the real-server proof

Run the script below against two isolated current-head serve processes, each with its own base directory and port. Its private input JSON is { "services": [{ "name": "service-a", "origin": "http://127.0.0.1:27731", "baseDir": "/tmp/t3code-26464-cloud/service-a", "bootstrapCredential": "<fresh CLI pairing code>" }, { "name": "service-b", "origin": "http://127.0.0.1:27732", "baseDir": "/tmp/t3code-26464-cloud/service-b", "bootstrapCredential": "<fresh CLI pairing code>" }] }. Keep that file private. Run prepare, restart both processes with the same arguments, then run verify-after-restart. No external account or relay server is needed for these production server-handler checks.

import assert from 'node:assert/strict';
import { createHash, createPublicKey, generateKeyPairSync, verify } from 'node:crypto';
import { readFile, writeFile, stat, access } from 'node:fs/promises';
import path from 'node:path';

const [phase, inputPath] = process.argv.slice(2);
assert(['prepare', 'verify-after-restart'].includes(phase), 'phase: prepare | verify-after-restart');
assert(inputPath, 'provide the private JSON input file path');
const input = JSON.parse(await readFile(inputPath, 'utf8'));
const statePath = input.statePath ?? path.join(path.dirname(inputPath), 'proof-state.private.json');
const transcript = [];
const log = (line) => { transcript.push(line); console.log(line); };
const relayUrl = input.relayUrl ?? 'https://127.0.0.1:27740';
assert.equal(new URL(relayUrl).protocol, 'https:');
const serviceKeys = ['cloud-linked-user-id', 'cloud-relay-url', 'cloud-relay-issuer', 'cloud-relay-environment-credential', 'cloud-mint-ed25519-public-key', 'cloud-publish-agent-activity'];
const stableState = (value) => Object.fromEntries(['linked', 'cloudUserId', 'relayUrl', 'relayIssuer', 'managedTunnelActive', 'publishAgentActivity', 'holdWebhooksWhileOffline'].map((key) => [key, value[key]]));
const secretDir = (service) => path.join(service.baseDir, 'userdata', 'secrets');
const secretPath = (service, name) => path.join(secretDir(service), name + '.bin');
function validateService(service) {
  const u = new URL(service.origin);
  assert(['127.0.0.1', 'localhost', '[::1]'].includes(u.hostname), 'test only loopback servers');
  assert(service.baseDir.startsWith('/tmp/t3code-26464-cloud/'), 'test only this task\'s isolated server homes');
}
async function request(service, route, {method = 'GET', payload, token = service.token, expected = 200, form = false} = {}) {
  const response = await fetch(new URL(route, service.origin), {
    method,
    headers: { ...(token ? {authorization: `Bearer ${token}`} : {}), ...(payload !== undefined ? {'content-type': form ? 'application/x-www-form-urlencoded' : 'application/json'} : {}) },
    ...(payload !== undefined ? {body: form ? new URLSearchParams(payload).toString() : JSON.stringify(payload)} : {}),
  });
  const raw = await response.text();
  let result;
  try { result = raw ? JSON.parse(raw) : null; } catch { result = raw; }
  assert.equal(response.status, expected, `${service.name} ${method} ${route}: expected ${expected}, got ${response.status}; ${response.status >= 400 ? JSON.stringify(result) : 'response redacted'}`);
  log(`${service.name}: ${method} ${route} -> ${response.status}`);
  return {response, result};
}
async function authenticate(service) {
  if (service.token) return service;
  assert(service.bootstrapCredential, 'provide token or a fresh, unconsumed bootstrapCredential');
  const {result} = await request(service, '/oauth/token', {method:'POST', token:null, form:true, payload:{
    grant_type:'urn:ietf:params:oauth:grant-type:token-exchange', subject_token:service.bootstrapCredential,
    subject_token_type:'urn:t3:params:oauth:token-type:environment-bootstrap',
    requested_token_type:'urn:ietf:params:oauth:token-type:access_token',
  }});
  assert.equal(result.token_type, 'Bearer');
  assert.equal(typeof result.access_token, 'string');
  return {...service, token:result.access_token, bootstrapCredential:undefined};
}
async function fingerprint(service) {
  const result = {};
  for (const name of serviceKeys) {
    const filename = secretPath(service, name);
    const bytes = await readFile(filename);
    const info = await stat(filename);
    assert.equal(info.mode & 0o777, 0o600, `${service.name}: private ${name} file mode`);
    result[name] = createHash('sha256').update(bytes).digest('hex');
  }
  return result;
}
async function proof(service, providerKind) {
  const port = Number(new URL(service.origin).port);
  const endpoint = {providerKind, httpBaseUrl:service.origin, wsBaseUrl:service.origin.replace(/^http/, 'ws')};
  const payload = {challenge:`fixture-challenge-${service.name}-${providerKind}`, relayIssuer:relayUrl, endpoint, origin:{localHttpHost:'127.0.0.1',localHttpPort:port}};
  const {result:jwt,response} = await request(service, '/api/connect/link-proof', {method:'POST',payload});
  assert.equal(typeof jwt,'string');
  const [protectedText, claimsText, signatureText] = jwt.split('.');
  const header = JSON.parse(Buffer.from(protectedText,'base64url'));
  const claims = JSON.parse(Buffer.from(claimsText,'base64url'));
  assert.equal(header.alg,'EdDSA');
  assert.equal(header.typ,'t3-env-link+jwt');
  assert.equal(claims.challenge,payload.challenge);
  assert.deepEqual(claims.endpoint,endpoint);
  assert.deepEqual(claims.origin,payload.origin);
  assert.equal(claims.aud,relayUrl.replace(/\/+$/, ''));
  assert.equal(claims.iss,`t3-env:${claims.environmentId}`);
  assert.equal(claims.sub,claims.environmentId);
  assert.equal(claims.descriptor.environmentId,claims.environmentId);
  assert(claims.exp > Math.floor(Date.now()/1000));
  assert(claims.exp - claims.iat <= 300);
  assert.equal(response.headers.get('cache-control'),'no-store');
  assert(claims.scopes.includes('agent_activity_notifications'));
  assert.equal(claims.scopes.includes('managed_tunnels'),providerKind === 'cloudflare_tunnel');
  const savedPair = JSON.parse(await readFile(secretPath(service,'cloud-link-ed25519-key-pair'),'utf8'));
  assert.equal(savedPair.publicKey.trim(),claims.environmentPublicKey.trim());
  assert(verify(null,Buffer.from(`${protectedText}.${claimsText}`),createPublicKey(savedPair.publicKey),Buffer.from(signatureText,'base64url')), 'real environment Ed25519 signature');
  log(`${service.name}: verified ${providerKind} link-proof signature against persisted public key; challenge, environment, endpoint, loopback port, audience, TTL and scopes match`);
  return {environmentId:claims.environmentId, publicKey:savedPair.publicKey, payload};
}
async function assertRemoved(service) {
  for (const name of serviceKeys) {
    let missing = false;
    try { await access(secretPath(service,name)); } catch (error) { if(error.code === 'ENOENT') missing=true; else throw error; }
    assert(missing,`${service.name}: ${name} removed from secret store`);
  }
  const {result} = await request(service,'/api/connect/link-state');
  assert.equal(result.linked,false); assert.equal(result.publishAgentActivity,false); assert.equal(result.managedTunnelActive,false);
}

if (phase === 'prepare') {
  assert.equal(input.services.length,2,'two isolated servers');
  const services=[];
  for(const candidate of input.services){validateService(candidate);services.push(await authenticate(candidate));}
  assert.notEqual(services[0].token,services[1].token,'distinct real server access tokens');
  const mintPublicKey=input.cloudMintPublicKey ?? generateKeyPairSync('ed25519',{publicKeyEncoding:{format:'pem',type:'spki'},privateKeyEncoding:{format:'pem',type:'pkcs8'}}).publicKey;
  for(const service of services) {
    const {result:baseline}=await request(service,'/api/connect/link-state');
    assert.equal(baseline.linked,false,'start with clean, unlinked task server');
    const manual=await proof(service,'manual'); const managed=await proof(service,'cloudflare_tunnel');
    assert.equal(manual.environmentId,managed.environmentId);
    assert.equal(manual.publicKey,managed.publicKey);
    service.environmentId=manual.environmentId; service.publicKey=manual.publicKey;
    await request(service,'/api/connect/link-proof',{method:'POST',payload:{...manual.payload,origin:{...manual.payload.origin,localHttpPort:Number(new URL(service.origin).port)+1}},expected:400});
    await request(service,'/api/connect/link-proof',{method:'POST',payload:manual.payload,token:null,expected:401});
    const payload={relayUrl,relayIssuer:relayUrl,cloudUserId:'fixture-cloud-user',environmentCredential:`fixture-relay-credential-${service.name}`,cloudMintPublicKey:mintPublicKey,endpointRuntime:null};
    await request(service,'/api/connect/relay-config',{method:'POST',payload});
    await request(service,'/api/connect/preferences',{method:'POST',payload:{publishAgentActivity:true}});
    const {result:linked}=await request(service,'/api/connect/link-state');
    assert.equal(linked.linked,true);assert.equal(linked.cloudUserId,payload.cloudUserId);assert.equal(linked.relayUrl,relayUrl);assert.equal(linked.relayIssuer,relayUrl);assert.equal(linked.publishAgentActivity,true);assert.equal(linked.managedTunnelActive,false);
    for(const [key,value] of Object.entries({'cloud-linked-user-id':payload.cloudUserId,'cloud-relay-url':relayUrl,'cloud-relay-issuer':relayUrl,'cloud-relay-environment-credential':payload.environmentCredential,'cloud-mint-ed25519-public-key':mintPublicKey,'cloud-publish-agent-activity':'true'}))assert.equal(await readFile(secretPath(service,key),'utf8'),value);
    service.savedState=stableState(linked); service.fingerprints=await fingerprint(service);
    await request(service,'/api/connect/relay-config',{method:'POST',payload:{...payload,cloudUserId:'another-fixture-account'},expected:409});
    assert.deepEqual(await fingerprint(service),service.fingerprints,'account mismatch must not mutate persisted relay configuration');
    log(`${service.name}: real relay-config and preference handlers persisted six private secret files; exact values and 0600 modes verified; account mismatch rejected without mutation`);
  }
  assert.notEqual(services[0].environmentId,services[1].environmentId);
  assert.notEqual(services[0].publicKey,services[1].publicKey);
  for(let i=0;i<2;i++)await request(services[i],'/api/connect/link-state',{token:services[1-i].token,expected:401});
  await writeFile(statePath,JSON.stringify({relayUrl,services},null,2),{mode:0o600});
  log('PREPARE PASS. Stop/restart both isolated servers using their original base directories and ports, then run verify-after-restart with this same input file. No server, relay or process was mocked by this script.');
} else {
  const saved=JSON.parse(await readFile(statePath,'utf8'));
  const services=saved.services;
  for(const service of services) {
    validateService(service);
    const {result:state}=await request(service,'/api/connect/link-state');
    assert.deepEqual(stableState(state),service.savedState,'real linked state survives server restart');
    assert.deepEqual(await fingerprint(service),service.fingerprints,'persisted configuration bytes survive restart');
    const nextProof=await proof(service,'manual');assert.equal(nextProof.publicKey,service.publicKey,'environment signing key survives restart');
    log(`${service.name}: restart retained linked state, publishing preference, exact relay-config secret bytes and signing key`);
  }
  const [a,b]=services;
  const {result:bBefore}=await request(b,'/api/connect/link-state');const bFiles=await fingerprint(b);
  await request(a,'/api/connect/unlink',{method:'POST'});await assertRemoved(a);
  const {result:bAfter}=await request(b,'/api/connect/link-state');assert.deepEqual(stableState(bAfter),stableState(bBefore));assert.deepEqual(await fingerprint(b),bFiles);
  log(`${a.name}: unlink removed only A's relay secrets; B's real link state and persisted bytes are unchanged`);
  await request(b,'/api/connect/unlink',{method:'POST'});await assertRemoved(b);
  log(`${b.name}: unlink removed B's relay secrets`);
  log('VERIFY-AFTER-RESTART PASS. Real production HTTP auth, link-proof signing, configuration persistence, restart reads and selected-server unlink isolation verified. External Clerk/relay/tunnel/guest-link behavior remains unverified.');
}
Recording fixture boundary and isolation assertions

The recording fixture below only fulfills external clerk.fixture.test network requests and listens as the external relay. The application's service requests go directly to the unmodified real servers. The account widget is stubbed; T3's controls and business logic are not. Browser input uses cua-driver, and CDP screencast records the owned Electron viewport.

// External test fixture only. No application or SDK code is replaced.
const now = Date.now();
const exp = Math.floor(now / 1000) + 3600;
const encode = (v) => Buffer.from(JSON.stringify(v)).toString('base64url');
export const jwt = `${encode({ alg: 'RS256', typ: 'JWT' })}.${encode({ iss: 'https://clerk.fixture.test', sub: 'user_fixture', sid: 'sess_fixture', iat: Math.floor(now / 1000), nbf: Math.floor(now / 1000) - 60, exp, azp: 't3://app', v: 2, fva: [0, -1], sts: 'active' })}.fixture-signature`;
const publicUser = { first_name: 'Connect', last_name: 'Fixture', image_url: '', has_image: false, identifier: 'connect-fixture@example.test', user_id: 'user_fixture' };
export const user = {
  object: 'user', id: 'user_fixture', username: 'connect_fixture', first_name: 'Connect', last_name: 'Fixture', image_url: '', has_image: false,
  primary_email_address_id: 'idn_fixture', primary_phone_number_id: null, primary_web3_wallet_id: null,
  email_addresses: [{ id: 'idn_fixture', object: 'email_address', email_address: 'connect-fixture@example.test', verification: {status: 'verified', strategy: 'email_code'}, linked_to: [] }],
  phone_numbers: [], web3_wallets: [], passkeys: [], external_accounts: [], enterprise_accounts: [], organization_memberships: [],
  password_enabled: false, two_factor_enabled: false, totp_enabled: false, backup_code_enabled: false, public_metadata: {}, unsafe_metadata: {},
  create_organization_enabled: false, delete_self_enabled: false, last_sign_in_at: now, created_at: now, updated_at: now,
};
export const session = {
  object: 'session', id: 'sess_fixture', status: 'active', expire_at: now + 86400000, abandon_at: now + 86400000,
  last_active_at: now, last_active_organization_id: null, actor: null, factor_verification_age: [0,-1], tasks: [], user, public_user_data: publicUser,
  last_active_token: {object: 'token', jwt}, created_at: now, updated_at: now,
};
export const client = {object: 'client', id: 'client_fixture', sessions: [session], last_active_session_id: 'sess_fixture', captcha_bypass: true, cookie_expires_at: now + 86400000, created_at: now, updated_at: now};
export const environment = {
  object: 'environment', id: 'env_fixture', auth_config: {single_session_mode: true, session_minter: false},
  display_config: { application_name: 'Connect fixture', instance_environment_type: 'development', show_devmode_warning: false, home_url: 't3://app/', sign_in_url: 't3://app/', sign_up_url: 't3://app/', after_sign_in_url: 't3://app/', after_sign_out_all_url: 't3://app/' },
  user_settings: null, organization_settings: null, commerce_settings: null, api_keys_settings: null, protect_config: null,
  maintenance_mode: false, client_debug_mode: false,
};
export function clerkFixtureResponse(url, method = 'GET') {
  const pathname = new URL(url).pathname;
  if (pathname === '/v1/environment') return {response: environment};
  if (pathname === '/v1/client') return {response: client};
  if (pathname === '/v1/client/sessions/sess_fixture/touch') return {response: session, client};
  if (pathname.startsWith('/v1/client/sessions/sess_fixture/tokens')) return {object: 'token', jwt};
  if (pathname === '/v1/me') return {response: user, client};
  if (pathname.endsWith('/jwks')) return {keys: []};
  return {response: {}, client};
}
export const fixtureHeaders = [{name: 'Content-Type', value: 'application/json'}, {name: 'Access-Control-Allow-Origin', value: '*'}, {name: 'Access-Control-Allow-Headers', value: '*'}, {name: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS,PATCH,DELETE'}];
import https from 'node:https';
import fs from 'node:fs';
import {generateKeyPairSync,createPublicKey,verify,randomUUID} from 'node:crypto';
import {clerkFixtureResponse,fixtureHeaders} from './clerk-fapi-fixture.mjs';
const dir='/tmp/t3code-26464-cloud';
const mint=generateKeyPairSync('ed25519',{publicKeyEncoding:{type:'spki',format:'pem'},privateKeyEncoding:{type:'pkcs8',format:'pem'}}).publicKey;
const challenges=new Set();
https.createServer({key:fs.readFileSync(dir+'/cert/key.pem'),cert:fs.readFileSync(dir+'/cert/cert.pem')},async(req,res)=>{
 const body=[];for await(const b of req)body.push(b);const text=Buffer.concat(body).toString();const data=text?JSON.parse(text):{};
 res.setHeader('content-type','application/json');res.setHeader('access-control-allow-origin','*');res.setHeader('access-control-allow-headers','*');res.setHeader('access-control-allow-methods','GET,POST,DELETE,OPTIONS');
 let value={};let status=200;
 try{
 if(req.method==='OPTIONS'){}else if(req.url==='/v1/client/environment-link-challenges') {const challenge=randomUUID();challenges.add(challenge);value={challenge,expiresAt:new Date(Date.now()+300000).toISOString()};}
 else if(req.url==='/v1/client/environment-links'&&req.method==='POST'){
  const [a,b,c]=data.proof.split('.');const claims=JSON.parse(Buffer.from(b,'base64url'));if(!challenges.delete(claims.challenge)||!verify(null,Buffer.from(a+'.'+b),createPublicKey(claims.environmentPublicKey),Buffer.from(c,'base64url')))throw Error('Invalid real-server proof');
  console.log('RELAY fixture verified real Ed25519 proof',claims.environmentId,claims.endpoint.httpBaseUrl);
  value={ok:true,cloudUserId:'fixture-cloud-user',environmentId:claims.environmentId,endpoint:claims.endpoint,endpointRuntime:null,relayIssuer:'https://127.0.0.1:27740',environmentCredential:'fixture-'+claims.environmentId,cloudMintPublicKey:mint};
 }else if(req.method==='DELETE'&&req.url.startsWith('/v1/client/environment-links/')){console.log('RELAY fixture unlink',req.url);value={ok:true};}
 else if(req.url==='/v1/environments')value={environments:[]};
 else if(req.url.includes('devices'))value={devices:[]};
 else {status=503;value={error:'External fixture does not implement this operation'};}
 }catch(e){status=400;value={error:e.message};console.log('ERROR',e.message);}
 res.writeHead(status);res.end(JSON.stringify(value));
}).listen(27740,'127.0.0.1',()=>console.log('External relay fixture listening 27740'));
const tabs=await fetch('http://127.0.0.1:27739/json/list').then(r=>r.json());
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);await new Promise(r=>ws.addEventListener('open',r,{once:true}));
let id=0;const pending=new Map();const call=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
let recording=false;let frame=0;const frames=[];fs.mkdirSync(dir+'/frames',{recursive:true});
ws.addEventListener('message',async(e)=>{const m=JSON.parse(e.data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p?.reject(m.error):p?.resolve(m.result);return;}
 if(m.method==='Runtime.exceptionThrown')console.log('EXCEPTION',JSON.stringify(m.params));
 if(m.method==='Runtime.consoleAPICalled')console.log('CONSOLE',m.params.type,JSON.stringify(m.params.args.map(a=>a.value??a.description)));
 if(m.method==='Log.entryAdded')console.log('BROWSER LOG',m.params.entry.text);
 if(m.method==='Fetch.requestPaused'){
 const p=m.params;const url=p.request.url;
 if(url.includes('clerk.fixture.test')){
 let body,headers=fixtureHeaders;
 if(new URL(url).pathname.includes('/npm/')){body="window.__internal_ClerkUICtor=class{static version='fixture';async ensureMounted(){return this}updateProps(){}mountComponent(){}unmountComponent(){}destroy(){}};";headers=[...fixtureHeaders.filter(h=>h.name.toLowerCase()!=='content-type'),{name:'content-type',value:'application/javascript'}];}
 else body=JSON.stringify(clerkFixtureResponse(url,p.request.method));
 console.log('CLERK fixture',p.request.method,new URL(url).pathname);
 await call('Fetch.fulfillRequest',{requestId:p.requestId,responseCode:200,responseHeaders:headers,body:Buffer.from(body).toString('base64')});
 }else await call('Fetch.continueRequest',{requestId:p.requestId});
 }
 if(m.method==='Page.screencastFrame'){const p=m.params;const name=`frame-${String(frame++).padStart(6,'0')}.jpg`;fs.writeFileSync(dir+'/frames/'+name,Buffer.from(p.data,'base64'));frames.push({name,time:p.metadata.timestamp});await call('Page.screencastFrameAck',{sessionId:p.sessionId});}
});
await call('Runtime.enable');await call('Log.enable');
await call('Fetch.enable',{patterns:[{urlPattern:'*clerk.fixture.test*'}]});
await call('Page.reload',{ignoreCache:true});console.log('Clerk network fixture ready; production renderer reloaded');
setInterval(async()=>{if(fs.existsSync(dir+'/record.start')&&!recording){recording=true;await call('Page.startScreencast',{format:'jpeg',quality:85,maxWidth:1200,maxHeight:900,everyNthFrame:1});console.log('RECORDING START');}
if(fs.existsSync(dir+'/record.stop')&&recording){recording=false;await call('Page.stopScreencast');fs.writeFileSync(dir+'/frames.json',JSON.stringify(frames));fs.unlinkSync(dir+'/record.start');fs.unlinkSync(dir+'/record.stop');console.log('RECORDING STOP',frames.length);}},300);

After each UI action, run node ui-state-check.mjs <baseline|link-a|link-b|unlink-a|unlink-b>:

import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
import {createHash} from 'node:crypto';
const dir='/tmp/t3code-26464-cloud';
const {services}=JSON.parse(await fs.readFile(dir+'/proof-state.private.json','utf8'));
const stage=process.argv[2];
const expected={'baseline':[false,false],'link-a':[true,false],'link-b':[true,true],'unlink-a':[false,true],'unlink-b':[false,false]}[stage];assert(expected);
const result=[];
for(let i=0;i<2;i++){
 const s=services[i];const r=await fetch(s.origin+'/api/connect/link-state',{headers:{authorization:'Bearer '+s.token}});assert.equal(r.status,200);const state=await r.json();assert.equal(state.linked,expected[i]);assert.equal(state.publishAgentActivity,expected[i]);
 const secrets={};for(const name of ['cloud-linked-user-id','cloud-relay-url','cloud-relay-issuer','cloud-relay-environment-credential','cloud-mint-ed25519-public-key','cloud-publish-agent-activity']){
 try{const b=await fs.readFile(s.baseDir+'/userdata/secrets/'+name+'.bin');secrets[name]=createHash('sha256').update(b).digest('hex');}catch(e){if(e.code!=='ENOENT')throw e;}
 }
 result.push({name:s.name,origin:s.origin,linked:state.linked,publishAgentActivity:state.publishAgentActivity,secrets});
}
const file=dir+'/ui-isolation-state.private.json';let previous=null;try{previous=JSON.parse(await fs.readFile(file,'utf8'));}catch{}
if(previous&&stage==='link-a')assert.deepEqual(result[1],previous[1],'B unchanged while linking A');
if(previous&&stage==='link-b')assert.deepEqual(result[0],previous[0],'A unchanged while linking B');
if(previous&&stage==='unlink-a')assert.deepEqual(result[1],previous[1],'B unchanged while unlinking A');
if(previous&&stage==='unlink-b')assert.deepEqual(result[0],previous[0],'A unchanged while unlinking B');
await fs.writeFile(file,JSON.stringify(result),{mode:0o600});
console.log(stage+' PASS: '+result.map(s=>s.origin+' linked='+s.linked+' publishing='+s.publishAgentActivity).join('; ')+'; other service state and persisted file hashes unchanged.');

Checks and review

vp run check:changed "$FLEET_SOURCE_BASE" passed on this head against d897d3f3f00f2cb534c813e637154d39c8d1cff6: 67 tests across 8 files, 39 affected package typechecks, 15 selected React compiler checks, format, lint, both knip checks and unused patches. The browser regression covers opposing primary and saved-service organization policies and failed with the primary-only lookup restored. The cloud HTTP regression also failed first with the wrong bearer token.

Current-head changed-file command output

Changed-file check output
PR #26464 / Boat changed-file checks
Head: d2bc956caf92ebc74dbe49e2b5952af2e16c1c55
Base: d897d3f3f00f2cb534c813e637154d39c8d1cff6
Job: j_2ny2fvknm4upi24osl5q / managed-policy-rebase-typecheck
Worker: bx_tb4uuesm / large / attempt 1 / exit 0
Command: vp i --frozen-lockfile && vp run check:changed "$FLEET_SOURCE_BASE"

 ✓  @t3tools/web (unit)  src/components/settings/ConnectionsSettings.logic.test.ts (17 tests) 16ms
 ✓  @t3tools/web (browser) (chromium)  src/components/settings/AuthorizedClientsSettings.browser.test.tsx (2 tests) 302ms
 ✓  @t3tools/web (browser) (chromium)  src/cloud/useCloudLinkController.browser.test.tsx (8 tests) 450ms
 ✓  @t3tools/client-runtime  src/state/auth.test.ts (2 tests) 7ms
 ✓  @t3tools/client-runtime  src/state/environmentHttpAuth.test.ts (31 tests) 416ms
 ✓  @t3tools/web (unit)  src/cloud/linkEnvironment.test.ts (1 test) 79ms
 ✓  @t3tools/web (browser) (chromium)  src/components/settings/ScheduledTasksSettings.browser.test.tsx (5 tests) 2169ms
 ✓  t3  src/provider/AgentContextBudget.test.ts (1 test) 213ms
 Test Files  8 passed (8)
      Tests  67 passed (67)
 ✓  @t3tools/web (unit)  src/reactCompilerCoverage.test.ts (1302 tests | 1287 skipped) 463ms
 Test Files  1 passed (1)
      Tests  15 passed | 1287 skipped (1302)
check:changed summary
  pass  unused patches  20.2s
  pass  format  0.5s
  pass  lint  1.7s
  pass  knip 1/2  18.3s
  pass  knip 2/2  32.3s
  pass  typecheck (39 packages)  71.8s
  pass  test (8 files)  62.9s
  pass  react compiler (15 files)  17.7s
  pass not cached: files changed during the run

Ready check also compared this head with live main at 30654b3e91a51937d1dba18fc83af19d8003f5ca. The merge is clean. Since the tested base, the overlapping main changes only trim pasted scheduled-task paths and select scope grants without changing their result. Access selection, cloud operations and pairing paths are unchanged.

GitHub reports 29 passed checks and 8 skipped checks, with none pending or failing. Macroscope approved; the independent Codex review found no open findings. Maria's earlier independent Linux verification also verified pairing a fresh browser and revoking its access at 286d53a8f18ce56c9a431d0ed7bec0f79c4ec711.

Implemented and rebased by Claude Opus 5.5 in Claude Code. Investigated, tested and independently reviewed by GPT-6.1 Sol in Codex through T3 Code.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Oct 28, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at d2bc956

Macroscope's review found this PR approvable — Access settings can now manage saved environments when Local environment is off. Operations retain environment-specific credentials and permission checks, while local backend controls remain limited to the primary environment.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: d897d3f · PR result: d2bc956 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 286d53a and d2bc956.

Files selected for processing: 5
  • apps/web/src/components/cloud/ConnectOnboardingDialog.tsx
  • apps/web/src/components/settings/AuthorizedClientsSettings.browser.test.tsx
  • apps/web/src/components/settings/AuthorizedClientsSettings.tsx
  • apps/web/src/components/settings/ConnectionsSettings.tsx
  • apps/web/src/components/settings/SavedEnvironmentRows.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

Access settings can select an environment and use its session, permissions, and authentication commands. Cloud-link state and operations use the selected environment’s connected HTTP services. The changes also add tests and remote-access documentation.

Changes: Environment-scoped access and cloud links

Layer / File(s) Summary
Connected-environment authentication commands
packages/client-runtime/src/state/environmentHttpAuth.ts, packages/client-runtime/src/state/auth.ts, packages/client-runtime/package.json, apps/web/src/environments/primary/auth.ts, apps/web/src/environments/primary/index.ts, apps/web/src/environments/primary/sessionState.ts, apps/web/src/state/session.ts
The client runtime adds authenticated HTTP requests and pairing or client-management commands for connected environments. The primary-environment pairing, revocation, and session-state helpers are removed. Session state accepts a nullable environment ID.
Environment-scoped cloud-link state and operations
apps/web/src/cloud/cloudLinkState.ts, apps/web/src/cloud/linkEnvironment.ts, apps/web/src/cloud/linkEnvironmentAtoms.ts, apps/web/src/cloud/primaryCloudLinkState.ts, apps/web/src/cloud/linkEnvironment.test.ts
Cloud-link reads and mutations use a target environment’s prepared connection. State, refresh, and cached reads are scoped to an environment. Tests check target-specific origins and bearer tokens.
Environment selection and access management
apps/web/src/components/settings/ConnectionsSettings.logic.ts, apps/web/src/components/settings/ConnectionsSettings.logic.test.ts, apps/web/src/components/settings/ConnectionsSettings.tsx, apps/web/src/components/settings/AuthorizedClientsSettings.tsx, apps/web/src/components/settings/AuthorizedClientsSettings.browser.test.tsx, apps/web/src/components/settings/SettingsScopeSentence.tsx, docs/user/remote-access.md
Access settings resolve a selected environment and use its session state, permissions, and authentication commands. Pairing and client revocation target that environment. The documentation describes selecting an environment and managing a same-machine background service.
Cloud-link controls and settings integration
apps/web/src/cloud/useCloudLinkController.ts, apps/web/src/cloud/useCloudLinkController.browser.test.tsx, apps/web/src/components/cloud/ConnectOnboardingDialog.tsx, apps/web/src/components/settings/SavedEnvironmentRows.tsx, apps/web/src/components/settings/ScheduledTasksSettings.tsx, apps/web/src/components/settings/useAvailableSettingsSearchItems.ts
The cloud-link controller and settings rows receive an environment ID. The onboarding dialog passes the primary environment ID.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant ConnectionsSettings
  participant useEnvironmentSessionState
  participant authEnvironmentCommand
  participant ConnectedEnvironment
  User->>ConnectionsSettings: Select an environment
  ConnectionsSettings->>useEnvironmentSessionState: Read session for selected environment
  ConnectionsSettings->>authEnvironmentCommand: Create pairing credential or revoke client
  authEnvironmentCommand->>ConnectedEnvironment: Send authenticated request
  ConnectedEnvironment-->>authEnvironmentCommand: Return command result
Loading

Suggested reviewers: maria-rcks

Merge Risk: Minimal · up to d2bc9

The change lets Sharing & access work for a connected service when Local environment is off. No blocking risk was found in the reviewed changes. Signed-in T3 Connect and multi-environment selection were not exercised by the author. The T3 Connect onboarding dialog still acts on the primary environment; it behaved that way before this change.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise, conventional, and accurately describes the main change: showing saved-service access when Local environment is off.
Linked Issues check Passed Issue #26420 is the only active directly linked target. Its coding requirement: Settings → Sharing & access must show a connected non-primary service when Local environment is off. The page must offer…
Out of Scope Changes check Passed The whole-PR summary (merge base d897d3f to head d2bc956) shows changes that serve #26420:
- Environment selection and picker in ConnectionsSettings and resolveAccessEnvironmentId.
- Environment-s…
Description check Passed The description is complete and directly addresses the problem, implementation, scope, linked issue, verification steps, evidence, limitations, and review status. It uses custom headings instead of th…
Full details: Linked Issues check

Explanation

Issue #26420 is the only active directly linked target. Its coding requirement: Settings → Sharing & access must show a connected non-primary service when Local environment is off. The page must offer that service's settings and T3 Connect link/unlink controls, not 'No environment selected'.

Evidence at head d2bc956 (whole PR, merge base d897d3f):

  • resolveAccessEnvironmentId in ConnectionsSettings.logic.ts keeps a listed explicit selection. Otherwise it uses the listed primary environment, then a connected listed environment, then the first listed environment. It returns null only when no environments exist. ConnectionsSettings.tsx offers a picker when several are listed.
  • Access queries, session state, scopes, pairing, client revocation and cloud-link rows target the selected environment. SavedEnvironmentRows.tsx gives CloudLinkRow an environmentId, so the T3 Connect row renders for the selected service.
  • Cloud-link requests in linkEnvironment.ts and environment-scoped commands in packages/client-runtime/src/state/auth.ts run through the selected environment's prepared connection (executeConnectedEnvironmentHttpRequest).
  • The rebase adds managed-policy checks that follow the selected service. AuthorizedClientsSettings.browser.test.tsx (read at head) covers the selected service denying remote clients (pairing disabled) and an unmanaged selected service with a managed primary (pairing enabled). This matches the PR description.
  • Unproven at head: signed-in T3 Connect link/unlink and guest-link creation through a live tunnel. The issue requires the UI path and environment-scoped requests, and the code provides both. Live tunnel behavior does not change this verdict. Maria's run covers saved-service access and pairing on head 286d53a. It does not cover the later rebase at d2bc956.
Full details: Out of Scope Changes check

Explanation

The whole-PR summary (merge base d897d3f to head d2bc956) shows changes that serve #26420:

  • Environment selection and picker in ConnectionsSettings and resolveAccessEnvironmentId.
  • Environment-scoped cloud-link state, controller and commands (cloudLinkState.ts, linkEnvironment.ts, linkEnvironmentAtoms.ts, useCloudLinkController.ts, SavedEnvironmentRows.tsx, ConnectOnboardingDialog.tsx passing the primary ID).
  • Environment-scoped pairing and revocation in packages/client-runtime/src/state/auth.ts and environmentHttpAuth.ts. These replace primary-only helpers removed from apps/web/src/environments/primary/auth.ts, index.ts and sessionState.ts.
  • AuthorizedClientsSettings.tsx changes: selected-environment pairing and managed-policy checks for that environment. The rebase moved policy checks from the primary to the selected service, which is part of making pairing for the selected service correct.
  • Primary-only desktop network, Tailscale, WSL and onboarding controls keep the primary target. The PR description and summary state this, and it matches the issue's scope.
  • Comment, import and docs updates (SettingsScopeSentence.tsx, useAvailableSettingsSearchItems.ts, ScheduledTasksSettings.tsx, docs/user/remote-access.md, the environmentHttpAuth package export) and tests for the changed behavior.
    The summary shows no server, server-authorization or native mobile changes. The rebase over PR #27197 did not add a file that the summary lists as unrelated. The server typecheck issue in AgentSessionScanner.ts, if present, is a main-branch condition and not a change of this PR.
Full details: Description check

Explanation

The description is complete and directly addresses the problem, implementation, scope, linked issue, verification steps, evidence, limitations, and review status. It uses custom headings instead of the template headings, but it provides the required information in equivalent sections.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@maria-rcks

maria-rcks commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

Verified head 286d53a8f18ce56c9a431d0ed7bec0f79c4ec711 on a Linux Boat VM. Reproduced on github/main (8b74a69019): real Electron with Local environment off and a connected saved service showed "No environment selected" in Sharing & access. On this PR, the same saved connection shows Permissions, Authorized clients, and Create link.

Used the real dev server, Electron renderer, and repository Playwright with system Chrome. Added a real git project through the app and ran Codex gpt-6.1-sol turns on both revisions. Both read README.md through a terminal; the PR returned PR_RUNTIME_OK. No mocks or injected desktop bridge.

Created a pairing code from saved-service access, paired a fresh Chrome client, then revoked it. The client lost its connection and returned to pairing on reload. Also created/revoked an unused link and verified primary web pairing creation/revocation. Desktop-only network controls remain absent for the saved service.

Actual saved-service requests, credentials omitted:

POST http://127.0.0.1:16733/api/auth/pairing-token -> 200 bearer=true
POST http://127.0.0.1:16733/api/auth/pairing-links/revoke -> 200 bearer=true

vp run check:changed 85bd3f2a2a0ad4a8ddf2950e01db80765e9a1bf4 passed through blacksmith-check: 39 package typechecks, 65 tests across 7 files, 15 selected React compiler checks, format, lint, both knip passes, and unused patches. No code changes.

before: main shows no environment selected with the saved service connected

after: PR shows the saved service access controls

desktop local-off navigation, pairing creation and revocation

real Codex turns read the repository on main and PR

Not verified

Signed-in T3 Connect linking/unlinking, managed tunnels, and guest links require an external test account that was unavailable. Multi-environment selection was not exercised because this pass used one server. macOS, Windows, native iOS and Android were not exercised on this Linux VM.

PR Batch Tester and others added 2 commits November 1, 2026 20:19
…nvironment is off

With Local environment off there is no primary environment, so Sharing &
access showed "No environment selected" even while a background service on
the same machine was connected, and there was no way to link it to T3 Connect.

The page now picks an environment: the primary when there is one, otherwise a
connected saved environment, with a picker when several are connected.
Access changes, pairing links, revocations, session permissions and the
T3 Connect link all run on that environment with this client's own
credentials for it, through the shared authenticated environment HTTP helper.
Desktop network, Tailscale and WSL controls still appear only for the
desktop-managed local environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

size:XXL 1,000+ 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.

Sharing & access shows "No environment selected" when the desktop app connects to a background service with Local environment off

2 participants