@mono-agent/agent-runtime 0.4.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.
- package/ARCHITECTURE.md +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +227 -45
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +21 -10
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -35
package/src/agent/compaction.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
// Context-compaction policy + heuristics for the pi-native bridge.
|
|
2
4
|
//
|
|
3
5
|
// The hand-rolled in-loop compaction manager (transformContext/afterToolCall)
|
|
@@ -10,6 +12,28 @@
|
|
|
10
12
|
// - isLikelyContextTermination: classifies a provider error/termination as a
|
|
11
13
|
// context-pressure event.
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} AgentCompactionPolicy
|
|
17
|
+
* @property {boolean} enabled
|
|
18
|
+
* @property {number} contextWindow
|
|
19
|
+
* @property {number} triggerRatio
|
|
20
|
+
* @property {number} triggerTokens
|
|
21
|
+
* @property {number} keepRecentTokens
|
|
22
|
+
* @property {number} summaryMaxTokens
|
|
23
|
+
* @property {boolean} fixedOverheadEnabled
|
|
24
|
+
* @property {number} compactionMinSavingsTokens
|
|
25
|
+
* @property {number} toolPayloadCompactionTriggerChars
|
|
26
|
+
* @property {number} toolPruneTriggerTokens
|
|
27
|
+
* @property {number} toolTextLimitChars
|
|
28
|
+
* @property {number} bashOutputLimitChars
|
|
29
|
+
* @property {number} mcpTextLimitChars
|
|
30
|
+
* @property {number} searchResultLimit
|
|
31
|
+
* @property {number} imageInlineMaxBytes
|
|
32
|
+
* @property {number} toolPayloadMaxBytes
|
|
33
|
+
* @property {number} mcpCallTimeoutMs
|
|
34
|
+
* @property {number} mcpCallMaxTotalTimeoutMs
|
|
35
|
+
*/
|
|
36
|
+
|
|
13
37
|
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
14
38
|
const DEFAULT_TRIGGER_RATIO = 0.85;
|
|
15
39
|
const DEFAULT_KEEP_RECENT_TOKENS = 24000;
|
|
@@ -25,20 +49,49 @@ const DEFAULT_TOOL_TEXT_LIMIT_CHARS = 64000;
|
|
|
25
49
|
const DEFAULT_BASH_OUTPUT_LIMIT_CHARS = 64000;
|
|
26
50
|
const DEFAULT_MCP_TEXT_LIMIT_CHARS = 48000;
|
|
27
51
|
const DEFAULT_SEARCH_RESULT_LIMIT = 100;
|
|
28
|
-
|
|
52
|
+
// Images are returned to vision models whole (a Read of an image attachment, an
|
|
53
|
+
// MCP screenshot). The byte size is large but token cost is driven by image
|
|
54
|
+
// tokens, not base64 length, so allow multi-MB screenshots through instead of
|
|
55
|
+
// clipping them to a "[truncated]" summary the model can't see. Clamp ceiling
|
|
56
|
+
// (10MB) is enforced in resolveAgentCompactionPolicy.
|
|
57
|
+
const DEFAULT_IMAGE_INLINE_MAX_BYTES = 5_000_000;
|
|
29
58
|
const DEFAULT_TOOL_PAYLOAD_MAX_BYTES = 262144;
|
|
30
59
|
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
|
|
60
|
+
// Hard wall clock for a single MCP tool call. Progress notifications reset the
|
|
61
|
+
// inactivity timeout above but must never extend a call past this cap (45 min) —
|
|
62
|
+
// sized for legitimately long tools (audio transcription, ask-the-user waits).
|
|
63
|
+
const DEFAULT_MCP_CALL_MAX_TOTAL_TIMEOUT_MS = 2_700_000;
|
|
31
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {*} value
|
|
67
|
+
* @param {number} fallback
|
|
68
|
+
* @param {number} min
|
|
69
|
+
* @param {number} max
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
32
72
|
function clampNumber(value, fallback, min, max) {
|
|
33
73
|
const n = Number(value);
|
|
34
74
|
if (!Number.isFinite(n)) return fallback;
|
|
35
75
|
return Math.min(Math.max(n, min), max);
|
|
36
76
|
}
|
|
37
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @param {*} value
|
|
80
|
+
* @param {number} fallback
|
|
81
|
+
* @param {number} min
|
|
82
|
+
* @param {number} max
|
|
83
|
+
* @returns {number}
|
|
84
|
+
*/
|
|
38
85
|
function clampInteger(value, fallback, min, max) {
|
|
39
86
|
return Math.floor(clampNumber(value, fallback, min, max));
|
|
40
87
|
}
|
|
41
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object<string, *>} [settings]
|
|
91
|
+
* @param {Object} [model]
|
|
92
|
+
* @param {number} [model.contextWindow]
|
|
93
|
+
* @returns {AgentCompactionPolicy}
|
|
94
|
+
*/
|
|
42
95
|
export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
43
96
|
const contextWindow = clampInteger(model?.contextWindow, DEFAULT_CONTEXT_WINDOW, 32000, 10_000_000);
|
|
44
97
|
const triggerRatio = clampNumber(
|
|
@@ -57,6 +110,11 @@ export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
|
57
110
|
triggerTokens: Math.min(ratioTrigger, reserveTrigger),
|
|
58
111
|
keepRecentTokens: clampInteger(settings.agent_compaction_keep_recent_tokens, DEFAULT_KEEP_RECENT_TOKENS, 4000, 200000),
|
|
59
112
|
summaryMaxTokens: clampInteger(settings.agent_compaction_summary_max_tokens, DEFAULT_SUMMARY_MAX_TOKENS, 1000, 64000),
|
|
113
|
+
// ON by default; the proactive fixed-overhead correction (system prompt +
|
|
114
|
+
// tool schemas + per-turn message) is disabled only when explicitly false.
|
|
115
|
+
// Read by the compaction driver off the resolved policy so it never has to
|
|
116
|
+
// re-sniff the raw settings/policy inputs.
|
|
117
|
+
fixedOverheadEnabled: settings.agent_compaction_fixed_overhead_enabled !== false,
|
|
60
118
|
compactionMinSavingsTokens: clampInteger(settings.agent_compaction_min_savings_tokens, DEFAULT_MIN_SAVINGS_TOKENS, 0, 500000),
|
|
61
119
|
toolPayloadCompactionTriggerChars: clampInteger(
|
|
62
120
|
settings.agent_tool_payload_compaction_trigger_chars,
|
|
@@ -72,9 +130,194 @@ export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
|
72
130
|
imageInlineMaxBytes: clampInteger(settings.agent_image_inline_max_bytes, DEFAULT_IMAGE_INLINE_MAX_BYTES, 0, 10 * 1024 * 1024),
|
|
73
131
|
toolPayloadMaxBytes: clampInteger(settings.agent_tool_payload_max_bytes, DEFAULT_TOOL_PAYLOAD_MAX_BYTES, 0, 16 * 1024 * 1024),
|
|
74
132
|
mcpCallTimeoutMs: clampInteger(settings.agent_mcp_call_timeout_ms, DEFAULT_MCP_CALL_TIMEOUT_MS, 1000, Number.MAX_SAFE_INTEGER),
|
|
133
|
+
mcpCallMaxTotalTimeoutMs: clampInteger(
|
|
134
|
+
settings.agent_mcp_call_max_total_timeout_ms,
|
|
135
|
+
DEFAULT_MCP_CALL_MAX_TOTAL_TIMEOUT_MS,
|
|
136
|
+
1000,
|
|
137
|
+
Number.MAX_SAFE_INTEGER,
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Typed policy objects <-> deprecated `settings` shim -------------------
|
|
143
|
+
//
|
|
144
|
+
// RuntimeRunOptions now carries typed `toolLimits` / `compaction` policy
|
|
145
|
+
// objects. The DEPRECATED `settings` bag remains a per-group fallback: it is
|
|
146
|
+
// consumed only when the corresponding typed object is ABSENT, and consuming it
|
|
147
|
+
// surfaces one `deprecated_settings_option` runtime_warning per run.
|
|
148
|
+
//
|
|
149
|
+
// resolveAgentCompactionPolicy stays the canonical settings->policy clamp/mapper
|
|
150
|
+
// (its signature is unchanged — worklab deep-imports it). The helpers below
|
|
151
|
+
// project the typed objects back onto the same snake_case settings keys so the
|
|
152
|
+
// resolution goes through that one clamp path unchanged, whether the values came
|
|
153
|
+
// from a typed object or the legacy settings bag.
|
|
154
|
+
|
|
155
|
+
// Typed toolLimits field -> settings key. `bashTimeoutMs` is intentionally
|
|
156
|
+
// ABSENT: no `agent_bash_*_timeout` setting exists today and this phase does not
|
|
157
|
+
// invent new timeout behavior, so the field is documented on the RuntimeToolLimits
|
|
158
|
+
// typedef but not wired through the settings shim or any tool.
|
|
159
|
+
const TOOL_LIMIT_SETTINGS_KEYS = /** @type {const} */ ({
|
|
160
|
+
toolTextLimitChars: "agent_tool_text_limit_chars",
|
|
161
|
+
bashOutputLimitChars: "agent_bash_output_limit_chars",
|
|
162
|
+
mcpTextLimitChars: "agent_mcp_text_limit_chars",
|
|
163
|
+
searchResultLimit: "agent_search_result_limit",
|
|
164
|
+
imageInlineMaxBytes: "agent_image_inline_max_bytes",
|
|
165
|
+
toolPayloadMaxBytes: "agent_tool_payload_max_bytes",
|
|
166
|
+
mcpCallTimeoutMs: "agent_mcp_call_timeout_ms",
|
|
167
|
+
mcpCallMaxTotalTimeoutMs: "agent_mcp_call_max_total_timeout_ms",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Typed compaction field -> settings key. `contextWindowOverride` is ABSENT: it
|
|
171
|
+
// has no legacy settings equivalent and is applied directly at the live-window
|
|
172
|
+
// resolution site (resolveLiveCompactionPolicy), not through this shim.
|
|
173
|
+
const COMPACTION_SETTINGS_KEYS = /** @type {const} */ ({
|
|
174
|
+
enabled: "agent_compaction_enabled",
|
|
175
|
+
triggerRatio: "agent_compaction_trigger_ratio",
|
|
176
|
+
keepRecentTokens: "agent_compaction_keep_recent_tokens",
|
|
177
|
+
summaryMaxTokens: "agent_compaction_summary_max_tokens",
|
|
178
|
+
minSavingsTokens: "agent_compaction_min_savings_tokens",
|
|
179
|
+
fixedOverheadEnabled: "agent_compaction_fixed_overhead_enabled",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const DEPRECATED_SETTINGS_WARNING_KIND = "deprecated_settings_option";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {Object<string, *>|null|undefined} group
|
|
186
|
+
* @param {Record<string, string>} keyMap
|
|
187
|
+
* @param {Object<string, *>} out
|
|
188
|
+
*/
|
|
189
|
+
function copyTypedGroup(group, keyMap, out) {
|
|
190
|
+
for (const [field, settingKey] of Object.entries(keyMap)) {
|
|
191
|
+
if (group && group[field] !== undefined) out[settingKey] = group[field];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Object<string, *>|null|undefined} settings
|
|
197
|
+
* @param {Record<string, string>} keyMap
|
|
198
|
+
* @param {Object<string, *>} out
|
|
199
|
+
* @param {Array<string>} consumed
|
|
200
|
+
*/
|
|
201
|
+
function copySettingsGroup(settings, keyMap, out, consumed) {
|
|
202
|
+
if (!settings || typeof settings !== "object") return;
|
|
203
|
+
for (const settingKey of Object.values(keyMap)) {
|
|
204
|
+
if (settings[settingKey] !== undefined) {
|
|
205
|
+
out[settingKey] = settings[settingKey];
|
|
206
|
+
consumed.push(settingKey);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fold the typed `toolLimits` / `compaction` policy objects and the deprecated
|
|
213
|
+
* `settings` bag into ONE settings-like object resolveAgentCompactionPolicy
|
|
214
|
+
* consumes, honoring PER-GROUP precedence: when a typed object is present its
|
|
215
|
+
* fields win and the legacy settings keys for that group are ignored entirely;
|
|
216
|
+
* when the typed object is absent, that group's settings keys are consumed (and
|
|
217
|
+
* reported in `consumedSettingsKeys` so the caller can emit exactly one
|
|
218
|
+
* deprecation warning per run).
|
|
219
|
+
* @param {{toolLimits?: Object<string, *>, compaction?: Object<string, *>, settings?: Object<string, *>}} [options]
|
|
220
|
+
* @returns {{settingsLike: Object<string, *>, consumedSettingsKeys: Array<string>}}
|
|
221
|
+
*/
|
|
222
|
+
export function resolveRuntimePolicyInputs({ toolLimits, compaction, settings } = {}) {
|
|
223
|
+
/** @type {Object<string, *>} */
|
|
224
|
+
const settingsLike = {};
|
|
225
|
+
/** @type {Array<string>} */
|
|
226
|
+
const consumedSettingsKeys = [];
|
|
227
|
+
|
|
228
|
+
if (toolLimits && typeof toolLimits === "object") {
|
|
229
|
+
copyTypedGroup(toolLimits, TOOL_LIMIT_SETTINGS_KEYS, settingsLike);
|
|
230
|
+
} else {
|
|
231
|
+
copySettingsGroup(settings, TOOL_LIMIT_SETTINGS_KEYS, settingsLike, consumedSettingsKeys);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (compaction && typeof compaction === "object") {
|
|
235
|
+
copyTypedGroup(compaction, COMPACTION_SETTINGS_KEYS, settingsLike);
|
|
236
|
+
} else {
|
|
237
|
+
copySettingsGroup(settings, COMPACTION_SETTINGS_KEYS, settingsLike, consumedSettingsKeys);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { settingsLike, consumedSettingsKeys };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build the one-per-run deprecation warning fired when the legacy `settings`
|
|
245
|
+
* bag was consumed as a policy fallback. Shape matches the other pi/claude
|
|
246
|
+
* bridge runtime warnings ({warning_kind, source, message}).
|
|
247
|
+
* @param {ReadonlyArray<string>} consumedKeys
|
|
248
|
+
* @returns {{warning_kind: string, source: string, message: string, settings_keys: Array<string>}}
|
|
249
|
+
*/
|
|
250
|
+
export function deprecatedSettingsWarning(consumedKeys) {
|
|
251
|
+
const keys = Array.from(consumedKeys || []);
|
|
252
|
+
return {
|
|
253
|
+
warning_kind: DEPRECATED_SETTINGS_WARNING_KIND,
|
|
254
|
+
source: "runtime",
|
|
255
|
+
message:
|
|
256
|
+
"runOptions.settings is deprecated; pass the typed `toolLimits` / `compaction` policy objects instead "
|
|
257
|
+
+ "(host migration helper: resolveRuntimePolicies in @mono-agent/runtime-adapter). Consumed settings keys: "
|
|
258
|
+
+ `${keys.join(", ")}.`,
|
|
259
|
+
settings_keys: keys,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Estimate the FIXED per-request overhead the provider meters but the raw
|
|
264
|
+
// transcript estimate excludes: the system prompt, the tool/MCP schemas, and the
|
|
265
|
+
// per-turn user message(s). estimateCurrentContextTokens' raw branch sums ONLY
|
|
266
|
+
// session.buildContext().messages (the transcript), so on a seeded session whose
|
|
267
|
+
// last-assistant usage is stale/0 the proactive-compaction trigger under-counts
|
|
268
|
+
// and under-fires, letting the real request overflow the window. Adding this
|
|
269
|
+
// overhead to the raw estimate makes the trigger reflect what the provider counts.
|
|
270
|
+
//
|
|
271
|
+
// Uses Math.ceil(len/4) to mirror pi-ai's chars/4 heuristic — consistency with
|
|
272
|
+
// the transcript estimate matters more than precision. Pure + dependency-free.
|
|
273
|
+
/**
|
|
274
|
+
* @param {Object} [options]
|
|
275
|
+
* @param {string} [options.systemPrompt]
|
|
276
|
+
* @param {Array<Object>} [options.tools]
|
|
277
|
+
* @param {Array<Object>} [options.messages]
|
|
278
|
+
* @returns {{systemPromptTokens: number, toolSchemaTokens: number, userMessageTokens: number, fixedOverheadTokens: number}}
|
|
279
|
+
*/
|
|
280
|
+
export function estimateFixedOverheadTokens({ systemPrompt, tools, messages } = {}) {
|
|
281
|
+
const tokensForChars = (value) => Math.ceil(String(value ?? "").length / 4);
|
|
282
|
+
|
|
283
|
+
const systemPromptTokens = tokensForChars(systemPrompt);
|
|
284
|
+
|
|
285
|
+
let toolSchemaTokens = 0;
|
|
286
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
287
|
+
try {
|
|
288
|
+
const serialized = JSON.stringify({
|
|
289
|
+
name: tool?.name,
|
|
290
|
+
description: tool?.description,
|
|
291
|
+
parameters: tool?.parameters ?? tool?.inputSchema ?? {},
|
|
292
|
+
});
|
|
293
|
+
toolSchemaTokens += tokensForChars(serialized);
|
|
294
|
+
} catch {
|
|
295
|
+
// Circular/unserializable tool schema — count it as 0 rather than throw.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let userMessageTokens = 0;
|
|
300
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
301
|
+
try {
|
|
302
|
+
userMessageTokens += tokensForChars(JSON.stringify(message?.content ?? ""));
|
|
303
|
+
} catch {
|
|
304
|
+
// Unserializable content — count it as 0 rather than throw.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
systemPromptTokens,
|
|
310
|
+
toolSchemaTokens,
|
|
311
|
+
userMessageTokens,
|
|
312
|
+
fixedOverheadTokens: systemPromptTokens + toolSchemaTokens + userMessageTokens,
|
|
75
313
|
};
|
|
76
314
|
}
|
|
77
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @param {string} message
|
|
318
|
+
* @param {Object<string, *>} [diagnostics]
|
|
319
|
+
* @returns {boolean}
|
|
320
|
+
*/
|
|
78
321
|
export function isLikelyContextTermination(message, diagnostics = {}) {
|
|
79
322
|
const text = String(message || "");
|
|
80
323
|
if (!/terminated|aborted before final output|aborted before final|stream.*aborted|context window|context budget/i.test(text)) return false;
|
|
@@ -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 };
|
package/src/agent/tool-bloat.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/agent/tools/bash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
}
|
package/src/agent/tools/edit.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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");
|