nterminal 1.2.76 → 1.2.77

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 (54) hide show
  1. package/.env.example +3 -0
  2. package/README.md +1 -1
  3. package/dist/client/assets/MarkdownPreview-D5NthhhG.js +1 -0
  4. package/dist/client/assets/{TranscriptMarkdownBody-DwgFO6m9.js → TranscriptMarkdownBody-BkGu6YVq.js} +1 -1
  5. package/dist/client/assets/{index-73oYtL4l.js → index-Bgqck-vt.js} +1 -1
  6. package/dist/client/assets/{index-DGAIhvX2.js → index-gSePR0Ob.js} +35 -35
  7. package/dist/client/index.html +1 -1
  8. package/dist/scripts/onboarding.js +106 -41
  9. package/dist/scripts/onboarding.js.map +1 -1
  10. package/dist/server/agent/agentProxy.d.ts +2 -2
  11. package/dist/server/agent/agentProxy.js.map +1 -1
  12. package/dist/server/agent/agentWebSocketProxy.d.ts +2 -2
  13. package/dist/server/agent/agentWebSocketProxy.js.map +1 -1
  14. package/dist/server/agent/outboundAgentClient.d.ts +21 -0
  15. package/dist/server/agent/outboundAgentClient.js +237 -0
  16. package/dist/server/agent/outboundAgentClient.js.map +1 -0
  17. package/dist/server/agent/outboundAgentRegistry.d.ts +25 -0
  18. package/dist/server/agent/outboundAgentRegistry.js +190 -0
  19. package/dist/server/agent/outboundAgentRegistry.js.map +1 -0
  20. package/dist/server/agent/socketWebSocketRelay.d.ts +6 -0
  21. package/dist/server/agent/socketWebSocketRelay.js +107 -0
  22. package/dist/server/agent/socketWebSocketRelay.js.map +1 -0
  23. package/dist/server/config.d.ts +3 -0
  24. package/dist/server/config.js +33 -0
  25. package/dist/server/config.js.map +1 -1
  26. package/dist/server/http.d.ts +1 -0
  27. package/dist/server/http.js +18 -2
  28. package/dist/server/http.js.map +1 -1
  29. package/dist/server/index.js +14 -2
  30. package/dist/server/index.js.map +1 -1
  31. package/dist/server/notifications/agentCompletionMonitor.d.ts +4 -1
  32. package/dist/server/notifications/agentCompletionMonitor.js +6 -1
  33. package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
  34. package/dist/server/routes/agentLinkRoutes.d.ts +9 -0
  35. package/dist/server/routes/agentLinkRoutes.js +129 -0
  36. package/dist/server/routes/agentLinkRoutes.js.map +1 -0
  37. package/dist/server/routes/agentManagementRoutes.d.ts +2 -0
  38. package/dist/server/routes/agentManagementRoutes.js +129 -75
  39. package/dist/server/routes/agentManagementRoutes.js.map +1 -1
  40. package/dist/server/runtimeState.js +9 -0
  41. package/dist/server/runtimeState.js.map +1 -1
  42. package/dist/server/storage/fileStore.d.ts +2 -1
  43. package/dist/server/storage/fileStore.js +7 -1
  44. package/dist/server/storage/fileStore.js.map +1 -1
  45. package/dist/shared/agentLinkProtocol.d.ts +26 -0
  46. package/dist/shared/agentLinkProtocol.js +49 -0
  47. package/dist/shared/agentLinkProtocol.js.map +1 -0
  48. package/dist/shared/protocol.d.ts +11 -2
  49. package/dist/shared/protocol.js.map +1 -1
  50. package/docs/configuration.md +3 -0
  51. package/docs/onboarding.md +9 -12
  52. package/package.json +1 -1
  53. package/scripts/nterminalctl +21 -0
  54. package/dist/client/assets/MarkdownPreview-DTx6dhOx.js +0 -1
@@ -8,6 +8,7 @@ import { requireAllowedOrigin } from './authRoutes.js';
8
8
  import { authenticateTerminalRequest } from './terminalRoutes.js';
9
9
  import { proxyAgentRequest } from '../agent/agentProxy.js';
10
10
  import { proxyAgentWebSocket } from '../agent/agentWebSocketProxy.js';
11
+ import { OutboundAgentRegistry } from '../agent/outboundAgentRegistry.js';
11
12
  import { collectSystemStats } from '../system/stats.js';
12
13
  import { createManualUpdateInfo, isPermissionUpdateFailure } from '../../shared/manualUpdate.js';
13
14
  import { MAIN_SERVER_ID } from '../../shared/protocol.js';
@@ -28,6 +29,7 @@ const hopByHopHeaders = new Set([
28
29
  'upgrade'
29
30
  ]);
30
31
  export async function registerAgentManagementRoutes(app, config, services) {
32
+ const outboundAgentRegistry = services.outboundAgentRegistry ?? new OutboundAgentRegistry();
31
33
  // List agents
32
34
  app.get('/api/agents', async (request, reply) => {
33
35
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
@@ -35,7 +37,13 @@ export async function registerAgentManagementRoutes(app, config, services) {
35
37
  return reply;
36
38
  const state = await services.fileStore.read();
37
39
  return {
38
- agents: state.agents.map(({ id, name, url }) => ({ id, name, url })),
40
+ agents: state.agents.map(({ id, name, url, transport }) => ({
41
+ id,
42
+ name,
43
+ url,
44
+ ...(transport === 'outbound' ? { transport } : {}),
45
+ ...(transport === 'outbound' ? { connected: outboundAgentRegistry.status(id).connected } : {})
46
+ })),
39
47
  serverOrder: state.serverOrder
40
48
  };
41
49
  });
@@ -69,22 +77,29 @@ export async function registerAgentManagementRoutes(app, config, services) {
69
77
  if (!auth)
70
78
  return reply;
71
79
  const { name, url, token } = request.body ?? {};
80
+ const transport = request.body?.transport === 'outbound' ? 'outbound' : 'direct';
72
81
  if (typeof name !== 'string' || !name.trim())
73
82
  return reply.code(400).send({ error: 'name_required' });
74
- if (typeof url !== 'string' || !url.trim())
83
+ if (request.body?.transport !== undefined && request.body.transport !== 'direct' && request.body.transport !== 'outbound') {
84
+ return reply.code(400).send({ error: 'invalid_transport' });
85
+ }
86
+ if (transport === 'direct' && (typeof url !== 'string' || !url.trim()))
75
87
  return reply.code(400).send({ error: 'url_required' });
76
88
  if (typeof token !== 'string' || token.length < 32)
77
89
  return reply.code(400).send({ error: 'token_too_short' });
78
- const normalizedUrl = normalizeAgentUrl(url);
90
+ const normalizedUrl = transport === 'direct' ? normalizeAgentUrl(url) : { ok: true, url: null };
79
91
  if (!normalizedUrl.ok) {
80
92
  return reply.code(400).send({ error: normalizedUrl.error });
81
93
  }
82
94
  const agentName = name.trim();
83
95
  let agent = null;
84
96
  let created = false;
97
+ const removedAgentIds = [];
85
98
  await services.fileStore.update((state) => {
86
99
  const tokenMatchIndex = state.agents.findIndex((a) => a.token === token);
87
- const urlMatchIndex = state.agents.findIndex((a) => a.url === normalizedUrl.url);
100
+ const urlMatchIndex = normalizedUrl.url
101
+ ? state.agents.findIndex((a) => a.transport === 'direct' && a.url === normalizedUrl.url)
102
+ : -1;
88
103
  const existingIndex = tokenMatchIndex >= 0 ? tokenMatchIndex : urlMatchIndex;
89
104
  if (existingIndex === -1) {
90
105
  created = true;
@@ -92,6 +107,7 @@ export async function registerAgentManagementRoutes(app, config, services) {
92
107
  id: randomUUID(),
93
108
  name: agentName,
94
109
  url: normalizedUrl.url,
110
+ transport,
95
111
  token,
96
112
  createdAt: new Date().toISOString()
97
113
  };
@@ -103,14 +119,14 @@ export async function registerAgentManagementRoutes(app, config, services) {
103
119
  ...existing,
104
120
  name: agentName,
105
121
  url: normalizedUrl.url,
122
+ transport,
106
123
  token
107
124
  };
108
- const removedAgentIds = [];
109
125
  const agents = state.agents.flatMap((current, index) => {
110
126
  if (index === existingIndex) {
111
127
  return [agent];
112
128
  }
113
- if (current.url === normalizedUrl.url || current.token === token) {
129
+ if ((normalizedUrl.url !== null && current.transport === 'direct' && current.url === normalizedUrl.url) || current.token === token) {
114
130
  removedAgentIds.push(current.id);
115
131
  return [];
116
132
  }
@@ -142,7 +158,19 @@ export async function registerAgentManagementRoutes(app, config, services) {
142
158
  }
143
159
  return { ...state, agents, serverOrder, agentLayouts, agentManualUpdates };
144
160
  }, { flush: 'immediate' });
145
- return reply.code(created ? 201 : 200).send({ agent: { id: agent.id, name: agent.name, url: agent.url } });
161
+ for (const removedAgentId of removedAgentIds)
162
+ outboundAgentRegistry.disconnect(removedAgentId);
163
+ if (agent.transport === 'direct')
164
+ outboundAgentRegistry.disconnect(agent.id);
165
+ return reply.code(created ? 201 : 200).send({
166
+ agent: {
167
+ id: agent.id,
168
+ name: agent.name,
169
+ url: agent.url,
170
+ ...(agent.transport === 'outbound' ? { transport: agent.transport } : {}),
171
+ ...(agent.transport === 'outbound' ? { connected: outboundAgentRegistry.status(agent.id).connected } : {})
172
+ }
173
+ });
146
174
  });
147
175
  app.put('/api/agents/order', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
148
176
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
@@ -183,7 +211,13 @@ export async function registerAgentManagementRoutes(app, config, services) {
183
211
  return nextAgentIds.shift();
184
212
  });
185
213
  }
186
- reorderedAgents = agents.map(({ id, name, url }) => ({ id, name, url }));
214
+ reorderedAgents = agents.map(({ id, name, url, transport }) => ({
215
+ id,
216
+ name,
217
+ url,
218
+ ...(transport === 'outbound' ? { transport } : {}),
219
+ ...(transport === 'outbound' ? { connected: outboundAgentRegistry.status(id).connected } : {})
220
+ }));
187
221
  return { ...state, agents, serverOrder };
188
222
  }, { flush: 'immediate' });
189
223
  if (!validOrder || !reorderedAgents || !serverOrder) {
@@ -222,6 +256,7 @@ export async function registerAgentManagementRoutes(app, config, services) {
222
256
  const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
223
257
  if (!found)
224
258
  return reply.code(404).send({ error: 'agent_not_found' });
259
+ outboundAgentRegistry.disconnect(request.params.agentId);
225
260
  return reply.code(204).send();
226
261
  });
227
262
  app.post('/api/agents/:agentId/uninstall', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
@@ -230,13 +265,14 @@ export async function registerAgentManagementRoutes(app, config, services) {
230
265
  return reply;
231
266
  if (config.previewMode)
232
267
  return reply.code(403).send({ error: 'preview_mode' });
233
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
268
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
234
269
  if (!agent)
235
- return reply.code(404).send({ error: 'agent_not_found' });
270
+ return reply;
236
271
  const uninstall = await requestAgentUninstall(agent);
237
272
  const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
238
273
  if (!found)
239
274
  return reply.code(404).send({ error: 'agent_not_found' });
275
+ outboundAgentRegistry.disconnect(request.params.agentId);
240
276
  return {
241
277
  ok: true,
242
278
  message: uninstall.message,
@@ -258,9 +294,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
258
294
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
259
295
  if (!auth)
260
296
  return reply;
261
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
297
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
262
298
  if (!agent)
263
- return reply.code(404).send({ error: 'agent_not_found' });
299
+ return reply;
264
300
  return proxyAgentRequest(agent, '/stats', request, reply, {
265
301
  timeoutMs: 2500,
266
302
  transformJson: (json, status) => {
@@ -278,10 +314,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
278
314
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
279
315
  if (!auth)
280
316
  return reply;
281
- const state = await services.fileStore.read();
282
- const agent = state.agents.find((a) => a.id === request.params.agentId);
317
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
283
318
  if (!agent)
284
- return reply.code(404).send({ error: 'agent_not_found' });
319
+ return reply;
285
320
  const start = Date.now();
286
321
  try {
287
322
  const res = await fetch(`${agent.url}/api/agent/terminals`, {
@@ -301,9 +336,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
301
336
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
302
337
  if (!auth)
303
338
  return reply;
304
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
339
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
305
340
  if (!agent)
306
- return reply.code(404).send({ error: 'agent_not_found' });
341
+ return reply;
307
342
  return proxyAgentRequest(agent, '/version', request, reply, {
308
343
  transformJson: async (json) => {
309
344
  if (!isVersionInfoBody(json)) {
@@ -322,9 +357,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
322
357
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
323
358
  if (!auth)
324
359
  return reply;
325
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
360
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
326
361
  if (!agent)
327
- return reply.code(404).send({ error: 'agent_not_found' });
362
+ return reply;
328
363
  return proxyAgentRequest(agent, '/update/check', request, reply, {
329
364
  transformJson: async (json) => {
330
365
  if (isUpdateCheckBody(json) && !json.error && json.updateAvailable === false) {
@@ -341,9 +376,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
341
376
  if (config.previewMode) {
342
377
  return { ok: false, changed: false, message: 'Agent updates are disabled in preview mode.', restarting: false };
343
378
  }
344
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
379
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
345
380
  if (!agent)
346
- return reply.code(404).send({ error: 'agent_not_found' });
381
+ return reply;
347
382
  return proxyAgentRequest(agent, '/update', request, reply, {
348
383
  transformJson: async (json) => {
349
384
  if (!isUpdateResponseBody(json)) {
@@ -372,9 +407,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
372
407
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
373
408
  if (!auth)
374
409
  return reply;
375
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
410
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
376
411
  if (!agent)
377
- return reply.code(404).send({ error: 'agent_not_found' });
412
+ return reply;
378
413
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
379
414
  return proxyAgentRequest(agent, `/code-server/status${query}`, request, reply, {
380
415
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
@@ -389,9 +424,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
389
424
  // on the production agent, so preview blocks them all.
390
425
  if (config.previewMode)
391
426
  return reply.code(403).send({ error: 'preview_mode' });
392
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
427
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
393
428
  if (!agent)
394
- return reply.code(404).send({ error: 'agent_not_found' });
429
+ return reply;
395
430
  return proxyAgentRequest(agent, '/code-server/install', request, reply, {
396
431
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
397
432
  });
@@ -402,9 +437,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
402
437
  return reply;
403
438
  if (config.previewMode)
404
439
  return reply.code(403).send({ error: 'preview_mode' });
405
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
440
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
406
441
  if (!agent)
407
- return reply.code(404).send({ error: 'agent_not_found' });
442
+ return reply;
408
443
  return proxyAgentRequest(agent, '/code-server/update', request, reply, {
409
444
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
410
445
  });
@@ -415,9 +450,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
415
450
  return reply;
416
451
  if (config.previewMode)
417
452
  return reply.code(403).send({ error: 'preview_mode' });
418
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
453
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
419
454
  if (!agent)
420
- return reply.code(404).send({ error: 'agent_not_found' });
455
+ return reply;
421
456
  return proxyAgentRequest(agent, '/code-server/open', request, reply, {
422
457
  bodyOverride: {
423
458
  ...(request.body ?? {}),
@@ -432,9 +467,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
432
467
  return reply;
433
468
  if (config.previewMode)
434
469
  return reply.code(403).send({ error: 'preview_mode' });
435
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
470
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
436
471
  if (!agent)
437
- return reply.code(404).send({ error: 'agent_not_found' });
472
+ return reply;
438
473
  return proxyAgentRequest(agent, '/code-server/stop', request, reply, {
439
474
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
440
475
  });
@@ -445,9 +480,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
445
480
  return reply;
446
481
  if (config.previewMode)
447
482
  return reply.code(403).send({ error: 'preview_mode' });
448
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
483
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
449
484
  if (!agent)
450
- return reply.code(404).send({ error: 'agent_not_found' });
485
+ return reply;
451
486
  return proxyAgentRequest(agent, '/code-server/uninstall', request, reply, {
452
487
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
453
488
  });
@@ -457,18 +492,18 @@ export async function registerAgentManagementRoutes(app, config, services) {
457
492
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
458
493
  if (!auth)
459
494
  return reply;
460
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
495
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
461
496
  if (!agent)
462
- return reply.code(404).send({ error: 'agent_not_found' });
497
+ return reply;
463
498
  return proxyAgentRequest(agent, '/terminals', request, reply);
464
499
  });
465
500
  app.get('/api/agents/:agentId/terminals/:terminalId/history', async (request, reply) => {
466
501
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
467
502
  if (!auth)
468
503
  return reply;
469
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
504
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
470
505
  if (!agent)
471
- return reply.code(404).send({ error: 'agent_not_found' });
506
+ return reply;
472
507
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
473
508
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/history${query}`, request, reply);
474
509
  });
@@ -476,9 +511,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
476
511
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
477
512
  if (!auth)
478
513
  return reply;
479
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
514
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
480
515
  if (!agent)
481
- return reply.code(404).send({ error: 'agent_not_found' });
516
+ return reply;
482
517
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
483
518
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-history${query}`, request, reply, {
484
519
  unavailableFallback: {
@@ -494,9 +529,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
494
529
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
495
530
  if (!auth)
496
531
  return reply;
497
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
532
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
498
533
  if (!agent)
499
- return reply.code(404).send({ error: 'agent_not_found' });
534
+ return reply;
500
535
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-source`, request, reply, {
501
536
  unavailableFallback: { terminalId: request.params.terminalId, source: null }
502
537
  });
@@ -506,9 +541,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
506
541
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
507
542
  if (!auth)
508
543
  return reply;
509
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
544
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
510
545
  if (!agent)
511
- return reply.code(404).send({ error: 'agent_not_found' });
546
+ return reply;
512
547
  return proxyAgentRequest(agent, '/terminals', request, reply);
513
548
  });
514
549
  // Proxy: close terminal
@@ -516,9 +551,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
516
551
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
517
552
  if (!auth)
518
553
  return reply;
519
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
554
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
520
555
  if (!agent)
521
- return reply.code(404).send({ error: 'agent_not_found' });
556
+ return reply;
522
557
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
523
558
  });
524
559
  // Proxy: rename terminal
@@ -526,9 +561,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
526
561
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
527
562
  if (!auth)
528
563
  return reply;
529
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
564
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
530
565
  if (!agent)
531
- return reply.code(404).send({ error: 'agent_not_found' });
566
+ return reply;
532
567
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
533
568
  });
534
569
  // Proxy: terminal WebSocket
@@ -542,13 +577,12 @@ export async function registerAgentManagementRoutes(app, config, services) {
542
577
  socket.close(1008, 'unauthorized');
543
578
  return;
544
579
  }
545
- const state = await services.fileStore.read();
546
- const agent = state.agents.find((a) => a.id === request.params.agentId);
547
- if (!agent) {
548
- socket.close(1008, 'agent not found');
580
+ const resolution = await resolveAgentConnection(services.fileStore, outboundAgentRegistry, request.params.agentId);
581
+ if (resolution.status !== 'resolved') {
582
+ closeAgentWebSocket(socket, resolution.status);
549
583
  return;
550
584
  }
551
- proxyAgentWebSocket(socket, agent, request.params.terminalId);
585
+ proxyAgentWebSocket(socket, resolution.agent, request.params.terminalId);
552
586
  });
553
587
  // Proxy: file routes (all file operations)
554
588
  const filePathParts = ['root', 'list', 'read', 'write', 'create', 'move', 'delete-preview', 'delete', 'open', 'preview'];
@@ -557,9 +591,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
557
591
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
558
592
  if (!auth)
559
593
  return reply;
560
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
594
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
561
595
  if (!agent)
562
- return reply.code(404).send({ error: 'agent_not_found' });
596
+ return reply;
563
597
  return proxyAgentRequest(agent, `/files/${part}`, request, reply);
564
598
  });
565
599
  }
@@ -567,14 +601,13 @@ export async function registerAgentManagementRoutes(app, config, services) {
567
601
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
568
602
  if (!auth)
569
603
  return reply;
570
- if (!await resolveAgent(services.fileStore, request.params.agentId)) {
571
- return reply.code(404).send({ error: 'agent_not_found' });
572
- }
604
+ if (!await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply))
605
+ return reply;
573
606
  return reply.redirect(`${agentCodeServerPublicPrefix(request.params.agentId, request.params.instanceId)}/`, 302);
574
607
  });
575
- await registerAgentCodeServerProxyRoutes(app, config, services);
608
+ await registerAgentCodeServerProxyRoutes(app, config, services, outboundAgentRegistry);
576
609
  }
577
- async function registerAgentCodeServerProxyRoutes(app, config, services) {
610
+ async function registerAgentCodeServerProxyRoutes(app, config, services, outboundAgentRegistry) {
578
611
  await app.register(async (proxyApp) => {
579
612
  registerStreamingBodyParser(proxyApp);
580
613
  proxyApp.route({
@@ -587,9 +620,9 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
587
620
  return reply;
588
621
  if (!isProxyOriginAcceptable(config, request))
589
622
  return reply.code(403).send({ error: 'origin_not_allowed' });
590
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
623
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
591
624
  if (!agent)
592
- return reply.code(404).send({ error: 'agent_not_found' });
625
+ return reply;
593
626
  proxyAgentCodeServerHttp(agent, request, reply);
594
627
  return reply;
595
628
  },
@@ -603,12 +636,12 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
603
636
  socket.close(1008, 'unauthorized');
604
637
  return;
605
638
  }
606
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
607
- if (!agent) {
608
- socket.close(1008, 'agent not found');
639
+ const resolution = await resolveAgentConnection(services.fileStore, outboundAgentRegistry, request.params.agentId);
640
+ if (resolution.status !== 'resolved') {
641
+ closeAgentWebSocket(socket, resolution.status);
609
642
  return;
610
643
  }
611
- proxyAgentCodeServerWebSocket(agent, socket, request);
644
+ proxyAgentCodeServerWebSocket(resolution.agent, socket, request);
612
645
  }
613
646
  });
614
647
  for (const method of httpProxyMethods) {
@@ -622,9 +655,9 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
622
655
  return reply;
623
656
  if (!isProxyOriginAcceptable(config, request))
624
657
  return reply.code(403).send({ error: 'origin_not_allowed' });
625
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
658
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
626
659
  if (!agent)
627
- return reply.code(404).send({ error: 'agent_not_found' });
660
+ return reply;
628
661
  proxyAgentCodeServerHttp(agent, request, reply);
629
662
  return reply;
630
663
  }
@@ -774,12 +807,10 @@ function rewriteAgentCodeServerLocation(value, agent, agentId, instanceId) {
774
807
  }
775
808
  try {
776
809
  const parsed = new URL(value);
777
- const agentOrigin = new URL(agent.url).origin;
778
- if (parsed.origin === agentOrigin) {
779
- const rewritten = rewriteAgentCodeServerLocation(`${parsed.pathname}${parsed.search}${parsed.hash}`, agent, agentId, instanceId);
780
- if (rewritten !== `${parsed.pathname}${parsed.search}${parsed.hash}`) {
781
- return rewritten;
782
- }
810
+ const pathAndSuffix = `${parsed.pathname}${parsed.search}${parsed.hash}`;
811
+ const rewritten = rewriteAgentCodeServerLocation(pathAndSuffix, agent, agentId, instanceId);
812
+ if (rewritten !== pathAndSuffix) {
813
+ return rewritten;
783
814
  }
784
815
  }
785
816
  catch {
@@ -863,9 +894,32 @@ function websocketProtocols(value) {
863
894
  function isRecord(value) {
864
895
  return value !== null && typeof value === 'object' && !Array.isArray(value);
865
896
  }
866
- async function resolveAgent(fileStore, agentId) {
897
+ async function resolveAgent(fileStore, outboundAgentRegistry, agentId, reply) {
898
+ const resolution = await resolveAgentConnection(fileStore, outboundAgentRegistry, agentId);
899
+ if (resolution.status === 'resolved')
900
+ return resolution.agent;
901
+ if (resolution.status === 'not_found') {
902
+ reply?.code(404).send({ error: 'agent_not_found' });
903
+ }
904
+ else {
905
+ reply?.code(502).send({ error: 'agent_unreachable' });
906
+ }
907
+ return null;
908
+ }
909
+ async function resolveAgentConnection(fileStore, outboundAgentRegistry, agentId) {
867
910
  const state = await fileStore.read();
868
- return state.agents.find((a) => a.id === agentId) ?? null;
911
+ const agent = state.agents.find((candidate) => candidate.id === agentId);
912
+ if (!agent)
913
+ return { status: 'not_found' };
914
+ const resolved = outboundAgentRegistry.resolve(agent);
915
+ return resolved ? { status: 'resolved', agent: resolved } : { status: 'unreachable' };
916
+ }
917
+ function closeAgentWebSocket(socket, status) {
918
+ if (status === 'not_found') {
919
+ socket.close(1008, 'agent not found');
920
+ return;
921
+ }
922
+ socket.close(1013, 'agent temporarily unavailable');
869
923
  }
870
924
  function pickPreferredAgentLayout(current, candidate) {
871
925
  if (!current)