feat(relay): Add managed relay tunnels and APN service#2837
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
554a1d2 to
8480c92
Compare
65c36c0 to
a7ed828
Compare
8480c92 to
e3ab348
Compare
a7ed828 to
b868fee
Compare
e3ab348 to
436b1b9
Compare
b868fee to
589e2ed
Compare
436b1b9 to
d20a8ce
Compare
63a525d to
8027af0
Compare
6c0e54d to
f15e2ba
Compare
8027af0 to
1a912f6
Compare
f15e2ba to
71e0186
Compare
1a912f6 to
90bf2b3
Compare
71e0186 to
e721336
Compare
e63e3f4 to
ba9802d
Compare
22e103a to
60b7d8d
Compare
ba9802d to
8789910
Compare
60b7d8d to
ee4ec05
Compare
8789910 to
f7ac694
Compare
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>
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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) withelectronApp.exit(0)(immediate process termination) so the duplicate instance cannot proceed with backend bootstrap or window creation.
- Replaced
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; | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 640dc63. Configure here.
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 7 total unresolved issues (including 6 from previous reviews).
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.
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(" "), | ||
| ]); | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit ee69e93. Configure here.



Stack
codex/collection-performance-refactors->main(merged)t3code/mobile-remote-connect->maincodex/relay-managed-tunnels-auth-infra->t3code/mobile-remote-connectSummary
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.effect/Cryptowhile retaining Expo-compatible implementationsValidation
bun fmtbun lint(passes with 8 existing web warnings)bun lint:mobilebun typecheckcd infra/relay && bun run test(103passed,5skipped)cd apps/mobile && bun run test(135passed)cd apps/web && bun run test(1005passed)cd apps/server && bun run test(1075passed,4skipped)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.exampledocuments public Clerk/relay settings, and CI gains a production relay deploy onmainplus 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 recordrelayManagedmetadata.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
ManagedRelayClient,ManagedRelayDpopSigner, and platform-specific crypto/signer layers for mobile (Expo Crypto) and web (WebCrypto/IndexedDB)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 devicesEnvironmentAuth,SessionStore,PairingGrantStore) to support DPoP-bound access tokens with replay prevention via aproof_key_thumbprintclaim and per-request DPoP proof verificationt3 cloudCLI commands (status, link, auth) with relay client install/management via a bundled cloudflared binary; the CLI is conditionally exposed based on build-time public configproofKeyThumbprintor fails, which is a breaking change for existing pairing flows that do not supply itMacroscope summarized ee69e93.