@librechat/agents 3.1.83 → 3.1.84
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/dist/cjs/agents/AgentContext.cjs +26 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/events.cjs +2 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +5 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -3
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +28 -2
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +107 -34
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -4
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +27 -4
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/events.mjs +2 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +6 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +27 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +108 -35
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -4
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +3 -1
- package/dist/types/tools/CodeExecutor.d.ts +5 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +14 -3
- package/dist/types/types/tools.d.ts +6 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +32 -3
- package/src/agents/__tests__/AgentContext.test.ts +36 -3
- package/src/events.ts +4 -1
- package/src/tools/BashExecutor.ts +14 -3
- package/src/tools/BashProgrammaticToolCalling.ts +6 -3
- package/src/tools/CodeExecutor.ts +36 -2
- package/src/tools/ProgrammaticToolCalling.ts +175 -30
- package/src/tools/ToolNode.ts +3 -4
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +321 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +31 -1
- package/src/types/tools.ts +10 -0
|
@@ -5,7 +5,12 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
|
5
5
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
6
6
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
7
7
|
import type * as t from '@/types';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildCodeApiHttpErrorMessage,
|
|
10
|
+
emptyOutputMessage,
|
|
11
|
+
getCodeBaseURL,
|
|
12
|
+
resolveCodeApiAuthHeaders,
|
|
13
|
+
} from './CodeExecutor';
|
|
9
14
|
import { Constants } from '@/common';
|
|
10
15
|
|
|
11
16
|
config();
|
|
@@ -147,6 +152,113 @@ const PYTHON_KEYWORDS = new Set([
|
|
|
147
152
|
'yield',
|
|
148
153
|
]);
|
|
149
154
|
|
|
155
|
+
export type FetchSessionFilesScope =
|
|
156
|
+
| { kind: 'skill'; id: string; version: number }
|
|
157
|
+
| { kind: 'agent' | 'user'; id: string; version?: never };
|
|
158
|
+
|
|
159
|
+
type CodeApiSessionFileWire = {
|
|
160
|
+
id?: unknown;
|
|
161
|
+
name?: unknown;
|
|
162
|
+
metadata?: unknown;
|
|
163
|
+
resource_id?: unknown;
|
|
164
|
+
storage_session_id?: unknown;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type CodeApiSessionFileMetadata = {
|
|
168
|
+
'original-filename'?: unknown;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function isFetchSessionFilesScope(
|
|
172
|
+
value: unknown
|
|
173
|
+
): value is FetchSessionFilesScope {
|
|
174
|
+
if (value == null || typeof value !== 'object') {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const scope = value as { kind?: unknown; id?: unknown; version?: unknown };
|
|
178
|
+
if (
|
|
179
|
+
(scope.kind === 'agent' || scope.kind === 'user') &&
|
|
180
|
+
typeof scope.id === 'string'
|
|
181
|
+
) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return (
|
|
185
|
+
scope.kind === 'skill' &&
|
|
186
|
+
typeof scope.id === 'string' &&
|
|
187
|
+
typeof scope.version === 'number'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isCodeApiAuthHeaders(
|
|
192
|
+
value: string | t.CodeApiAuthHeaders | undefined
|
|
193
|
+
): value is t.CodeApiAuthHeaders {
|
|
194
|
+
return value != null && typeof value !== 'string';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isCodeApiSessionFileWire(
|
|
198
|
+
value: unknown
|
|
199
|
+
): value is CodeApiSessionFileWire {
|
|
200
|
+
return value != null && typeof value === 'object';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isCodeApiSessionFileMetadata(
|
|
204
|
+
value: unknown
|
|
205
|
+
): value is CodeApiSessionFileMetadata {
|
|
206
|
+
return value != null && typeof value === 'object';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeSessionFile(
|
|
210
|
+
file: CodeApiSessionFileWire,
|
|
211
|
+
sessionId: string,
|
|
212
|
+
scope?: FetchSessionFilesScope
|
|
213
|
+
): t.CodeEnvFile {
|
|
214
|
+
const metadata = isCodeApiSessionFileMetadata(file.metadata)
|
|
215
|
+
? file.metadata
|
|
216
|
+
: undefined;
|
|
217
|
+
const rawName = typeof file.name === 'string' ? file.name : '';
|
|
218
|
+
const nameParts = rawName.split('/');
|
|
219
|
+
const fallbackId = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
|
|
220
|
+
const id =
|
|
221
|
+
typeof file.id === 'string' && file.id !== '' ? file.id : fallbackId;
|
|
222
|
+
const originalFilename = metadata?.['original-filename'];
|
|
223
|
+
const name =
|
|
224
|
+
typeof originalFilename === 'string' ? originalFilename : rawName;
|
|
225
|
+
const storage_session_id =
|
|
226
|
+
typeof file.storage_session_id === 'string'
|
|
227
|
+
? file.storage_session_id
|
|
228
|
+
: sessionId;
|
|
229
|
+
const resource_id =
|
|
230
|
+
typeof file.resource_id === 'string' && file.resource_id !== ''
|
|
231
|
+
? file.resource_id
|
|
232
|
+
: (scope?.id ?? id);
|
|
233
|
+
|
|
234
|
+
if (scope?.kind === 'skill') {
|
|
235
|
+
return {
|
|
236
|
+
storage_session_id,
|
|
237
|
+
kind: 'skill',
|
|
238
|
+
id,
|
|
239
|
+
resource_id,
|
|
240
|
+
name,
|
|
241
|
+
version: scope.version,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (scope != null) {
|
|
245
|
+
return {
|
|
246
|
+
storage_session_id,
|
|
247
|
+
kind: scope.kind,
|
|
248
|
+
id,
|
|
249
|
+
resource_id,
|
|
250
|
+
name,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
storage_session_id,
|
|
255
|
+
kind: 'user',
|
|
256
|
+
id,
|
|
257
|
+
resource_id: id,
|
|
258
|
+
name,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
150
262
|
/**
|
|
151
263
|
* Normalizes a tool name to Python identifier format.
|
|
152
264
|
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
@@ -250,20 +362,62 @@ export function filterToolsByUsage(
|
|
|
250
362
|
* Files are returned as CodeEnvFile references to be included in the request.
|
|
251
363
|
* @param baseUrl - The base URL for the Code API
|
|
252
364
|
* @param sessionId - The session ID to fetch files from
|
|
365
|
+
* @param scope - Resource scope used by CodeAPI to authorize the session
|
|
253
366
|
* @param proxy - Optional HTTP proxy URL
|
|
254
367
|
* @returns Array of CodeEnvFile references, or empty array if fetch fails
|
|
255
368
|
*/
|
|
256
369
|
export async function fetchSessionFiles(
|
|
257
370
|
baseUrl: string,
|
|
258
371
|
sessionId: string,
|
|
259
|
-
proxy?: string
|
|
372
|
+
proxy?: string,
|
|
373
|
+
authHeaders?: t.CodeApiAuthHeaders
|
|
374
|
+
): Promise<t.CodeEnvFile[]>;
|
|
375
|
+
export async function fetchSessionFiles(
|
|
376
|
+
baseUrl: string,
|
|
377
|
+
sessionId: string,
|
|
378
|
+
scope: FetchSessionFilesScope,
|
|
379
|
+
proxyOrAuthHeaders?: string | t.CodeApiAuthHeaders,
|
|
380
|
+
authHeaders?: t.CodeApiAuthHeaders
|
|
381
|
+
): Promise<t.CodeEnvFile[]>;
|
|
382
|
+
export async function fetchSessionFiles(
|
|
383
|
+
baseUrl: string,
|
|
384
|
+
sessionId: string,
|
|
385
|
+
scopeOrProxy?: FetchSessionFilesScope | string,
|
|
386
|
+
proxyOrAuthHeaders?: string | t.CodeApiAuthHeaders,
|
|
387
|
+
scopedAuthHeaders?: t.CodeApiAuthHeaders
|
|
260
388
|
): Promise<t.CodeEnvFile[]> {
|
|
261
389
|
try {
|
|
262
|
-
const
|
|
390
|
+
const scope = isFetchSessionFilesScope(scopeOrProxy)
|
|
391
|
+
? scopeOrProxy
|
|
392
|
+
: undefined;
|
|
393
|
+
let proxy: string | undefined;
|
|
394
|
+
let authHeaders: t.CodeApiAuthHeaders | undefined;
|
|
395
|
+
if (scope == null) {
|
|
396
|
+
proxy = typeof scopeOrProxy === 'string' ? scopeOrProxy : undefined;
|
|
397
|
+
authHeaders = isCodeApiAuthHeaders(proxyOrAuthHeaders)
|
|
398
|
+
? proxyOrAuthHeaders
|
|
399
|
+
: undefined;
|
|
400
|
+
} else if (typeof proxyOrAuthHeaders === 'string') {
|
|
401
|
+
proxy = proxyOrAuthHeaders;
|
|
402
|
+
authHeaders = scopedAuthHeaders;
|
|
403
|
+
} else {
|
|
404
|
+
authHeaders = proxyOrAuthHeaders ?? scopedAuthHeaders;
|
|
405
|
+
}
|
|
406
|
+
const query = new URLSearchParams({ detail: 'full' });
|
|
407
|
+
if (scope != null) {
|
|
408
|
+
query.set('kind', scope.kind);
|
|
409
|
+
query.set('id', scope.id);
|
|
410
|
+
if (scope.kind === 'skill') {
|
|
411
|
+
query.set('version', String(scope.version));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const filesEndpoint = `${baseUrl}/files/${encodeURIComponent(sessionId)}?${query.toString()}`;
|
|
415
|
+
const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
|
|
263
416
|
const fetchOptions: RequestInit = {
|
|
264
417
|
method: 'GET',
|
|
265
418
|
headers: {
|
|
266
419
|
'User-Agent': 'LibreChat/1.0',
|
|
420
|
+
...resolvedAuthHeaders,
|
|
267
421
|
},
|
|
268
422
|
};
|
|
269
423
|
|
|
@@ -273,7 +427,9 @@ export async function fetchSessionFiles(
|
|
|
273
427
|
|
|
274
428
|
const response = await fetch(filesEndpoint, fetchOptions);
|
|
275
429
|
if (!response.ok) {
|
|
276
|
-
throw new Error(
|
|
430
|
+
throw new Error(
|
|
431
|
+
await buildCodeApiHttpErrorMessage('GET', filesEndpoint, response)
|
|
432
|
+
);
|
|
277
433
|
}
|
|
278
434
|
|
|
279
435
|
const files = await response.json();
|
|
@@ -281,25 +437,9 @@ export async function fetchSessionFiles(
|
|
|
281
437
|
return [];
|
|
282
438
|
}
|
|
283
439
|
|
|
284
|
-
return files
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
storage_session_id: sessionId,
|
|
291
|
-
/* `/files` fallback returns code-output files belonging to
|
|
292
|
-
* the user; tag them user-private. */
|
|
293
|
-
kind: 'user' as const,
|
|
294
|
-
id,
|
|
295
|
-
/* `resource_id` informational for `kind: 'user'` —
|
|
296
|
-
* codeapi derives sessionKey from auth context. */
|
|
297
|
-
resource_id: id,
|
|
298
|
-
name: (file.metadata as Record<string, unknown>)[
|
|
299
|
-
'original-filename'
|
|
300
|
-
] as string,
|
|
301
|
-
};
|
|
302
|
-
});
|
|
440
|
+
return files
|
|
441
|
+
.filter(isCodeApiSessionFileWire)
|
|
442
|
+
.map((file) => normalizeSessionFile(file, sessionId, scope));
|
|
303
443
|
} catch (error) {
|
|
304
444
|
// eslint-disable-next-line no-console
|
|
305
445
|
console.warn(
|
|
@@ -319,13 +459,16 @@ export async function fetchSessionFiles(
|
|
|
319
459
|
export async function makeRequest(
|
|
320
460
|
endpoint: string,
|
|
321
461
|
body: Record<string, unknown>,
|
|
322
|
-
proxy?: string
|
|
462
|
+
proxy?: string,
|
|
463
|
+
authHeaders?: t.CodeApiAuthHeaders
|
|
323
464
|
): Promise<t.ProgrammaticExecutionResponse> {
|
|
465
|
+
const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
|
|
324
466
|
const fetchOptions: RequestInit = {
|
|
325
467
|
method: 'POST',
|
|
326
468
|
headers: {
|
|
327
469
|
'Content-Type': 'application/json',
|
|
328
470
|
'User-Agent': 'LibreChat/1.0',
|
|
471
|
+
...resolvedAuthHeaders,
|
|
329
472
|
},
|
|
330
473
|
body: JSON.stringify(body),
|
|
331
474
|
};
|
|
@@ -337,9 +480,8 @@ export async function makeRequest(
|
|
|
337
480
|
const response = await fetch(endpoint, fetchOptions);
|
|
338
481
|
|
|
339
482
|
if (!response.ok) {
|
|
340
|
-
const errorText = await response.text();
|
|
341
483
|
throw new Error(
|
|
342
|
-
|
|
484
|
+
await buildCodeApiHttpErrorMessage('POST', endpoint, response)
|
|
343
485
|
);
|
|
344
486
|
}
|
|
345
487
|
|
|
@@ -486,7 +628,8 @@ export function unwrapToolResponse(
|
|
|
486
628
|
*/
|
|
487
629
|
export async function executeTools(
|
|
488
630
|
toolCalls: t.PTCToolCall[],
|
|
489
|
-
toolMap: t.ToolMap
|
|
631
|
+
toolMap: t.ToolMap,
|
|
632
|
+
programmaticToolName = Constants.PROGRAMMATIC_TOOL_CALLING
|
|
490
633
|
): Promise<t.PTCToolResult[]> {
|
|
491
634
|
const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {
|
|
492
635
|
const tool = toolMap.get(call.name);
|
|
@@ -502,7 +645,7 @@ export async function executeTools(
|
|
|
502
645
|
|
|
503
646
|
try {
|
|
504
647
|
const result = await tool.invoke(call.input, {
|
|
505
|
-
metadata: { [
|
|
648
|
+
metadata: { [programmaticToolName]: true },
|
|
506
649
|
});
|
|
507
650
|
|
|
508
651
|
const isMCPTool = tool.mcp === true;
|
|
@@ -661,7 +804,8 @@ export function createProgrammaticToolCallingTool(
|
|
|
661
804
|
timeout,
|
|
662
805
|
...(files && files.length > 0 ? { files } : {}),
|
|
663
806
|
},
|
|
664
|
-
proxy
|
|
807
|
+
proxy,
|
|
808
|
+
initParams.authHeaders
|
|
665
809
|
);
|
|
666
810
|
|
|
667
811
|
// ====================================================================
|
|
@@ -697,7 +841,8 @@ export function createProgrammaticToolCallingTool(
|
|
|
697
841
|
continuation_token: response.continuation_token,
|
|
698
842
|
tool_results: toolResults,
|
|
699
843
|
},
|
|
700
|
-
proxy
|
|
844
|
+
proxy,
|
|
845
|
+
initParams.authHeaders
|
|
701
846
|
);
|
|
702
847
|
}
|
|
703
848
|
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -844,10 +844,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
844
844
|
// Plumb the hook context into the programmatic-tool path so
|
|
845
845
|
// inner tool calls made via the in-process bridge can run
|
|
846
846
|
// through `PreToolUse` (deny / updatedInput) before reaching
|
|
847
|
-
// the underlying tool. Without this,
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// `write_file` / `edit_file` (manual review finding A).
|
|
847
|
+
// the underlying tool. Without this, programmatic tool calls
|
|
848
|
+
// bypass every PreToolUse hook the host registered for the tools
|
|
849
|
+
// they dispatch — including HITL gates on `write_file` / `edit_file`.
|
|
851
850
|
hookContext: {
|
|
852
851
|
registry: this.hookRegistry,
|
|
853
852
|
runId: (config.configurable?.run_id as string | undefined) ?? '',
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
|
3
|
+
import type { RequestInit } from 'node-fetch';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
import {
|
|
6
|
+
createCodeExecutionTool,
|
|
7
|
+
resolveCodeApiAuthHeaders,
|
|
8
|
+
} from '../CodeExecutor';
|
|
9
|
+
import { createBashExecutionTool } from '../BashExecutor';
|
|
10
|
+
import {
|
|
11
|
+
createProgrammaticToolCallingTool,
|
|
12
|
+
fetchSessionFiles,
|
|
13
|
+
makeRequest,
|
|
14
|
+
} from '../ProgrammaticToolCalling';
|
|
15
|
+
import { createBashProgrammaticToolCallingTool } from '../BashProgrammaticToolCalling';
|
|
16
|
+
|
|
17
|
+
jest.mock('node-fetch', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
default: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
type FetchMock = jest.MockedFunction<
|
|
23
|
+
(url: unknown, init?: unknown) => Promise<unknown>
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
const fetchMock = fetch as unknown as FetchMock;
|
|
27
|
+
|
|
28
|
+
function jsonResponse(body: unknown): unknown {
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
json: jest.fn(async () => body),
|
|
32
|
+
text: jest.fn(async () => JSON.stringify(body)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function completedResponse(stdout = 'ok'): unknown {
|
|
37
|
+
return jsonResponse({
|
|
38
|
+
status: 'completed',
|
|
39
|
+
session_id: 'session_123',
|
|
40
|
+
stdout,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function errorResponse(status: number, body: string): unknown {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
status,
|
|
48
|
+
text: jest.fn(async () => body),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toolDefs = [
|
|
53
|
+
{
|
|
54
|
+
name: 'lookup_user',
|
|
55
|
+
description: 'Lookup a user',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
] as unknown as t.LCTool[];
|
|
62
|
+
|
|
63
|
+
function toolMap(): t.ToolMap {
|
|
64
|
+
return new Map([
|
|
65
|
+
[
|
|
66
|
+
'lookup_user',
|
|
67
|
+
{
|
|
68
|
+
name: 'lookup_user',
|
|
69
|
+
invoke: jest.fn(async () => ({ id: 'user_123' })),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
]) as unknown as t.ToolMap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('CodeAPI auth header injection', () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
fetchMock.mockReset();
|
|
78
|
+
fetchMock.mockResolvedValue(completedResponse());
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('resolves static and dynamic auth header params', async () => {
|
|
82
|
+
await expect(
|
|
83
|
+
resolveCodeApiAuthHeaders({ Authorization: 'Bearer static' })
|
|
84
|
+
).resolves.toEqual({
|
|
85
|
+
Authorization: 'Bearer static',
|
|
86
|
+
});
|
|
87
|
+
await expect(
|
|
88
|
+
resolveCodeApiAuthHeaders(async () => ({
|
|
89
|
+
Authorization: 'Bearer dynamic',
|
|
90
|
+
}))
|
|
91
|
+
).resolves.toEqual({
|
|
92
|
+
Authorization: 'Bearer dynamic',
|
|
93
|
+
});
|
|
94
|
+
await expect(resolveCodeApiAuthHeaders()).resolves.toEqual({});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('keeps the no-auth request path unchanged', async () => {
|
|
98
|
+
await makeRequest('https://code.example.com/exec/programmatic', {
|
|
99
|
+
code: 'print(1)',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
103
|
+
'https://code.example.com/exec/programmatic',
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
headers: expect.not.objectContaining({
|
|
106
|
+
Authorization: expect.any(String),
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('forwards Authorization for direct code execution', async () => {
|
|
113
|
+
fetchMock.mockResolvedValueOnce(
|
|
114
|
+
jsonResponse({ session_id: 'session_123', stdout: '1\n' })
|
|
115
|
+
);
|
|
116
|
+
const tool = createCodeExecutionTool({
|
|
117
|
+
authHeaders: async () => ({ Authorization: 'Bearer code-token' }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await tool.invoke({ lang: 'py', code: 'print(1)' });
|
|
121
|
+
|
|
122
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
123
|
+
expect.any(String),
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
headers: expect.objectContaining({
|
|
126
|
+
Authorization: 'Bearer code-token',
|
|
127
|
+
}),
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
expect(
|
|
131
|
+
JSON.parse((fetchMock.mock.calls[0]?.[1] as RequestInit).body as string)
|
|
132
|
+
).not.toHaveProperty('authHeaders');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('forwards Authorization for bash execution', async () => {
|
|
136
|
+
fetchMock.mockResolvedValueOnce(
|
|
137
|
+
jsonResponse({ session_id: 'session_123', stdout: '1\n' })
|
|
138
|
+
);
|
|
139
|
+
const tool = createBashExecutionTool({
|
|
140
|
+
authHeaders: { Authorization: 'Bearer bash-token' },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await tool.invoke({ command: 'echo 1' });
|
|
144
|
+
|
|
145
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
146
|
+
expect.any(String),
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
headers: expect.objectContaining({
|
|
149
|
+
Authorization: 'Bearer bash-token',
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
expect(
|
|
154
|
+
JSON.parse((fetchMock.mock.calls[0]?.[1] as RequestInit).body as string)
|
|
155
|
+
).not.toHaveProperty('authHeaders');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('includes the CodeAPI endpoint and response body on direct execution failures', async () => {
|
|
159
|
+
fetchMock.mockResolvedValueOnce(errorResponse(404, 'Cannot POST /exec'));
|
|
160
|
+
const tool = createBashExecutionTool();
|
|
161
|
+
|
|
162
|
+
await expect(tool.invoke({ command: 'echo 1' })).rejects.toThrow(
|
|
163
|
+
/CodeAPI request failed: POST .*\/exec returned 404, body: Cannot POST \/exec/
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('forwards Authorization on programmatic initial and continuation requests', async () => {
|
|
168
|
+
fetchMock
|
|
169
|
+
.mockResolvedValueOnce(
|
|
170
|
+
jsonResponse({
|
|
171
|
+
status: 'tool_call_required',
|
|
172
|
+
continuation_token: 'continue_123',
|
|
173
|
+
tool_calls: [{ id: 'call_1', name: 'lookup_user', input: {} }],
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
.mockResolvedValueOnce(completedResponse('done'));
|
|
177
|
+
|
|
178
|
+
const tool = createProgrammaticToolCallingTool({
|
|
179
|
+
authHeaders: () => ({ Authorization: 'Bearer ptc-token' }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await tool.invoke(
|
|
183
|
+
{ code: 'result = await lookup_user()\nprint(result)' },
|
|
184
|
+
{
|
|
185
|
+
toolCall: {
|
|
186
|
+
name: 'programmatic_code_execution',
|
|
187
|
+
args: {},
|
|
188
|
+
toolMap: toolMap(),
|
|
189
|
+
toolDefs,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
195
|
+
for (const call of fetchMock.mock.calls) {
|
|
196
|
+
expect(call[1]).toEqual(
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
headers: expect.objectContaining({
|
|
199
|
+
Authorization: 'Bearer ptc-token',
|
|
200
|
+
}),
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('forwards Authorization for bash programmatic requests', async () => {
|
|
207
|
+
const tool = createBashProgrammaticToolCallingTool({
|
|
208
|
+
authHeaders: { Authorization: 'Bearer bash-ptc-token' },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await tool.invoke(
|
|
212
|
+
{ code: 'lookup_user "{}"' },
|
|
213
|
+
{
|
|
214
|
+
toolCall: {
|
|
215
|
+
name: 'bash_programmatic_code_execution',
|
|
216
|
+
args: {},
|
|
217
|
+
toolMap: toolMap(),
|
|
218
|
+
toolDefs,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
224
|
+
expect.any(String),
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
headers: expect.objectContaining({
|
|
227
|
+
Authorization: 'Bearer bash-ptc-token',
|
|
228
|
+
}),
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('fetches session files with the CodeAPI resource scope and auth headers', async () => {
|
|
234
|
+
fetchMock.mockResolvedValueOnce(
|
|
235
|
+
jsonResponse([
|
|
236
|
+
{
|
|
237
|
+
id: 'file-1',
|
|
238
|
+
resource_id: 'skill-1',
|
|
239
|
+
storage_session_id: 'session_123',
|
|
240
|
+
name: 'skill/file.txt',
|
|
241
|
+
kind: 'skill',
|
|
242
|
+
version: 7,
|
|
243
|
+
},
|
|
244
|
+
])
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const files = await fetchSessionFiles(
|
|
248
|
+
'https://code.example.com',
|
|
249
|
+
'session_123',
|
|
250
|
+
{ kind: 'skill', id: 'skill-1', version: 7 },
|
|
251
|
+
undefined,
|
|
252
|
+
{ Authorization: 'Bearer files-token' }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(files).toHaveLength(1);
|
|
256
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
257
|
+
'https://code.example.com/files/session_123?detail=full&kind=skill&id=skill-1&version=7',
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
headers: expect.objectContaining({
|
|
260
|
+
Authorization: 'Bearer files-token',
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('fetches scoped session files with auth headers and no proxy placeholder', async () => {
|
|
267
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
268
|
+
|
|
269
|
+
await fetchSessionFiles(
|
|
270
|
+
'https://code.example.com',
|
|
271
|
+
'session_123',
|
|
272
|
+
{ kind: 'skill', id: 'skill-1', version: 7 },
|
|
273
|
+
{ Authorization: 'Bearer scoped-files-token' }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
277
|
+
'https://code.example.com/files/session_123?detail=full&kind=skill&id=skill-1&version=7',
|
|
278
|
+
expect.objectContaining({
|
|
279
|
+
headers: expect.objectContaining({
|
|
280
|
+
Authorization: 'Bearer scoped-files-token',
|
|
281
|
+
}),
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('preserves the legacy fetchSessionFiles proxy/auth argument order', async () => {
|
|
287
|
+
fetchMock.mockResolvedValueOnce(
|
|
288
|
+
jsonResponse([
|
|
289
|
+
{
|
|
290
|
+
name: 'session_123/file-1.txt',
|
|
291
|
+
metadata: { 'original-filename': 'file.txt' },
|
|
292
|
+
},
|
|
293
|
+
])
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const files = await fetchSessionFiles(
|
|
297
|
+
'https://code.example.com',
|
|
298
|
+
'session_123',
|
|
299
|
+
'',
|
|
300
|
+
{ Authorization: 'Bearer legacy-files-token' }
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(files).toEqual([
|
|
304
|
+
{
|
|
305
|
+
storage_session_id: 'session_123',
|
|
306
|
+
kind: 'user',
|
|
307
|
+
id: 'file-1',
|
|
308
|
+
resource_id: 'file-1',
|
|
309
|
+
name: 'file.txt',
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
313
|
+
'https://code.example.com/files/session_123?detail=full',
|
|
314
|
+
expect.objectContaining({
|
|
315
|
+
headers: expect.objectContaining({
|
|
316
|
+
Authorization: 'Bearer legacy-files-token',
|
|
317
|
+
}),
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Unit tests for Programmatic Tool Calling.
|
|
4
4
|
* Tests manual invocation with mock tools and Code API responses.
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
6
|
+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
7
7
|
import type * as t from '@/types';
|
|
8
|
+
import { Constants } from '@/common';
|
|
8
9
|
import {
|
|
9
10
|
createProgrammaticToolCallingTool,
|
|
10
11
|
formatCompletedResponse,
|
|
@@ -56,6 +57,35 @@ describe('ProgrammaticToolCalling', () => {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
it('marks bash PTC inner tool invocations with bash metadata', async () => {
|
|
61
|
+
const invoke = jest.fn<
|
|
62
|
+
(_input: unknown, _config: unknown) => Promise<{ ok: boolean }>
|
|
63
|
+
>(async () => ({ ok: true }));
|
|
64
|
+
const customTool = {
|
|
65
|
+
name: 'custom_tool',
|
|
66
|
+
invoke,
|
|
67
|
+
} as unknown as t.GenericTool;
|
|
68
|
+
const customToolMap: t.ToolMap = new Map([['custom_tool', customTool]]);
|
|
69
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
70
|
+
{
|
|
71
|
+
id: 'call_001',
|
|
72
|
+
name: 'custom_tool',
|
|
73
|
+
input: { value: 1 },
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
await executeTools(
|
|
78
|
+
toolCalls,
|
|
79
|
+
customToolMap,
|
|
80
|
+
Constants.BASH_PROGRAMMATIC_TOOL_CALLING
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(invoke).toHaveBeenCalledWith(
|
|
84
|
+
{ value: 1 },
|
|
85
|
+
{ metadata: { [Constants.BASH_PROGRAMMATIC_TOOL_CALLING]: true } }
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
59
89
|
it('executes multiple tools in parallel', async () => {
|
|
60
90
|
const toolCalls: t.PTCToolCall[] = [
|
|
61
91
|
{
|
package/src/types/tools.ts
CHANGED
|
@@ -219,8 +219,16 @@ export type CodeExecutionToolParams =
|
|
|
219
219
|
session_id?: string;
|
|
220
220
|
user_id?: string;
|
|
221
221
|
files?: CodeEnvFile[];
|
|
222
|
+
/** Optional host-supplied Code API auth headers. */
|
|
223
|
+
authHeaders?: CodeApiAuthHeaders;
|
|
222
224
|
};
|
|
223
225
|
|
|
226
|
+
export type CodeApiAuthHeaderMap = Record<string, string>;
|
|
227
|
+
|
|
228
|
+
export type CodeApiAuthHeaders =
|
|
229
|
+
| CodeApiAuthHeaderMap
|
|
230
|
+
| (() => CodeApiAuthHeaderMap | Promise<CodeApiAuthHeaderMap>);
|
|
231
|
+
|
|
224
232
|
export type FileRef = {
|
|
225
233
|
/**
|
|
226
234
|
* Storage file id (the per-file uuid). See `CodeEnvFile.id` for
|
|
@@ -896,6 +904,8 @@ export type ProgrammaticToolCallingParams = {
|
|
|
896
904
|
proxy?: string;
|
|
897
905
|
/** Enable debug logging (or set PTC_DEBUG=true env var) */
|
|
898
906
|
debug?: boolean;
|
|
907
|
+
/** Optional host-supplied Code API auth headers. */
|
|
908
|
+
authHeaders?: CodeApiAuthHeaders;
|
|
899
909
|
};
|
|
900
910
|
|
|
901
911
|
// ============================================================================
|