aws-runtime-bridge 1.9.131 → 1.9.134

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 (63) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +15 -0
  2. package/dist/adapter/AcodeSdkAdapter.js +5 -5
  3. package/dist/adapter/types.d.ts +9 -0
  4. package/dist/browser/built-in-browser-tools.js +1 -1
  5. package/dist/routes/system-metrics.js +1 -1
  6. package/dist/routes/terminal.js +19 -19
  7. package/dist/services/instance-features.d.ts +5 -0
  8. package/dist/services/instance-features.js +1 -1
  9. package/dist/services/process-detail.d.ts +77 -0
  10. package/dist/services/process-detail.js +32 -0
  11. package/dist/services/process-top.d.ts +53 -0
  12. package/dist/services/process-top.js +50 -0
  13. package/dist/services/session-output.d.ts +10 -0
  14. package/dist/services/session-output.js +3 -3
  15. package/package/acode/dist/built-in-file-tools.d.ts +25 -1
  16. package/package/acode/dist/built-in-file-tools.js +2 -2
  17. package/package/acode/dist/image-utils.d.ts +52 -0
  18. package/package/acode/dist/image-utils.js +2 -0
  19. package/package/acode/dist/index.d.ts +1 -0
  20. package/package/acode/dist/index.js +1 -1
  21. package/package/acode/dist/runtime.d.ts +15 -1
  22. package/package/acode/dist/runtime.js +26 -26
  23. package/package/acode/dist/types.d.ts +3 -1
  24. package/package/aws-client-agent-mcp/dist/http-client.d.ts +4 -0
  25. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  26. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +8 -0
  27. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -25
  28. package/package/aws-client-agent-mcp/dist/mcp-tools.js +6 -6
  29. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +6 -0
  30. package/package/aws-client-agent-mcp/dist/memory-tools.js +2 -2
  31. package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
  32. package/package/aws-client-agent-mcp/package.json +1 -1
  33. package/package.json +1 -1
  34. package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
  35. package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1066
  36. package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
  37. package/package/aws-client-agent-mcp/dist/config.test.js +0 -259
  38. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -2
  39. package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -312
  40. package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
  41. package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -808
  42. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +0 -1
  43. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +0 -110
  44. package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
  45. package/package/aws-client-agent-mcp/dist/logger.test.js +0 -27
  46. package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
  47. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -2798
  48. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
  49. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -152
  50. package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
  51. package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -664
  52. package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
  53. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -142
  54. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +0 -1
  55. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +0 -81
  56. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
  57. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -201
  58. package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
  59. package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -157
  60. package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +0 -1
  61. package/package/aws-client-agent-mcp/dist/watchdog.test.js +0 -124
  62. package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
  63. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +0 -725
@@ -1,808 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { afterEach, beforeEach, describe, it, mock } from 'node:test';
3
- import { HttpClient } from './http-client.js';
4
- describe('HttpClient', { concurrency: false }, () => {
5
- const originalFetch = global.fetch;
6
- let fetchCalls = [];
7
- beforeEach(() => {
8
- fetchCalls = [];
9
- global.fetch = async (url, options) => {
10
- fetchCalls.push([url.toString(), options]);
11
- return {
12
- ok: true,
13
- status: 200,
14
- statusText: 'OK',
15
- json: async () => ({ result: 'success' }),
16
- };
17
- };
18
- });
19
- afterEach(() => {
20
- global.fetch = originalFetch;
21
- mock.restoreAll();
22
- });
23
- describe('constructor and URL resolution', () => {
24
- it('uses explicit mcpHttpUrl when provided', () => {
25
- const config = {
26
- serverUrl: 'ws://localhost:8080/ws/agent',
27
- heartbeatInterval: 15000,
28
- reconnectInterval: 200,
29
- maxReconnectAttempts: -1,
30
- mcpHttpUrl: 'http://custom:3000/mcp/call',
31
- };
32
- const client = new HttpClient(config);
33
- assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
34
- });
35
- it('appends /mcp/call to mcpHttpUrl when not present', () => {
36
- const config = {
37
- serverUrl: 'ws://localhost:8080/ws/agent',
38
- heartbeatInterval: 15000,
39
- reconnectInterval: 200,
40
- maxReconnectAttempts: -1,
41
- mcpHttpUrl: 'http://custom:3000',
42
- };
43
- const client = new HttpClient(config);
44
- assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
45
- });
46
- it('appends /mcp/call to mcpHttpUrl with trailing slash', () => {
47
- const config = {
48
- serverUrl: 'ws://localhost:8080/ws/agent',
49
- heartbeatInterval: 15000,
50
- reconnectInterval: 200,
51
- maxReconnectAttempts: -1,
52
- mcpHttpUrl: 'http://custom:3000/',
53
- };
54
- const client = new HttpClient(config);
55
- assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
56
- });
57
- it('derives HTTP URL from WebSocket URL (ws://)', () => {
58
- const config = {
59
- serverUrl: 'ws://localhost:8080/ws/agent',
60
- heartbeatInterval: 15000,
61
- reconnectInterval: 200,
62
- maxReconnectAttempts: -1,
63
- };
64
- const client = new HttpClient(config);
65
- assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
66
- });
67
- it('derives HTTP URL from WebSocket URL (wss://)', () => {
68
- const config = {
69
- serverUrl: 'wss://secure.example.com/ws/agent',
70
- heartbeatInterval: 15000,
71
- reconnectInterval: 200,
72
- maxReconnectAttempts: -1,
73
- };
74
- const client = new HttpClient(config);
75
- assert.equal(client.getMcpHttpUrl(), 'https://secure.example.com/mcp/call');
76
- });
77
- it('handles empty mcpHttpUrl by deriving from serverUrl', () => {
78
- const config = {
79
- serverUrl: 'ws://localhost:8080/ws/agent',
80
- heartbeatInterval: 15000,
81
- reconnectInterval: 200,
82
- maxReconnectAttempts: -1,
83
- mcpHttpUrl: '',
84
- };
85
- const client = new HttpClient(config);
86
- assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
87
- });
88
- it('handles whitespace-only mcpHttpUrl', () => {
89
- const config = {
90
- serverUrl: 'ws://localhost:8080/ws/agent',
91
- heartbeatInterval: 15000,
92
- reconnectInterval: 200,
93
- maxReconnectAttempts: -1,
94
- mcpHttpUrl: ' ',
95
- };
96
- const client = new HttpClient(config);
97
- assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
98
- });
99
- it('repairs duplicated ports from configured mcpHttpUrl', () => {
100
- const config = {
101
- serverUrl: 'ws://localhost:8080/ws/agent',
102
- heartbeatInterval: 15000,
103
- reconnectInterval: 200,
104
- maxReconnectAttempts: -1,
105
- mcpHttpUrl: 'http://127.0.0.1:8080:8080/mcp/call',
106
- };
107
- const client = new HttpClient(config);
108
- assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
109
- });
110
- it('repairs duplicated ports from serverUrl', () => {
111
- const config = {
112
- serverUrl: 'ws://127.0.0.1:8080:8080/ws/agent',
113
- heartbeatInterval: 15000,
114
- reconnectInterval: 200,
115
- maxReconnectAttempts: -1,
116
- };
117
- const client = new HttpClient(config);
118
- assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
119
- });
120
- it('preserves configured MCP HTTP subpaths', () => {
121
- const config = {
122
- serverUrl: 'ws://localhost:8080/ws/agent',
123
- heartbeatInterval: 15000,
124
- reconnectInterval: 200,
125
- maxReconnectAttempts: -1,
126
- mcpHttpUrl: 'http://custom:3000/api',
127
- };
128
- const client = new HttpClient(config);
129
- assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/api/mcp/call');
130
- });
131
- });
132
- describe('callTool', { concurrency: false }, () => {
133
- it('makes POST request with runtime token headers', async () => {
134
- const config = {
135
- serverUrl: 'ws://localhost:8080/ws/agent',
136
- heartbeatInterval: 15000,
137
- reconnectInterval: 200,
138
- maxReconnectAttempts: -1,
139
- runtimeAccessToken: 'runtime-token',
140
- };
141
- const client = new HttpClient(config);
142
- await client.callTool('test-tool', { arg1: 'value1' });
143
- assert.equal(fetchCalls.length, 1);
144
- const [url, options] = fetchCalls[0];
145
- assert.equal(url, 'http://127.0.0.1:8080/mcp/call');
146
- assert.equal(options.method, 'POST');
147
- assert.equal(options.headers['Content-Type'], 'application/json');
148
- assert.equal(options.headers['X-Runtime-Token'], 'runtime-token');
149
- assert.equal(options.headers.Authorization, 'Bearer runtime-token');
150
- assert.equal(options.body, JSON.stringify({ tool: 'test-tool', arguments: { arg1: 'value1' } }));
151
- });
152
- it('makes POST request without runtime token when not configured', async () => {
153
- const config = {
154
- serverUrl: 'ws://localhost:8080/ws/agent',
155
- heartbeatInterval: 15000,
156
- reconnectInterval: 200,
157
- maxReconnectAttempts: -1,
158
- };
159
- const client = new HttpClient(config);
160
- await client.callTool('test-tool', {});
161
- const [, options] = fetchCalls[0];
162
- assert.equal(options.headers['X-Runtime-Token'], undefined);
163
- assert.equal(options.headers.Authorization, undefined);
164
- });
165
- it('sets an AbortSignal for poll_message long-poll requests', async () => {
166
- const config = {
167
- serverUrl: 'ws://localhost:8080/ws/agent',
168
- heartbeatInterval: 15000,
169
- reconnectInterval: 200,
170
- maxReconnectAttempts: -1,
171
- runtimeAccessToken: 'runtime-token',
172
- };
173
- const client = new HttpClient(config);
174
- await client.callTool('poll_message', { agentId: 'agent-1' });
175
- const [, options] = fetchCalls[0];
176
- assert.ok(options.signal instanceof AbortSignal);
177
- });
178
- it('keeps an AbortSignal for non-blocking MCP tools', async () => {
179
- const config = {
180
- serverUrl: 'ws://localhost:8080/ws/agent',
181
- heartbeatInterval: 15000,
182
- reconnectInterval: 200,
183
- maxReconnectAttempts: -1,
184
- runtimeAccessToken: 'runtime-token',
185
- };
186
- const client = new HttpClient(config);
187
- await client.callTool('get_message', { agentId: 'agent-1' });
188
- const [, options] = fetchCalls[0];
189
- assert.ok(options.signal instanceof AbortSignal);
190
- });
191
- it('returns parsed result on success', async () => {
192
- const config = {
193
- serverUrl: 'ws://localhost:8080/ws/agent',
194
- heartbeatInterval: 15000,
195
- reconnectInterval: 200,
196
- maxReconnectAttempts: -1,
197
- };
198
- const client = new HttpClient(config);
199
- global.fetch = async () => ({
200
- ok: true,
201
- json: async () => ({ data: 'test-data', count: 42 }),
202
- });
203
- const result = await client.callTool('test-tool', {});
204
- assert.deepEqual(result, { data: 'test-data', count: 42 });
205
- });
206
- it('throws error when response is not ok', async () => {
207
- const config = {
208
- serverUrl: 'ws://localhost:8080/ws/agent',
209
- heartbeatInterval: 15000,
210
- reconnectInterval: 200,
211
- maxReconnectAttempts: -1,
212
- mcpRetryMaxAccumulatedMs: 0,
213
- };
214
- const client = new HttpClient(config);
215
- global.fetch = async () => ({
216
- ok: false,
217
- status: 500,
218
- statusText: 'Internal Server Error',
219
- });
220
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 500 Internal Server Error/);
221
- });
222
- it('includes response body when response is not ok', async () => {
223
- const config = {
224
- serverUrl: 'ws://localhost:8080/ws/agent',
225
- heartbeatInterval: 15000,
226
- reconnectInterval: 200,
227
- maxReconnectAttempts: -1,
228
- mcpRetryMaxAccumulatedMs: 0,
229
- };
230
- const client = new HttpClient(config);
231
- global.fetch = async () => ({
232
- ok: false,
233
- status: 503,
234
- statusText: 'Service Unavailable',
235
- text: async () => '{"error":"Async request timed out"}',
236
- });
237
- await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message call failed: 503 Service Unavailable - {"error":"Async request timed out"}/);
238
- });
239
- it('throws a clear tool error when result contains error field', async () => {
240
- const config = {
241
- serverUrl: 'ws://localhost:8080/ws/agent',
242
- heartbeatInterval: 15000,
243
- reconnectInterval: 200,
244
- maxReconnectAttempts: -1,
245
- };
246
- const client = new HttpClient(config);
247
- global.fetch = async () => ({
248
- ok: true,
249
- json: async () => ({ error: 'Tool execution failed' }),
250
- });
251
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool failed: Tool execution failed/);
252
- });
253
- it('handles 401 unauthorized response', async () => {
254
- const config = {
255
- serverUrl: 'ws://localhost:8080/ws/agent',
256
- heartbeatInterval: 15000,
257
- reconnectInterval: 200,
258
- maxReconnectAttempts: -1,
259
- };
260
- const client = new HttpClient(config);
261
- global.fetch = async () => ({
262
- ok: false,
263
- status: 401,
264
- statusText: 'Unauthorized',
265
- });
266
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 401 Unauthorized/);
267
- });
268
- it('force-refreshes runtime token and retries once on 401', async () => {
269
- const config = {
270
- serverUrl: 'ws://localhost:8080/ws/agent',
271
- heartbeatInterval: 15000,
272
- reconnectInterval: 200,
273
- maxReconnectAttempts: -1,
274
- userId: 'user-1',
275
- runtimeAccessToken: 'stale-token',
276
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
277
- };
278
- const client = new HttpClient(config);
279
- global.fetch = async (url, options) => {
280
- fetchCalls.push([url.toString(), options]);
281
- if (url.toString() === 'http://127.0.0.1:8080/mcp/call' && fetchCalls.length === 1) {
282
- return {
283
- ok: false,
284
- status: 401,
285
- statusText: 'Unauthorized',
286
- };
287
- }
288
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
289
- return {
290
- ok: true,
291
- status: 200,
292
- statusText: 'OK',
293
- json: async () => ({ runtimeAccessToken: 'fresh-token' }),
294
- };
295
- }
296
- return {
297
- ok: true,
298
- status: 200,
299
- statusText: 'OK',
300
- json: async () => ({ data: 'retried' }),
301
- };
302
- };
303
- const result = await client.callTool('my_task', { agentId: 'agent-1' });
304
- assert.deepEqual(result, { data: 'retried' });
305
- assert.equal(fetchCalls.length, 3);
306
- assert.equal(fetchCalls[0][1].headers['X-Runtime-Token'], 'stale-token');
307
- assert.equal(fetchCalls[1][0], 'http://127.0.0.1:18081/runtime/binding/refresh-token');
308
- assert.equal(fetchCalls[1][1].body, JSON.stringify({
309
- agentId: 'agent-1',
310
- currentToken: 'stale-token',
311
- }));
312
- assert.equal(fetchCalls[2][1].headers['X-Runtime-Token'], 'fresh-token');
313
- assert.equal(config.runtimeAccessToken, 'fresh-token');
314
- });
315
- it('shares one bridge token refresh across concurrent 401 responses', async () => {
316
- const config = {
317
- serverUrl: 'ws://localhost:8080/ws/agent',
318
- heartbeatInterval: 15000,
319
- reconnectInterval: 200,
320
- maxReconnectAttempts: -1,
321
- userId: 'user-1',
322
- runtimeAccessToken: 'stale-token',
323
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
324
- };
325
- const client = new HttpClient(config);
326
- const staleTokenCalls = new Set();
327
- global.fetch = async (url, options) => {
328
- fetchCalls.push([url.toString(), options]);
329
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
330
- await new Promise((resolve) => setTimeout(resolve, 10));
331
- return {
332
- ok: true,
333
- status: 200,
334
- statusText: 'OK',
335
- json: async () => ({ runtimeAccessToken: 'fresh-token' }),
336
- };
337
- }
338
- const body = JSON.parse(String(options?.body || '{}'));
339
- const token = options.headers['X-Runtime-Token'];
340
- if (token === 'stale-token') {
341
- staleTokenCalls.add(body.tool || '');
342
- return {
343
- ok: false,
344
- status: 401,
345
- statusText: 'Unauthorized',
346
- };
347
- }
348
- return {
349
- ok: true,
350
- status: 200,
351
- statusText: 'OK',
352
- json: async () => ({ tool: body.tool, token }),
353
- };
354
- };
355
- const [first, second] = await Promise.all([
356
- client.callTool('get_memory_stats', { agentId: 'agent-1' }),
357
- client.callTool('discover_group_rooms', { agentId: 'agent-1' }),
358
- ]);
359
- assert.deepEqual(first, { tool: 'get_memory_stats', token: 'fresh-token' });
360
- assert.deepEqual(second, { tool: 'discover_group_rooms', token: 'fresh-token' });
361
- assert.deepEqual(staleTokenCalls, new Set(['get_memory_stats', 'discover_group_rooms']));
362
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
363
- assert.equal(config.runtimeAccessToken, 'fresh-token');
364
- });
365
- it('throws when no runtimeAccessToken and no bridge for startup', async () => {
366
- const config = {
367
- serverUrl: 'ws://localhost:8080/ws/agent',
368
- heartbeatInterval: 15000,
369
- reconnectInterval: 200,
370
- maxReconnectAttempts: -1,
371
- userId: 'user-1',
372
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
373
- };
374
- const client = new HttpClient(config);
375
- global.fetch = async () => ({
376
- ok: true,
377
- status: 200,
378
- statusText: 'OK',
379
- json: async () => ({ result: 'success' }),
380
- });
381
- await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /runtimeAccessToken is required; MCP nodes must claim a session token from launch binding/);
382
- });
383
- it('throws original 401 when bridge refresh fails', async () => {
384
- const config = {
385
- serverUrl: 'ws://localhost:8080/ws/agent',
386
- heartbeatInterval: 15000,
387
- reconnectInterval: 200,
388
- maxReconnectAttempts: -1,
389
- userId: 'user-1',
390
- runtimeAccessToken: 'stale-token',
391
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
392
- };
393
- const client = new HttpClient(config);
394
- global.fetch = async (url, options) => {
395
- fetchCalls.push([url.toString(), options]);
396
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
397
- return {
398
- ok: false,
399
- status: 503,
400
- statusText: 'Service Unavailable',
401
- };
402
- }
403
- return {
404
- ok: false,
405
- status: 401,
406
- statusText: 'Unauthorized',
407
- };
408
- };
409
- await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /MCP tool get_memory_stats call failed: 401 Unauthorized/);
410
- // Can retry again after failure
411
- global.fetch = async (url, options) => {
412
- fetchCalls.push([url.toString(), options]);
413
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
414
- return {
415
- ok: true,
416
- status: 200,
417
- statusText: 'OK',
418
- json: async () => ({ runtimeAccessToken: 'recovered-token' }),
419
- };
420
- }
421
- const token = options.headers['X-Runtime-Token'];
422
- if (token === 'recovered-token') {
423
- return {
424
- ok: true,
425
- status: 200,
426
- statusText: 'OK',
427
- json: async () => ({ token }),
428
- };
429
- }
430
- return {
431
- ok: false,
432
- status: 401,
433
- statusText: 'Unauthorized',
434
- };
435
- };
436
- const result = await client.callTool('get_memory_stats', { agentId: 'agent-1' });
437
- assert.deepEqual(result, { token: 'recovered-token' });
438
- assert.equal(config.runtimeAccessToken, 'recovered-token');
439
- });
440
- it('returns 401 when bridge refresh has no new token for running agent', async () => {
441
- const config = {
442
- serverUrl: 'ws://localhost:8080/ws/agent',
443
- heartbeatInterval: 15000,
444
- reconnectInterval: 200,
445
- maxReconnectAttempts: -1,
446
- runtimeAccessToken: 'stale-token',
447
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
448
- };
449
- const client = new HttpClient(config);
450
- global.fetch = async (url, options) => {
451
- fetchCalls.push([url.toString(), options]);
452
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
453
- // Bridge says no new token available (409 conflict)
454
- return {
455
- ok: false,
456
- status: 409,
457
- statusText: 'Conflict',
458
- json: async () => ({ error: 'token_unchanged' }),
459
- };
460
- }
461
- return {
462
- ok: false,
463
- status: 401,
464
- statusText: 'Unauthorized',
465
- };
466
- };
467
- await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /MCP tool get_memory_stats call failed: 401 Unauthorized/);
468
- // 1 initial MCP call + 1 refresh attempt + 0 retries (refresh failed)
469
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
470
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
471
- assert.equal(config.runtimeAccessToken, 'stale-token');
472
- });
473
- it('returns 404 from bridge refresh when agent has no cached token', async () => {
474
- const config = {
475
- serverUrl: 'ws://localhost:8080/ws/agent',
476
- heartbeatInterval: 15000,
477
- reconnectInterval: 200,
478
- maxReconnectAttempts: -1,
479
- userId: 'user-1',
480
- runtimeAccessToken: 'stale-token',
481
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
482
- };
483
- const client = new HttpClient(config);
484
- global.fetch = async (url, options) => {
485
- fetchCalls.push([url.toString(), options]);
486
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
487
- // Bridge has no cached token for this agent
488
- return {
489
- ok: false,
490
- status: 404,
491
- statusText: 'Not Found',
492
- json: async () => ({ error: 'no cached token for agent' }),
493
- };
494
- }
495
- return {
496
- ok: false,
497
- status: 401,
498
- statusText: 'Unauthorized',
499
- };
500
- };
501
- await assert.rejects(() => client.callTool('get_profile', { agentId: 'agent-unknown' }), /MCP tool get_profile call failed: 401 Unauthorized/);
502
- // 1 initial MCP call + 1 refresh attempt (404) + 0 retries
503
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
504
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
505
- assert.equal(config.runtimeAccessToken, 'stale-token');
506
- });
507
- it('does not retry 401 after bridge refresh returns same token', async () => {
508
- const config = {
509
- serverUrl: 'ws://localhost:8080/ws/agent',
510
- heartbeatInterval: 15000,
511
- reconnectInterval: 200,
512
- maxReconnectAttempts: -1,
513
- runtimeAccessToken: 'stale-token',
514
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
515
- };
516
- const client = new HttpClient(config);
517
- global.fetch = async (url, options) => {
518
- fetchCalls.push([url.toString(), options]);
519
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
520
- // Bridge returns same token
521
- return {
522
- ok: true,
523
- status: 200,
524
- statusText: 'OK',
525
- json: async () => ({ runtimeAccessToken: 'stale-token' }),
526
- };
527
- }
528
- return {
529
- ok: false,
530
- status: 401,
531
- statusText: 'Unauthorized',
532
- };
533
- };
534
- await assert.rejects(() => client.callTool('my_task', { agentId: 'agent-1' }), /MCP tool my_task call failed: 401 Unauthorized/);
535
- // 1 initial MCP call + 1 refresh + 0 retries (same token, not updated)
536
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
537
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
538
- });
539
- it('retries refresh on second call if first call 401 with different fresh token', async () => {
540
- const config = {
541
- serverUrl: 'ws://localhost:8080/ws/agent',
542
- heartbeatInterval: 15000,
543
- reconnectInterval: 200,
544
- maxReconnectAttempts: -1,
545
- userId: 'user-1',
546
- runtimeAccessToken: 'stale-token',
547
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
548
- };
549
- const client = new HttpClient(config);
550
- let refreshCount = 0;
551
- global.fetch = async (url, options) => {
552
- fetchCalls.push([url.toString(), options]);
553
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
554
- refreshCount += 1;
555
- return {
556
- ok: true,
557
- status: 200,
558
- statusText: 'OK',
559
- json: async () => ({ runtimeAccessToken: `fresh-token-${refreshCount}` }),
560
- };
561
- }
562
- const token = options.headers['X-Runtime-Token'];
563
- if (token && token.startsWith('fresh-token-')) {
564
- return {
565
- ok: false,
566
- status: 401,
567
- statusText: 'Unauthorized',
568
- };
569
- }
570
- return {
571
- ok: false,
572
- status: 401,
573
- statusText: 'Unauthorized',
574
- };
575
- };
576
- await assert.rejects(() => client.callTool('discover_group_rooms', { agentId: 'agent-1' }), /MCP tool discover_group_rooms call failed: 401 Unauthorized/);
577
- // Second call also tries refresh
578
- await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message call failed: 401 Unauthorized/);
579
- assert.equal(config.runtimeAccessToken, 'fresh-token-2');
580
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 2);
581
- });
582
- it('shares one refresh promise among concurrent 401 calls', async () => {
583
- const config = {
584
- serverUrl: 'ws://localhost:8080/ws/agent',
585
- heartbeatInterval: 15000,
586
- reconnectInterval: 200,
587
- maxReconnectAttempts: -1,
588
- userId: 'user-1',
589
- runtimeAccessToken: 'stale-token',
590
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
591
- };
592
- const client = new HttpClient(config);
593
- let resolveRefresh = () => { };
594
- const refreshGate = new Promise((resolve) => {
595
- resolveRefresh = resolve;
596
- });
597
- let refreshCallCount = 0;
598
- let freshTokenMcpCallCount = 0;
599
- global.fetch = async (url, options) => {
600
- fetchCalls.push([url.toString(), options]);
601
- if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
602
- refreshCallCount += 1;
603
- await refreshGate;
604
- return {
605
- ok: true,
606
- status: 200,
607
- statusText: 'OK',
608
- json: async () => ({ runtimeAccessToken: 'fresh-but-rejected-token' }),
609
- };
610
- }
611
- const requestHeaders = (options?.headers ?? {});
612
- if (requestHeaders.Authorization === 'Bearer fresh-but-rejected-token') {
613
- freshTokenMcpCallCount += 1;
614
- }
615
- return {
616
- ok: false,
617
- status: 401,
618
- statusText: 'Unauthorized',
619
- };
620
- };
621
- const firstCall = client.callTool('discover_group_rooms', { agentId: 'agent-1' });
622
- // Wait for the refresh to start
623
- await new Promise((resolve) => setTimeout(resolve, 20));
624
- // Second call should share the same refresh promise
625
- const secondCall = client.callTool('poll_message', { agentId: 'agent-1' });
626
- resolveRefresh();
627
- await assert.rejects(() => firstCall, /MCP tool discover_group_rooms call failed: 401 Unauthorized/);
628
- await assert.rejects(() => secondCall, /MCP tool poll_message call failed: 401 Unauthorized/);
629
- assert.equal(config.runtimeAccessToken, 'fresh-but-rejected-token');
630
- // Only one refresh call to bridge
631
- assert.equal(refreshCallCount, 1);
632
- assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
633
- // Both tools retried once with fresh token
634
- assert.equal(freshTokenMcpCallCount, 2);
635
- });
636
- it('handles 404 not found response', async () => {
637
- const config = {
638
- serverUrl: 'ws://localhost:8080/ws/agent',
639
- heartbeatInterval: 15000,
640
- reconnectInterval: 200,
641
- maxReconnectAttempts: -1,
642
- };
643
- const client = new HttpClient(config);
644
- global.fetch = async () => ({
645
- ok: false,
646
- status: 404,
647
- statusText: 'Not Found',
648
- });
649
- await assert.rejects(() => client.callTool('unknown-tool', {}), /MCP tool unknown-tool call failed: 404 Not Found/);
650
- });
651
- it('handles network errors', async () => {
652
- const config = {
653
- serverUrl: 'ws://localhost:8080/ws/agent',
654
- heartbeatInterval: 15000,
655
- reconnectInterval: 200,
656
- maxReconnectAttempts: -1,
657
- mcpRetryMaxAccumulatedMs: 0,
658
- };
659
- const client = new HttpClient(config);
660
- global.fetch = async () => {
661
- throw new Error('Network error');
662
- };
663
- await assert.rejects(() => client.callTool('test-tool', {}), /Network error/);
664
- });
665
- it('aborts MCP tool requests after configured timeout', async () => {
666
- const config = {
667
- serverUrl: 'ws://localhost:8080/ws/agent',
668
- heartbeatInterval: 15000,
669
- reconnectInterval: 200,
670
- maxReconnectAttempts: -1,
671
- mcpHttpRequestTimeoutMs: 10,
672
- mcpRetryMaxAccumulatedMs: 0,
673
- };
674
- const client = new HttpClient(config);
675
- global.fetch = async (url, options) => {
676
- fetchCalls.push([url.toString(), options]);
677
- return await new Promise((_resolve, reject) => {
678
- options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
679
- });
680
- };
681
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool timed out after 10ms/);
682
- assert.equal(fetchCalls.length, 1);
683
- assert.ok(fetchCalls[0][1].signal instanceof AbortSignal);
684
- });
685
- it('aborts poll_message requests after configured timeout', async () => {
686
- const config = {
687
- serverUrl: 'ws://localhost:8080/ws/agent',
688
- heartbeatInterval: 15000,
689
- reconnectInterval: 200,
690
- maxReconnectAttempts: -1,
691
- runtimeAccessToken: 'runtime-token',
692
- mcpHttpRequestTimeoutMs: 10,
693
- mcpRetryMaxAccumulatedMs: 0,
694
- };
695
- const client = new HttpClient(config);
696
- global.fetch = async (url, options) => {
697
- fetchCalls.push([url.toString(), options]);
698
- return await new Promise((_resolve, reject) => {
699
- options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
700
- });
701
- };
702
- await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message timed out after 10ms/);
703
- assert.equal(fetchCalls.length, 1);
704
- assert.ok(fetchCalls[0][1].signal instanceof AbortSignal);
705
- });
706
- it('retries on 5xx response until deadline exhausted', async () => {
707
- const config = {
708
- serverUrl: 'ws://localhost:8080/ws/agent',
709
- heartbeatInterval: 15000,
710
- reconnectInterval: 200,
711
- maxReconnectAttempts: -1,
712
- mcpRetryIntervalMs: 1,
713
- mcpRetryMaxAccumulatedMs: 20,
714
- };
715
- const client = new HttpClient(config);
716
- global.fetch = async (url, options) => {
717
- fetchCalls.push([url.toString(), options]);
718
- return {
719
- ok: false,
720
- status: 503,
721
- statusText: 'Service Unavailable',
722
- };
723
- };
724
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 503 Service Unavailable/);
725
- // Multiple fetch calls due to retries
726
- assert.ok(fetchCalls.length > 1);
727
- });
728
- it('retries on network error until deadline exhausted', async () => {
729
- const config = {
730
- serverUrl: 'ws://localhost:8080/ws/agent',
731
- heartbeatInterval: 15000,
732
- reconnectInterval: 200,
733
- maxReconnectAttempts: -1,
734
- mcpRetryIntervalMs: 1,
735
- mcpRetryMaxAccumulatedMs: 20,
736
- };
737
- const client = new HttpClient(config);
738
- global.fetch = async (url, options) => {
739
- fetchCalls.push([url.toString(), options]);
740
- throw new Error('Network error');
741
- };
742
- await assert.rejects(() => client.callTool('test-tool', {}), /Network error/);
743
- assert.ok(fetchCalls.length > 1);
744
- });
745
- it('retries on timeout until deadline exhausted', async () => {
746
- const config = {
747
- serverUrl: 'ws://localhost:8080/ws/agent',
748
- heartbeatInterval: 15000,
749
- reconnectInterval: 200,
750
- maxReconnectAttempts: -1,
751
- mcpHttpRequestTimeoutMs: 10,
752
- mcpRetryIntervalMs: 1,
753
- mcpRetryMaxAccumulatedMs: 30,
754
- };
755
- const client = new HttpClient(config);
756
- global.fetch = async (url, options) => {
757
- fetchCalls.push([url.toString(), options]);
758
- return await new Promise((_resolve, reject) => {
759
- options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
760
- });
761
- };
762
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool timed out after 10ms/);
763
- // Multiple fetch calls due to retries
764
- assert.ok(fetchCalls.length > 1);
765
- });
766
- it('does not retry on 404 client error', async () => {
767
- const config = {
768
- serverUrl: 'ws://localhost:8080/ws/agent',
769
- heartbeatInterval: 15000,
770
- reconnectInterval: 200,
771
- maxReconnectAttempts: -1,
772
- mcpRetryIntervalMs: 100,
773
- mcpRetryMaxAccumulatedMs: 30000,
774
- };
775
- const client = new HttpClient(config);
776
- global.fetch = async (url, options) => {
777
- fetchCalls.push([url.toString(), options]);
778
- return {
779
- ok: false,
780
- status: 404,
781
- statusText: 'Not Found',
782
- };
783
- };
784
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 404 Not Found/);
785
- // Only 1 fetch call — no retry on 4xx
786
- assert.equal(fetchCalls.length, 1);
787
- });
788
- it('does not retry on business error in response body', async () => {
789
- const config = {
790
- serverUrl: 'ws://localhost:8080/ws/agent',
791
- heartbeatInterval: 15000,
792
- reconnectInterval: 200,
793
- maxReconnectAttempts: -1,
794
- mcpRetryMaxAccumulatedMs: 0,
795
- };
796
- const client = new HttpClient(config);
797
- global.fetch = async (url, options) => {
798
- fetchCalls.push([url.toString(), options]);
799
- return {
800
- ok: true,
801
- json: async () => ({ error: 'Tool execution failed' }),
802
- };
803
- };
804
- await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool failed: Tool execution failed/);
805
- assert.equal(fetchCalls.length, 1);
806
- });
807
- });
808
- });