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.
- package/.env.example +3 -0
- package/README.md +1 -1
- package/dist/client/assets/MarkdownPreview-D5NthhhG.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-B-RKybd7.js → TranscriptMarkdownBody-BkGu6YVq.js} +1 -1
- package/dist/client/assets/index-B3_mYQzf.css +1 -0
- package/dist/client/assets/{index-CH_JmAuw.js → index-Bgqck-vt.js} +1 -1
- package/dist/client/assets/index-gSePR0Ob.js +57 -0
- package/dist/client/index.html +2 -2
- package/dist/scripts/onboarding.js +106 -41
- 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/auth/authService.js +4 -1
- package/dist/server/auth/authService.js.map +1 -1
- package/dist/server/code/codeServerManager.d.ts +7 -1
- package/dist/server/code/codeServerManager.js +95 -38
- package/dist/server/code/codeServerManager.js.map +1 -1
- 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 +21 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/notifications/agentCompletionMonitor.d.ts +4 -1
- package/dist/server/notifications/agentCompletionMonitor.js +23 -4
- package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
- package/dist/server/notifications/completionNotificationRouter.d.ts +2 -1
- package/dist/server/notifications/completionNotificationRouter.js +4 -2
- package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
- package/dist/server/notifications/pushNotificationService.d.ts +20 -2
- package/dist/server/notifications/pushNotificationService.js +129 -14
- package/dist/server/notifications/pushNotificationService.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 +147 -101
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/routes/codeServerRoutes.js +28 -31
- package/dist/server/routes/codeServerRoutes.js.map +1 -1
- package/dist/server/routes/httpProxyBody.d.ts +10 -0
- package/dist/server/routes/httpProxyBody.js +73 -0
- package/dist/server/routes/httpProxyBody.js.map +1 -0
- package/dist/server/routes/pushNotificationRoutes.js +31 -2
- package/dist/server/routes/pushNotificationRoutes.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/server/terminal/TerminalManager.d.ts +2 -1
- package/dist/server/terminal/TerminalManager.js +27 -7
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/agentTranscriptStatus.d.ts +2 -2
- package/dist/server/terminal/agentTranscriptStatus.js +224 -38
- package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
- package/dist/server/update/packageUpdate.d.ts +1 -0
- package/dist/server/update/packageUpdate.js +34 -2
- package/dist/server/update/packageUpdate.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 +18 -3
- package/dist/shared/protocol.js.map +1 -1
- package/dist/shared/types.d.ts +3 -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-CStdB1d5.js +0 -1
- package/dist/client/assets/index-DwmyYdjx.css +0 -1
- 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 }) => ({
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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 }) => ({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
609
|
-
if (
|
|
610
|
-
socket
|
|
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
|
|
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 =
|
|
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.
|
|
701
|
+
if (!reply.raw.writableEnded) {
|
|
702
|
+
reply.raw.destroy();
|
|
703
|
+
}
|
|
668
704
|
});
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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)
|