@mono-agent/agent-runtime 0.3.0 → 0.4.1

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 (123) hide show
  1. package/ARCHITECTURE.md +75 -9
  2. package/MIGRATION.md +293 -0
  3. package/README.md +46 -20
  4. package/package.json +104 -10
  5. package/src/agent/allowlists.js +3 -0
  6. package/src/agent/approval.js +3 -0
  7. package/src/agent/compaction.js +231 -654
  8. package/src/agent/index.js +1 -1
  9. package/src/agent/prompt/skill-index.js +6 -0
  10. package/src/agent/sandbox-seam.js +227 -0
  11. package/src/agent/tool-bloat.js +8 -2
  12. package/src/agent/tools/bash.js +16 -8
  13. package/src/agent/tools/edit.js +7 -3
  14. package/src/agent/tools/glob.js +11 -5
  15. package/src/agent/tools/grep.js +11 -5
  16. package/src/agent/tools/pi-bridge.js +272 -54
  17. package/src/agent/tools/read.js +28 -3
  18. package/src/agent/tools/shared/output-truncation.js +8 -3
  19. package/src/agent/tools/shared/path-resolver.js +24 -18
  20. package/src/agent/tools/shared/ripgrep.js +33 -6
  21. package/src/agent/tools/shared/runtime-context.js +60 -50
  22. package/src/agent/tools/shared/tool-context.js +157 -0
  23. package/src/agent/tools/web-fetch.js +65 -18
  24. package/src/agent/tools/web-search.js +11 -4
  25. package/src/agent/tools/write.js +7 -3
  26. package/src/agent/transcript.js +16 -1
  27. package/src/ai/cost.js +128 -3
  28. package/src/ai/failure.js +96 -4
  29. package/src/ai/index.js +1 -0
  30. package/src/ai/live-input-prompt.js +11 -1
  31. package/src/ai/providers/claude-cli.js +6 -2
  32. package/src/ai/providers/claude-sdk.js +55 -12
  33. package/src/ai/providers/codex-app.js +36 -23
  34. package/src/ai/providers/opencode-app.js +2 -2
  35. package/src/ai/providers/opencode-discovery.js +2 -2
  36. package/src/ai/providers/pi-errors.js +65 -0
  37. package/src/ai/providers/pi-events.js +5 -0
  38. package/src/ai/providers/pi-models.js +21 -4
  39. package/src/ai/providers/pi-native/compaction-driver.js +394 -0
  40. package/src/ai/providers/pi-native/result-builder.js +312 -0
  41. package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
  42. package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
  43. package/src/ai/providers/pi-native/structured-output.js +130 -0
  44. package/src/ai/providers/pi-native/turn-runner.js +278 -0
  45. package/src/ai/providers/pi-native.js +754 -0
  46. package/src/ai/runtime/capabilities.js +3 -0
  47. package/src/ai/runtime/model-refs.js +30 -0
  48. package/src/ai/runtime/registry.js +33 -2
  49. package/src/ai/runtime/router.js +119 -19
  50. package/src/ai/runtime/session-liveness.js +110 -0
  51. package/src/ai/runtime/sessions.js +19 -2
  52. package/src/ai/types.js +252 -20
  53. package/src/index.js +1 -1
  54. package/src/pi-auth.js +147 -16
  55. package/src/runtime-brand.js +21 -1
  56. package/src/runtime.js +75 -10
  57. package/types/agent/allowlists.d.ts +25 -0
  58. package/types/agent/approval.d.ts +30 -0
  59. package/types/agent/compaction.d.ts +97 -0
  60. package/types/agent/index.d.ts +5 -0
  61. package/types/agent/prompt/skill-index.d.ts +19 -0
  62. package/types/agent/sandbox-seam.d.ts +148 -0
  63. package/types/agent/tool-bloat.d.ts +22 -0
  64. package/types/agent/tools/bash.d.ts +16 -0
  65. package/types/agent/tools/edit.d.ts +14 -0
  66. package/types/agent/tools/glob.d.ts +16 -0
  67. package/types/agent/tools/grep.d.ts +22 -0
  68. package/types/agent/tools/index.d.ts +10 -0
  69. package/types/agent/tools/pi-bridge.d.ts +144 -0
  70. package/types/agent/tools/read.d.ts +19 -0
  71. package/types/agent/tools/shared/constants.d.ts +13 -0
  72. package/types/agent/tools/shared/dedup.d.ts +8 -0
  73. package/types/agent/tools/shared/output-truncation.d.ts +22 -0
  74. package/types/agent/tools/shared/path-resolver.d.ts +6 -0
  75. package/types/agent/tools/shared/ripgrep.d.ts +38 -0
  76. package/types/agent/tools/shared/runtime-context.d.ts +27 -0
  77. package/types/agent/tools/shared/tool-context.d.ts +48 -0
  78. package/types/agent/tools/web-fetch.d.ts +13 -0
  79. package/types/agent/tools/web-search.d.ts +11 -0
  80. package/types/agent/tools/write.d.ts +12 -0
  81. package/types/agent/transcript.d.ts +45 -0
  82. package/types/ai/backend.d.ts +41 -0
  83. package/types/ai/cost.d.ts +96 -0
  84. package/types/ai/failure.d.ts +117 -0
  85. package/types/ai/file-change-stats.d.ts +75 -0
  86. package/types/ai/index.d.ts +7 -0
  87. package/types/ai/live-input-prompt.d.ts +6 -0
  88. package/types/ai/observer.d.ts +68 -0
  89. package/types/ai/providers/claude-cli.d.ts +211 -0
  90. package/types/ai/providers/claude-sdk.d.ts +66 -0
  91. package/types/ai/providers/claude-subagents.d.ts +2 -0
  92. package/types/ai/providers/codex-app.d.ts +151 -0
  93. package/types/ai/providers/opencode-app.d.ts +95 -0
  94. package/types/ai/providers/opencode-discovery.d.ts +4 -0
  95. package/types/ai/providers/pi-errors.d.ts +3 -0
  96. package/types/ai/providers/pi-events.d.ts +24 -0
  97. package/types/ai/providers/pi-messages.d.ts +5 -0
  98. package/types/ai/providers/pi-models.d.ts +57 -0
  99. package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
  100. package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
  101. package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
  102. package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
  103. package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
  104. package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
  105. package/types/ai/providers/pi-native.d.ts +18 -0
  106. package/types/ai/registry.d.ts +1 -0
  107. package/types/ai/runtime/capabilities-used.d.ts +21 -0
  108. package/types/ai/runtime/capabilities.d.ts +33 -0
  109. package/types/ai/runtime/context-windows.d.ts +8 -0
  110. package/types/ai/runtime/fast-mode.d.ts +2 -0
  111. package/types/ai/runtime/model-refs.d.ts +35 -0
  112. package/types/ai/runtime/registry.d.ts +38 -0
  113. package/types/ai/runtime/router.d.ts +56 -0
  114. package/types/ai/runtime/session-liveness.d.ts +55 -0
  115. package/types/ai/runtime/sessions.d.ts +38 -0
  116. package/types/ai/streaming/codex-events.d.ts +30 -0
  117. package/types/ai/streaming/opencode-events.d.ts +41 -0
  118. package/types/ai/types.d.ts +702 -0
  119. package/types/index.d.ts +7 -0
  120. package/types/pi-auth.d.ts +28 -0
  121. package/types/runtime-brand.d.ts +30 -0
  122. package/types/runtime.d.ts +10 -0
  123. package/src/ai/providers/pi-sdk.js +0 -1310
@@ -5,8 +5,8 @@
5
5
  // from edge layers.
6
6
 
7
7
  export {
8
- createAgentCompactionManager,
9
8
  isLikelyContextTermination,
9
+ resolveAgentCompactionPolicy,
10
10
  } from "./compaction.js";
11
11
 
12
12
  export {
@@ -23,6 +23,9 @@ export function getSkillAccessDirs(skills = []) {
23
23
  return [...new Set(skills.map(skillDir).filter(Boolean))];
24
24
  }
25
25
 
26
+ /**
27
+ * @param {{assetsPath?: string, skillsRoot?: any}} [options]
28
+ */
26
29
  export function buildSkillPathNote({ assetsPath, skillsRoot } = {}) {
27
30
  const lines = [];
28
31
  if (skillsRoot) lines.push(`Configured skills root: ${resolve(skillsRoot)}`);
@@ -31,6 +34,9 @@ export function buildSkillPathNote({ assetsPath, skillsRoot } = {}) {
31
34
  return lines.join("\n");
32
35
  }
33
36
 
37
+ /**
38
+ * @param {{body?: string, assetsPath?: string, skillsRoot?: any, maxChars?: number}} [options]
39
+ */
34
40
  export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxChars = 12000 } = {}) {
35
41
  const text = [
36
42
  buildSkillPathNote({ assetsPath, skillsRoot }),
@@ -0,0 +1,227 @@
1
+ // Injectable sandbox seam.
2
+ //
3
+ // agent-runtime is a provider-agnostic kernel. This module removes its last
4
+ // workspace dependency by turning "how commands get sandboxed" into data a
5
+ // host hands the kernel, not code the kernel imports. A host constructs a
6
+ // `RuntimeSandbox` implementation and passes it to `createRuntime({sandbox})`
7
+ // (see runtime.js); every internal tool helper that used to import the sandbox
8
+ // implementation directly (bash.js, pi-bridge.js's MCP-stdio launcher,
9
+ // web-fetch.js, web-search.js,
10
+ // tool-context.js's policy merge) now reads `ctx.sandbox` and calls through
11
+ // this interface instead.
12
+ //
13
+ // `passthroughSandbox` is the zero-dependency default every ToolContext gets
14
+ // when no host supplies one (createToolContext/resetToolContext in
15
+ // ./tools/shared/tool-context.js). Its contract covers both no-injected-sandbox
16
+ // paths equally:
17
+ // - No policy at all (both sides of a merge undefined, or a resolved policy
18
+ // that is undefined): every operation is allowed, byte-identical to
19
+ // running with no sandbox implementation installed at all.
20
+ // - A policy IS present and its `mode` demands real enforcement ("native")
21
+ // but nothing was injected to actually enforce it: FAIL CLOSED. This is a
22
+ // deliberate behavior change from before this seam existed (where the
23
+ // real sandbox implementation was bundled in agent-runtime and always did
24
+ // the enforcing) — a host that configures a policy without wiring a real
25
+ // RuntimeSandbox implementation used to get silent, unenforced
26
+ // "enforcement"; now it gets a loud, actionable error instead. See
27
+ // MIGRATION.md.
28
+ // - `mergePolicies` still keeps I13's monotonic guarantee (a request-scoped
29
+ // policy can only tighten, never weaken, the host-configured one) for the
30
+ // two axes the passthrough itself inspects (`mode`, `network.mode`). It
31
+ // is deliberately SHALLOW — no readable/writableRoots intersection, no
32
+ // denyWrite union — the real, byte-identical merge algorithm is owned by
33
+ // @mono-agent/runtime-adapter in packages/runtime-adapter/src/sandbox.ts
34
+ // (`mergeSandboxPolicies`) and is wired in by every mono-agent host through
35
+ // runtime-adapter.
36
+ //
37
+ // Real hosts (mono-agent, via @mono-agent/runtime-adapter) inject
38
+ // `{mergePolicies: mergeSandboxPolicies, prepareCommand: prepareSandboxedCommand,
39
+ // networkAllowsUrl: networkPolicyAllowsUrl}` from packages/runtime-adapter/src/sandbox.ts.
40
+ // Those exports already conform to this interface exactly, so the injection is a direct pass-through (see runtime-adapter's
41
+ // sandbox-impl.ts for the thin TS-side adapter that satisfies the seam's
42
+ // type shape).
43
+
44
+ // @ts-check
45
+
46
+ /**
47
+ * @typedef {Object} SandboxCommandSpec
48
+ * @property {string} command
49
+ * @property {ReadonlyArray<string>} [args]
50
+ * @property {string} [cwd]
51
+ * @property {Object<string, string|undefined>} [env]
52
+ */
53
+
54
+ /**
55
+ * @typedef {SandboxCommandSpec & {sandboxed: boolean, cleanup?: () => Promise<void>}} PreparedSandboxCommand
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} SandboxNetworkPolicyLike
60
+ * @property {string} [mode]
61
+ * @property {ReadonlyArray<string>} [allowlist]
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} SandboxPolicy
66
+ * Opaque, host-defined sandbox policy. The kernel itself only ever inspects
67
+ * `mode` (passthroughSandbox's fail-closed check) and `network.mode`
68
+ * (passthroughSandbox's networkAllowsUrl) — every other field (root,
69
+ * readableRoots, writableRoots, denyWrite, ...) is read directly off whatever
70
+ * the injected `mergePolicies` returns by path-resolver.js, and is opaque
71
+ * pass-through data as far as this module is concerned. Real hosts get the
72
+ * full, richly-typed policy from @mono-agent/runtime-adapter
73
+ * (`packages/runtime-adapter/src/sandbox.ts`), whose `SandboxPolicy` is a
74
+ * structural superset of this shape.
75
+ * @property {string} [mode]
76
+ * @property {SandboxNetworkPolicyLike} [network]
77
+ * @property {ReadonlyArray<string>} [readableRoots]
78
+ * @property {ReadonlyArray<string>} [writableRoots]
79
+ * @property {ReadonlyArray<string>} [denyWrite]
80
+ * @property {string} [root]
81
+ */
82
+
83
+ /**
84
+ * @typedef {Object} RuntimeSandboxEngine
85
+ * Optional concrete sandboxing backend a caller can hand to `prepareCommand`
86
+ * (matches @mono-agent/runtime-adapter's `SandboxEngine`). Not used by
87
+ * `passthroughSandbox` (see module doc: adapters live in runtime-adapter, not
88
+ * the kernel) — documented here only because it is part of the
89
+ * `prepareCommand` input shape real implementations accept.
90
+ * @property {() => Promise<boolean>} isAvailable
91
+ * @property {(command: SandboxCommandSpec, policy: SandboxPolicy) => Promise<PreparedSandboxCommand>} prepareCommand
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object} PrepareSandboxedCommandInput
96
+ * @property {SandboxPolicy} [policy]
97
+ * @property {RuntimeSandboxEngine} [engine]
98
+ * @property {SandboxCommandSpec} command
99
+ */
100
+
101
+ /**
102
+ * @typedef {Object} RuntimeSandbox
103
+ * The injectable sandbox seam. A host constructs one (or reuses
104
+ * `passthroughSandbox`) and passes it to `createRuntime({sandbox})`.
105
+ * @property {(configured: (SandboxPolicy|undefined), request: (SandboxPolicy|undefined)) => (SandboxPolicy|undefined)} mergePolicies
106
+ * Monotonic tighten-only merge (I13): the result must never allow anything
107
+ * `configured` alone would have denied.
108
+ * @property {(input: PrepareSandboxedCommandInput) => Promise<PreparedSandboxCommand>} prepareCommand
109
+ * Resolves a command spec into whatever the underlying process spawner
110
+ * should actually exec (identity when no enforcement is needed).
111
+ * @property {(policy: (SandboxPolicy|undefined), url: string) => boolean} networkAllowsUrl
112
+ * Whether a tool call to `url` is allowed under `policy`.
113
+ * @property {ReadonlyArray<string>} [additionalReadPaths]
114
+ * Optional host-provided extra read-allowed roots beyond the tool
115
+ * context's workspace/repoRoot. Not consumed by any kernel helper today;
116
+ * documented for forward-compatibility with host-side path resolution.
117
+ */
118
+
119
+ const SANDBOX_UNAVAILABLE_CODE = "sandbox_unavailable";
120
+
121
+ /**
122
+ * Plain Error with a `.code`/`.details` shape matching the workspace's
123
+ * CodedError convention (code discriminant + details that echo it, see the
124
+ * agent-contracts package's CodedError) — duplicated here rather than
125
+ * imported, since the kernel must not depend on any workspace package.
126
+ */
127
+ class SandboxUnavailableError extends Error {
128
+ /**
129
+ * @param {string} message
130
+ * @param {Record<string, unknown>} [details]
131
+ */
132
+ constructor(message, details = {}) {
133
+ super(message);
134
+ this.name = "SandboxUnavailableError";
135
+ this.code = SANDBOX_UNAVAILABLE_CODE;
136
+ this.details = { ...details, code: SANDBOX_UNAVAILABLE_CODE };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {string|undefined} mode
142
+ * @returns {boolean}
143
+ */
144
+ function isRealSandboxMode(mode) {
145
+ return typeof mode === "string" && mode !== "off";
146
+ }
147
+
148
+ /**
149
+ * @param {SandboxNetworkPolicyLike|undefined} configured
150
+ * @param {SandboxNetworkPolicyLike|undefined} request
151
+ * @returns {SandboxNetworkPolicyLike|undefined}
152
+ */
153
+ function mergeNetwork(configured, request) {
154
+ if (configured === undefined) return request;
155
+ if (request === undefined) return configured;
156
+ // The passthrough only distinguishes "all" (unrestricted) from everything
157
+ // else (restricted); whichever side is NOT "all" tightens the result.
158
+ if (configured.mode === "all" && request.mode === "all") return { mode: "all" };
159
+ return configured.mode === "all" ? request : configured;
160
+ }
161
+
162
+ /**
163
+ * @param {SandboxPolicy|undefined} configured
164
+ * @param {SandboxPolicy|undefined} request
165
+ * @returns {SandboxPolicy|undefined}
166
+ */
167
+ function mergePolicies(configured, request) {
168
+ if (configured === undefined) return request;
169
+ if (request === undefined) return configured;
170
+ // Only two modes exist today ("native" | "off"); "native" tightens over
171
+ // "off" regardless of which side introduced it.
172
+ const configuredIsReal = isRealSandboxMode(configured.mode);
173
+ const requestIsReal = isRealSandboxMode(request.mode);
174
+ const mode = configuredIsReal ? configured.mode : (requestIsReal ? request.mode : (request.mode ?? configured.mode));
175
+ return {
176
+ ...configured,
177
+ ...request,
178
+ mode,
179
+ network: mergeNetwork(configured.network, request.network),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Identity command preparation — no sandboxing infrastructure of its own.
185
+ * Fails closed the moment a policy actually demands enforcement, rather than
186
+ * silently returning an unsandboxed command under a policy that looks
187
+ * enforced.
188
+ * @param {PrepareSandboxedCommandInput} input
189
+ * @returns {Promise<PreparedSandboxCommand>}
190
+ */
191
+ async function prepareCommand({ policy, command }) {
192
+ if (policy === undefined || !isRealSandboxMode(policy.mode)) {
193
+ return { ...command, args: command.args ?? [], cwd: command.cwd ?? process.cwd(), sandboxed: false };
194
+ }
195
+ throw new SandboxUnavailableError(
196
+ "Sandbox policy requires enforcement (mode !== \"off\") but no RuntimeSandbox implementation is configured. "
197
+ + "Inject a real implementation via createRuntime({sandbox}) (mono-agent hosts get this automatically through "
198
+ + "the runtime-adapter package) or relax the policy.",
199
+ { mode: policy.mode, command: command.command },
200
+ );
201
+ }
202
+
203
+ /**
204
+ * @param {SandboxPolicy|undefined} policy
205
+ * @param {string} _url
206
+ * @returns {boolean}
207
+ */
208
+ function networkAllowsUrl(policy, _url) {
209
+ if (policy === undefined) return true;
210
+ // A policy whose mode demands real enforcement must fail closed on network
211
+ // access too when no `network` sub-field was given — an absent `network`
212
+ // is not "all", and this is the passthrough's safety net for hand-built
213
+ // policies from hosts with no real RuntimeSandbox wired in (see module
214
+ // doc's fail-closed rationale, and prepareCommand above for the analogous
215
+ // command-side check this mirrors via isRealSandboxMode).
216
+ if (isRealSandboxMode(policy.mode)) return policy.network?.mode === "all";
217
+ return policy.network === undefined || policy.network.mode === "all";
218
+ }
219
+
220
+ /** @type {RuntimeSandbox} */
221
+ export const passthroughSandbox = {
222
+ mergePolicies,
223
+ prepareCommand,
224
+ networkAllowsUrl,
225
+ };
226
+
227
+ export { SandboxUnavailableError, SANDBOX_UNAVAILABLE_CODE };
@@ -88,12 +88,17 @@ function summaryText(toolName, originalBytes, maxBytes, savedPaths) {
88
88
  export function summarisePayload(toolName, contentBlocks, persistArtifact, options = {}) {
89
89
  const {
90
90
  maxBytes = MAX_TOOL_RESULT_BYTES,
91
+ imageMaxBytes = maxBytes,
91
92
  toolUseId = null,
92
93
  now = Date.now,
93
94
  } = options;
94
95
  const blocks = Array.isArray(contentBlocks) ? contentBlocks : [];
95
96
  const originalBytes = totalBytes(blocks);
96
- if (originalBytes <= maxBytes) {
97
+ // Images get their own (typically larger) budget so a vision model can still
98
+ // see large screenshots; text/other payloads stay bound by maxBytes.
99
+ const imageBytes = blocks.reduce((sum, block) => sum + (block?.type === "image" ? blockBytes(block) : 0), 0);
100
+ const otherBytes = originalBytes - imageBytes;
101
+ if (otherBytes <= maxBytes && imageBytes <= imageMaxBytes) {
97
102
  return { rewrittenBlocks: blocks, savedPaths: [], originalBytes, truncated: false };
98
103
  }
99
104
 
@@ -117,11 +122,12 @@ export async function applyToolBloatGuard(toolName, executePromise, options = {}
117
122
  persistArtifact = null,
118
123
  toolUseId = null,
119
124
  maxBytes = MAX_TOOL_RESULT_BYTES,
125
+ imageMaxBytes = maxBytes,
120
126
  onTruncate = null,
121
127
  } = options;
122
128
  const result = await executePromise;
123
129
  if (!result || typeof result !== "object" || !Array.isArray(result.content)) return result;
124
- const summary = summarisePayload(toolName, result.content, persistArtifact, { maxBytes, toolUseId });
130
+ const summary = summarisePayload(toolName, result.content, persistArtifact, { maxBytes, imageMaxBytes, toolUseId });
125
131
  if (!summary.truncated) return result;
126
132
  if (typeof onTruncate === "function") {
127
133
  try {
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { prepareSandboxedCommand } from "@mono-agent/sandbox";
3
+ import { passthroughSandbox } from "../sandbox-seam.js";
4
4
  import { DEFAULT_MAX_BASH_OUTPUT_CHARS } from "./shared/constants.js";
5
5
  import { capChars } from "./shared/output-truncation.js";
6
6
  import {
@@ -8,7 +8,8 @@ import {
8
8
  isWorkdirAllowed,
9
9
  workspaceRoot,
10
10
  } from "./shared/path-resolver.js";
11
- import { resolveSandboxPolicy } from "./shared/runtime-context.js";
11
+ import { readToolRuntime } from "./shared/runtime-context.js";
12
+ import { resolveSandboxPolicy } from "./shared/tool-context.js";
12
13
 
13
14
  const DEFAULT_BASH_TIMEOUT_MS = 120000;
14
15
  const BASH_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
@@ -112,18 +113,24 @@ function runCommand(commandSpec, { timeoutMs, signal, maxBufferBytes = BASH_MAX_
112
113
  });
113
114
  }
114
115
 
115
- export async function bashToolImpl({ command, timeout = DEFAULT_BASH_TIMEOUT_MS, max_output_chars, workdir }, { signal, sandboxPolicy, sandboxEngine } = {}) {
116
- const policy = resolveSandboxPolicy(sandboxPolicy);
117
- const pathOptions = { sandboxPolicy: policy };
116
+ /**
117
+ * @param {{command: string, timeout?: number, max_output_chars?: number, workdir?: string}} params
118
+ * @param {{signal?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
119
+ */
120
+ export async function bashToolImpl({ command, timeout = DEFAULT_BASH_TIMEOUT_MS, max_output_chars, workdir }, { signal, sandboxPolicy, sandboxEngine, ctx } = {}) {
121
+ const resolvedCtx = ctx ?? readToolRuntime();
122
+ const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
123
+ const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
124
+ const pathOptions = { sandboxPolicy: policy, ctx };
118
125
  if (workdir && !isWorkdirAllowed(workdir, pathOptions)) return `Error: Working directory not allowed: ${workdir}`;
119
- const cwd = workspaceRoot(workdir);
126
+ const cwd = workspaceRoot(workdir, ctx);
120
127
  if (!isPathAllowed(cwd, workdir, pathOptions)) return `Error: Working directory not allowed: ${cwd}`;
121
128
  if (!existsSync(cwd)) return `Error: Working directory not found: ${cwd}`;
122
129
  const maxChars = Number(max_output_chars) || DEFAULT_MAX_BASH_OUTPUT_CHARS;
123
130
  const timeoutMs = normalizeBashTimeoutMs(timeout);
124
131
  let prepared;
125
132
  try {
126
- prepared = await prepareSandboxedCommand({
133
+ prepared = await sandbox.prepareCommand({
127
134
  policy,
128
135
  engine: sandboxEngine ?? undefined,
129
136
  command: { command: "/bin/bash", args: ["-lc", command], cwd },
@@ -146,11 +153,12 @@ export async function bashToolImpl({ command, timeout = DEFAULT_BASH_TIMEOUT_MS,
146
153
  label: "Bash",
147
154
  maxChars,
148
155
  strategy: "head_tail",
156
+ ctx,
149
157
  });
150
158
  }
151
159
  if (result.signal) return `Exit code 1:\nCommand terminated by ${result.signal}`;
152
160
  const output = result.stdout && result.stderr
153
161
  ? `STDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`
154
162
  : (result.stdout || result.stderr || "(no output)");
155
- return capChars(output, { label: "Bash", maxChars, strategy: "head_tail" });
163
+ return capChars(output, { label: "Bash", maxChars, strategy: "head_tail", ctx });
156
164
  }
@@ -1,9 +1,13 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { isPathAllowed, isWritablePathAllowed, resolveToolPath } from "./shared/path-resolver.js";
3
3
 
4
- export async function editToolImpl({ file_path, old_string, new_string, replace_all = false, workdir }, { sandboxPolicy } = {}) {
5
- const target = resolveToolPath(file_path, workdir);
6
- const pathOptions = { sandboxPolicy };
4
+ /**
5
+ * @param {{file_path: string, old_string: string, new_string: string, replace_all?: boolean, workdir?: string}} params
6
+ * @param {{sandboxPolicy?: any, ctx?: any}} [options]
7
+ */
8
+ export async function editToolImpl({ file_path, old_string, new_string, replace_all = false, workdir }, { sandboxPolicy, ctx } = {}) {
9
+ const target = resolveToolPath(file_path, workdir, ctx);
10
+ const pathOptions = { sandboxPolicy, ctx };
7
11
  if (!isPathAllowed(target, workdir, pathOptions) || !isWritablePathAllowed(target, workdir, pathOptions)) return `Error: Path not allowed: ${file_path}`;
8
12
  if (!existsSync(target)) return `Error: File not found: ${file_path}`;
9
13
  const content = readFileSync(target, "utf8");
@@ -24,9 +24,13 @@ import {
24
24
 
25
25
  const execFileAsync = promisify(execFile);
26
26
 
27
- export async function globToolImpl({ pattern, path, limit, offset = 0, max_matches, max_output_chars, workdir }, { sandboxPolicy } = {}) {
28
- const cwd = resolveToolPath(path || workspaceRoot(workdir), workdir);
29
- if (!isPathAllowed(cwd, workdir, { sandboxPolicy })) return `Error: Path not allowed: ${cwd}`;
27
+ /**
28
+ * @param {{pattern: string, path?: string, limit?: number, offset?: number, max_matches?: number, max_output_chars?: number, workdir?: string}} params
29
+ * @param {{sandboxPolicy?: any, ctx?: any}} [options]
30
+ */
31
+ export async function globToolImpl({ pattern, path, limit, offset = 0, max_matches, max_output_chars, workdir }, { sandboxPolicy, ctx } = {}) {
32
+ const cwd = resolveToolPath(path || workspaceRoot(workdir, ctx), workdir, ctx);
33
+ if (!isPathAllowed(cwd, workdir, { sandboxPolicy, ctx })) return `Error: Path not allowed: ${cwd}`;
30
34
  const stat = safeStat(cwd);
31
35
  if (!stat?.isDirectory()) return `Error: Glob path is not a directory: ${cwd}`;
32
36
  const resultLimit = boundedInt(limit ?? max_matches, DEFAULT_MAX_SEARCH_LINES, { min: 1, max: 1000 });
@@ -38,8 +42,8 @@ export async function globToolImpl({ pattern, path, limit, offset = 0, max_match
38
42
  normalizeGlobPattern(pattern),
39
43
  ...excludedGlobArgs(),
40
44
  ];
41
- const rgPath = resolveRgPath();
42
- if (!rgPath) return ripgrepMissingMessage();
45
+ const rgPath = resolveRgPath({ ctx });
46
+ if (!rgPath) return ripgrepMissingMessage(ctx);
43
47
  try {
44
48
  const { stdout } = await execFileAsync(rgPath, args, { cwd, timeout: 15000, maxBuffer: SEARCH_MAX_BUFFER });
45
49
  const lines = stdout.trim().split("\n").filter(Boolean).sort((a, b) => {
@@ -53,6 +57,7 @@ export async function globToolImpl({ pattern, path, limit, offset = 0, max_match
53
57
  maxLines: resultLimit,
54
58
  maxChars: Number(max_output_chars) || DEFAULT_MAX_SEARCH_CHARS,
55
59
  offset,
60
+ ctx,
56
61
  });
57
62
  return result === "No files found matching pattern." ? result : `${result}\n\n${excludedPathSummary()}`;
58
63
  } catch (err) {
@@ -64,6 +69,7 @@ export async function globToolImpl({ pattern, path, limit, offset = 0, max_match
64
69
  maxLines: resultLimit,
65
70
  maxChars: Number(max_output_chars) || DEFAULT_MAX_SEARCH_CHARS,
66
71
  offset,
72
+ ctx,
67
73
  })}\n\n${excludedPathSummary()}`;
68
74
  }
69
75
  return `Error: ${err.message}`;
@@ -22,6 +22,10 @@ import {
22
22
 
23
23
  const execFileAsync = promisify(execFile);
24
24
 
25
+ /**
26
+ * @param {{pattern: string, path?: string, glob?: string, type?: string, output_mode?: string, context?: number, case_insensitive?: boolean, multiline?: boolean, head_limit?: number, offset?: number, max_matches?: number, max_output_chars?: number, workdir?: string}} params
27
+ * @param {{sandboxPolicy?: any, ctx?: any}} [options]
28
+ */
25
29
  export async function grepToolImpl({
26
30
  pattern,
27
31
  path,
@@ -36,9 +40,9 @@ export async function grepToolImpl({
36
40
  max_matches,
37
41
  max_output_chars,
38
42
  workdir,
39
- }, { sandboxPolicy } = {}) {
40
- const target = resolveToolPath(path || workspaceRoot(workdir), workdir);
41
- if (!isPathAllowed(target, workdir, { sandboxPolicy })) return `Error: Path not allowed: ${target}`;
43
+ }, { sandboxPolicy, ctx } = {}) {
44
+ const target = resolveToolPath(path || workspaceRoot(workdir, ctx), workdir, ctx);
45
+ if (!isPathAllowed(target, workdir, { sandboxPolicy, ctx })) return `Error: Path not allowed: ${target}`;
42
46
  const stat = safeStat(target);
43
47
  if (!stat) return `Error: Path not found: ${target}`;
44
48
  const cwd = stat.isDirectory() ? target : dirname(target);
@@ -55,8 +59,8 @@ export async function grepToolImpl({
55
59
  if (type) args.push("--type", type);
56
60
  args.push(...excludedGlobArgs(), "--", pattern, searchTarget);
57
61
  const resultLimit = boundedInt(head_limit ?? max_matches, DEFAULT_MAX_SEARCH_LINES, { min: 1, max: 1000 });
58
- const rgPath = resolveRgPath();
59
- if (!rgPath) return ripgrepMissingMessage();
62
+ const rgPath = resolveRgPath({ ctx });
63
+ if (!rgPath) return ripgrepMissingMessage(ctx);
60
64
  try {
61
65
  const { stdout } = await execFileAsync(rgPath, args, { cwd, timeout: 15000, maxBuffer: SEARCH_MAX_BUFFER });
62
66
  const normalized = stdout.trim().split("\n").filter(Boolean).map((line) => line.replace(/^\.\//, ""));
@@ -66,6 +70,7 @@ export async function grepToolImpl({
66
70
  maxLines: resultLimit,
67
71
  maxChars: Number(max_output_chars) || DEFAULT_MAX_SEARCH_CHARS,
68
72
  offset,
73
+ ctx,
69
74
  });
70
75
  return formatted === "No matches found." ? formatted : `${formatted}\n\n${excludedPathSummary()}`;
71
76
  } catch (err) {
@@ -77,6 +82,7 @@ export async function grepToolImpl({
77
82
  maxLines: resultLimit,
78
83
  maxChars: Number(max_output_chars) || DEFAULT_MAX_SEARCH_CHARS,
79
84
  offset,
85
+ ctx,
80
86
  })}\n\n${excludedPathSummary()}`;
81
87
  }
82
88
  return `Error: ${err.message}`;