feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(relay): Add managed relay tunnels and APN service - #2837

Merged
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra
Jun 5, 2026
Merged

feat(relay): Add managed relay tunnels and APN service#2837
juliusmarminge merged 66 commits into
mainfrom
codex/relay-managed-tunnels-auth-infra

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented May 28, 2026

Copy link
Copy Markdown
Member

Stack

Summary

This stacked draft PR adds the relay-managed tunnel and cloud authentication work on top of the mobile remote-runtime PR. General collection/performance rewrites from #2854 and the TypeScript/Effect tooling base are now on main.

  • add the relay worker/infrastructure package, persistence, APNs delivery, managed endpoint provisioning, observability, migrations, and tests
  • add standards-oriented relay authentication: DPoP proof handling, JWT/JWS signing and verification, OAuth-style token exchange/scopes, replay protections, and environment proof flows
  • add shared client-runtime/contracts/shared modules for managed relay operation across web and Expo mobile clients
  • add web, desktop, and mobile cloud linking and managed-environment flows, including mobile agent-awareness/live-activity registration
  • route relay-specific hashing and randomness through effect/Crypto while retaining Expo-compatible implementations

Validation

  • bun fmt
  • bun lint (passes with 8 existing web warnings)
  • bun lint:mobile
  • bun typecheck
  • cd infra/relay && bun run test (103 passed, 5 skipped)
  • cd apps/mobile && bun run test (135 passed)
  • cd apps/web && bun run test (1005 passed)
  • cd apps/server && bun run test (1075 passed, 4 skipped)

Rebase Note

General collection/performance rewrites from #2854 are now merged into main; mobile command metadata, pairing-URL redaction, and shared-runtime Crypto cleanup remain in #2013. This PR retains the managed-relay changes to the mobile connection contract and runtime above those inherited lower-layer changes.


Note

High Risk
Touches authentication (Clerk OAuth, protocol callbacks, token storage), new production relay deploy, and release pipeline env wiring; mobile raises minimum iOS to 18.0.

Overview
Adds T3 Cloud as an optional, config-gated product path: root .env.example documents public Clerk/relay settings, and CI gains a production relay deploy on main plus a release job that resolves relay URL and Clerk keys into desktop, CLI, and Vercel web builds.

Desktop gains end-to-end cloud sign-in: custom URL schemes (t3code / t3code-dev), macOS launcher/protocol registration for dev, DesktopCloudAuth (state-validated callbacks, single-instance routing), encrypted Clerk JWT storage, and IPC that proxies only allowed Clerk Frontend API hosts.

Mobile integrates Clerk (CloudAuthProvider), a Settings stack (environments, waitlist, T3 Cloud connect rows), agent push notification deep-linking, Live Activity preferences synced via relay when signed in, Expo widgets/notifications plugins, and iOS deployment target 18.0. Saved environments can record relayManaged metadata.

Docs and tooling shifts: README/AGENTS/mobile README describe optional cloud setup; desktop dev launcher and window navigation send off-origin OAuth to the system browser.

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

Note

Add managed relay tunnels with DPoP auth, APNs live activity delivery, and cloud CLI commands

  • Introduces a managed relay system where mobile and web clients connect to server environments via Cloudflare tunnels using DPoP-bound tokens; adds ManagedRelayClient, ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)
  • Adds a relay Cloudflare Worker (infra/relay) with HTTP APIs for environment linking, credential issuance, agent awareness publishing, and APNs delivery of live activity updates and push notifications to mobile devices
  • Extends server auth (EnvironmentAuth, SessionStore, PairingGrantStore) to support DPoP-bound access tokens with replay prevention via a proof_key_thumbprint claim and per-request DPoP proof verification
  • Adds t3 cloud CLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public config
  • Adds desktop Clerk integration: a fetch proxy routing Clerk Frontend API calls through the desktop bridge, OAuth sign-in flow with native callback handling, and encrypted JWT token storage
  • Adds mobile settings screens for cloud/waitlist enrollment, environments management, and agent awareness notification permissions including iOS Live Activities widget support
  • Deploys relay via a new GitHub Actions workflow on push to main; release pipeline now resolves and injects Clerk/relay public config into all build and deploy steps
  • Risk: DPoP proof verification adds a secret-store write (replay guard) on every authenticated request; consuming bootstrap credentials now requires a matching proofKeyThumbprint or fails, which is a breaking change for existing pairing flows that do not supply it

Macroscope summarized ee69e93.

@coderabbitai

coderabbitaiBot commented May 28, 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

Run ID: 5ae0a773-2770-4d4a-982e-05b54e8f6bf5

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
  • Commit unit tests in branch codex/relay-managed-tunnels-auth-infra

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 28, 2026
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadapps/mobile/src/app/settings/index.tsx
Comment threadpackages/client-runtime/src/remote.ts Outdated
Comment threadapps/server/src/cloud/http.ts Outdated
Comment threadinfra/relay/src/api.ts Outdated
Comment threadinfra/relay/src/services/EnvironmentConnector.ts
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/cloud/ManagedEndpointRuntime.ts
Comment threadapps/desktop/src/app/DesktopCloudAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 8480c92 to e3ab348CompareMay 28, 2026 08:13
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from a7ed828 to b868feeCompareMay 28, 2026 08:16
Comment threadapps/mobile/src/features/cloud/linkEnvironment.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from e3ab348 to 436b1b9CompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from b868fee to 589e2edCompareMay 28, 2026 16:38
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 436b1b9 to d20a8ceCompareMay 28, 2026 16:46
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from 63a525d to 8027af0CompareMay 28, 2026 17:41
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch 2 times, most recently from 6c0e54d to f15e2baCompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8027af0 to 1a912f6CompareMay 28, 2026 18:00
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from f15e2ba to 71e0186CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 1a912f6 to 90bf2b3CompareMay 28, 2026 18:14
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 71e0186 to e721336CompareMay 28, 2026 19:50
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch 2 times, most recently from e63e3f4 to ba9802dCompareMay 28, 2026 20:26
Comment threadinfra/relay/src/environments/EnvironmentCredentials.ts
Comment threadapps/server/src/auth/dpop.ts Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopClerk.tsx Outdated
Comment threadapps/web/src/cloud/desktopAuth.ts
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 22e103a to 60b7d8dCompareMay 28, 2026 21:01
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from ba9802d to 8789910CompareMay 28, 2026 21:02
Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated
Comment threadapps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Comment threadinfra/relay/src/observability/Metrics.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-remote-connect branch from 60b7d8d to ee4ec05CompareMay 28, 2026 21:42
@juliusmarminge
juliusmarmingeforce-pushed the codex/relay-managed-tunnels-auth-infra branch from 8789910 to f7ac694CompareMay 28, 2026 21:43
juliusmarmingeand others added 20 commits June 4, 2026 14:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OAuth callback ignored cold start
    • Added process.argv scanning in configure after event handler registration, using a new matchCloudAuthCallbackRoute helper (scheme/host/path only, no state validation) to detect and forward cold-start callback URLs directly to the renderer via IPC on Windows/Linux.

Create PR

Or push these changes by commenting:

@cursor push 4ee9127816
Preview (4ee9127816)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.test.ts b/apps/desktop/src/app/DesktopCloudAuth.test.ts--- a/apps/desktop/src/app/DesktopCloudAuth.test.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.test.ts@@ -299,4 +299,34 @@
}).pipe(Effect.provide(harness.layer), Effect.scoped);
},
);
++ it.effect("dispatches cold-start callback URL from process.argv on Windows/Linux", () => {+ const callbackUrl =+ "t3code://auth/callback?t3_state=prev-session-state&rotating_token_nonce=nonce-1";+ const originalArgv = process.argv;+ process.argv = ["electron", callbackUrl];+ const harness = makeHarness({ isDevelopment: false });++ return Effect.gen(function* () {+ const cloudAuth = yield* DesktopCloudAuth.DesktopCloudAuth;+ yield* cloudAuth.configure;+ yield* flushCloudAuthDispatch;++ assert.deepEqual(harness.sends, [+ {+ channel: IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ args: [callbackUrl],+ },+ ]);+ assert.lengthOf(harness.reveals, 1);+ }).pipe(+ Effect.ensuring(+ Effect.sync(() => {+ process.argv = originalArgv;+ }),+ ),+ Effect.provide(harness.layer),+ Effect.scoped,+ );+ });
});
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -66,10 +66,9 @@
return url.toString();
}
-export function parseCloudAuthCallbackUrl(input: {+export function matchCloudAuthCallbackRoute(input: {
readonly rawUrl: unknown;
readonly scheme: string;
- readonly state: string;
}): URL | null {
if (typeof input.rawUrl !== "string") {
return null;
@@ -80,13 +79,23 @@
if (url.protocol !== `${input.scheme}:`) return null;
if (url.hostname !== CLOUD_AUTH_CALLBACK_HOST) return null;
if (url.pathname !== CLOUD_AUTH_CALLBACK_PATHNAME) return null;
- if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;
return url;
} catch {
return null;
}
}
+export function parseCloudAuthCallbackUrl(input: {+ readonly rawUrl: unknown;+ readonly scheme: string;+ readonly state: string;+}): URL | null {+ const url = matchCloudAuthCallbackRoute(input);+ if (!url) return null;+ if (url.searchParams.get(CLOUD_AUTH_CALLBACK_STATE_PARAM) !== input.state) return null;+ return url;+}+
export function findCloudAuthCallbackUrl(input: {
readonly values: readonly unknown[];
readonly scheme: string;
@@ -323,6 +332,28 @@
);
},
);
++ // On Windows/Linux cold start, the protocol callback URL is delivered+ // via process.argv rather than a second-instance or open-url event.+ const coldStartValues = resolveProtocolClientLaunchArgs({ argv: process.argv });+ for (const value of coldStartValues) {+ const coldStartUrl = matchCloudAuthCallbackRoute({ rawUrl: value, scheme });+ if (!coldStartUrl) continue;+ pendingAuthRequest = closeCloudAuthRequest(pendingAuthRequest);+ void runPromise(+ Effect.gen(function* () {+ yield* electronWindow.sendAll(+ IpcChannels.CLOUD_AUTH_CALLBACK_CHANNEL,+ coldStartUrl.toString(),+ );+ const mainWindow = yield* electronWindow.currentMainOrFirst;+ if (Option.isSome(mainWindow)) {+ yield* electronWindow.reveal(mainWindow.value);+ }+ }),+ );+ break;+ }
}).pipe(Effect.withSpan("desktop.cloudAuth.configure")),
});
});

You can send follow-ups to the cloud agent here.

Comment threadapps/desktop/src/app/DesktopCloudAuth.ts

@cursorcursorBot 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 and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate instance continues startup
    • Replaced electronApp.quit (async graceful shutdown) with electronApp.exit(0) (immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.

Create PR

Or push these changes by commenting:

@cursor push 1dbb6b6ec0
Preview (1dbb6b6ec0)
diff --git a/apps/desktop/src/app/DesktopCloudAuth.ts b/apps/desktop/src/app/DesktopCloudAuth.ts--- a/apps/desktop/src/app/DesktopCloudAuth.ts+++ b/apps/desktop/src/app/DesktopCloudAuth.ts@@ -292,7 +292,7 @@
const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
- return yield* electronApp.quit;+ return yield* electronApp.exit(0);
}
yield* electronApp.on<[Electron.Event, string]>("open-url", (event, rawUrl) => {

You can send follow-ups to the cloud agent here.

const hasInstanceLock = yield* electronApp.requestSingleInstanceLock;
if (!hasInstanceLock) {
return yield* electronApp.quit;
}

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.

Duplicate instance continues startup

Medium Severity

When requestSingleInstanceLock is false, configure only calls app.quit() and returns. Startup in DesktopApp still proceeds to whenReady and backend bootstrap, so a duplicate desktop process can briefly run two servers or windows instead of exiting immediately.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 640dc63. Configure here.

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Disabled live activities still sync
    • Added setLocalLiveActivitiesEnabled export that is called when the user toggles the preference, and added initializeLiveActivityPreferenceState to hydrate the in-memory flag from stored preferences on app startup before environments connect.

Create PR

Or push these changes by commenting:

@cursor push a98d0e596a
Preview (a98d0e596a)
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityController.ts b/apps/mobile/src/features/agent-awareness/liveActivityController.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityController.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityController.ts@@ -598,6 +598,10 @@
return error instanceof Error && error.message.includes("Can't find live activity with id:");
}
+export function setLocalLiveActivitiesEnabled(enabled: boolean): void {+ localLiveActivitiesEnabled = enabled;+}+
export function __resetAgentLiveActivitiesForTest(): void {
localLiveActivitiesEnabled = true;
activeActivity = null;
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts@@ -8,7 +8,7 @@
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { setLiveActivityUpdatesEnabled } from "./liveActivityPreferences";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
@@ -22,6 +22,7 @@
vi.mock("./liveActivityController", () => ({
endAllAgentLiveActivities: vi.fn(() => Effect.void),
+ setLocalLiveActivitiesEnabled: vi.fn(),
}));
vi.mock("./remoteRegistration", () => ({
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts@@ -3,11 +3,18 @@
import { ManagedRelayClient } from "@t3tools/client-runtime";
import type { SavedRemoteConnection } from "../../lib/connection";
-import { savePreferencesPatch } from "../../lib/storage";+import { loadPreferences, savePreferencesPatch } from "../../lib/storage";
import { linkEnvironmentToCloud } from "../cloud/linkEnvironment";
-import { endAllAgentLiveActivities } from "./liveActivityController";+import { endAllAgentLiveActivities, setLocalLiveActivitiesEnabled } from "./liveActivityController";
import { refreshAgentAwarenessRegistration } from "./remoteRegistration";
+export async function initializeLiveActivityPreferenceState(): Promise<void> {+ const preferences = await loadPreferences();+ if (preferences.liveActivitiesEnabled === false) {+ setLocalLiveActivitiesEnabled(false);+ }+}+
export function setLiveActivityUpdatesEnabled(input: {
readonly enabled: boolean;
readonly clerkToken: string | null;
@@ -19,6 +26,8 @@
catch: (error) => error,
});
+ setLocalLiveActivitiesEnabled(input.enabled);+
if (!input.enabled) {
yield* endAllAgentLiveActivities();
}
diff --git a/apps/mobile/src/state/use-remote-environment-registry.ts b/apps/mobile/src/state/use-remote-environment-registry.ts--- a/apps/mobile/src/state/use-remote-environment-registry.ts+++ b/apps/mobile/src/state/use-remote-environment-registry.ts@@ -59,6 +59,7 @@
stopAgentAwarenessForEnvironment,
stopAllAgentAwareness,
} from "../features/agent-awareness/shellLiveActivitySync";
+import { initializeLiveActivityPreferenceState } from "../features/agent-awareness/liveActivityPreferences";
import { environmentRuntimeManager, useEnvironmentRuntimeStates } from "./use-environment-runtime";
import {
clearCachedShellSnapshotMetadata,
@@ -565,6 +566,11 @@
return;
}
+ await initializeLiveActivityPreferenceState();+ if (cancelled) {+ return;+ }+
replaceSavedConnections(
Object.fromEntries(
connections.map((connection) => [connection.environmentId, connection]),

You can send follow-ups to the cloud agent here.

Comment threadapps/mobile/src/features/agent-awareness/liveActivityController.ts Outdated

@cursorcursorBot 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 and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Shared dev protocol scheme collision
    • Made APP_PROTOCOL_SCHEMES per-worktree using devBundleIdSuffix (matching APP_BUNDLE_ID), passed the scheme to Electron via T3CODE_DESKTOP_PROTOCOL_SCHEME env var, updated DesktopCloudAuth to use the override, and updated the launcher script URL pattern to use the dynamic scheme.

Create PR

Or push these changes by commenting:

@cursor push 4d22a1f898

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

"if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);",
].join(" "),
]);
}

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.

Shared dev protocol scheme collision

Medium Severity

Development builds give each worktree a unique bundle id but still register the same t3code-dev URL scheme and call LSSetDefaultHandlerForURLScheme for it. macOS keeps one default handler per scheme, so the last dev launch steals cloud OAuth callbacks from other local worktrees.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit ee69e93. Configure here.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR 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

@juliusmarminge