@librechat/agents 3.6.13 → 3.6.15
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/llm/contextPressureMeter.cjs +8 -6
- package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +33 -14
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +3 -2
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +10 -14
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +67 -3
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/syntaxCheck.cjs +10 -14
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
- package/dist/cjs/tools/local/workspaceFS.cjs +2 -2
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +2 -2
- package/dist/cjs/utils/toolContent.cjs.map +1 -1
- package/dist/esm/llm/contextPressureMeter.mjs +8 -6
- package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +33 -15
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +4 -3
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +11 -15
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +63 -4
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/syntaxCheck.mjs +11 -15
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
- package/dist/esm/tools/local/workspaceFS.mjs +2 -2
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +3 -3
- package/dist/esm/utils/toolContent.mjs.map +1 -1
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +6 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +22 -0
- package/dist/types/tools/local/workspaceFS.d.ts +1 -1
- package/dist/types/types/tools.d.ts +22 -37
- package/package.json +2 -1
- package/src/llm/contextPressureMeter.ts +23 -13
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +68 -17
- package/src/tools/cloudflare/CloudflareSandboxTools.ts +4 -3
- package/src/tools/local/LocalCodingTools.ts +25 -35
- package/src/tools/local/LocalExecutionEngine.ts +120 -3
- package/src/tools/local/syntaxCheck.ts +23 -26
- package/src/tools/local/workspaceFS.ts +2 -3
- package/src/types/tools.ts +25 -37
- package/src/utils/toolContent.ts +9 -3
|
@@ -4,16 +4,19 @@ import { tool } from '@langchain/core/tools';
|
|
|
4
4
|
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
5
5
|
import type * as t from '@/types';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
createLocalProgrammaticToolCallingTool,
|
|
9
|
-
} from './LocalProgrammaticToolCalling';
|
|
10
|
-
import {
|
|
7
|
+
commandAvailabilityEnvCacheKey,
|
|
11
8
|
getSpawn,
|
|
12
9
|
getWorkspaceFS,
|
|
10
|
+
probeLocalCommandAvailability,
|
|
13
11
|
resolveWorkspacePathSafe,
|
|
12
|
+
setCommandAvailabilityCacheEntry,
|
|
14
13
|
spawnLocalProcess,
|
|
15
14
|
truncateLocalOutput,
|
|
16
15
|
} from './LocalExecutionEngine';
|
|
16
|
+
import {
|
|
17
|
+
createLocalBashProgrammaticToolCallingTool,
|
|
18
|
+
createLocalProgrammaticToolCallingTool,
|
|
19
|
+
} from './LocalProgrammaticToolCalling';
|
|
17
20
|
import {
|
|
18
21
|
createLocalBashExecutionTool,
|
|
19
22
|
createLocalCodeExecutionTool,
|
|
@@ -736,30 +739,14 @@ export function createLocalEditFileTool(
|
|
|
736
739
|
* spawn-per-search.
|
|
737
740
|
*/
|
|
738
741
|
// Per-backend × per-env cache. Codex P1 #34 — keying by spawn
|
|
739
|
-
// backend alone misses the case where two Runs share a backend but
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// instead of falling back to the Node walker. The inner Map is
|
|
743
|
-
// keyed by a stable JSON hash of the effective env so each unique
|
|
744
|
-
// env gets its own probe.
|
|
742
|
+
// backend alone misses the case where two Runs share a backend but vary
|
|
743
|
+
// `local.env`. The inner map uses non-plaintext environment hashes and is
|
|
744
|
+
// bounded so stable worlds do not retain unbounded variants or raw secrets.
|
|
745
745
|
let ripgrepAvailabilityByBackend = new WeakMap<
|
|
746
746
|
t.LocalSpawn,
|
|
747
|
-
Map<string,
|
|
747
|
+
Map<string, ReturnType<typeof probeLocalCommandAvailability>>
|
|
748
748
|
>();
|
|
749
749
|
|
|
750
|
-
function envCacheKey(env: NodeJS.ProcessEnv | undefined): string {
|
|
751
|
-
// PATH is the only env entry that affects command lookup, but
|
|
752
|
-
// hashing the whole env keeps the key correct for hosts that
|
|
753
|
-
// vary anything else relevant. Stable JSON via sorted keys so
|
|
754
|
-
// {A:1,B:2} and {B:2,A:1} produce the same hash.
|
|
755
|
-
if (env == null) return '';
|
|
756
|
-
const sorted: Record<string, string | undefined> = {};
|
|
757
|
-
for (const k of Object.keys(env).sort()) {
|
|
758
|
-
sorted[k] = env[k];
|
|
759
|
-
}
|
|
760
|
-
return JSON.stringify(sorted);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
750
|
async function isRipgrepAvailable(
|
|
764
751
|
config: t.LocalExecutionConfig
|
|
765
752
|
): Promise<boolean> {
|
|
@@ -769,20 +756,23 @@ async function isRipgrepAvailable(
|
|
|
769
756
|
envMap = new Map();
|
|
770
757
|
ripgrepAvailabilityByBackend.set(backend, envMap);
|
|
771
758
|
}
|
|
772
|
-
const envKey =
|
|
759
|
+
const envKey = commandAvailabilityEnvCacheKey(config.env);
|
|
773
760
|
let probePromise = envMap.get(envKey);
|
|
774
761
|
if (probePromise == null) {
|
|
775
|
-
probePromise =
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
envMap.
|
|
762
|
+
probePromise = probeLocalCommandAvailability('rg', ['--version'], config);
|
|
763
|
+
setCommandAvailabilityCacheEntry(envMap, envKey, probePromise);
|
|
764
|
+
}
|
|
765
|
+
const result = await probePromise;
|
|
766
|
+
if (!result.cacheable && envMap.get(envKey) === probePromise) {
|
|
767
|
+
envMap.delete(envKey);
|
|
768
|
+
}
|
|
769
|
+
if (result.cacheUntil != null && result.cacheUntil <= Date.now()) {
|
|
770
|
+
if (envMap.get(envKey) === probePromise) {
|
|
771
|
+
envMap.delete(envKey);
|
|
772
|
+
}
|
|
773
|
+
return isRipgrepAvailable(config);
|
|
784
774
|
}
|
|
785
|
-
return
|
|
775
|
+
return result.available;
|
|
786
776
|
}
|
|
787
777
|
|
|
788
778
|
/**
|
|
@@ -21,6 +21,36 @@ const DEFAULT_MAX_OUTPUT_CHARS = 200000;
|
|
|
21
21
|
const DEFAULT_MAX_SPAWNED_BYTES = 50 * 1024 * 1024;
|
|
22
22
|
const DEFAULT_LOCAL_SESSION_ID = 'local';
|
|
23
23
|
const DEFAULT_SHELL = process.platform === 'win32' ? 'bash.exe' : 'bash';
|
|
24
|
+
const MAX_COMMAND_AVAILABILITY_ENVIRONMENTS = 16;
|
|
25
|
+
const NEGATIVE_COMMAND_AVAILABILITY_TTL_MS = 5000;
|
|
26
|
+
|
|
27
|
+
/** Produces a stable, non-plaintext key for environment-sensitive probes. */
|
|
28
|
+
export function commandAvailabilityEnvCacheKey(
|
|
29
|
+
env: NodeJS.ProcessEnv | undefined
|
|
30
|
+
): string {
|
|
31
|
+
if (env == null) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
const sorted = Object.keys(env)
|
|
35
|
+
.sort()
|
|
36
|
+
.map((key): [string, string | null] => [key, env[key] ?? null]);
|
|
37
|
+
return createHash('sha256').update(JSON.stringify(sorted)).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adds a probe entry while bounding retained environment variants. */
|
|
41
|
+
export function setCommandAvailabilityCacheEntry<T>(
|
|
42
|
+
cache: Map<string, T>,
|
|
43
|
+
key: string,
|
|
44
|
+
value: T
|
|
45
|
+
): void {
|
|
46
|
+
if (!cache.has(key) && cache.size >= MAX_COMMAND_AVAILABILITY_ENVIRONMENTS) {
|
|
47
|
+
const oldestKey = cache.keys().next().value;
|
|
48
|
+
if (oldestKey !== undefined) {
|
|
49
|
+
cache.delete(oldestKey);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
cache.set(key, value);
|
|
53
|
+
}
|
|
24
54
|
|
|
25
55
|
// `(?:--\s+)?` before each destructive-target alternation: GNU/BSD
|
|
26
56
|
// utilities accept `--` as an end-of-options marker, so `rm -rf -- /`
|
|
@@ -259,13 +289,34 @@ export function getWorkspaceRoots(config?: t.LocalExecutionConfig): string[] {
|
|
|
259
289
|
return out;
|
|
260
290
|
}
|
|
261
291
|
|
|
292
|
+
/** Node-host execution world used when no backend override is configured. */
|
|
293
|
+
export const nodeExecutionWorld: t.ExecutionWorld = Object.freeze({
|
|
294
|
+
spawn: spawn as t.LocalSpawn,
|
|
295
|
+
fs: nodeWorkspaceFS,
|
|
296
|
+
sandboxed: false,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** Resolves filesystem and subprocess capabilities as one execution world. */
|
|
300
|
+
export function getExecutionWorld(
|
|
301
|
+
config?: t.LocalExecutionConfig
|
|
302
|
+
): t.ExecutionWorld {
|
|
303
|
+
if (config?.exec == null && config?.spawn == null) {
|
|
304
|
+
return nodeExecutionWorld;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
spawn: config.exec?.spawn ?? config.spawn ?? nodeExecutionWorld.spawn,
|
|
308
|
+
fs: config.exec?.fs ?? nodeExecutionWorld.fs,
|
|
309
|
+
sandboxed: config.exec?.sandboxed ?? false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
262
313
|
/**
|
|
263
314
|
* Pluggable spawn resolver. Honours `local.exec.spawn` first, falls
|
|
264
315
|
* back to the legacy top-level `local.spawn`, then to Node's
|
|
265
316
|
* `child_process.spawn`. Centralised so engine swapping is one knob.
|
|
266
317
|
*/
|
|
267
318
|
export function getSpawn(config?: t.LocalExecutionConfig): t.LocalSpawn {
|
|
268
|
-
return (config
|
|
319
|
+
return getExecutionWorld(config).spawn;
|
|
269
320
|
}
|
|
270
321
|
|
|
271
322
|
/**
|
|
@@ -274,7 +325,7 @@ export function getSpawn(config?: t.LocalExecutionConfig): t.LocalSpawn {
|
|
|
274
325
|
* its own implementation here and inherits every file-touching tool.
|
|
275
326
|
*/
|
|
276
327
|
export function getWorkspaceFS(config?: t.LocalExecutionConfig): WorkspaceFS {
|
|
277
|
-
return config
|
|
328
|
+
return getExecutionWorld(config).fs;
|
|
278
329
|
}
|
|
279
330
|
|
|
280
331
|
/**
|
|
@@ -345,7 +396,7 @@ function maybeWarnSandboxOff(config: t.LocalExecutionConfig): void {
|
|
|
345
396
|
if (
|
|
346
397
|
sandboxOffWarned ||
|
|
347
398
|
shouldUseLocalSandbox(config) ||
|
|
348
|
-
config.
|
|
399
|
+
getExecutionWorld(config).sandboxed
|
|
349
400
|
) {
|
|
350
401
|
return;
|
|
351
402
|
}
|
|
@@ -904,6 +955,72 @@ export async function spawnLocalProcess(
|
|
|
904
955
|
});
|
|
905
956
|
}
|
|
906
957
|
|
|
958
|
+
/** Result of a command-availability probe and whether its verdict is stable. */
|
|
959
|
+
export type CommandAvailabilityProbe = {
|
|
960
|
+
available: boolean;
|
|
961
|
+
cacheable: boolean;
|
|
962
|
+
cacheUntil?: number;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
async function isDurableCommandLookupError(
|
|
966
|
+
error: object,
|
|
967
|
+
config: t.LocalExecutionConfig
|
|
968
|
+
): Promise<boolean> {
|
|
969
|
+
if (!('code' in error) || error.code !== 'ENOENT') {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const cwd = getLocalCwd(config);
|
|
974
|
+
return (await getWorkspaceFS(config).stat(cwd)).isDirectory();
|
|
975
|
+
} catch {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Probes one executable without turning transient backend failures into
|
|
982
|
+
* permanent capability facts. Exit 126/127 is a definite unusable/missing
|
|
983
|
+
* executable. A native ENOENT is stable only when the current working
|
|
984
|
+
* directory exists; timeouts, transport failures, and ambiguous lookup
|
|
985
|
+
* failures are retried.
|
|
986
|
+
*/
|
|
987
|
+
export async function probeLocalCommandAvailability(
|
|
988
|
+
command: string,
|
|
989
|
+
args: string[],
|
|
990
|
+
config: t.LocalExecutionConfig
|
|
991
|
+
): Promise<CommandAvailabilityProbe> {
|
|
992
|
+
try {
|
|
993
|
+
const result = await spawnLocalProcess(
|
|
994
|
+
command,
|
|
995
|
+
args,
|
|
996
|
+
{ ...config, timeoutMs: 5000, sandbox: { enabled: false } },
|
|
997
|
+
{ internal: true }
|
|
998
|
+
);
|
|
999
|
+
const available = result.exitCode === 0;
|
|
1000
|
+
const durableNegative =
|
|
1001
|
+
result.exitCode === 126 || result.exitCode === 127;
|
|
1002
|
+
return {
|
|
1003
|
+
available,
|
|
1004
|
+
cacheable: available || durableNegative,
|
|
1005
|
+
...(durableNegative
|
|
1006
|
+
? { cacheUntil: Date.now() + NEGATIVE_COMMAND_AVAILABILITY_TTL_MS }
|
|
1007
|
+
: {}),
|
|
1008
|
+
};
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const cacheable =
|
|
1011
|
+
typeof error === 'object' &&
|
|
1012
|
+
error != null &&
|
|
1013
|
+
(await isDurableCommandLookupError(error, config));
|
|
1014
|
+
return {
|
|
1015
|
+
available: false,
|
|
1016
|
+
cacheable,
|
|
1017
|
+
...(cacheable
|
|
1018
|
+
? { cacheUntil: Date.now() + NEGATIVE_COMMAND_AVAILABILITY_TTL_MS }
|
|
1019
|
+
: {}),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
907
1024
|
export async function executeLocalBash(
|
|
908
1025
|
command: string,
|
|
909
1026
|
config: t.LocalExecutionConfig = {}
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
|
|
19
19
|
import { extname } from 'path';
|
|
20
20
|
import type * as t from '@/types';
|
|
21
|
-
import { isWorkspaceClientTimeoutError } from './workspaceFS';
|
|
22
21
|
import {
|
|
22
|
+
commandAvailabilityEnvCacheKey,
|
|
23
23
|
getSpawn,
|
|
24
24
|
getWorkspaceFS,
|
|
25
|
+
probeLocalCommandAvailability,
|
|
26
|
+
setCommandAvailabilityCacheEntry,
|
|
25
27
|
spawnLocalProcess,
|
|
26
28
|
} from './LocalExecutionEngine';
|
|
29
|
+
import { isWorkspaceClientTimeoutError } from './workspaceFS';
|
|
27
30
|
|
|
28
31
|
export type SyntaxCheckOutcome =
|
|
29
32
|
| { ok: true }
|
|
@@ -48,26 +51,17 @@ export type SyntaxChecker = (
|
|
|
48
51
|
* reset hook re-creates the map.
|
|
49
52
|
*/
|
|
50
53
|
type ProbeKind = 'hasNode' | 'hasPython' | 'hasBash';
|
|
51
|
-
type ProbeCache = Partial<
|
|
54
|
+
type ProbeCache = Partial<
|
|
55
|
+
Record<ProbeKind, ReturnType<typeof probeLocalCommandAvailability>>
|
|
56
|
+
>;
|
|
52
57
|
|
|
53
|
-
// Per-backend ×
|
|
54
|
-
//
|
|
55
|
-
// loses node between Runs that share the same backend). Same fix
|
|
56
|
-
// shape as the ripgrep cache (Codex P1 #34).
|
|
58
|
+
// Per-backend × hashed-env cache. The inner map is bounded so a long-lived
|
|
59
|
+
// world cannot retain unbounded environment variants.
|
|
57
60
|
let probeCacheByBackend = new WeakMap<
|
|
58
61
|
t.LocalSpawn,
|
|
59
62
|
Map<string, ProbeCache>
|
|
60
63
|
>();
|
|
61
64
|
|
|
62
|
-
function envCacheKey(env: NodeJS.ProcessEnv | undefined): string {
|
|
63
|
-
if (env == null) return '';
|
|
64
|
-
const sorted: Record<string, string | undefined> = {};
|
|
65
|
-
for (const k of Object.keys(env).sort()) {
|
|
66
|
-
sorted[k] = env[k];
|
|
67
|
-
}
|
|
68
|
-
return JSON.stringify(sorted);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
function cacheFor(
|
|
72
66
|
config: t.LocalExecutionConfig
|
|
73
67
|
): ProbeCache {
|
|
@@ -77,11 +71,11 @@ function cacheFor(
|
|
|
77
71
|
envMap = new Map();
|
|
78
72
|
probeCacheByBackend.set(backend, envMap);
|
|
79
73
|
}
|
|
80
|
-
const envKey =
|
|
74
|
+
const envKey = commandAvailabilityEnvCacheKey(config.env);
|
|
81
75
|
let entry = envMap.get(envKey);
|
|
82
76
|
if (entry == null) {
|
|
83
77
|
entry = {};
|
|
84
|
-
envMap
|
|
78
|
+
setCommandAvailabilityCacheEntry(envMap, envKey, entry);
|
|
85
79
|
}
|
|
86
80
|
return entry;
|
|
87
81
|
}
|
|
@@ -95,17 +89,20 @@ async function probe(
|
|
|
95
89
|
const entry = cacheFor(config);
|
|
96
90
|
let probePromise = entry[cached];
|
|
97
91
|
if (probePromise == null) {
|
|
98
|
-
probePromise =
|
|
99
|
-
command,
|
|
100
|
-
args,
|
|
101
|
-
{ ...config, timeoutMs: 5000, sandbox: { enabled: false } },
|
|
102
|
-
{ internal: true }
|
|
103
|
-
)
|
|
104
|
-
.then((result) => result != null && result.exitCode === 0)
|
|
105
|
-
.catch(() => false);
|
|
92
|
+
probePromise = probeLocalCommandAvailability(command, args, config);
|
|
106
93
|
entry[cached] = probePromise;
|
|
107
94
|
}
|
|
108
|
-
|
|
95
|
+
const result = await probePromise;
|
|
96
|
+
if (!result.cacheable && entry[cached] === probePromise) {
|
|
97
|
+
delete entry[cached];
|
|
98
|
+
}
|
|
99
|
+
if (result.cacheUntil != null && result.cacheUntil <= Date.now()) {
|
|
100
|
+
if (entry[cached] === probePromise) {
|
|
101
|
+
delete entry[cached];
|
|
102
|
+
}
|
|
103
|
+
return probe(command, args, cached, config);
|
|
104
|
+
}
|
|
105
|
+
return result.available;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
/**
|
|
@@ -92,10 +92,9 @@ export interface WorkspaceFS {
|
|
|
92
92
|
* Returned by `getWorkspaceFS(config)` when the host hasn't supplied
|
|
93
93
|
* an override on `local.exec.fs`.
|
|
94
94
|
*/
|
|
95
|
-
export const nodeWorkspaceFS
|
|
95
|
+
export const nodeWorkspaceFS = Object.freeze<WorkspaceFS>({
|
|
96
96
|
// The runtime impl ignores the encoding-vs-buffer distinction; the
|
|
97
97
|
// overload signatures above are what callers see.
|
|
98
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
98
|
readFile: ((path: string, encoding?: 'utf8') =>
|
|
100
99
|
encoding != null
|
|
101
100
|
? fsReadFile(path, encoding)
|
|
@@ -113,4 +112,4 @@ export const nodeWorkspaceFS: WorkspaceFS = {
|
|
|
113
112
|
realpath: (path) => fsRealpath(path),
|
|
114
113
|
unlink: (path) => fsUnlink(path),
|
|
115
114
|
open: (path, flags) => fsOpen(path, flags),
|
|
116
|
-
};
|
|
115
|
+
});
|
package/src/types/tools.ts
CHANGED
|
@@ -820,47 +820,35 @@ export type LocalWorkspaceConfig = {
|
|
|
820
820
|
};
|
|
821
821
|
|
|
822
822
|
/**
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
|
|
823
|
+
* One execution world shared by filesystem and subprocess operations.
|
|
824
|
+
* Backend identity is stable so capability probes remain warm when tool
|
|
825
|
+
* bundles are rebuilt for later agent bindings.
|
|
826
|
+
*/
|
|
827
|
+
export interface ExecutionWorld {
|
|
828
|
+
/** Launches a process inside this world's filesystem namespace. */
|
|
829
|
+
readonly spawn: LocalSpawn;
|
|
830
|
+
/** Reads and writes the same namespace observed by `spawn`. */
|
|
831
|
+
readonly fs: Readonly<import('@/tools/local/workspaceFS').WorkspaceFS>;
|
|
832
|
+
/** Whether the world already enforces a sandbox boundary. */
|
|
833
|
+
readonly sandboxed: boolean;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Backward-compatible partial execution-world override. Omitted fields use
|
|
838
|
+
* the Node host world; remote backends should provide the complete trio.
|
|
827
839
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
* set of helpers — currently the `execute_code` non-bash temp-file
|
|
832
|
-
* write (Codex P2 [48]) — still uses host `fs/promises` directly to
|
|
833
|
-
* stage source on disk before invoking the spawn. If you override
|
|
834
|
-
* `spawn` to point at a remote runtime (SSH, container, etc.) you
|
|
835
|
-
* MUST also override `fs` with the corresponding remote
|
|
836
|
-
* implementation; otherwise temp source files written to the host
|
|
837
|
-
* `/tmp` won't be visible to the remote interpreter and `py`/`js`/
|
|
838
|
-
* `ts`/etc. executions will fail. (Bash-style executions go through
|
|
839
|
-
* `executeLocalBash` which doesn't stage temp files, so they're
|
|
840
|
-
* unaffected.)
|
|
840
|
+
* Non-bash `execute_code` still stages source through the host temporary
|
|
841
|
+
* directory. Remote runtimes should use their dedicated execution engine for
|
|
842
|
+
* that tool until temporary lifecycle operations join this seam.
|
|
841
843
|
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* The hard security boundary is `local.sandbox.enabled: true` (which
|
|
846
|
-
* wraps execution in `@anthropic-ai/sandbox-runtime`); for adversarial-
|
|
847
|
-
* model threat models, do NOT rely on the regex layer alone.
|
|
844
|
+
* Command validators and workspace policies are best-effort tripwires. For an
|
|
845
|
+
* adversarial-model threat model, use a backend sandbox boundary rather than
|
|
846
|
+
* relying on validation alone.
|
|
848
847
|
*/
|
|
849
848
|
export type LocalExecConfig = {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
* Pluggable filesystem (for remote-workspace engines). Pair with
|
|
854
|
-
* `spawn` — see the type-level note above on why both should be
|
|
855
|
-
* overridden together for non-host engines.
|
|
856
|
-
*/
|
|
857
|
-
fs?: import('@/tools/local/workspaceFS').WorkspaceFS;
|
|
858
|
-
/**
|
|
859
|
-
* Set by custom execution backends that already provide their own
|
|
860
|
-
* sandbox boundary. Suppresses the local host-sandbox warning while
|
|
861
|
-
* preserving the warning for plain host `child_process` execution.
|
|
862
|
-
*/
|
|
863
|
-
sandboxed?: boolean;
|
|
849
|
+
-readonly [Key in keyof ExecutionWorld]?: Key extends 'fs'
|
|
850
|
+
? import('@/tools/local/workspaceFS').WorkspaceFS
|
|
851
|
+
: ExecutionWorld[Key];
|
|
864
852
|
};
|
|
865
853
|
|
|
866
854
|
export type LocalExecutionConfig = {
|
package/src/utils/toolContent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isProxy } from 'node:util/types';
|
|
1
|
+
import { isArrayBuffer, isDate, isProxy } from 'node:util/types';
|
|
2
2
|
import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
|
|
3
3
|
import {
|
|
4
4
|
HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
|
|
@@ -151,11 +151,17 @@ function jsonStringLength(
|
|
|
151
151
|
|
|
152
152
|
type NativeDateRead = { matched: true; time: number } | { matched: false };
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* `isArrayBuffer` reads the internal slot natively (no user code, cross-realm safe) and
|
|
156
|
+
* costs nothing for the plain objects that make up almost all content; the getter's
|
|
157
|
+
* throw path was the expensive part, so it only runs once the brand is already known.
|
|
158
|
+
*/
|
|
154
159
|
function readNativeArrayBufferByteLength(value: unknown): number | undefined {
|
|
155
160
|
if (
|
|
156
161
|
value == null ||
|
|
157
162
|
typeof value !== 'object' ||
|
|
158
|
-
NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER == null
|
|
163
|
+
NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER == null ||
|
|
164
|
+
!isArrayBuffer(value)
|
|
159
165
|
) {
|
|
160
166
|
return undefined;
|
|
161
167
|
}
|
|
@@ -167,7 +173,7 @@ function readNativeArrayBufferByteLength(value: unknown): number | undefined {
|
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
function readNativeDate(value: unknown): NativeDateRead {
|
|
170
|
-
if (value == null || typeof value !== 'object') {
|
|
176
|
+
if (value == null || typeof value !== 'object' || !isDate(value)) {
|
|
171
177
|
return { matched: false };
|
|
172
178
|
}
|
|
173
179
|
try {
|