nterminal 1.2.75 → 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 (85) 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-B-RKybd7.js → TranscriptMarkdownBody-BkGu6YVq.js} +1 -1
  5. package/dist/client/assets/index-B3_mYQzf.css +1 -0
  6. package/dist/client/assets/{index-CH_JmAuw.js → index-Bgqck-vt.js} +1 -1
  7. package/dist/client/assets/index-gSePR0Ob.js +57 -0
  8. package/dist/client/index.html +2 -2
  9. package/dist/scripts/onboarding.js +106 -41
  10. package/dist/scripts/onboarding.js.map +1 -1
  11. package/dist/server/agent/agentProxy.d.ts +2 -2
  12. package/dist/server/agent/agentProxy.js.map +1 -1
  13. package/dist/server/agent/agentWebSocketProxy.d.ts +2 -2
  14. package/dist/server/agent/agentWebSocketProxy.js.map +1 -1
  15. package/dist/server/agent/outboundAgentClient.d.ts +21 -0
  16. package/dist/server/agent/outboundAgentClient.js +237 -0
  17. package/dist/server/agent/outboundAgentClient.js.map +1 -0
  18. package/dist/server/agent/outboundAgentRegistry.d.ts +25 -0
  19. package/dist/server/agent/outboundAgentRegistry.js +190 -0
  20. package/dist/server/agent/outboundAgentRegistry.js.map +1 -0
  21. package/dist/server/agent/socketWebSocketRelay.d.ts +6 -0
  22. package/dist/server/agent/socketWebSocketRelay.js +107 -0
  23. package/dist/server/agent/socketWebSocketRelay.js.map +1 -0
  24. package/dist/server/auth/authService.js +4 -1
  25. package/dist/server/auth/authService.js.map +1 -1
  26. package/dist/server/code/codeServerManager.d.ts +7 -1
  27. package/dist/server/code/codeServerManager.js +95 -38
  28. package/dist/server/code/codeServerManager.js.map +1 -1
  29. package/dist/server/config.d.ts +3 -0
  30. package/dist/server/config.js +33 -0
  31. package/dist/server/config.js.map +1 -1
  32. package/dist/server/http.d.ts +1 -0
  33. package/dist/server/http.js +18 -2
  34. package/dist/server/http.js.map +1 -1
  35. package/dist/server/index.js +21 -2
  36. package/dist/server/index.js.map +1 -1
  37. package/dist/server/notifications/agentCompletionMonitor.d.ts +4 -1
  38. package/dist/server/notifications/agentCompletionMonitor.js +23 -4
  39. package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
  40. package/dist/server/notifications/completionNotificationRouter.d.ts +2 -1
  41. package/dist/server/notifications/completionNotificationRouter.js +4 -2
  42. package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
  43. package/dist/server/notifications/pushNotificationService.d.ts +20 -2
  44. package/dist/server/notifications/pushNotificationService.js +129 -14
  45. package/dist/server/notifications/pushNotificationService.js.map +1 -1
  46. package/dist/server/routes/agentLinkRoutes.d.ts +9 -0
  47. package/dist/server/routes/agentLinkRoutes.js +129 -0
  48. package/dist/server/routes/agentLinkRoutes.js.map +1 -0
  49. package/dist/server/routes/agentManagementRoutes.d.ts +2 -0
  50. package/dist/server/routes/agentManagementRoutes.js +147 -101
  51. package/dist/server/routes/agentManagementRoutes.js.map +1 -1
  52. package/dist/server/routes/codeServerRoutes.js +28 -31
  53. package/dist/server/routes/codeServerRoutes.js.map +1 -1
  54. package/dist/server/routes/httpProxyBody.d.ts +10 -0
  55. package/dist/server/routes/httpProxyBody.js +73 -0
  56. package/dist/server/routes/httpProxyBody.js.map +1 -0
  57. package/dist/server/routes/pushNotificationRoutes.js +31 -2
  58. package/dist/server/routes/pushNotificationRoutes.js.map +1 -1
  59. package/dist/server/runtimeState.js +9 -0
  60. package/dist/server/runtimeState.js.map +1 -1
  61. package/dist/server/storage/fileStore.d.ts +2 -1
  62. package/dist/server/storage/fileStore.js +7 -1
  63. package/dist/server/storage/fileStore.js.map +1 -1
  64. package/dist/server/terminal/TerminalManager.d.ts +2 -1
  65. package/dist/server/terminal/TerminalManager.js +27 -7
  66. package/dist/server/terminal/TerminalManager.js.map +1 -1
  67. package/dist/server/terminal/agentTranscriptStatus.d.ts +2 -2
  68. package/dist/server/terminal/agentTranscriptStatus.js +224 -38
  69. package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
  70. package/dist/server/update/packageUpdate.d.ts +1 -0
  71. package/dist/server/update/packageUpdate.js +34 -2
  72. package/dist/server/update/packageUpdate.js.map +1 -1
  73. package/dist/shared/agentLinkProtocol.d.ts +26 -0
  74. package/dist/shared/agentLinkProtocol.js +49 -0
  75. package/dist/shared/agentLinkProtocol.js.map +1 -0
  76. package/dist/shared/protocol.d.ts +18 -3
  77. package/dist/shared/protocol.js.map +1 -1
  78. package/dist/shared/types.d.ts +3 -1
  79. package/docs/configuration.md +3 -0
  80. package/docs/onboarding.md +9 -12
  81. package/package.json +1 -1
  82. package/scripts/nterminalctl +21 -0
  83. package/dist/client/assets/MarkdownPreview-CStdB1d5.js +0 -1
  84. package/dist/client/assets/index-DwmyYdjx.css +0 -1
  85. package/dist/client/assets/index-idJa2lVq.js +0 -57
@@ -8,11 +8,13 @@ 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';
14
15
  import { clearAgentManualUpdate, readAgentManualUpdate, rememberAgentManualUpdate } from '../update/manualUpdateState.js';
15
16
  import { relayWebSocketPair } from './webSocketRelay.js';
17
+ import { attachProxyAbortHandling, forwardProxyRequestBody, proxyRequestBody, proxyRequestBodyLength, registerStreamingBodyParser, rejectOversizedProxyBody } from './httpProxyBody.js';
16
18
  const agentCodeServerProxyRoute = '/code/agents/:agentId/instances/:instanceId/*';
17
19
  const agentCodeServerProxyBodyLimitBytes = 128 * 1024 * 1024;
18
20
  const httpProxyMethods = ['HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
@@ -27,6 +29,7 @@ const hopByHopHeaders = new Set([
27
29
  'upgrade'
28
30
  ]);
29
31
  export async function registerAgentManagementRoutes(app, config, services) {
32
+ const outboundAgentRegistry = services.outboundAgentRegistry ?? new OutboundAgentRegistry();
30
33
  // List agents
31
34
  app.get('/api/agents', async (request, reply) => {
32
35
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
@@ -34,7 +37,13 @@ export async function registerAgentManagementRoutes(app, config, services) {
34
37
  return reply;
35
38
  const state = await services.fileStore.read();
36
39
  return {
37
- 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
+ })),
38
47
  serverOrder: state.serverOrder
39
48
  };
40
49
  });
@@ -68,22 +77,29 @@ export async function registerAgentManagementRoutes(app, config, services) {
68
77
  if (!auth)
69
78
  return reply;
70
79
  const { name, url, token } = request.body ?? {};
80
+ const transport = request.body?.transport === 'outbound' ? 'outbound' : 'direct';
71
81
  if (typeof name !== 'string' || !name.trim())
72
82
  return reply.code(400).send({ error: 'name_required' });
73
- 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()))
74
87
  return reply.code(400).send({ error: 'url_required' });
75
88
  if (typeof token !== 'string' || token.length < 32)
76
89
  return reply.code(400).send({ error: 'token_too_short' });
77
- const normalizedUrl = normalizeAgentUrl(url);
90
+ const normalizedUrl = transport === 'direct' ? normalizeAgentUrl(url) : { ok: true, url: null };
78
91
  if (!normalizedUrl.ok) {
79
92
  return reply.code(400).send({ error: normalizedUrl.error });
80
93
  }
81
94
  const agentName = name.trim();
82
95
  let agent = null;
83
96
  let created = false;
97
+ const removedAgentIds = [];
84
98
  await services.fileStore.update((state) => {
85
99
  const tokenMatchIndex = state.agents.findIndex((a) => a.token === token);
86
- 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;
87
103
  const existingIndex = tokenMatchIndex >= 0 ? tokenMatchIndex : urlMatchIndex;
88
104
  if (existingIndex === -1) {
89
105
  created = true;
@@ -91,6 +107,7 @@ export async function registerAgentManagementRoutes(app, config, services) {
91
107
  id: randomUUID(),
92
108
  name: agentName,
93
109
  url: normalizedUrl.url,
110
+ transport,
94
111
  token,
95
112
  createdAt: new Date().toISOString()
96
113
  };
@@ -102,14 +119,14 @@ export async function registerAgentManagementRoutes(app, config, services) {
102
119
  ...existing,
103
120
  name: agentName,
104
121
  url: normalizedUrl.url,
122
+ transport,
105
123
  token
106
124
  };
107
- const removedAgentIds = [];
108
125
  const agents = state.agents.flatMap((current, index) => {
109
126
  if (index === existingIndex) {
110
127
  return [agent];
111
128
  }
112
- if (current.url === normalizedUrl.url || current.token === token) {
129
+ if ((normalizedUrl.url !== null && current.transport === 'direct' && current.url === normalizedUrl.url) || current.token === token) {
113
130
  removedAgentIds.push(current.id);
114
131
  return [];
115
132
  }
@@ -141,7 +158,19 @@ export async function registerAgentManagementRoutes(app, config, services) {
141
158
  }
142
159
  return { ...state, agents, serverOrder, agentLayouts, agentManualUpdates };
143
160
  }, { flush: 'immediate' });
144
- 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
+ });
145
174
  });
146
175
  app.put('/api/agents/order', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
147
176
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
@@ -182,7 +211,13 @@ export async function registerAgentManagementRoutes(app, config, services) {
182
211
  return nextAgentIds.shift();
183
212
  });
184
213
  }
185
- 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
+ }));
186
221
  return { ...state, agents, serverOrder };
187
222
  }, { flush: 'immediate' });
188
223
  if (!validOrder || !reorderedAgents || !serverOrder) {
@@ -221,6 +256,7 @@ export async function registerAgentManagementRoutes(app, config, services) {
221
256
  const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
222
257
  if (!found)
223
258
  return reply.code(404).send({ error: 'agent_not_found' });
259
+ outboundAgentRegistry.disconnect(request.params.agentId);
224
260
  return reply.code(204).send();
225
261
  });
226
262
  app.post('/api/agents/:agentId/uninstall', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
@@ -229,13 +265,14 @@ export async function registerAgentManagementRoutes(app, config, services) {
229
265
  return reply;
230
266
  if (config.previewMode)
231
267
  return reply.code(403).send({ error: 'preview_mode' });
232
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
268
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
233
269
  if (!agent)
234
- return reply.code(404).send({ error: 'agent_not_found' });
270
+ return reply;
235
271
  const uninstall = await requestAgentUninstall(agent);
236
272
  const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
237
273
  if (!found)
238
274
  return reply.code(404).send({ error: 'agent_not_found' });
275
+ outboundAgentRegistry.disconnect(request.params.agentId);
239
276
  return {
240
277
  ok: true,
241
278
  message: uninstall.message,
@@ -257,9 +294,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
257
294
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
258
295
  if (!auth)
259
296
  return reply;
260
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
297
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
261
298
  if (!agent)
262
- return reply.code(404).send({ error: 'agent_not_found' });
299
+ return reply;
263
300
  return proxyAgentRequest(agent, '/stats', request, reply, {
264
301
  timeoutMs: 2500,
265
302
  transformJson: (json, status) => {
@@ -277,10 +314,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
277
314
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
278
315
  if (!auth)
279
316
  return reply;
280
- const state = await services.fileStore.read();
281
- const agent = state.agents.find((a) => a.id === request.params.agentId);
317
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
282
318
  if (!agent)
283
- return reply.code(404).send({ error: 'agent_not_found' });
319
+ return reply;
284
320
  const start = Date.now();
285
321
  try {
286
322
  const res = await fetch(`${agent.url}/api/agent/terminals`, {
@@ -300,9 +336,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
300
336
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
301
337
  if (!auth)
302
338
  return reply;
303
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
339
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
304
340
  if (!agent)
305
- return reply.code(404).send({ error: 'agent_not_found' });
341
+ return reply;
306
342
  return proxyAgentRequest(agent, '/version', request, reply, {
307
343
  transformJson: async (json) => {
308
344
  if (!isVersionInfoBody(json)) {
@@ -321,9 +357,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
321
357
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
322
358
  if (!auth)
323
359
  return reply;
324
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
360
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
325
361
  if (!agent)
326
- return reply.code(404).send({ error: 'agent_not_found' });
362
+ return reply;
327
363
  return proxyAgentRequest(agent, '/update/check', request, reply, {
328
364
  transformJson: async (json) => {
329
365
  if (isUpdateCheckBody(json) && !json.error && json.updateAvailable === false) {
@@ -340,9 +376,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
340
376
  if (config.previewMode) {
341
377
  return { ok: false, changed: false, message: 'Agent updates are disabled in preview mode.', restarting: false };
342
378
  }
343
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
379
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
344
380
  if (!agent)
345
- return reply.code(404).send({ error: 'agent_not_found' });
381
+ return reply;
346
382
  return proxyAgentRequest(agent, '/update', request, reply, {
347
383
  transformJson: async (json) => {
348
384
  if (!isUpdateResponseBody(json)) {
@@ -371,9 +407,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
371
407
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
372
408
  if (!auth)
373
409
  return reply;
374
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
410
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
375
411
  if (!agent)
376
- return reply.code(404).send({ error: 'agent_not_found' });
412
+ return reply;
377
413
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
378
414
  return proxyAgentRequest(agent, `/code-server/status${query}`, request, reply, {
379
415
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
@@ -388,9 +424,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
388
424
  // on the production agent, so preview blocks them all.
389
425
  if (config.previewMode)
390
426
  return reply.code(403).send({ error: 'preview_mode' });
391
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
427
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
392
428
  if (!agent)
393
- return reply.code(404).send({ error: 'agent_not_found' });
429
+ return reply;
394
430
  return proxyAgentRequest(agent, '/code-server/install', request, reply, {
395
431
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
396
432
  });
@@ -401,9 +437,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
401
437
  return reply;
402
438
  if (config.previewMode)
403
439
  return reply.code(403).send({ error: 'preview_mode' });
404
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
440
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
405
441
  if (!agent)
406
- return reply.code(404).send({ error: 'agent_not_found' });
442
+ return reply;
407
443
  return proxyAgentRequest(agent, '/code-server/update', request, reply, {
408
444
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
409
445
  });
@@ -414,9 +450,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
414
450
  return reply;
415
451
  if (config.previewMode)
416
452
  return reply.code(403).send({ error: 'preview_mode' });
417
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
453
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
418
454
  if (!agent)
419
- return reply.code(404).send({ error: 'agent_not_found' });
455
+ return reply;
420
456
  return proxyAgentRequest(agent, '/code-server/open', request, reply, {
421
457
  bodyOverride: {
422
458
  ...(request.body ?? {}),
@@ -431,9 +467,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
431
467
  return reply;
432
468
  if (config.previewMode)
433
469
  return reply.code(403).send({ error: 'preview_mode' });
434
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
470
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
435
471
  if (!agent)
436
- return reply.code(404).send({ error: 'agent_not_found' });
472
+ return reply;
437
473
  return proxyAgentRequest(agent, '/code-server/stop', request, reply, {
438
474
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
439
475
  });
@@ -444,9 +480,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
444
480
  return reply;
445
481
  if (config.previewMode)
446
482
  return reply.code(403).send({ error: 'preview_mode' });
447
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
483
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
448
484
  if (!agent)
449
- return reply.code(404).send({ error: 'agent_not_found' });
485
+ return reply;
450
486
  return proxyAgentRequest(agent, '/code-server/uninstall', request, reply, {
451
487
  transformJson: (json) => rewriteCodeServerResponseForAgent(request.params.agentId, json)
452
488
  });
@@ -456,18 +492,18 @@ export async function registerAgentManagementRoutes(app, config, services) {
456
492
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
457
493
  if (!auth)
458
494
  return reply;
459
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
495
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
460
496
  if (!agent)
461
- return reply.code(404).send({ error: 'agent_not_found' });
497
+ return reply;
462
498
  return proxyAgentRequest(agent, '/terminals', request, reply);
463
499
  });
464
500
  app.get('/api/agents/:agentId/terminals/:terminalId/history', async (request, reply) => {
465
501
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
466
502
  if (!auth)
467
503
  return reply;
468
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
504
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
469
505
  if (!agent)
470
- return reply.code(404).send({ error: 'agent_not_found' });
506
+ return reply;
471
507
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
472
508
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/history${query}`, request, reply);
473
509
  });
@@ -475,9 +511,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
475
511
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
476
512
  if (!auth)
477
513
  return reply;
478
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
514
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
479
515
  if (!agent)
480
- return reply.code(404).send({ error: 'agent_not_found' });
516
+ return reply;
481
517
  const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
482
518
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-history${query}`, request, reply, {
483
519
  unavailableFallback: {
@@ -493,9 +529,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
493
529
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
494
530
  if (!auth)
495
531
  return reply;
496
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
532
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
497
533
  if (!agent)
498
- return reply.code(404).send({ error: 'agent_not_found' });
534
+ return reply;
499
535
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-source`, request, reply, {
500
536
  unavailableFallback: { terminalId: request.params.terminalId, source: null }
501
537
  });
@@ -505,9 +541,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
505
541
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
506
542
  if (!auth)
507
543
  return reply;
508
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
544
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
509
545
  if (!agent)
510
- return reply.code(404).send({ error: 'agent_not_found' });
546
+ return reply;
511
547
  return proxyAgentRequest(agent, '/terminals', request, reply);
512
548
  });
513
549
  // Proxy: close terminal
@@ -515,9 +551,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
515
551
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
516
552
  if (!auth)
517
553
  return reply;
518
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
554
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
519
555
  if (!agent)
520
- return reply.code(404).send({ error: 'agent_not_found' });
556
+ return reply;
521
557
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
522
558
  });
523
559
  // Proxy: rename terminal
@@ -525,9 +561,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
525
561
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
526
562
  if (!auth)
527
563
  return reply;
528
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
564
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
529
565
  if (!agent)
530
- return reply.code(404).send({ error: 'agent_not_found' });
566
+ return reply;
531
567
  return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
532
568
  });
533
569
  // Proxy: terminal WebSocket
@@ -541,13 +577,12 @@ export async function registerAgentManagementRoutes(app, config, services) {
541
577
  socket.close(1008, 'unauthorized');
542
578
  return;
543
579
  }
544
- const state = await services.fileStore.read();
545
- const agent = state.agents.find((a) => a.id === request.params.agentId);
546
- if (!agent) {
547
- 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);
548
583
  return;
549
584
  }
550
- proxyAgentWebSocket(socket, agent, request.params.terminalId);
585
+ proxyAgentWebSocket(socket, resolution.agent, request.params.terminalId);
551
586
  });
552
587
  // Proxy: file routes (all file operations)
553
588
  const filePathParts = ['root', 'list', 'read', 'write', 'create', 'move', 'delete-preview', 'delete', 'open', 'preview'];
@@ -556,9 +591,9 @@ export async function registerAgentManagementRoutes(app, config, services) {
556
591
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
557
592
  if (!auth)
558
593
  return reply;
559
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
594
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
560
595
  if (!agent)
561
- return reply.code(404).send({ error: 'agent_not_found' });
596
+ return reply;
562
597
  return proxyAgentRequest(agent, `/files/${part}`, request, reply);
563
598
  });
564
599
  }
@@ -566,19 +601,15 @@ export async function registerAgentManagementRoutes(app, config, services) {
566
601
  const auth = await authenticateTerminalRequest(services.authService, request, reply);
567
602
  if (!auth)
568
603
  return reply;
569
- if (!await resolveAgent(services.fileStore, request.params.agentId)) {
570
- return reply.code(404).send({ error: 'agent_not_found' });
571
- }
604
+ if (!await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply))
605
+ return reply;
572
606
  return reply.redirect(`${agentCodeServerPublicPrefix(request.params.agentId, request.params.instanceId)}/`, 302);
573
607
  });
574
- await registerAgentCodeServerProxyRoutes(app, config, services);
608
+ await registerAgentCodeServerProxyRoutes(app, config, services, outboundAgentRegistry);
575
609
  }
576
- async function registerAgentCodeServerProxyRoutes(app, config, services) {
610
+ async function registerAgentCodeServerProxyRoutes(app, config, services, outboundAgentRegistry) {
577
611
  await app.register(async (proxyApp) => {
578
- proxyApp.removeAllContentTypeParsers();
579
- proxyApp.addContentTypeParser('*', { parseAs: 'buffer', bodyLimit: agentCodeServerProxyBodyLimitBytes }, (_request, body, done) => {
580
- done(null, body);
581
- });
612
+ registerStreamingBodyParser(proxyApp);
582
613
  proxyApp.route({
583
614
  method: 'GET',
584
615
  url: agentCodeServerProxyRoute,
@@ -589,9 +620,9 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
589
620
  return reply;
590
621
  if (!isProxyOriginAcceptable(config, request))
591
622
  return reply.code(403).send({ error: 'origin_not_allowed' });
592
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
623
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
593
624
  if (!agent)
594
- return reply.code(404).send({ error: 'agent_not_found' });
625
+ return reply;
595
626
  proxyAgentCodeServerHttp(agent, request, reply);
596
627
  return reply;
597
628
  },
@@ -605,12 +636,12 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
605
636
  socket.close(1008, 'unauthorized');
606
637
  return;
607
638
  }
608
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
609
- if (!agent) {
610
- 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);
611
642
  return;
612
643
  }
613
- proxyAgentCodeServerWebSocket(agent, socket, request);
644
+ proxyAgentCodeServerWebSocket(resolution.agent, socket, request);
614
645
  }
615
646
  });
616
647
  for (const method of httpProxyMethods) {
@@ -624,9 +655,9 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
624
655
  return reply;
625
656
  if (!isProxyOriginAcceptable(config, request))
626
657
  return reply.code(403).send({ error: 'origin_not_allowed' });
627
- const agent = await resolveAgent(services.fileStore, request.params.agentId);
658
+ const agent = await resolveAgent(services.fileStore, outboundAgentRegistry, request.params.agentId, reply);
628
659
  if (!agent)
629
- return reply.code(404).send({ error: 'agent_not_found' });
660
+ return reply;
630
661
  proxyAgentCodeServerHttp(agent, request, reply);
631
662
  return reply;
632
663
  }
@@ -635,9 +666,12 @@ async function registerAgentCodeServerProxyRoutes(app, config, services) {
635
666
  });
636
667
  }
637
668
  function proxyAgentCodeServerHttp(agent, request, reply) {
669
+ if (rejectOversizedProxyBody(request, reply, agentCodeServerProxyBodyLimitBytes)) {
670
+ return;
671
+ }
638
672
  const publicPrefix = agentCodeServerPublicPrefix(request.params.agentId, request.params.instanceId);
639
673
  const target = agentCodeServerTargetUrl(agent, request);
640
- const body = serializedProxyBody(request);
674
+ const body = proxyRequestBody(request);
641
675
  const headers = agentCodeServerRequestHeaders(agent, request, body, publicPrefix, target);
642
676
  const requestLib = target.protocol === 'https:' ? https : http;
643
677
  reply.hijack();
@@ -664,12 +698,12 @@ function proxyAgentCodeServerHttp(agent, request, reply) {
664
698
  reply.raw.end(JSON.stringify({ error: 'agent_code_server_proxy_failed' }));
665
699
  return;
666
700
  }
667
- reply.raw.destroy();
701
+ if (!reply.raw.writableEnded) {
702
+ reply.raw.destroy();
703
+ }
668
704
  });
669
- if (body) {
670
- upstream.write(body);
671
- }
672
- upstream.end();
705
+ attachProxyAbortHandling(request, reply, upstream);
706
+ forwardProxyRequestBody(body, upstream, reply, agentCodeServerProxyBodyLimitBytes);
673
707
  }
674
708
  function proxyAgentCodeServerWebSocket(agent, socket, request) {
675
709
  const publicPrefix = agentCodeServerPublicPrefix(request.params.agentId, request.params.instanceId);
@@ -691,18 +725,6 @@ function agentCodeServerTargetUrl(agent, request) {
691
725
  const targetPath = `/api/agent/code-server/instances/${encodeURIComponent(request.params.instanceId)}${rest.startsWith('/') ? rest : `/${rest}`}${rawSearch}`;
692
726
  return new URL(targetPath, agent.url);
693
727
  }
694
- function serializedProxyBody(request) {
695
- if (request.method === 'GET' || request.method === 'HEAD') {
696
- return undefined;
697
- }
698
- if (request.body === undefined || request.body === null) {
699
- return undefined;
700
- }
701
- if (Buffer.isBuffer(request.body) || typeof request.body === 'string') {
702
- return request.body;
703
- }
704
- return JSON.stringify(request.body);
705
- }
706
728
  function agentCodeServerRequestHeaders(agent, request, body, publicPrefix, target) {
707
729
  const next = {};
708
730
  // accept-encoding passes through: this hop never rewrites bodies (the
@@ -713,8 +735,10 @@ function agentCodeServerRequestHeaders(agent, request, body, publicPrefix, targe
713
735
  if (hopByHopHeaders.has(lower) ||
714
736
  lower === 'host' ||
715
737
  lower === 'cookie' ||
716
- lower === 'authorization' ||
717
- lower === 'content-length') {
738
+ lower === 'authorization') {
739
+ continue;
740
+ }
741
+ if (lower === 'content-length' && body === undefined) {
718
742
  continue;
719
743
  }
720
744
  next[name] = value;
@@ -724,8 +748,9 @@ function agentCodeServerRequestHeaders(agent, request, body, publicPrefix, targe
724
748
  next['x-forwarded-host'] = request.headers['x-forwarded-host'] ?? request.headers.host;
725
749
  next['x-forwarded-proto'] = request.headers['x-forwarded-proto'] ?? request.protocol;
726
750
  next['x-forwarded-prefix'] = publicPrefix;
727
- if (body !== undefined) {
728
- next['content-length'] = Buffer.byteLength(body);
751
+ const bodyLength = proxyRequestBodyLength(body);
752
+ if (bodyLength !== null) {
753
+ next['content-length'] = bodyLength;
729
754
  }
730
755
  return next;
731
756
  }
@@ -782,12 +807,10 @@ function rewriteAgentCodeServerLocation(value, agent, agentId, instanceId) {
782
807
  }
783
808
  try {
784
809
  const parsed = new URL(value);
785
- const agentOrigin = new URL(agent.url).origin;
786
- if (parsed.origin === agentOrigin) {
787
- const rewritten = rewriteAgentCodeServerLocation(`${parsed.pathname}${parsed.search}${parsed.hash}`, agent, agentId, instanceId);
788
- if (rewritten !== `${parsed.pathname}${parsed.search}${parsed.hash}`) {
789
- return rewritten;
790
- }
810
+ const pathAndSuffix = `${parsed.pathname}${parsed.search}${parsed.hash}`;
811
+ const rewritten = rewriteAgentCodeServerLocation(pathAndSuffix, agent, agentId, instanceId);
812
+ if (rewritten !== pathAndSuffix) {
813
+ return rewritten;
791
814
  }
792
815
  }
793
816
  catch {
@@ -871,9 +894,32 @@ function websocketProtocols(value) {
871
894
  function isRecord(value) {
872
895
  return value !== null && typeof value === 'object' && !Array.isArray(value);
873
896
  }
874
- 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) {
875
910
  const state = await fileStore.read();
876
- 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');
877
923
  }
878
924
  function pickPreferredAgentLayout(current, candidate) {
879
925
  if (!current)