@librechat/agents 3.6.13 → 3.6.14

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 (38) hide show
  1. package/dist/cjs/main.cjs +6 -0
  2. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +33 -14
  3. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  4. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +3 -2
  5. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  6. package/dist/cjs/tools/local/LocalCodingTools.cjs +10 -14
  7. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  8. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +67 -3
  9. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  10. package/dist/cjs/tools/local/syntaxCheck.cjs +10 -14
  11. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
  12. package/dist/cjs/tools/local/workspaceFS.cjs +2 -2
  13. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
  14. package/dist/esm/main.mjs +3 -3
  15. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +33 -15
  16. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  17. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +4 -3
  18. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  19. package/dist/esm/tools/local/LocalCodingTools.mjs +11 -15
  20. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  21. package/dist/esm/tools/local/LocalExecutionEngine.mjs +63 -4
  22. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  23. package/dist/esm/tools/local/syntaxCheck.mjs +11 -15
  24. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
  25. package/dist/esm/tools/local/workspaceFS.mjs +2 -2
  26. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
  27. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +6 -0
  28. package/dist/types/tools/local/LocalExecutionEngine.d.ts +22 -0
  29. package/dist/types/tools/local/workspaceFS.d.ts +1 -1
  30. package/dist/types/types/tools.d.ts +22 -37
  31. package/package.json +2 -1
  32. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +68 -17
  33. package/src/tools/cloudflare/CloudflareSandboxTools.ts +4 -3
  34. package/src/tools/local/LocalCodingTools.ts +25 -35
  35. package/src/tools/local/LocalExecutionEngine.ts +120 -3
  36. package/src/tools/local/syntaxCheck.ts +23 -26
  37. package/src/tools/local/workspaceFS.ts +2 -3
  38. package/src/types/tools.ts +25 -37
@@ -1,14 +1,8 @@
1
1
  import { isWorkspaceClientTimeoutError } from "./workspaceFS.mjs";
2
- import { getSpawn, getWorkspaceFS, spawnLocalProcess } from "./LocalExecutionEngine.mjs";
2
+ import { commandAvailabilityEnvCacheKey, getSpawn, getWorkspaceFS, probeLocalCommandAvailability, setCommandAvailabilityCacheEntry, spawnLocalProcess } from "./LocalExecutionEngine.mjs";
3
3
  import { extname } from "path";
4
4
  //#region src/tools/local/syntaxCheck.ts
5
5
  let probeCacheByBackend = /* @__PURE__ */ new WeakMap();
6
- function envCacheKey(env) {
7
- if (env == null) return "";
8
- const sorted = {};
9
- for (const k of Object.keys(env).sort()) sorted[k] = env[k];
10
- return JSON.stringify(sorted);
11
- }
12
6
  function cacheFor(config) {
13
7
  const backend = getSpawn(config);
14
8
  let envMap = probeCacheByBackend.get(backend);
@@ -16,11 +10,11 @@ function cacheFor(config) {
16
10
  envMap = /* @__PURE__ */ new Map();
17
11
  probeCacheByBackend.set(backend, envMap);
18
12
  }
19
- const envKey = envCacheKey(config.env);
13
+ const envKey = commandAvailabilityEnvCacheKey(config.env);
20
14
  let entry = envMap.get(envKey);
21
15
  if (entry == null) {
22
16
  entry = {};
23
- envMap.set(envKey, entry);
17
+ setCommandAvailabilityCacheEntry(envMap, envKey, entry);
24
18
  }
25
19
  return entry;
26
20
  }
@@ -28,14 +22,16 @@ async function probe(command, args, cached, config) {
28
22
  const entry = cacheFor(config);
29
23
  let probePromise = entry[cached];
30
24
  if (probePromise == null) {
31
- probePromise = spawnLocalProcess(command, args, {
32
- ...config,
33
- timeoutMs: 5e3,
34
- sandbox: { enabled: false }
35
- }, { internal: true }).then((result) => result != null && result.exitCode === 0).catch(() => false);
25
+ probePromise = probeLocalCommandAvailability(command, args, config);
36
26
  entry[cached] = probePromise;
37
27
  }
38
- return probePromise;
28
+ const result = await probePromise;
29
+ if (!result.cacheable && entry[cached] === probePromise) delete entry[cached];
30
+ if (result.cacheUntil != null && result.cacheUntil <= Date.now()) {
31
+ if (entry[cached] === probePromise) delete entry[cached];
32
+ return probe(command, args, cached, config);
33
+ }
34
+ return result.available;
39
35
  }
40
36
  function _resetSyntaxCheckProbeCacheForTests() {
41
37
  probeCacheByBackend = /* @__PURE__ */ new WeakMap();
@@ -1 +1 @@
1
- {"version":3,"file":"syntaxCheck.mjs","names":[],"sources":["../../../../src/tools/local/syntaxCheck.ts"],"mappings":"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"}
1
+ {"version":3,"file":"syntaxCheck.mjs","names":[],"sources":["../../../../src/tools/local/syntaxCheck.ts"],"mappings":"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"}
@@ -10,7 +10,7 @@ var WorkspaceClientTimeoutError = class extends Error {
10
10
  function isWorkspaceClientTimeoutError(error) {
11
11
  return error instanceof WorkspaceClientTimeoutError || typeof error === "object" && error !== null && error.code === "WORKSPACE_CLIENT_TIMEOUT";
12
12
  }
13
- const nodeWorkspaceFS = {
13
+ const nodeWorkspaceFS = Object.freeze({
14
14
  readFile: ((path, encoding) => encoding != null ? readFile(path, encoding) : readFile(path)),
15
15
  writeFile: (path, content, options) => writeFile(path, content, options ?? "utf8"),
16
16
  stat: (path) => stat(path),
@@ -21,7 +21,7 @@ const nodeWorkspaceFS = {
21
21
  realpath: (path) => realpath(path),
22
22
  unlink: (path) => unlink(path),
23
23
  open: (path, flags) => open(path, flags)
24
- };
24
+ });
25
25
  //#endregion
26
26
  export { WorkspaceClientTimeoutError, isWorkspaceClientTimeoutError, nodeWorkspaceFS };
27
27
 
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceFS.mjs","names":["fsReadFile","fsWriteFile","fsStat","fsReaddir","fsMkdir","fsRealpath","fsUnlink","fsOpen"],"sources":["../../../../src/tools/local/workspaceFS.ts"],"mappings":";;AAiDA,IAAa,8BAAb,cAAiD,MAAM;CACrD,OAAgB;CAChB,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,8BACd,OACsC;CACtC,OACE,iBAAiB,+BAChB,OAAO,UAAU,YAChB,UAAU,QACT,MAA6B,SAAS;AAE7C;AA4BA,MAAa,kBAA+B;CAI1C,YAAY,MAAc,aACxB,YAAY,OACRA,SAAW,MAAM,QAAQ,IACzBA,SAAW,IAAI;CACrB,YAAY,MAAM,SAAS,YACzBC,UAAY,MAAM,SAAS,WAAW,MAAM;CAC9C,OAAO,SAASC,KAAO,IAAI;CAC3B,WAAW,MAAc,YACvB,SAAS,kBAAkB,OACvBC,QAAU,MAAM,EAAE,eAAe,KAAK,CAAC,IACvCA,QAAU,IAAI;CACpB,OAAO,OAAO,MAAM,YAAY;EAC9B,MAAMC,MAAQ,MAAM,OAAO;CAC7B;CACA,WAAW,SAASC,SAAW,IAAI;CACnC,SAAS,SAASC,OAAS,IAAI;CAC/B,OAAO,MAAM,UAAUC,KAAO,MAAM,KAAK;AAC3C"}
1
+ {"version":3,"file":"workspaceFS.mjs","names":["fsReadFile","fsWriteFile","fsStat","fsReaddir","fsMkdir","fsRealpath","fsUnlink","fsOpen"],"sources":["../../../../src/tools/local/workspaceFS.ts"],"mappings":";;AAiDA,IAAa,8BAAb,cAAiD,MAAM;CACrD,OAAgB;CAChB,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,8BACd,OACsC;CACtC,OACE,iBAAiB,+BAChB,OAAO,UAAU,YAChB,UAAU,QACT,MAA6B,SAAS;AAE7C;AA4BA,MAAa,kBAAkB,OAAO,OAAoB;CAGxD,YAAY,MAAc,aACxB,YAAY,OACRA,SAAW,MAAM,QAAQ,IACzBA,SAAW,IAAI;CACrB,YAAY,MAAM,SAAS,YACzBC,UAAY,MAAM,SAAS,WAAW,MAAM;CAC9C,OAAO,SAASC,KAAO,IAAI;CAC3B,WAAW,MAAc,YACvB,SAAS,kBAAkB,OACvBC,QAAU,MAAM,EAAE,eAAe,KAAK,CAAC,IACvCA,QAAU,IAAI;CACpB,OAAO,OAAO,MAAM,YAAY;EAC9B,MAAMC,MAAQ,MAAM,OAAO;CAC7B;CACA,WAAW,SAASC,SAAW,IAAI;CACnC,SAAS,SAASC,OAAS,IAAI;CAC/B,OAAO,MAAM,UAAUC,KAAO,MAAM,KAAK;AAC3C,CAAC"}
@@ -62,6 +62,12 @@ export declare function execWithClientTimeout(sandbox: t.CloudflareSandboxRuntim
62
62
  unref?: boolean;
63
63
  }): Promise<t.CloudflareSandboxExecResult>;
64
64
  export declare function createCloudflareWorkspaceFS(config: t.CloudflareSandboxExecutionConfig): WorkspaceFS;
65
+ /**
66
+ * Returns one stable filesystem/process world for a Cloudflare configuration.
67
+ * Probe caches key on the spawn identity, so retaining it avoids repeating
68
+ * remote capability checks whenever an agent binding rebuilds its tools.
69
+ */
70
+ export declare function createCloudflareExecutionWorld(config: t.CloudflareSandboxExecutionConfig): t.ExecutionWorld;
65
71
  export declare function createCloudflareLocalExecutionConfig(config: t.CloudflareSandboxExecutionConfig): t.LocalExecutionConfig;
66
72
  export declare function validateCloudflareBashCommand(command: string, args: readonly string[], config: t.CloudflareSandboxExecutionConfig): Promise<void>;
67
73
  export declare function executeCloudflareBash(command: string, config: t.CloudflareSandboxExecutionConfig, args?: readonly string[]): Promise<SpawnResult>;
@@ -1,5 +1,9 @@
1
1
  import type { WorkspaceFS } from './workspaceFS';
2
2
  import type * as t from '@/types';
3
+ /** Produces a stable, non-plaintext key for environment-sensitive probes. */
4
+ export declare function commandAvailabilityEnvCacheKey(env: NodeJS.ProcessEnv | undefined): string;
5
+ /** Adds a probe entry while bounding retained environment variants. */
6
+ export declare function setCommandAvailabilityCacheEntry<T>(cache: Map<string, T>, key: string, value: T): void;
3
7
  type SpawnResult = {
4
8
  stdout: string;
5
9
  stderr: string;
@@ -45,6 +49,10 @@ export declare function getLocalCwd(config?: t.LocalExecutionConfig): string;
45
49
  * need realpath equality (see `resolveWorkspacePathSafe`).
46
50
  */
47
51
  export declare function getWorkspaceRoots(config?: t.LocalExecutionConfig): string[];
52
+ /** Node-host execution world used when no backend override is configured. */
53
+ export declare const nodeExecutionWorld: t.ExecutionWorld;
54
+ /** Resolves filesystem and subprocess capabilities as one execution world. */
55
+ export declare function getExecutionWorld(config?: t.LocalExecutionConfig): t.ExecutionWorld;
48
56
  /**
49
57
  * Pluggable spawn resolver. Honours `local.exec.spawn` first, falls
50
58
  * back to the legacy top-level `local.spawn`, then to Node's
@@ -130,6 +138,20 @@ export interface SpawnLocalProcessOptions {
130
138
  }
131
139
  export declare const LOCAL_SPAWN_TIMEOUT_MS: unique symbol;
132
140
  export declare function spawnLocalProcess(command: string, args: string[], config?: t.LocalExecutionConfig, options?: SpawnLocalProcessOptions): Promise<SpawnResult>;
141
+ /** Result of a command-availability probe and whether its verdict is stable. */
142
+ export type CommandAvailabilityProbe = {
143
+ available: boolean;
144
+ cacheable: boolean;
145
+ cacheUntil?: number;
146
+ };
147
+ /**
148
+ * Probes one executable without turning transient backend failures into
149
+ * permanent capability facts. Exit 126/127 is a definite unusable/missing
150
+ * executable. A native ENOENT is stable only when the current working
151
+ * directory exists; timeouts, transport failures, and ambiguous lookup
152
+ * failures are retried.
153
+ */
154
+ export declare function probeLocalCommandAvailability(command: string, args: string[], config: t.LocalExecutionConfig): Promise<CommandAvailabilityProbe>;
133
155
  export declare function executeLocalBash(command: string, config?: t.LocalExecutionConfig): Promise<SpawnResult>;
134
156
  export declare function executeLocalBashWithArgs(command: string, args: readonly string[], config?: t.LocalExecutionConfig): Promise<SpawnResult>;
135
157
  export declare function executeLocalCode(input: {
@@ -59,4 +59,4 @@ export interface WorkspaceFS {
59
59
  * Returned by `getWorkspaceFS(config)` when the host hasn't supplied
60
60
  * an override on `local.exec.fs`.
61
61
  */
62
- export declare const nodeWorkspaceFS: WorkspaceFS;
62
+ export declare const nodeWorkspaceFS: Readonly<WorkspaceFS>;
@@ -759,47 +759,32 @@ export type LocalWorkspaceConfig = {
759
759
  allowWriteOutside?: boolean;
760
760
  };
761
761
  /**
762
- * Engine-agnostic execution seam. Default uses Node's
763
- * `child_process.spawn` and `fs/promises`. A future engine (e.g.
764
- * stateful remote sandbox) supplies its own `spawn` and `fs` and
765
- * inherits every tool factory unchanged.
762
+ * One execution world shared by filesystem and subprocess operations.
763
+ * Backend identity is stable so capability probes remain warm when tool
764
+ * bundles are rebuilt for later agent bindings.
765
+ */
766
+ export interface ExecutionWorld {
767
+ /** Launches a process inside this world's filesystem namespace. */
768
+ readonly spawn: LocalSpawn;
769
+ /** Reads and writes the same namespace observed by `spawn`. */
770
+ readonly fs: Readonly<import('@/tools/local/workspaceFS').WorkspaceFS>;
771
+ /** Whether the world already enforces a sandbox boundary. */
772
+ readonly sandboxed: boolean;
773
+ }
774
+ /**
775
+ * Backward-compatible partial execution-world override. Omitted fields use
776
+ * the Node host world; remote backends should provide the complete trio.
766
777
  *
767
- * **Important — pair `spawn` and `fs` together.** Most file-touching
768
- * surfaces in the local engine route through `getWorkspaceFS(config)`
769
- * so a host can transparently swap in a remote/in-memory FS. A small
770
- * set of helpers — currently the `execute_code` non-bash temp-file
771
- * write (Codex P2 [48]) — still uses host `fs/promises` directly to
772
- * stage source on disk before invoking the spawn. If you override
773
- * `spawn` to point at a remote runtime (SSH, container, etc.) you
774
- * MUST also override `fs` with the corresponding remote
775
- * implementation; otherwise temp source files written to the host
776
- * `/tmp` won't be visible to the remote interpreter and `py`/`js`/
777
- * `ts`/etc. executions will fail. (Bash-style executions go through
778
- * `executeLocalBash` which doesn't stage temp files, so they're
779
- * unaffected.)
778
+ * Non-bash `execute_code` still stages source through the host temporary
779
+ * directory. Remote runtimes should use their dedicated execution engine for
780
+ * that tool until temporary lifecycle operations join this seam.
780
781
  *
781
- * Threat-model note: the regex-based command validators
782
- * (`dangerousCommandPatterns`, `quotedDestructivePatterns`, etc.) and
783
- * the workspace policy hook are documented as best-effort tripwires.
784
- * The hard security boundary is `local.sandbox.enabled: true` (which
785
- * wraps execution in `@anthropic-ai/sandbox-runtime`); for adversarial-
786
- * model threat models, do NOT rely on the regex layer alone.
782
+ * Command validators and workspace policies are best-effort tripwires. For an
783
+ * adversarial-model threat model, use a backend sandbox boundary rather than
784
+ * relying on validation alone.
787
785
  */
788
786
  export type LocalExecConfig = {
789
- /** Pluggable spawn (for SSH, container, remote workers, etc.). */
790
- spawn?: LocalSpawn;
791
- /**
792
- * Pluggable filesystem (for remote-workspace engines). Pair with
793
- * `spawn` — see the type-level note above on why both should be
794
- * overridden together for non-host engines.
795
- */
796
- fs?: import('@/tools/local/workspaceFS').WorkspaceFS;
797
- /**
798
- * Set by custom execution backends that already provide their own
799
- * sandbox boundary. Suppresses the local host-sandbox warning while
800
- * preserving the warning for plain host `child_process` execution.
801
- */
802
- sandboxed?: boolean;
787
+ -readonly [Key in keyof ExecutionWorld]?: Key extends 'fs' ? import('@/tools/local/workspaceFS').WorkspaceFS : ExecutionWorld[Key];
803
788
  };
804
789
  export type LocalExecutionConfig = {
805
790
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.6.13",
3
+ "version": "3.6.14",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -154,6 +154,7 @@
154
154
  "tool_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search.ts",
155
155
  "bench:cache": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-prompt-cache.ts",
156
156
  "bench:context-pressure": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-context-pressure-cache.ts",
157
+ "bench:execution-world": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-execution-world.ts",
157
158
  "probe:overflow": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/context-overflow-probe.ts",
158
159
  "subagent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-subagent.ts",
159
160
  "subagent:events": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/subagent-event-driven-debug.ts",
@@ -5,11 +5,11 @@ import type { WriteFileOptions, MakeDirectoryOptions, Stats } from 'fs';
5
5
  import type { ChildProcessWithoutNullStreams } from 'child_process';
6
6
  import type { FileHandle } from 'fs/promises';
7
7
  import type { WorkspaceFS, ReaddirEntry } from '@/tools/local/workspaceFS';
8
+ import type * as t from '@/types';
8
9
  import {
9
10
  WorkspaceClientTimeoutError,
10
11
  isWorkspaceClientTimeoutError,
11
12
  } from '@/tools/local/workspaceFS';
12
- import type * as t from '@/types';
13
13
  import {
14
14
  LOCAL_SPAWN_TIMEOUT_MS,
15
15
  validateBashCommand,
@@ -44,9 +44,28 @@ type SandboxRuntimeContext = {
44
44
  shell: string;
45
45
  };
46
46
 
47
+ type ExecutionWorldCacheEntry = {
48
+ world: t.ExecutionWorld;
49
+ workspaceRoot: string;
50
+ timeoutMs: number;
51
+ sandbox: t.CloudflareSandboxExecutionConfig['sandbox'];
52
+ };
53
+
54
+ const executionWorldCache = new WeakMap<
55
+ t.CloudflareSandboxExecutionConfig,
56
+ ExecutionWorldCacheEntry
57
+ >();
58
+
59
+ type SandboxFactoryCacheEntry = {
60
+ sandbox: () =>
61
+ | t.CloudflareSandboxRuntime
62
+ | Promise<t.CloudflareSandboxRuntime>;
63
+ promise: Promise<t.CloudflareSandboxRuntime>;
64
+ };
65
+
47
66
  const sandboxFactoryCache = new WeakMap<
48
67
  t.CloudflareSandboxExecutionConfig,
49
- Promise<t.CloudflareSandboxRuntime>
68
+ SandboxFactoryCacheEntry
50
69
  >();
51
70
 
52
71
  function normalizeWorkspaceRoot(workspaceRoot: string): string {
@@ -69,17 +88,20 @@ export async function resolveCloudflareSandbox(
69
88
  if (typeof sandbox !== 'function') {
70
89
  return sandbox;
71
90
  }
72
- let cached = sandboxFactoryCache.get(config);
73
- if (cached == null) {
74
- cached = Promise.resolve()
75
- .then(() => sandbox())
76
- .catch((error: unknown) => {
77
- sandboxFactoryCache.delete(config);
78
- throw error;
79
- });
80
- sandboxFactoryCache.set(config, cached);
91
+ const cached = sandboxFactoryCache.get(config);
92
+ if (cached?.sandbox === sandbox) {
93
+ return cached.promise;
81
94
  }
82
- return cached;
95
+ const promise = Promise.resolve()
96
+ .then(() => sandbox())
97
+ .catch((error: unknown) => {
98
+ if (sandboxFactoryCache.get(config)?.promise === promise) {
99
+ sandboxFactoryCache.delete(config);
100
+ }
101
+ throw error;
102
+ });
103
+ sandboxFactoryCache.set(config, { sandbox, promise });
104
+ return promise;
83
105
  }
84
106
 
85
107
  async function getRuntimeContext(
@@ -687,6 +709,39 @@ function createCloudflareSpawn(
687
709
  };
688
710
  }
689
711
 
712
+ /**
713
+ * Returns one stable filesystem/process world for a Cloudflare configuration.
714
+ * Probe caches key on the spawn identity, so retaining it avoids repeating
715
+ * remote capability checks whenever an agent binding rebuilds its tools.
716
+ */
717
+ export function createCloudflareExecutionWorld(
718
+ config: t.CloudflareSandboxExecutionConfig
719
+ ): t.ExecutionWorld {
720
+ const workspaceRoot = getCloudflareWorkspaceRoot(config);
721
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
722
+ const cached = executionWorldCache.get(config);
723
+ if (
724
+ cached?.workspaceRoot === workspaceRoot &&
725
+ Object.is(cached.timeoutMs, timeoutMs) &&
726
+ cached.sandbox === config.sandbox
727
+ ) {
728
+ return cached.world;
729
+ }
730
+ const fs = Object.freeze(createCloudflareWorkspaceFS(config));
731
+ const world = Object.freeze({
732
+ spawn: createCloudflareSpawn(config),
733
+ fs,
734
+ sandboxed: true,
735
+ });
736
+ executionWorldCache.set(config, {
737
+ world,
738
+ workspaceRoot,
739
+ timeoutMs,
740
+ sandbox: config.sandbox,
741
+ });
742
+ return world;
743
+ }
744
+
690
745
  export function createCloudflareLocalExecutionConfig(
691
746
  config: t.CloudflareSandboxExecutionConfig
692
747
  ): t.LocalExecutionConfig {
@@ -694,11 +749,7 @@ export function createCloudflareLocalExecutionConfig(
694
749
  return {
695
750
  cwd: workspaceRoot,
696
751
  workspace: { root: workspaceRoot },
697
- exec: {
698
- spawn: createCloudflareSpawn(config),
699
- fs: createCloudflareWorkspaceFS(config),
700
- sandboxed: true,
701
- },
752
+ exec: createCloudflareExecutionWorld(config),
702
753
  shell: config.shell ?? 'bash',
703
754
  timeoutMs: config.timeoutMs,
704
755
  maxOutputChars: config.maxOutputChars,
@@ -2,8 +2,8 @@ import { tool } from '@langchain/core/tools';
2
2
  import type { DynamicStructuredTool } from '@langchain/core/tools';
3
3
  import type * as t from '@/types';
4
4
  import {
5
+ createCloudflareExecutionWorld,
5
6
  createCloudflareLocalExecutionConfig,
6
- createCloudflareWorkspaceFS,
7
7
  executeCloudflareBash,
8
8
  executeCloudflareCode,
9
9
  formatCloudflareOutput,
@@ -161,7 +161,7 @@ export function createCloudflareCodingTools(
161
161
  options.checkpointer ??
162
162
  (config.fileCheckpointing === true
163
163
  ? createLocalFileCheckpointer({
164
- fs: createCloudflareWorkspaceFS(config),
164
+ fs: createCloudflareExecutionWorld(config).fs,
165
165
  })
166
166
  : undefined);
167
167
  const tools = [
@@ -193,11 +193,12 @@ export function createCloudflareCodingToolBundle(
193
193
  config: t.CloudflareSandboxExecutionConfig,
194
194
  options: { checkpointer?: t.LocalFileCheckpointer } = {}
195
195
  ): CloudflareCodingToolBundle {
196
+ const world = createCloudflareExecutionWorld(config);
196
197
  const checkpointer =
197
198
  options.checkpointer ??
198
199
  (config.fileCheckpointing === true
199
200
  ? createLocalFileCheckpointer({
200
- fs: createCloudflareWorkspaceFS(config),
201
+ fs: world.fs,
201
202
  })
202
203
  : undefined);
203
204
  return {
@@ -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
- createLocalBashProgrammaticToolCallingTool,
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
- // vary `local.env` (especially PATH). Stale cache then claims `rg`
741
- // is available, the rg path runs, and the spawn fails with ENOENT
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, Promise<boolean>>
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 = envCacheKey(config.env);
759
+ const envKey = commandAvailabilityEnvCacheKey(config.env);
773
760
  let probePromise = envMap.get(envKey);
774
761
  if (probePromise == null) {
775
- probePromise = spawnLocalProcess(
776
- 'rg',
777
- ['--version'],
778
- { ...config, timeoutMs: 5000, sandbox: { enabled: false } },
779
- { internal: true }
780
- )
781
- .then((probe) => probe.exitCode === 0)
782
- .catch(() => false);
783
- envMap.set(envKey, probePromise);
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 probePromise;
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?.exec?.spawn ?? config?.spawn ?? spawn) as t.LocalSpawn;
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?.exec?.fs ?? nodeWorkspaceFS;
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.exec?.sandboxed === true
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 = {}