nterminal 1.2.76 → 1.2.78
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.
- package/.env.example +3 -0
- package/README.md +1 -1
- package/dist/client/assets/MarkdownPreview-D5NthhhG.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-DwgFO6m9.js → TranscriptMarkdownBody-BkGu6YVq.js} +1 -1
- package/dist/client/assets/{index-73oYtL4l.js → index-Bgqck-vt.js} +1 -1
- package/dist/client/assets/{index-DGAIhvX2.js → index-gSePR0Ob.js} +35 -35
- package/dist/client/index.html +1 -1
- package/dist/scripts/onboarding.js +110 -42
- package/dist/scripts/onboarding.js.map +1 -1
- package/dist/server/agent/agentProxy.d.ts +2 -2
- package/dist/server/agent/agentProxy.js.map +1 -1
- package/dist/server/agent/agentWebSocketProxy.d.ts +2 -2
- package/dist/server/agent/agentWebSocketProxy.js.map +1 -1
- package/dist/server/agent/outboundAgentClient.d.ts +21 -0
- package/dist/server/agent/outboundAgentClient.js +237 -0
- package/dist/server/agent/outboundAgentClient.js.map +1 -0
- package/dist/server/agent/outboundAgentRegistry.d.ts +25 -0
- package/dist/server/agent/outboundAgentRegistry.js +190 -0
- package/dist/server/agent/outboundAgentRegistry.js.map +1 -0
- package/dist/server/agent/socketWebSocketRelay.d.ts +6 -0
- package/dist/server/agent/socketWebSocketRelay.js +107 -0
- package/dist/server/agent/socketWebSocketRelay.js.map +1 -0
- package/dist/server/config.d.ts +3 -0
- package/dist/server/config.js +33 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +18 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.js +14 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/notifications/agentCompletionMonitor.d.ts +4 -1
- package/dist/server/notifications/agentCompletionMonitor.js +6 -1
- package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
- package/dist/server/routes/agentLinkRoutes.d.ts +9 -0
- package/dist/server/routes/agentLinkRoutes.js +129 -0
- package/dist/server/routes/agentLinkRoutes.js.map +1 -0
- package/dist/server/routes/agentManagementRoutes.d.ts +2 -0
- package/dist/server/routes/agentManagementRoutes.js +129 -75
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/runtimeState.js +9 -0
- package/dist/server/runtimeState.js.map +1 -1
- package/dist/server/storage/fileStore.d.ts +2 -1
- package/dist/server/storage/fileStore.js +7 -1
- package/dist/server/storage/fileStore.js.map +1 -1
- package/dist/shared/agentLinkProtocol.d.ts +26 -0
- package/dist/shared/agentLinkProtocol.js +49 -0
- package/dist/shared/agentLinkProtocol.js.map +1 -0
- package/dist/shared/protocol.d.ts +11 -2
- package/dist/shared/protocol.js.map +1 -1
- package/docs/configuration.md +3 -0
- package/docs/onboarding.md +9 -12
- package/package.json +1 -1
- package/scripts/nterminalctl +21 -0
- 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 }) => ({
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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 }) => ({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
546
|
-
|
|
547
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
607
|
-
if (
|
|
608
|
-
socket
|
|
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
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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)
|