agent-relay 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.trajectories/agent-relay-322-324.md +17 -0
  2. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  5. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  6. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  7. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  8. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  9. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  10. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  11. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  12. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  13. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  14. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  15. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  16. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  17. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  18. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  19. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  20. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  21. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  22. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  23. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  24. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  25. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  26. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  27. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  28. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  29. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  30. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  31. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  32. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  33. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  34. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  35. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  36. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  37. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  38. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  39. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  40. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  41. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  42. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  43. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  44. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  45. package/.trajectories/consolidate-settings-panel.md +24 -0
  46. package/.trajectories/gh-cli-user-token.md +26 -0
  47. package/.trajectories/index.json +155 -1
  48. package/deploy/workspace/codex.config.toml +15 -0
  49. package/deploy/workspace/entrypoint.sh +167 -7
  50. package/deploy/workspace/git-credential-relay +17 -2
  51. package/dist/bridge/spawner.d.ts +7 -0
  52. package/dist/bridge/spawner.js +40 -9
  53. package/dist/bridge/types.d.ts +2 -0
  54. package/dist/cli/index.js +210 -168
  55. package/dist/cloud/api/admin.d.ts +8 -0
  56. package/dist/cloud/api/admin.js +212 -0
  57. package/dist/cloud/api/auth.js +8 -0
  58. package/dist/cloud/api/billing.d.ts +0 -10
  59. package/dist/cloud/api/billing.js +248 -58
  60. package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
  61. package/dist/cloud/api/codex-auth-helper.js +215 -8
  62. package/dist/cloud/api/coordinators.js +402 -0
  63. package/dist/cloud/api/daemons.js +15 -11
  64. package/dist/cloud/api/git.js +104 -17
  65. package/dist/cloud/api/github-app.js +42 -8
  66. package/dist/cloud/api/nango-auth.js +297 -16
  67. package/dist/cloud/api/onboarding.js +97 -33
  68. package/dist/cloud/api/providers.js +12 -16
  69. package/dist/cloud/api/repos.js +200 -124
  70. package/dist/cloud/api/test-helpers.js +40 -0
  71. package/dist/cloud/api/usage.js +13 -0
  72. package/dist/cloud/api/webhooks.js +1 -1
  73. package/dist/cloud/api/workspaces.d.ts +18 -0
  74. package/dist/cloud/api/workspaces.js +945 -15
  75. package/dist/cloud/config.d.ts +8 -0
  76. package/dist/cloud/config.js +15 -0
  77. package/dist/cloud/db/drizzle.d.ts +5 -2
  78. package/dist/cloud/db/drizzle.js +27 -20
  79. package/dist/cloud/db/schema.d.ts +19 -51
  80. package/dist/cloud/db/schema.js +5 -4
  81. package/dist/cloud/index.d.ts +0 -1
  82. package/dist/cloud/index.js +0 -1
  83. package/dist/cloud/provisioner/index.d.ts +93 -1
  84. package/dist/cloud/provisioner/index.js +608 -63
  85. package/dist/cloud/server.js +156 -16
  86. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  87. package/dist/cloud/services/compute-enforcement.js +175 -0
  88. package/dist/cloud/services/index.d.ts +2 -0
  89. package/dist/cloud/services/index.js +4 -0
  90. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  91. package/dist/cloud/services/intro-expiration.js +211 -0
  92. package/dist/cloud/services/nango.d.ts +14 -0
  93. package/dist/cloud/services/nango.js +74 -14
  94. package/dist/cloud/services/ssh-security.d.ts +31 -0
  95. package/dist/cloud/services/ssh-security.js +63 -0
  96. package/dist/continuity/manager.d.ts +5 -0
  97. package/dist/continuity/manager.js +56 -2
  98. package/dist/daemon/api.d.ts +2 -0
  99. package/dist/daemon/api.js +214 -5
  100. package/dist/daemon/cli-auth.d.ts +13 -1
  101. package/dist/daemon/cli-auth.js +166 -47
  102. package/dist/daemon/connection.d.ts +7 -1
  103. package/dist/daemon/connection.js +15 -0
  104. package/dist/daemon/orchestrator.d.ts +2 -0
  105. package/dist/daemon/orchestrator.js +26 -0
  106. package/dist/daemon/repo-manager.d.ts +116 -0
  107. package/dist/daemon/repo-manager.js +384 -0
  108. package/dist/daemon/router.d.ts +60 -1
  109. package/dist/daemon/router.js +281 -20
  110. package/dist/daemon/user-directory.d.ts +111 -0
  111. package/dist/daemon/user-directory.js +233 -0
  112. package/dist/dashboard/out/404.html +1 -1
  113. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  114. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  117. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +1 -0
  118. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/{page-3fdfa60e53f2810d.js → page-8553743baca53a00.js} +1 -1
  119. package/dist/dashboard/out/_next/static/chunks/app/app/page-c617745b81344f4f.js +1 -0
  120. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
  121. package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-dc786c183425c2ac.js} +1 -1
  122. package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
  123. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
  124. package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
  125. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +1 -0
  126. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  127. package/dist/dashboard/out/_next/static/sDcbGRTYLcpPvyTs_rsNb/_ssgManifest.js +1 -0
  128. package/dist/dashboard/out/app/onboarding.html +1 -1
  129. package/dist/dashboard/out/app/onboarding.txt +3 -3
  130. package/dist/dashboard/out/app.html +1 -1
  131. package/dist/dashboard/out/app.txt +3 -3
  132. package/dist/dashboard/out/apple-icon.png +0 -0
  133. package/dist/dashboard/out/connect-repos.html +1 -1
  134. package/dist/dashboard/out/connect-repos.txt +2 -2
  135. package/dist/dashboard/out/history.html +1 -1
  136. package/dist/dashboard/out/history.txt +2 -2
  137. package/dist/dashboard/out/index.html +1 -1
  138. package/dist/dashboard/out/index.txt +3 -3
  139. package/dist/dashboard/out/login.html +2 -2
  140. package/dist/dashboard/out/login.txt +2 -2
  141. package/dist/dashboard/out/metrics.html +1 -1
  142. package/dist/dashboard/out/metrics.txt +3 -3
  143. package/dist/dashboard/out/pricing.html +2 -2
  144. package/dist/dashboard/out/pricing.txt +3 -3
  145. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  146. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  147. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  148. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  149. package/dist/dashboard/out/providers.html +1 -1
  150. package/dist/dashboard/out/providers.txt +3 -3
  151. package/dist/dashboard/out/signup.html +2 -2
  152. package/dist/dashboard/out/signup.txt +2 -2
  153. package/dist/dashboard-server/server.js +316 -12
  154. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  155. package/dist/dashboard-server/user-bridge.js +189 -0
  156. package/dist/protocol/channels.d.ts +205 -0
  157. package/dist/protocol/channels.js +154 -0
  158. package/dist/protocol/types.d.ts +13 -1
  159. package/dist/resiliency/provider-context.js +2 -0
  160. package/dist/shared/cli-auth-config.d.ts +19 -0
  161. package/dist/shared/cli-auth-config.js +58 -2
  162. package/dist/utils/agent-config.js +1 -1
  163. package/dist/wrapper/auth-detection.d.ts +49 -0
  164. package/dist/wrapper/auth-detection.js +192 -0
  165. package/dist/wrapper/base-wrapper.d.ts +153 -0
  166. package/dist/wrapper/base-wrapper.js +393 -0
  167. package/dist/wrapper/client.d.ts +7 -1
  168. package/dist/wrapper/client.js +3 -0
  169. package/dist/wrapper/index.d.ts +1 -0
  170. package/dist/wrapper/index.js +4 -3
  171. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  172. package/dist/wrapper/pty-wrapper.js +154 -180
  173. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  174. package/dist/wrapper/tmux-wrapper.js +90 -134
  175. package/package.json +4 -2
  176. package/scripts/postinstall.js +11 -155
  177. package/scripts/test-interactive-terminal.sh +248 -0
  178. package/dist/cloud/vault/index.d.ts +0 -76
  179. package/dist/cloud/vault/index.js +0 -219
  180. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
  182. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
  183. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
  186. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  187. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
  188. package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
  189. /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → sDcbGRTYLcpPvyTs_rsNb}/_buildManifest.js +0 -0
@@ -11,6 +11,7 @@ import { resolveCommand } from '../utils/command-resolver.js';
11
11
  import { PtyWrapper } from '../wrapper/pty-wrapper.js';
12
12
  import { selectShadowCli } from './shadow-cli.js';
13
13
  import { AgentPolicyService } from '../policy/agent-policy.js';
14
+ import { buildClaudeArgs } from '../utils/agent-config.js';
14
15
  /**
15
16
  * Get a minimal relay reminder.
16
17
  * Agents already have full relay docs via CLAUDE.md - this is just a brief reminder.
@@ -192,6 +193,16 @@ export class AgentSpawner {
192
193
  if (isClaudeCli && !args.includes('--dangerously-skip-permissions')) {
193
194
  args.push('--dangerously-skip-permissions');
194
195
  }
196
+ // Apply agent config (model, --agent flag) from .claude/agents/ if available
197
+ // This ensures spawned agents respect their profile settings
198
+ if (isClaudeCli) {
199
+ const configuredArgs = buildClaudeArgs(name, args, this.projectRoot);
200
+ // Replace args with configured version (includes --model and --agent if found)
201
+ args.length = 0;
202
+ args.push(...configuredArgs);
203
+ if (debug)
204
+ console.log(`[spawner:debug] Applied agent config for ${name}: ${args.join(' ')}`);
205
+ }
195
206
  // Add --dangerously-bypass-approvals-and-sandbox for Codex agents
196
207
  const isCodexCli = commandName.startsWith('codex');
197
208
  if (isCodexCli && !args.includes('--dangerously-bypass-approvals-and-sandbox')) {
@@ -216,6 +227,8 @@ export class AgentSpawner {
216
227
  cwd: agentCwd,
217
228
  logsDir: this.logsDir,
218
229
  dashboardPort: this.dashboardPort,
230
+ // Interactive mode - disables auto-accept for auth setup flows
231
+ interactive: request.interactive,
219
232
  // Shadow agent configuration
220
233
  shadowOf: request.shadowOf,
221
234
  shadowSpeakOn: request.shadowSpeakOn,
@@ -302,17 +315,22 @@ export class AgentSpawner {
302
315
  };
303
316
  }
304
317
  // Build the full message: minimal relay reminder + policy instructions (if any) + task
318
+ // Only build message if there's an actual task - empty task means interactive mode
305
319
  let fullMessage = task || '';
306
- // Prepend a brief relay reminder (agents have full docs via CLAUDE.md)
307
- // Note: Previously loaded full 400+ line docs which overwhelmed agents
308
- const relayReminder = getMinimalRelayReminder();
309
- if (relayReminder) {
310
- fullMessage = `${relayReminder}\n\n---\n\n${fullMessage}`;
311
- if (debug)
312
- console.log(`[spawner:debug] Prepended relay reminder for ${name}`);
320
+ // Only prepend relay reminder if we have an actual task
321
+ // Empty task = interactive mode, user will respond to prompts directly
322
+ if (fullMessage.trim()) {
323
+ // Prepend a brief relay reminder (agents have full docs via CLAUDE.md)
324
+ // Note: Previously loaded full 400+ line docs which overwhelmed agents
325
+ const relayReminder = getMinimalRelayReminder();
326
+ if (relayReminder) {
327
+ fullMessage = `${relayReminder}\n\n---\n\n${fullMessage}`;
328
+ if (debug)
329
+ console.log(`[spawner:debug] Prepended relay reminder for ${name}`);
330
+ }
313
331
  }
314
- // Prepend policy instructions if enforcement is enabled
315
- if (this.policyEnforcementEnabled && this.policyService) {
332
+ // Prepend policy instructions if enforcement is enabled (only if we have a task)
333
+ if (fullMessage.trim() && this.policyEnforcementEnabled && this.policyService) {
316
334
  const policyInstruction = await this.policyService.getPolicyInstruction(name);
317
335
  if (policyInstruction) {
318
336
  fullMessage = `${policyInstruction}\n\n${fullMessage}`;
@@ -601,6 +619,19 @@ export class AgentSpawner {
601
619
  return null;
602
620
  return worker.pty.getRawOutput();
603
621
  }
622
+ /**
623
+ * Send input to a worker's PTY (for interactive terminal support)
624
+ * @param name - Worker name
625
+ * @param data - Input data to send (keystrokes, text, etc.)
626
+ * @returns true if input was sent, false if worker not found
627
+ */
628
+ sendWorkerInput(name, data) {
629
+ const worker = this.activeWorkers.get(name);
630
+ if (!worker)
631
+ return false;
632
+ worker.pty.write(data);
633
+ return true;
634
+ }
604
635
  /**
605
636
  * Wait for an agent to appear in the registry (agents.json)
606
637
  */
@@ -41,6 +41,8 @@ export interface SpawnRequest {
41
41
  cwd?: string;
42
42
  /** Name of the agent requesting the spawn (for policy enforcement) */
43
43
  spawnerName?: string;
44
+ /** Interactive mode - disables auto-accept of permission prompts (for auth setup flows) */
45
+ interactive?: boolean;
44
46
  /** Shadow execution mode (subagent = no extra process) */
45
47
  shadowMode?: 'subagent' | 'process';
46
48
  /** Primary agent to shadow (if this agent is a shadow) */
package/dist/cli/index.js CHANGED
@@ -2226,214 +2226,256 @@ program
2226
2226
  console.log(`Profiling ${agentName}... Press Ctrl+C to stop.`);
2227
2227
  });
2228
2228
  // ============================================================================
2229
- // codex-auth - Local OAuth callback helper for Codex/OpenAI authentication
2229
+ // codex-auth - SSH tunnel helper for Codex/OpenAI authentication
2230
2230
  // ============================================================================
2231
2231
  program
2232
2232
  .command('codex-auth')
2233
- .description('Capture Codex OAuth callback locally (run this when connecting Codex in Agent Relay)')
2234
- .option('--token <token>', 'Auth session token from Agent Relay dashboard')
2233
+ .description('Connect Codex via SSH tunnel to workspace (run this when connecting Codex in Agent Relay)')
2234
+ .option('--workspace <id>', 'Workspace ID to connect to')
2235
2235
  .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://agent-relay.com')
2236
- .option('--port <port>', 'Callback port (default: 1455)', '1455')
2236
+ .option('--token <token>', 'CLI authentication token (from dashboard)')
2237
+ .option('--session-cookie <cookie>', 'Session cookie for authentication (deprecated, use --token)')
2237
2238
  .option('--timeout <seconds>', 'Timeout in seconds (default: 300)', '300')
2238
2239
  .action(async (options) => {
2239
- const http = await import('node:http');
2240
- const { URL } = await import('node:url');
2241
- const CALLBACK_PORT = parseInt(options.port, 10);
2242
2240
  const TIMEOUT_MS = parseInt(options.timeout, 10) * 1000;
2243
2241
  const CLOUD_URL = options.cloudUrl.replace(/\/$/, '');
2242
+ const TUNNEL_PORT = 1455;
2244
2243
  // Colors for terminal output
2245
2244
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
2246
2245
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
2247
2246
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
2248
2247
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
2248
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
2249
2249
  console.log('');
2250
2250
  console.log(cyan('═══════════════════════════════════════════════════'));
2251
2251
  console.log(cyan(' Codex Authentication Helper'));
2252
2252
  console.log(cyan('═══════════════════════════════════════════════════'));
2253
2253
  console.log('');
2254
- // Get or create auth session
2255
- let authSessionId = options.token;
2256
- if (!authSessionId) {
2257
- // No token provided - create a session via the API
2258
- console.log('Creating auth session...');
2259
- try {
2260
- const response = await fetch(`${CLOUD_URL}/api/auth/codex-helper/cli-session`, {
2261
- method: 'POST',
2262
- headers: { 'Content-Type': 'application/json' },
2254
+ if (!options.workspace) {
2255
+ console.log(red('Missing --workspace parameter.'));
2256
+ console.log('');
2257
+ console.log('To connect Codex, follow these steps:');
2258
+ console.log('');
2259
+ console.log(' 1. Go to the Agent Relay dashboard');
2260
+ console.log(' 2. Click "Connect with Codex" (Settings → AI Providers)');
2261
+ console.log(' 3. Copy the command shown (it includes the workspace ID and token)');
2262
+ console.log(' 4. Run the command in your terminal');
2263
+ console.log('');
2264
+ console.log('The command will look like:');
2265
+ console.log(cyan(' npx agent-relay codex-auth --workspace=<ID> --token=<TOKEN>'));
2266
+ console.log('');
2267
+ process.exit(1);
2268
+ }
2269
+ const workspaceId = options.workspace;
2270
+ console.log(`Workspace: ${workspaceId.slice(0, 8)}...`);
2271
+ // Get tunnel info from cloud API
2272
+ console.log('Getting workspace connection info...');
2273
+ const headers = { 'Content-Type': 'application/json' };
2274
+ if (options.sessionCookie) {
2275
+ headers['Cookie'] = options.sessionCookie;
2276
+ }
2277
+ // Validate token is provided
2278
+ if (!options.token && !options.sessionCookie) {
2279
+ console.log(red('Missing --token parameter.'));
2280
+ console.log('');
2281
+ console.log('The token is provided by the dashboard when you click "Connect with Codex".');
2282
+ console.log('Copy the complete command from the dashboard and paste it here.');
2283
+ console.log('');
2284
+ process.exit(1);
2285
+ }
2286
+ let tunnelInfo;
2287
+ try {
2288
+ // Build URL with token query parameter
2289
+ const tunnelInfoUrl = new URL(`${CLOUD_URL}/api/auth/codex-helper/tunnel-info/${workspaceId}`);
2290
+ if (options.token) {
2291
+ tunnelInfoUrl.searchParams.set('token', options.token);
2292
+ }
2293
+ const response = await fetch(tunnelInfoUrl.toString(), {
2294
+ method: 'GET',
2295
+ headers,
2296
+ credentials: 'include',
2297
+ });
2298
+ if (!response.ok) {
2299
+ const errorData = await response.json();
2300
+ console.log(red(`Failed to get tunnel info: ${errorData.error || response.statusText}`));
2301
+ process.exit(1);
2302
+ }
2303
+ tunnelInfo = await response.json();
2304
+ }
2305
+ catch (err) {
2306
+ console.log(red(`Failed to connect to cloud API: ${err instanceof Error ? err.message : String(err)}`));
2307
+ process.exit(1);
2308
+ }
2309
+ console.log(`Workspace: ${cyan(tunnelInfo.workspaceName)}`);
2310
+ console.log('');
2311
+ // Establish SSH tunnel using ssh2 library (no external tools needed)
2312
+ console.log(yellow('Establishing SSH tunnel...'));
2313
+ console.log(dim(` SSH: ${tunnelInfo.host}:${tunnelInfo.port}`));
2314
+ console.log(dim(` Tunnel: localhost:${TUNNEL_PORT} → workspace:${tunnelInfo.tunnelPort}`));
2315
+ console.log('');
2316
+ const { Client } = await import('ssh2');
2317
+ const net = await import('node:net');
2318
+ const sshClient = new Client();
2319
+ // Use object to hold server reference (avoids TypeScript narrowing issues)
2320
+ const tunnel = { server: null };
2321
+ let tunnelReady = false;
2322
+ let tunnelError = null;
2323
+ // Create a promise that resolves when tunnel is ready or rejects on error
2324
+ const tunnelPromise = new Promise((resolve, reject) => {
2325
+ sshClient.on('ready', () => {
2326
+ // Create local server that forwards connections through SSH
2327
+ tunnel.server = net.createServer((localSocket) => {
2328
+ sshClient.forwardOut('127.0.0.1', TUNNEL_PORT, 'localhost', tunnelInfo.tunnelPort, (err, stream) => {
2329
+ if (err) {
2330
+ localSocket.end();
2331
+ return;
2332
+ }
2333
+ localSocket.pipe(stream).pipe(localSocket);
2334
+ });
2263
2335
  });
2264
- if (!response.ok) {
2265
- const error = await response.json().catch(() => ({}));
2266
- console.log('');
2267
- console.log(red('Failed to create auth session.'));
2268
- console.log('');
2269
- if (response.status === 401) {
2270
- console.log('To use this command, run it with a token from the Agent Relay dashboard:');
2271
- console.log('');
2272
- console.log(cyan(' npx agent-relay codex-auth --token=<TOKEN>'));
2273
- console.log('');
2274
- console.log('Get the token from: Settings → Connect Codex → "Use CLI helper"');
2336
+ tunnel.server.on('error', (err) => {
2337
+ if (err.code === 'EADDRINUSE') {
2338
+ tunnelError = `Port ${TUNNEL_PORT} is already in use. Close any other applications using this port.`;
2275
2339
  }
2276
2340
  else {
2277
- console.log(`Error: ${error.error || response.statusText}`);
2341
+ tunnelError = err.message;
2278
2342
  }
2279
- process.exit(1);
2343
+ reject(new Error(tunnelError));
2344
+ });
2345
+ tunnel.server.listen(TUNNEL_PORT, '127.0.0.1', () => {
2346
+ tunnelReady = true;
2347
+ resolve();
2348
+ });
2349
+ });
2350
+ sshClient.on('error', (err) => {
2351
+ if (err.message.includes('Authentication')) {
2352
+ tunnelError = 'SSH authentication failed. Check the password.';
2280
2353
  }
2281
- const data = await response.json();
2282
- authSessionId = data.authSessionId;
2283
- }
2284
- catch (err) {
2285
- console.log(red('Failed to connect to Agent Relay Cloud.'));
2286
- console.log(`URL: ${CLOUD_URL}`);
2287
- console.log(`Error: ${err instanceof Error ? err.message : String(err)}`);
2288
- process.exit(1);
2289
- }
2290
- }
2291
- console.log(`Session: ${authSessionId?.slice(0, 8)}...`);
2292
- console.log(`Listening on port: ${cyan(String(CALLBACK_PORT))}`);
2293
- console.log('');
2294
- // Success HTML page
2295
- const successHtml = `<!DOCTYPE html>
2296
- <html>
2297
- <head>
2298
- <title>Authentication Successful</title>
2299
- <style>
2300
- body { font-family: -apple-system, sans-serif; background: #0a0f1a; color: #e2e8f0;
2301
- display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
2302
- .card { background: #1a1f2e; padding: 2rem; border-radius: 12px; text-align: center;
2303
- border: 1px solid rgba(0, 212, 255, 0.3); max-width: 400px; }
2304
- h1 { color: #00d4ff; margin-bottom: 1rem; }
2305
- p { color: #94a3b8; margin: 0.5rem 0; }
2306
- </style>
2307
- </head>
2308
- <body>
2309
- <div class="card">
2310
- <h1>✓ Authentication Successful!</h1>
2311
- <p>Your Codex account is now connected.</p>
2312
- <p style="margin-top: 1rem;">You can close this window.</p>
2313
- </div>
2314
- </body>
2315
- </html>`;
2316
- // Create HTTP server to capture callback
2317
- let authCode = null;
2318
- let serverClosed = false;
2319
- const server = http.createServer(async (req, res) => {
2320
- const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
2321
- if (url.pathname === '/auth/callback') {
2322
- const code = url.searchParams.get('code');
2323
- const error = url.searchParams.get('error');
2324
- if (error) {
2325
- res.writeHead(400, { 'Content-Type': 'text/plain' });
2326
- res.end(`OAuth Error: ${error}`);
2327
- console.log(red(`OAuth error: ${error}`));
2328
- return;
2354
+ else if (err.message.includes('ECONNREFUSED')) {
2355
+ tunnelError = `Cannot connect to SSH server at ${tunnelInfo.host}:${tunnelInfo.port}. Is the workspace running and SSH enabled?`;
2329
2356
  }
2330
- if (code) {
2331
- authCode = code;
2332
- res.writeHead(200, { 'Content-Type': 'text/html' });
2333
- res.end(successHtml);
2334
- console.log('');
2335
- console.log(green('✓ Received OAuth callback!'));
2357
+ else if (err.message.includes('ENOTFOUND') || err.message.includes('getaddrinfo')) {
2358
+ tunnelError = `Cannot resolve hostname: ${tunnelInfo.host}. Check network connectivity.`;
2359
+ }
2360
+ else if (err.message.includes('ETIMEDOUT')) {
2361
+ tunnelError = `Connection timed out to ${tunnelInfo.host}:${tunnelInfo.port}. Is the workspace running?`;
2336
2362
  }
2337
2363
  else {
2338
- res.writeHead(400, { 'Content-Type': 'text/plain' });
2339
- res.end('Missing authorization code');
2364
+ tunnelError = `SSH error: ${err.message}`;
2340
2365
  }
2341
- }
2342
- else if (url.pathname === '/favicon.ico') {
2343
- res.writeHead(404);
2344
- res.end();
2345
- }
2346
- else {
2347
- res.writeHead(200, { 'Content-Type': 'text/plain' });
2348
- res.end('Waiting for OAuth callback on /auth/callback...');
2349
- }
2350
- });
2351
- // Handle server errors
2352
- server.on('error', (err) => {
2353
- if (err.code === 'EADDRINUSE') {
2354
- console.log(red(`Port ${CALLBACK_PORT} is already in use.`));
2355
- console.log('');
2356
- console.log('Another process may be using this port.');
2357
- console.log('Close it and try again, or specify a different port with --port');
2358
- process.exit(1);
2359
- }
2360
- throw err;
2361
- });
2362
- // Start listening
2363
- await new Promise((resolve) => {
2364
- server.listen(CALLBACK_PORT, () => {
2365
- resolve();
2366
+ reject(new Error(tunnelError));
2367
+ });
2368
+ sshClient.on('close', () => {
2369
+ if (!tunnelReady) {
2370
+ // Only set error if not already set by error handler
2371
+ if (!tunnelError) {
2372
+ tunnelError = `SSH connection to ${tunnelInfo.host}:${tunnelInfo.port} closed unexpectedly. The workspace may not have SSH enabled or the port may be blocked.`;
2373
+ }
2374
+ reject(new Error(tunnelError));
2375
+ }
2376
+ });
2377
+ // Connect to SSH server
2378
+ sshClient.connect({
2379
+ host: tunnelInfo.host,
2380
+ port: tunnelInfo.port,
2381
+ username: tunnelInfo.user,
2382
+ password: tunnelInfo.password,
2383
+ readyTimeout: 10000,
2384
+ // Disable host key checking for simplicity (workspace containers)
2385
+ hostVerifier: () => true,
2366
2386
  });
2367
2387
  });
2368
- console.log(green('Server ready!'));
2369
- console.log('');
2370
- console.log(yellow('Next steps:'));
2371
- console.log(' 1. Go to the Agent Relay dashboard');
2372
- console.log(' 2. Click "Open Codex Login Page" to start OAuth');
2373
- console.log(' 3. Sign in with your OpenAI account');
2374
- console.log(' 4. The callback will be captured automatically');
2388
+ // Wait for tunnel to establish
2389
+ try {
2390
+ await Promise.race([
2391
+ tunnelPromise,
2392
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SSH connection timeout')), 15000)),
2393
+ ]);
2394
+ }
2395
+ catch (err) {
2396
+ console.log(red(`Failed to establish tunnel: ${err instanceof Error ? err.message : String(err)}`));
2397
+ sshClient.end();
2398
+ process.exit(1);
2399
+ }
2400
+ console.log(green('✓ SSH tunnel established!'));
2375
2401
  console.log('');
2376
- console.log(cyan(`Waiting for callback... (timeout: ${options.timeout}s)`));
2377
- // Wait for callback or timeout
2378
- const startTime = Date.now();
2379
- while (!authCode && !serverClosed && (Date.now() - startTime) < TIMEOUT_MS) {
2380
- await new Promise(resolve => setTimeout(resolve, 1000));
2381
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
2382
- if (elapsed > 0 && elapsed % 30 === 0) {
2383
- console.log(` Still waiting... (${elapsed}s)`);
2402
+ // Handle Ctrl+C gracefully
2403
+ const cleanup = () => {
2404
+ console.log('');
2405
+ console.log(dim('Shutting down...'));
2406
+ if (tunnel.server) {
2407
+ tunnel.server.close();
2384
2408
  }
2409
+ sshClient.end();
2410
+ process.exit(0);
2411
+ };
2412
+ process.on('SIGINT', cleanup);
2413
+ process.on('SIGTERM', cleanup);
2414
+ // Display the OAuth URL
2415
+ if (tunnelInfo.authUrl) {
2416
+ console.log('');
2417
+ console.log(green('Ready! Open this URL in your browser to complete authentication:'));
2418
+ console.log('');
2419
+ console.log(cyan(tunnelInfo.authUrl));
2420
+ console.log('');
2421
+ console.log(dim('The browser will redirect to localhost:1455, which tunnels to the workspace.'));
2422
+ console.log(dim('The Codex CLI in the workspace will receive the callback and complete auth.'));
2423
+ console.log('');
2385
2424
  }
2386
- // Close server
2387
- server.close();
2388
- serverClosed = true;
2389
- if (!authCode) {
2425
+ else {
2390
2426
  console.log('');
2391
- console.log(red('Timeout waiting for OAuth callback.'));
2427
+ console.log(yellow('OAuth URL not available. Please start authentication from the dashboard.'));
2392
2428
  console.log('');
2393
- console.log('If you completed sign-in, the callback may have failed.');
2394
- console.log('Try copying the localhost URL from your browser and pasting');
2395
- console.log('it into the Agent Relay dashboard manually.');
2396
- process.exit(1);
2397
2429
  }
2398
- // Send code to cloud API
2399
- console.log('Sending auth code to Agent Relay...');
2400
- try {
2401
- const response = await fetch(`${CLOUD_URL}/api/auth/codex-helper/callback`, {
2402
- method: 'POST',
2403
- headers: { 'Content-Type': 'application/json' },
2404
- body: JSON.stringify({
2405
- authSessionId,
2406
- code: authCode,
2407
- }),
2408
- });
2409
- const data = await response.json();
2410
- if (response.ok && data.success) {
2411
- console.log('');
2412
- console.log(green('═══════════════════════════════════════════════════'));
2413
- console.log(green(' Authentication Complete!'));
2414
- console.log(green('═══════════════════════════════════════════════════'));
2415
- console.log('');
2416
- console.log('Your Codex account is now connected to Agent Relay.');
2417
- console.log('You can close this terminal and return to the dashboard.');
2418
- console.log('');
2430
+ // Poll for authentication completion
2431
+ console.log(cyan(`Waiting for authentication... (timeout: ${options.timeout}s)`));
2432
+ const startTime = Date.now();
2433
+ let authenticated = false;
2434
+ while (!authenticated && (Date.now() - startTime) < TIMEOUT_MS) {
2435
+ await new Promise(resolve => setTimeout(resolve, 3000));
2436
+ try {
2437
+ // Build URL with token for authentication
2438
+ const authStatusUrl = new URL(`${CLOUD_URL}/api/auth/codex-helper/auth-status/${workspaceId}`);
2439
+ if (options.token) {
2440
+ authStatusUrl.searchParams.set('token', options.token);
2441
+ }
2442
+ const statusResponse = await fetch(authStatusUrl.toString(), { method: 'GET', headers, credentials: 'include' });
2443
+ if (statusResponse.ok) {
2444
+ const statusData = await statusResponse.json();
2445
+ if (statusData.authenticated) {
2446
+ authenticated = true;
2447
+ }
2448
+ }
2419
2449
  }
2420
- else {
2421
- console.log('');
2422
- console.log(red('Failed to send auth code to Agent Relay.'));
2423
- console.log(`Error: ${data.error || 'Unknown error'}`);
2424
- console.log('');
2425
- console.log('You can try pasting this code manually in the dashboard:');
2426
- console.log(cyan(authCode));
2427
- process.exit(1);
2450
+ catch {
2451
+ // Ignore polling errors
2452
+ }
2453
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
2454
+ if (!authenticated && elapsed > 0 && elapsed % 30 === 0) {
2455
+ console.log(` Still waiting... (${elapsed}s)`);
2428
2456
  }
2429
2457
  }
2430
- catch (err) {
2458
+ // Cleanup SSH tunnel
2459
+ if (tunnel.server) {
2460
+ tunnel.server.close();
2461
+ }
2462
+ sshClient.end();
2463
+ if (authenticated) {
2464
+ console.log('');
2465
+ console.log(green('═══════════════════════════════════════════════════'));
2466
+ console.log(green(' Authentication Complete!'));
2467
+ console.log(green('═══════════════════════════════════════════════════'));
2468
+ console.log('');
2469
+ console.log('Your Codex account is now connected to the workspace.');
2470
+ console.log('You can close this terminal and return to the dashboard.');
2471
+ console.log('');
2472
+ }
2473
+ else {
2431
2474
  console.log('');
2432
- console.log(red('Failed to connect to Agent Relay Cloud.'));
2433
- console.log(`Error: ${err instanceof Error ? err.message : String(err)}`);
2475
+ console.log(red('Timeout waiting for authentication.'));
2434
2476
  console.log('');
2435
- console.log('You can try pasting this code manually in the dashboard:');
2436
- console.log(cyan(authCode));
2477
+ console.log('If you completed sign-in, the workspace may not have received');
2478
+ console.log('the callback. Check if the SSH tunnel was working correctly.');
2437
2479
  process.exit(1);
2438
2480
  }
2439
2481
  });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Admin API Routes
3
+ *
4
+ * Administrative endpoints for managing workspaces at scale.
5
+ * Protected by admin secret (ADMIN_API_SECRET environment variable).
6
+ */
7
+ export declare const adminRouter: import("express-serve-static-core").Router;
8
+ //# sourceMappingURL=admin.d.ts.map