@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.
- package/ARCHITECTURE.md +75 -9
- package/MIGRATION.md +293 -0
- package/README.md +46 -20
- package/package.json +104 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +231 -654
- package/src/agent/index.js +1 -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 +272 -54
- 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 +65 -18
- 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/index.js +1 -0
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +6 -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-errors.js +65 -0
- 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 +754 -0
- package/src/ai/runtime/capabilities.js +3 -0
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +33 -2
- package/src/ai/runtime/router.js +119 -19
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +19 -2
- package/src/ai/types.js +252 -20
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +75 -10
- 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 -1310
package/src/agent/index.js
CHANGED
|
@@ -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");
|
package/src/agent/tools/glob.js
CHANGED
|
@@ -24,9 +24,13 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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}`;
|
package/src/agent/tools/grep.js
CHANGED
|
@@ -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}`;
|