@mono-agent/agent-runtime 0.19.1 → 0.20.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/MIGRATION.md +1 -1
- package/README.md +101 -4
- package/package.json +1 -1
- package/src/agent/sandbox-seam.js +16 -2
- package/src/agent/tools/bash.js +26 -4
- package/src/agent/tools/edit.js +72 -5
- package/src/agent/tools/exec.js +22 -4
- package/src/agent/tools/glob.js +65 -9
- package/src/agent/tools/grep.js +66 -11
- package/src/agent/tools/node-repl.js +5 -2
- package/src/agent/tools/pi-bridge.js +263 -48
- package/src/agent/tools/read.js +50 -10
- package/src/agent/tools/shared/path-resolver.js +67 -2
- package/src/agent/tools/shared/process-jobs.js +188 -0
- package/src/agent/tools/shared/process-runner.js +541 -30
- package/src/agent/tools/shared/protected-filesystem.js +150 -0
- package/src/agent/tools/web-search.js +63 -8
- package/src/agent/tools/write.js +52 -6
- package/src/ai/providers/acp.js +4 -0
- package/src/ai/providers/claude-cli.js +35 -2
- package/src/ai/providers/claude-sdk.js +12 -0
- package/src/ai/providers/codex-app.js +15 -2
- package/src/ai/providers/pi-native/stream-subscriber.js +29 -2
- package/src/ai/providers/pi-native/turn-runner.js +3 -0
- package/src/ai/providers/pi-native.js +7 -1
- package/src/ai/runtime/capabilities.js +2 -0
- package/src/ai/runtime/router.js +78 -6
- package/src/ai/streaming/codex-events.js +15 -0
- package/src/ai/streaming/opencode-events.js +5 -0
- package/src/ai/tool-lifecycle.js +347 -0
- package/src/ai/types.js +58 -0
- package/src/runtime.js +35 -21
- package/types/agent/sandbox-seam.d.ts +19 -6
- package/types/agent/tools/bash.d.ts +11 -26
- package/types/agent/tools/edit.d.ts +3 -2
- package/types/agent/tools/exec.d.ts +13 -26
- package/types/agent/tools/glob.d.ts +3 -2
- package/types/agent/tools/grep.d.ts +3 -2
- package/types/agent/tools/pi-bridge.d.ts +11 -4
- package/types/agent/tools/read.d.ts +3 -2
- package/types/agent/tools/shared/path-resolver.d.ts +8 -0
- package/types/agent/tools/shared/process-jobs.d.ts +64 -0
- package/types/agent/tools/shared/process-runner.d.ts +45 -3
- package/types/agent/tools/shared/protected-filesystem.d.ts +51 -0
- package/types/agent/tools/write.d.ts +3 -2
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -0
- package/types/ai/runtime/capabilities.d.ts +3 -0
- package/types/ai/streaming/codex-events.d.ts +1 -0
- package/types/ai/streaming/opencode-events.d.ts +1 -0
- package/types/ai/tool-lifecycle.d.ts +43 -0
- package/types/ai/types.d.ts +118 -0
package/MIGRATION.md
CHANGED
|
@@ -490,7 +490,7 @@ a compatibility subpath.
|
|
|
490
490
|
|
|
491
491
|
## Version
|
|
492
492
|
|
|
493
|
-
This guide describes the published `0.
|
|
493
|
+
This guide describes the published `0.20.x` package contract. Keep
|
|
494
494
|
`@mono-agent/agent-runtime`, `@mono-agent/runtime-adapter`, and other
|
|
495
495
|
`@mono-agent/*` packages on the same lockstep version when upgrading. The paired
|
|
496
496
|
runtime adapter no longer exposes `piReasoningSummary` in its run-options type.
|
package/README.md
CHANGED
|
@@ -53,6 +53,18 @@ console.log(result.text);
|
|
|
53
53
|
`Glob` and `Grep` prefer the packaged `@vscode/ripgrep` binary on supported
|
|
54
54
|
platforms. An explicit `ripgrepPath` wins, with `PATH` as the final fallback.
|
|
55
55
|
|
|
56
|
+
### Pi-native MCP Apps bridge
|
|
57
|
+
|
|
58
|
+
Only the Pi-native bridge declares `supports_mcp_apps`. When a host supplies an
|
|
59
|
+
MCP Apps registry, the MCP client advertises the standard ext-apps UI MIME
|
|
60
|
+
capability. The host then explicitly intersects the two reviewed protocol
|
|
61
|
+
revisions, reads only the originating tool's declared `ui://` resource, and
|
|
62
|
+
receives one exact connection capability. Successful registration retains that
|
|
63
|
+
existing MCP client instead of creating a client per UI call; host LRU/idle
|
|
64
|
+
eviction closes the client, transport, and sandbox cleanup. Other runtime
|
|
65
|
+
backends do not advertise or receive this extension.
|
|
66
|
+
See [Reply files and MCP Apps](https://mono-agent-docs.vercel.app/tools/rich-replies/).
|
|
67
|
+
|
|
56
68
|
## Architecture
|
|
57
69
|
|
|
58
70
|
The package uses a fixed registry of bridge descriptors and loads provider code
|
|
@@ -64,10 +76,14 @@ only after a run selects a matching model reference and execution mode:
|
|
|
64
76
|
2. `resolveRuntimeBridge()` checks the six static bridge descriptors in order.
|
|
65
77
|
3. The selected descriptor lazily imports its provider implementation.
|
|
66
78
|
4. The bridge prepares the runtime inputs it supports, including managed or MCP
|
|
67
|
-
tools only where that bridge can represent them, streams normalized
|
|
68
|
-
|
|
69
|
-
5.
|
|
70
|
-
|
|
79
|
+
tools only where that bridge can represent them, and streams normalized
|
|
80
|
+
events through the lifecycle gate.
|
|
81
|
+
5. For each managed-tool start/result, the gate awaits the host's incremental
|
|
82
|
+
persistence sink before publishing the same block with history metadata to
|
|
83
|
+
the client; provider events remain deterministically ordered even for
|
|
84
|
+
parallel tools.
|
|
85
|
+
6. The bridge returns a provider-neutral `RuntimeResult`; the host owns storage,
|
|
86
|
+
recovery, and UI effects.
|
|
71
87
|
|
|
72
88
|
### Package structure
|
|
73
89
|
|
|
@@ -82,6 +98,58 @@ only after a run selects a matching model reference and execution mode:
|
|
|
82
98
|
The detailed lifecycle, provider-session differences, and host boundary are in
|
|
83
99
|
the [architecture guide](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/ARCHITECTURE.md).
|
|
84
100
|
|
|
101
|
+
### Managed-tool lifecycle fidelity
|
|
102
|
+
|
|
103
|
+
`RuntimeRunOptions.toolLifecycleSink` is an awaited host-owned boundary. The
|
|
104
|
+
runtime sends redaction-eligible raw arguments/content plus stable provider call
|
|
105
|
+
id and name; the host returns only record/sequence, persistence/truncation byte
|
|
106
|
+
metadata, and opaque artifact references. That returned metadata is attached to
|
|
107
|
+
the exact normalized `tool_use` / `tool_result` event rendered by clients. Sink
|
|
108
|
+
failure is fail-soft and explicit (`persistence: "failed"` plus an error code),
|
|
109
|
+
never fake success. A callback exception cannot duplicate a client event.
|
|
110
|
+
|
|
111
|
+
Provider bridges only claim terminal distinctions their structured protocol
|
|
112
|
+
actually supplies:
|
|
113
|
+
|
|
114
|
+
| Bridge | Genuine tool-result distinctions | Conservative fallback |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| Pi native | `success`, `error`, numeric `exit_nonzero`, structured `timeout`, structured `signal`, and abort-backed `cancelled`; host approval denial/expiry adds `rejected`/`timeout` | Unknown failed outcome → `error` |
|
|
117
|
+
| Codex app-server | `success`, `error`, and numeric command `exit_nonzero`; shared host abort rules still apply | Other failed item → `error`; no result before run end remains dangling for host closure/recovery |
|
|
118
|
+
| Claude SDK | `success` / `error`; shared host approval events can add `rejected`/`timeout` and an aborted error result can add `cancelled` | Undistinguished failed result → `error` |
|
|
119
|
+
| Claude Code CLI | `success` / `error`; a Codex-shaped command item with an explicit non-zero code is `exit_nonzero`; shared approval/abort rules still apply | Undistinguished failed result → `error` |
|
|
120
|
+
| OpenCode app-server | `success` / `error`; shared approval/abort rules still apply | Undistinguished failed result → `error` |
|
|
121
|
+
| ACP v1 | `completed` → `success`, `failed` → `error`; shared approval/abort rules still apply | ACP exposes no tool-level signal/exit/timeout distinction, so failed → `error` |
|
|
122
|
+
|
|
123
|
+
The runtime never derives a state from result prose. Structured timeout, signal,
|
|
124
|
+
non-zero exit, completed success, and a specific non-runtime, non-cancellation
|
|
125
|
+
provider failure win over a later outer abort. For a failed result with an
|
|
126
|
+
aborted outer signal, cancellation wins over a provider bridge's otherwise
|
|
127
|
+
generic `error` / `runtime_error` fallback. Whenever that outer abort determines
|
|
128
|
+
the result—including when the bridge supplied a trusted `cancelled` hint—the
|
|
129
|
+
failure kind comes only from host abort provenance: the cross-package own
|
|
130
|
+
data-property brand `channelUserCancel === true` maps to `cancelled_user`; every
|
|
131
|
+
unbranded reason maps to generic `cancelled`. Accessors, Proxies, inherited or
|
|
132
|
+
brand-shaped impostors, error prose, class/channel names, reason `kind` or `code`
|
|
133
|
+
strings, and raw provider result data never select host provenance.
|
|
134
|
+
Without an outer abort, a trusted `cancelled` hint retains a known cancellation
|
|
135
|
+
failure kind and otherwise falls back to `cancelled`.
|
|
136
|
+
|
|
137
|
+
This outer-abort rule matches the harness run-level classifier. The standalone
|
|
138
|
+
runtime keeps its zero-`@mono-agent/*` dependency boundary by recognizing the
|
|
139
|
+
shared brand structurally rather than importing `@mono-agent/agent-contracts`.
|
|
140
|
+
Its deliberate extra fidelity is tool-level only: a trusted bridge hint can
|
|
141
|
+
retain a cancellation subtype when no outer abort exists. The run-level harness
|
|
142
|
+
has no corresponding tool hint, so this does not widen a shared core contract.
|
|
143
|
+
A started call with no result is closed by the harness as `cancelled`, `error`,
|
|
144
|
+
or `interrupted` from the run boundary; process-startup recovery specifically
|
|
145
|
+
uses `interrupted` and does not rerun the tool.
|
|
146
|
+
|
|
147
|
+
Terminal states reuse the observability taxonomy: `success` has no failure kind;
|
|
148
|
+
`signal` and `interrupted` map to `process_death`; `cancelled` preserves a known
|
|
149
|
+
`cancelled`, `cancelled_user`, `cancelled_stale`, `cancelled_shutdown`, or
|
|
150
|
+
`cancelled_signal`; `rejected`, `error`, `exit_nonzero`, and `timeout` use a
|
|
151
|
+
provider-supplied known kind when available and otherwise `runtime_error`.
|
|
152
|
+
|
|
85
153
|
## Public API
|
|
86
154
|
|
|
87
155
|
### Start here
|
|
@@ -753,6 +821,7 @@ Per-call options (a non-exhaustive selection):
|
|
|
753
821
|
| `nativeSubagents` | `object` | Caller-defined Claude native `Task` profiles. Direct Codex rejects configured teammate definitions because Codex owns its collaboration agents. |
|
|
754
822
|
| `settingSources` | `("user" \| "project" \| "local")[]` | Claude Agent SDK filesystem settings opt-in. Omitted/empty disables those three sources; Anthropic managed settings still apply. |
|
|
755
823
|
| `codexLoadProjectDocs` | `boolean` | Codex app-server repository-instruction opt-in. Omitted/false sets `project_doc_max_bytes=0`; true restores Codex defaults. Explicit `codexAppServerArgs` wins. |
|
|
824
|
+
| `codexSandboxNetworkAccess` | `boolean` | Code-only Codex app-server per-turn network control. Only strict `true` enables it for plan/default/acceptEdits; omitted or any other value disables it. |
|
|
756
825
|
| `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http); on direct Codex, each forwarded server authorizes its own tool calls. |
|
|
757
826
|
| `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
|
|
758
827
|
| `webSearchConfig` | `{ backend?, endpoint? }` | Run-scoped local SearXNG/keyless WebSearch backend selection. |
|
|
@@ -818,6 +887,15 @@ Codex and its own collaboration agents should load repository instructions. If
|
|
|
818
887
|
`codexAppServerArgs` is supplied, that explicit argument vector is authoritative
|
|
819
888
|
and `codexLoadProjectDocs` does not alter it.
|
|
820
889
|
|
|
890
|
+
`codexSandboxNetworkAccess` is a separate code-only, provider-native control.
|
|
891
|
+
It is unrelated to `RuntimeRunOptions.sandboxPolicy`, which controls
|
|
892
|
+
mono-agent's own sandbox and is not consumed by Codex's provider-owned tool
|
|
893
|
+
loop. Only strict `true` enables network access for plan/read-only and
|
|
894
|
+
default/acceptEdits/workspace-write turns; the no-tools probe remains offline
|
|
895
|
+
and bypass remains danger-full-access. Combining workspace-write with network
|
|
896
|
+
access grants repository read and network egress in the same turn. Prefer
|
|
897
|
+
`permissionMode: "plan"` when only read-only browsing is needed.
|
|
898
|
+
|
|
821
899
|
Provider-native and in-process delegation share `subagent_activity` telemetry.
|
|
822
900
|
`subagent.id` is the canonical parent attachment key: the initiating parent
|
|
823
901
|
tool-use id whenever the provider exposes it, or a stable synthetic key for an
|
|
@@ -907,6 +985,17 @@ by one lazily started Node.js REPL child per run. You select them via
|
|
|
907
985
|
- The runtime context's `workspace` / `repoRoot` allow-list (paths outside both, plus `/tmp` and `process.cwd()`, are rejected)
|
|
908
986
|
- Output truncation with optional artifact persistence (`{toolArtifactDir}/tool-output/{runId}/...` when `toolArtifactDir` is configured)
|
|
909
987
|
|
|
988
|
+
The Pi-native tool context may structurally receive a host process-job
|
|
989
|
+
controller. Only then do Exec and Bash add optional `background`; with no
|
|
990
|
+
controller their schemas and foreground path are unchanged. A background call
|
|
991
|
+
hands the exact prepared command to the controller and stops awaiting it. The
|
|
992
|
+
kernel first creates a command-agnostic detached POSIX group leader; only after
|
|
993
|
+
the host durably records its PID, equal PGID, and process incarnation does the
|
|
994
|
+
kernel release the exact target over an anonymous pipe. The kernel also owns
|
|
995
|
+
whole-tree completion, while the host owns durable policy, persistence, wake
|
|
996
|
+
delivery, and operator state. The kernel intentionally imports no workspace
|
|
997
|
+
contract package for this boundary.
|
|
998
|
+
|
|
910
999
|
`NodeRepl` uses Node's default `node:repl` evaluator, so variables, `_`, `_error`, and loaded modules persist across calls in the same run. It supports multiline input and top-level `await`, resolves workspace-installed packages, and is closed with the run. Its child is prepared through the same sandbox seam as `Exec`/`Bash` and communicates through token-authenticated, length-prefixed JSON frames on ordinary stdin/stdout; abort, the fixed 120-second timeout, child exit, or hard output overflow resets the session. It deliberately has no session ids, persistent history, terminal commands, or package-install surface.
|
|
911
1000
|
|
|
912
1001
|
`WebSearch` uses a configured loopback SearXNG endpoint and/or deterministic
|
|
@@ -987,6 +1076,12 @@ Behaviour:
|
|
|
987
1076
|
- Chain exhausted → `failureKind: "provider_unavailable_exhausted"`, `failoverHistory` lists every attempt.
|
|
988
1077
|
- `uniform` safety keeps the shared monotonic runtime; `per-route-native` isolates
|
|
989
1078
|
route runtimes and records each bounded safety contract/status.
|
|
1079
|
+
- A `per-route-native` non-Pi route cannot project non-empty internal
|
|
1080
|
+
`sandboxPolicy.protectedRoots`; the router records `safety_unavailable` and
|
|
1081
|
+
advances before route resolution or provider invocation. Empty protected-root
|
|
1082
|
+
sets preserve ordinary provider-native behavior, while Pi routes retain the
|
|
1083
|
+
policy. The same invariant covers model routes reached through `Agent`
|
|
1084
|
+
children.
|
|
990
1085
|
- Pi route telemetry distinguishes `disabled`, fail-closed `mono-agent-srt`,
|
|
991
1086
|
and `mono-agent-srt-unsafe-host-fallback`; the last describes a configured
|
|
992
1087
|
policy that prefers SRT but permits host execution, not which branch ran.
|
|
@@ -1198,6 +1293,8 @@ runtime fails closed.
|
|
|
1198
1293
|
shows the code-only host hooks.
|
|
1199
1294
|
- [Local-first web research](https://mono-agent-docs.vercel.app/tools/web-research/)
|
|
1200
1295
|
documents SearXNG, extraction, retry, browser isolation, and sandbox policy.
|
|
1296
|
+
- [Reply files and MCP Apps](https://mono-agent-docs.vercel.app/tools/rich-replies/)
|
|
1297
|
+
documents the host bridge, browser sandbox, and lifecycle limits.
|
|
1201
1298
|
- [Architecture](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/ARCHITECTURE.md)
|
|
1202
1299
|
and [migration guide](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/MIGRATION.md)
|
|
1203
1300
|
cover internal flow and upgrades from `0.3.x`.
|
package/package.json
CHANGED
|
@@ -48,12 +48,20 @@
|
|
|
48
48
|
* @property {string} command
|
|
49
49
|
* @property {ReadonlyArray<string>} [args]
|
|
50
50
|
* @property {string} [cwd]
|
|
51
|
-
* @property {
|
|
51
|
+
* @property {Record<string, string|undefined>} [env]
|
|
52
52
|
* @property {boolean} [allowLocalBinding] Trusted per-command capability.
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* @typedef {
|
|
56
|
+
* @typedef {Object} PreparedSandboxCommand
|
|
57
|
+
* @property {string} command
|
|
58
|
+
* @property {ReadonlyArray<string>} args
|
|
59
|
+
* @property {string} cwd
|
|
60
|
+
* @property {Record<string, string|undefined>} [env]
|
|
61
|
+
* @property {boolean} [allowLocalBinding]
|
|
62
|
+
* @property {boolean} sandboxed
|
|
63
|
+
* @property {string} [sandboxSettingsPath]
|
|
64
|
+
* @property {() => Promise<void>} [cleanup]
|
|
57
65
|
*/
|
|
58
66
|
|
|
59
67
|
/**
|
|
@@ -77,6 +85,7 @@
|
|
|
77
85
|
* @property {SandboxNetworkPolicyLike} [network]
|
|
78
86
|
* @property {ReadonlyArray<string>} [readableRoots]
|
|
79
87
|
* @property {ReadonlyArray<string>} [writableRoots]
|
|
88
|
+
* @property {ReadonlyArray<string>} [protectedRoots] Host-internal roots denied for both reads and writes.
|
|
80
89
|
* @property {ReadonlyArray<string>} [denyWrite]
|
|
81
90
|
* @property {string} [root]
|
|
82
91
|
*/
|
|
@@ -173,11 +182,16 @@ function mergePolicies(configured, request) {
|
|
|
173
182
|
const configuredIsReal = isRealSandboxMode(configured.mode);
|
|
174
183
|
const requestIsReal = isRealSandboxMode(request.mode);
|
|
175
184
|
const mode = configuredIsReal ? configured.mode : (requestIsReal ? request.mode : (request.mode ?? configured.mode));
|
|
185
|
+
const protectedRoots = [...new Set([
|
|
186
|
+
...(configured.protectedRoots ?? []),
|
|
187
|
+
...(request.protectedRoots ?? []),
|
|
188
|
+
])].sort();
|
|
176
189
|
return {
|
|
177
190
|
...configured,
|
|
178
191
|
...request,
|
|
179
192
|
mode,
|
|
180
193
|
network: mergeNetwork(configured.network, request.network),
|
|
194
|
+
protectedRoots,
|
|
181
195
|
};
|
|
182
196
|
}
|
|
183
197
|
|
package/src/agent/tools/bash.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DEFAULT_PROCESS_BUFFER_BYTES,
|
|
15
15
|
runPreparedProcess,
|
|
16
16
|
} from "./shared/process-runner.js";
|
|
17
|
+
import { handOffProcessJob } from "./shared/process-jobs.js";
|
|
17
18
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
18
19
|
import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
19
20
|
|
|
@@ -56,8 +57,8 @@ export function normalizeProcessTimeoutMs(value, fallback = DEFAULT_BASH_TIMEOUT
|
|
|
56
57
|
/**
|
|
57
58
|
* Compatibility wrapper retained for direct callers and tests.
|
|
58
59
|
*
|
|
59
|
-
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string}} params
|
|
60
|
-
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
60
|
+
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean}} params
|
|
61
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: import("./shared/process-jobs.js").ProcessJobsController}} [options]
|
|
61
62
|
*/
|
|
62
63
|
export async function bashToolImpl(params, options = {}) {
|
|
63
64
|
return (await bashToolRun(params, options)).text;
|
|
@@ -66,8 +67,8 @@ export async function bashToolImpl(params, options = {}) {
|
|
|
66
67
|
/**
|
|
67
68
|
* Structured Bash execution used by the Pi bridge.
|
|
68
69
|
*
|
|
69
|
-
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string}} params
|
|
70
|
-
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
70
|
+
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean}} params
|
|
71
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: import("./shared/process-jobs.js").ProcessJobsController}} [options]
|
|
71
72
|
*/
|
|
72
73
|
export async function bashToolRun(
|
|
73
74
|
{
|
|
@@ -76,12 +77,14 @@ export async function bashToolRun(
|
|
|
76
77
|
timeout_ms,
|
|
77
78
|
max_output_chars,
|
|
78
79
|
workdir,
|
|
80
|
+
background,
|
|
79
81
|
},
|
|
80
82
|
{
|
|
81
83
|
signal,
|
|
82
84
|
sandboxPolicy,
|
|
83
85
|
sandboxEngine,
|
|
84
86
|
ctx,
|
|
87
|
+
processJobsController,
|
|
85
88
|
} = {},
|
|
86
89
|
) {
|
|
87
90
|
const startedAt = Date.now();
|
|
@@ -127,6 +130,25 @@ export async function bashToolRun(
|
|
|
127
130
|
return failed(`Error: ${error?.message || String(error)}`, "sandbox_prepare_failed", startedAt);
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
if (background === true && processJobsController) {
|
|
134
|
+
const requestedTimeoutMs = timeout_ms !== undefined
|
|
135
|
+
? timeoutMs
|
|
136
|
+
: (timeout === undefined ? undefined : timeoutMs);
|
|
137
|
+
const handedOff = await handOffProcessJob({
|
|
138
|
+
controller: processJobsController,
|
|
139
|
+
tool: "Bash",
|
|
140
|
+
prepared,
|
|
141
|
+
summary: `Bash command (${command.length} characters; content redacted)`,
|
|
142
|
+
timeoutMs: requestedTimeoutMs,
|
|
143
|
+
maxOutputChars: max_output_chars === undefined ? undefined : maxChars,
|
|
144
|
+
startedAt,
|
|
145
|
+
failed,
|
|
146
|
+
});
|
|
147
|
+
return handedOff.error || !legacyTimeoutUsed
|
|
148
|
+
? handedOff
|
|
149
|
+
: { ...handedOff, outcome: { ...handedOff.outcome, legacyTimeoutUsed: true } };
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
let result;
|
|
131
153
|
let cleanupError;
|
|
132
154
|
try {
|
package/src/agent/tools/edit.js
CHANGED
|
@@ -1,15 +1,82 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isPathAllowed,
|
|
4
|
+
isPathLexicallyAllowed,
|
|
5
|
+
isWritablePathAllowed,
|
|
6
|
+
isWritablePathLexicallyAllowed,
|
|
7
|
+
resolveToolPath,
|
|
8
|
+
} from "./shared/path-resolver.js";
|
|
9
|
+
import {
|
|
10
|
+
protectedCommandSucceeded,
|
|
11
|
+
protectedFilesystemTargetPlan,
|
|
12
|
+
runProtectedFilesystemCommand,
|
|
13
|
+
} from "./shared/protected-filesystem.js";
|
|
14
|
+
|
|
15
|
+
const PROTECTED_EDIT_SOURCE = String.raw`
|
|
16
|
+
"use strict";
|
|
17
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
18
|
+
const chunks = [];
|
|
19
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
20
|
+
process.stdin.on("end", () => {
|
|
21
|
+
const request = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
22
|
+
const content = readFileSync(process.argv[1], "utf8");
|
|
23
|
+
const count = content.split(request.oldString).length - 1;
|
|
24
|
+
if (count === 0) return process.stdout.write(JSON.stringify({ status: "missing", count }));
|
|
25
|
+
if (!request.replaceAll && count > 1) return process.stdout.write(JSON.stringify({ status: "ambiguous", count }));
|
|
26
|
+
writeFileSync(
|
|
27
|
+
process.argv[1],
|
|
28
|
+
request.replaceAll
|
|
29
|
+
? content.replaceAll(request.oldString, request.newString)
|
|
30
|
+
: content.replace(request.oldString, request.newString),
|
|
31
|
+
"utf8",
|
|
32
|
+
);
|
|
33
|
+
process.stdout.write(JSON.stringify({ status: "edited", count }));
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
3
36
|
|
|
4
37
|
/**
|
|
5
38
|
* @param {{file_path: string, old_string: string, new_string: string, replace_all?: boolean, workdir?: string}} params
|
|
6
|
-
* @param {{sandboxPolicy?: any, ctx?: any}} [options]
|
|
39
|
+
* @param {{sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
7
40
|
*/
|
|
8
|
-
export async function editToolImpl({ file_path, old_string, new_string, replace_all = false, workdir }, { sandboxPolicy, ctx } = {}) {
|
|
41
|
+
export async function editToolImpl({ file_path, old_string, new_string, replace_all = false, workdir }, { sandboxPolicy, sandboxEngine, ctx } = {}) {
|
|
9
42
|
const target = resolveToolPath(file_path, workdir, ctx);
|
|
10
43
|
const pathOptions = { sandboxPolicy, ctx };
|
|
11
|
-
|
|
12
|
-
|
|
44
|
+
const protectedTarget = protectedFilesystemTargetPlan(target, { sandboxPolicy, ctx });
|
|
45
|
+
const protectedExecution = protectedTarget !== null;
|
|
46
|
+
if (protectedExecution) {
|
|
47
|
+
if (!isPathLexicallyAllowed(target, workdir, pathOptions)
|
|
48
|
+
|| !isWritablePathLexicallyAllowed(target, workdir, pathOptions)) {
|
|
49
|
+
return "Error: Protected filesystem edit was denied.";
|
|
50
|
+
}
|
|
51
|
+
} else if (!isPathAllowed(target, workdir, pathOptions)
|
|
52
|
+
|| !isWritablePathAllowed(target, workdir, pathOptions)) {
|
|
53
|
+
return `Error: Path not allowed: ${file_path}`;
|
|
54
|
+
}
|
|
55
|
+
if (!protectedExecution && !existsSync(target)) return `Error: File not found: ${file_path}`;
|
|
56
|
+
if (protectedExecution) {
|
|
57
|
+
try {
|
|
58
|
+
const protectedResult = await runProtectedFilesystemCommand({
|
|
59
|
+
command: process.execPath,
|
|
60
|
+
args: ["--input-type=commonjs", "--eval", PROTECTED_EDIT_SOURCE, target],
|
|
61
|
+
cwd: protectedTarget.cwd,
|
|
62
|
+
}, {
|
|
63
|
+
sandboxPolicy,
|
|
64
|
+
sandboxEngine,
|
|
65
|
+
ctx,
|
|
66
|
+
input: JSON.stringify({ oldString: old_string, newString: new_string, replaceAll: replace_all }),
|
|
67
|
+
});
|
|
68
|
+
if (!protectedCommandSucceeded(protectedResult)) {
|
|
69
|
+
return "Error: Protected filesystem edit was denied.";
|
|
70
|
+
}
|
|
71
|
+
const result = JSON.parse(protectedResult.stdout);
|
|
72
|
+
if (result.status === "missing") return `Error: old_string not found in ${target}`;
|
|
73
|
+
if (result.status === "ambiguous") return `Error: old_string found ${result.count} times`;
|
|
74
|
+
if (result.status !== "edited") return "Error: Protected filesystem edit was denied.";
|
|
75
|
+
return `Successfully edited ${target}`;
|
|
76
|
+
} catch {
|
|
77
|
+
return "Error: Protected filesystem edit was denied.";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
13
80
|
const content = readFileSync(target, "utf8");
|
|
14
81
|
const count = content.split(old_string).length - 1;
|
|
15
82
|
if (count === 0) return `Error: old_string not found in ${target}`;
|
package/src/agent/tools/exec.js
CHANGED
|
@@ -15,15 +15,18 @@ import {
|
|
|
15
15
|
DEFAULT_PROCESS_BUFFER_BYTES,
|
|
16
16
|
runPreparedProcess,
|
|
17
17
|
} from "./shared/process-runner.js";
|
|
18
|
+
import { handOffProcessJob } from "./shared/process-jobs.js";
|
|
18
19
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
19
20
|
import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
20
21
|
|
|
21
22
|
const DEFAULT_EXEC_TIMEOUT_MS = 120_000;
|
|
22
23
|
const MAX_EXEC_ARGS = 256;
|
|
23
24
|
|
|
25
|
+
/** @typedef {import("./shared/process-jobs.js").ProcessJobsController} ProcessJobsController */
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
|
-
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number}} params
|
|
26
|
-
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
28
|
+
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean}} params
|
|
29
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: ProcessJobsController}} [options]
|
|
27
30
|
*/
|
|
28
31
|
export async function execToolImpl(params, options = {}) {
|
|
29
32
|
return (await execToolRun(params, options)).text;
|
|
@@ -32,8 +35,8 @@ export async function execToolImpl(params, options = {}) {
|
|
|
32
35
|
/**
|
|
33
36
|
* Execute an argv vector directly, without shell parsing.
|
|
34
37
|
*
|
|
35
|
-
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number}} params
|
|
36
|
-
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
38
|
+
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean}} params
|
|
39
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: ProcessJobsController}} [options]
|
|
37
40
|
*/
|
|
38
41
|
export async function execToolRun(
|
|
39
42
|
{
|
|
@@ -42,12 +45,14 @@ export async function execToolRun(
|
|
|
42
45
|
workdir,
|
|
43
46
|
timeout_ms,
|
|
44
47
|
max_output_chars,
|
|
48
|
+
background,
|
|
45
49
|
},
|
|
46
50
|
{
|
|
47
51
|
signal,
|
|
48
52
|
sandboxPolicy,
|
|
49
53
|
sandboxEngine,
|
|
50
54
|
ctx,
|
|
55
|
+
processJobsController,
|
|
51
56
|
} = {},
|
|
52
57
|
) {
|
|
53
58
|
const startedAt = Date.now();
|
|
@@ -90,6 +95,19 @@ export async function execToolRun(
|
|
|
90
95
|
return failed(`Error: ${error?.message || String(error)}`, "sandbox_prepare_failed", startedAt);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
if (background === true && processJobsController) {
|
|
99
|
+
return handOffProcessJob({
|
|
100
|
+
controller: processJobsController,
|
|
101
|
+
tool: "Exec",
|
|
102
|
+
prepared,
|
|
103
|
+
summary: `Exec command (${args.length} argument${args.length === 1 ? "" : "s"}; values redacted)`,
|
|
104
|
+
timeoutMs: timeout_ms === undefined ? undefined : timeoutMs,
|
|
105
|
+
maxOutputChars: max_output_chars === undefined ? undefined : maxChars,
|
|
106
|
+
startedAt,
|
|
107
|
+
failed,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
let result;
|
|
94
112
|
let cleanupError;
|
|
95
113
|
try {
|
package/src/agent/tools/glob.js
CHANGED
|
@@ -9,9 +9,18 @@ import {
|
|
|
9
9
|
import { boundedInt, safeStat } from "./shared/dedup.js";
|
|
10
10
|
import {
|
|
11
11
|
isPathAllowed,
|
|
12
|
+
isPathLexicallyAllowed,
|
|
13
|
+
protectedRelativePaths,
|
|
12
14
|
resolveToolPath,
|
|
13
15
|
workspaceRoot,
|
|
14
16
|
} from "./shared/path-resolver.js";
|
|
17
|
+
import {
|
|
18
|
+
normalizeProtectedSearchLine,
|
|
19
|
+
protectedDirectorySearchTarget,
|
|
20
|
+
protectedFilesystemTargetPlan,
|
|
21
|
+
runProtectedFilesystemCommand,
|
|
22
|
+
scopeProtectedSearchGlob,
|
|
23
|
+
} from "./shared/protected-filesystem.js";
|
|
15
24
|
import {
|
|
16
25
|
capLines,
|
|
17
26
|
excludedGlobArgs,
|
|
@@ -26,31 +35,77 @@ const execFileAsync = promisify(execFile);
|
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
37
|
* @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]
|
|
38
|
+
* @param {{sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
30
39
|
*/
|
|
31
|
-
export async function globToolImpl({ pattern, path, limit, offset = 0, max_matches, max_output_chars, workdir }, { sandboxPolicy, ctx } = {}) {
|
|
40
|
+
export async function globToolImpl({ pattern, path, limit, offset = 0, max_matches, max_output_chars, workdir }, { sandboxPolicy, sandboxEngine, ctx } = {}) {
|
|
32
41
|
const cwd = resolveToolPath(path || workspaceRoot(workdir, ctx), workdir, ctx);
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
42
|
+
const protectedSearch = protectedFilesystemTargetPlan(cwd, { sandboxPolicy, ctx });
|
|
43
|
+
const protectedExecution = protectedSearch !== null;
|
|
44
|
+
if (protectedExecution) {
|
|
45
|
+
if (!isPathLexicallyAllowed(cwd, workdir, { sandboxPolicy, ctx })) {
|
|
46
|
+
return "Error: Protected filesystem search was denied.";
|
|
47
|
+
}
|
|
48
|
+
} else if (!isPathAllowed(cwd, workdir, { sandboxPolicy, ctx })) {
|
|
49
|
+
return `Error: Path not allowed: ${cwd}`;
|
|
50
|
+
}
|
|
51
|
+
if (!protectedExecution) {
|
|
52
|
+
const stat = safeStat(cwd);
|
|
53
|
+
if (!stat?.isDirectory()) return `Error: Glob path is not a directory: ${cwd}`;
|
|
54
|
+
}
|
|
36
55
|
const resultLimit = boundedInt(limit ?? max_matches, DEFAULT_MAX_SEARCH_LINES, { min: 1, max: 1000 });
|
|
56
|
+
const normalizedPattern = normalizeGlobPattern(pattern);
|
|
37
57
|
const args = [
|
|
38
58
|
"--files",
|
|
39
59
|
"--hidden",
|
|
40
60
|
"--color=never",
|
|
41
61
|
"--glob",
|
|
42
|
-
|
|
62
|
+
protectedExecution
|
|
63
|
+
? scopeProtectedSearchGlob(normalizedPattern, protectedSearch.searchTarget)
|
|
64
|
+
: normalizedPattern,
|
|
43
65
|
...excludedGlobArgs(),
|
|
44
66
|
];
|
|
67
|
+
const searchCwd = protectedSearch?.cwd ?? cwd;
|
|
68
|
+
const protectedPaths = protectedRelativePaths(searchCwd, { sandboxPolicy, ctx });
|
|
69
|
+
for (const protectedPath of protectedPaths) {
|
|
70
|
+
args.push("--glob", `!${protectedPath}`, "--glob", `!${protectedPath}/**`);
|
|
71
|
+
}
|
|
72
|
+
if (protectedExecution) args.push("--", protectedDirectorySearchTarget(protectedSearch.searchTarget));
|
|
45
73
|
const rgPath = resolveRgPath({ ctx });
|
|
46
74
|
if (!rgPath) return ripgrepMissingMessage(ctx);
|
|
47
75
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
let stdout;
|
|
77
|
+
if (!protectedExecution) {
|
|
78
|
+
({ stdout } = await execFileAsync(rgPath, args, { cwd, timeout: 15000, maxBuffer: SEARCH_MAX_BUFFER }));
|
|
79
|
+
} else {
|
|
80
|
+
const protectedResult = await runProtectedFilesystemCommand({
|
|
81
|
+
command: rgPath,
|
|
82
|
+
args,
|
|
83
|
+
cwd: searchCwd,
|
|
84
|
+
}, { sandboxPolicy, sandboxEngine, ctx, maxBufferBytes: SEARCH_MAX_BUFFER });
|
|
85
|
+
if (protectedResult?.code === 1) return "No files found matching pattern.";
|
|
86
|
+
if (protectedResult === null
|
|
87
|
+
|| protectedResult.code !== 0
|
|
88
|
+
|| protectedResult.bufferExceeded
|
|
89
|
+
|| protectedResult.timedOut) {
|
|
90
|
+
return "Error: Protected filesystem search was denied.";
|
|
91
|
+
}
|
|
92
|
+
stdout = protectedResult.stdout;
|
|
93
|
+
}
|
|
94
|
+
const lines = stdout.trim().split("\n")
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.filter((line) => !protectedPaths.some((path) => line === path || line.startsWith(`${path}/`)))
|
|
97
|
+
.map((line) => protectedExecution
|
|
98
|
+
? normalizeProtectedSearchLine(line, protectedSearch.searchTarget)
|
|
99
|
+
: line)
|
|
100
|
+
.sort((a, b) => {
|
|
101
|
+
// Do not follow a model-controlled output symlink on the host merely to
|
|
102
|
+
// rank results: with protected roots active, the actual search already
|
|
103
|
+
// crossed SRT and lexical ordering avoids a post-search metadata race.
|
|
104
|
+
if (protectedExecution) return a.localeCompare(b);
|
|
50
105
|
const aStat = safeStat(resolve(cwd, a));
|
|
51
106
|
const bStat = safeStat(resolve(cwd, b));
|
|
52
107
|
return (bStat?.mtimeMs || 0) - (aStat?.mtimeMs || 0) || a.localeCompare(b);
|
|
53
|
-
|
|
108
|
+
});
|
|
54
109
|
const result = formatSearchLines(lines, {
|
|
55
110
|
label: "Glob",
|
|
56
111
|
noMatches: "No files found matching pattern.",
|
|
@@ -61,6 +116,7 @@ export async function globToolImpl({ pattern, path, limit, offset = 0, max_match
|
|
|
61
116
|
});
|
|
62
117
|
return result === "No files found matching pattern." ? result : `${result}\n\n${excludedPathSummary()}`;
|
|
63
118
|
} catch (err) {
|
|
119
|
+
if (protectedExecution) return "Error: Protected filesystem search was denied.";
|
|
64
120
|
if (err.code === 1) return "No files found matching pattern.";
|
|
65
121
|
if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || /maxBuffer/i.test(err.message || "")) {
|
|
66
122
|
return `${capLines(err.stdout || "", {
|