@mono-agent/agent-runtime 0.20.11 → 0.21.0
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 +50 -11
- package/MIGRATION.md +288 -26
- package/README.md +352 -477
- package/package.json +13 -44
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +108 -9
- package/src/agent/tools/bash.js +11 -26
- package/src/agent/tools/codex-subscription-search.js +123 -29
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +149 -0
- package/src/agent/tools/pi-bridge.js +123 -19
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +293 -0
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +26 -6
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/cost.js +13 -68
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +5 -17
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +221 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +94 -42
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +376 -0
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +38 -14
- package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
- package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +279 -28
- package/src/ai/providers/pi-native.js +206 -61
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +76 -417
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +37 -112
- package/src/index.js +0 -6
- package/src/runtime.js +29 -16
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -2
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +7 -3
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +47 -0
- package/types/agent/tools/pi-bridge.d.ts +7 -4
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +98 -0
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +14 -4
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +2 -4
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +81 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +162 -256
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -7,8 +7,8 @@ import { resolveSandboxPolicy } from "./tool-context.js";
|
|
|
7
7
|
// ToolContext is threaded (`ctx ?? readToolRuntime()`), so hosts that only call
|
|
8
8
|
// the deep-path configureToolRuntime keep their historical behavior.
|
|
9
9
|
function configured(ctx) {
|
|
10
|
-
const { workspace, repoRoot } = ctx ?? readToolRuntime();
|
|
11
|
-
return { workspace, repoRoot };
|
|
10
|
+
const { workspace, repoRoot, additionalReadRoots, additionalWriteRoots } = ctx ?? readToolRuntime();
|
|
11
|
+
return { workspace, repoRoot, additionalReadRoots, additionalWriteRoots };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function workspaceRoot(workdir, ctx) {
|
|
@@ -50,8 +50,12 @@ function isPathAllowedFor(path, workdir, access, options) {
|
|
|
50
50
|
&& insideSandboxRoots(Array.isArray(field) ? field : [], r)
|
|
51
51
|
&& (access !== "write" || !sandboxDeniesWrite(policy, r, ctx));
|
|
52
52
|
}
|
|
53
|
-
const { workspace, repoRoot } = configured(ctx);
|
|
54
|
-
|
|
53
|
+
const { workspace, repoRoot, additionalReadRoots, additionalWriteRoots } = configured(ctx);
|
|
54
|
+
const additionalRoots = access === "write"
|
|
55
|
+
? additionalWriteRoots
|
|
56
|
+
: [...(additionalReadRoots ?? []), ...(additionalWriteRoots ?? [])];
|
|
57
|
+
return insideLegacyRoots([workdir, workspace, repoRoot, process.cwd(), "/tmp"], r)
|
|
58
|
+
|| insideAdditionalRoots(Array.isArray(additionalRoots) ? additionalRoots : [], r);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
function isPathLexicallyAllowedFor(path, workdir, access, options) {
|
|
@@ -64,8 +68,12 @@ function isPathLexicallyAllowedFor(path, workdir, access, options) {
|
|
|
64
68
|
&& insideLexicalRoots(Array.isArray(field) ? field : [], r)
|
|
65
69
|
&& (access !== "write" || !sandboxLexicallyDeniesWrite(policy, r, ctx));
|
|
66
70
|
}
|
|
67
|
-
const { workspace, repoRoot } = configured(ctx);
|
|
68
|
-
|
|
71
|
+
const { workspace, repoRoot, additionalReadRoots, additionalWriteRoots } = configured(ctx);
|
|
72
|
+
const additionalRoots = access === "write"
|
|
73
|
+
? additionalWriteRoots
|
|
74
|
+
: [...(additionalReadRoots ?? []), ...(additionalWriteRoots ?? [])];
|
|
75
|
+
return insideLegacyRoots([workdir, workspace, repoRoot, process.cwd(), "/tmp"], r)
|
|
76
|
+
|| insideLexicalRoots(Array.isArray(additionalRoots) ? additionalRoots : [], r);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export function isWorkdirAllowed(workdir, options = {}) {
|
|
@@ -108,6 +116,17 @@ function insideSandboxRoots(roots, target) {
|
|
|
108
116
|
&& allowedRoots.some((root) => isInsidePath(root, real));
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
// Additional file-tool roots are an operator-authored capability boundary even
|
|
120
|
+
// when process sandboxing is off. Unlike the legacy workspace allowance, keep
|
|
121
|
+
// both lexical and real paths inside the configured set so an allowed symlink
|
|
122
|
+
// cannot expose an unrelated path.
|
|
123
|
+
function insideAdditionalRoots(roots, target) {
|
|
124
|
+
const allowedRoots = normalizeRoots(roots);
|
|
125
|
+
const real = realTargetPath(target);
|
|
126
|
+
return allowedRoots.some((root) => isInsidePath(root, target))
|
|
127
|
+
&& allowedRoots.some((root) => isInsidePath(root, real));
|
|
128
|
+
}
|
|
129
|
+
|
|
111
130
|
// A protected root rejects either spelling: the lexical request and its
|
|
112
131
|
// existing/nearest-existing realpath. This closes symlink aliases in both
|
|
113
132
|
// directions without weakening ordinary readable/writable root checks.
|
|
@@ -14,6 +14,7 @@ import { startPreparedProcess } from "./process-runner.js";
|
|
|
14
14
|
* prepared: import("../../sandbox-seam.js").PreparedSandboxCommand,
|
|
15
15
|
* summary: string,
|
|
16
16
|
* description?: string,
|
|
17
|
+
* wakeOnCompletion?: boolean,
|
|
17
18
|
* timeoutMs?: number,
|
|
18
19
|
* maxOutputChars?: number,
|
|
19
20
|
* launch: (options?: {timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, onStdout?: (chunk: Buffer) => void, onStderr?: (chunk: Buffer) => void}) => ReturnType<typeof startPreparedProcess>,
|
|
@@ -30,6 +31,7 @@ import { startPreparedProcess } from "./process-runner.js";
|
|
|
30
31
|
* prepared: import("../../sandbox-seam.js").PreparedSandboxCommand,
|
|
31
32
|
* summary: string,
|
|
32
33
|
* description?: string,
|
|
34
|
+
* wakeOnCompletion?: boolean,
|
|
33
35
|
* timeoutMs?: number,
|
|
34
36
|
* maxOutputChars?: number,
|
|
35
37
|
* startedAt: number,
|
|
@@ -42,6 +44,7 @@ export async function handOffProcessJob({
|
|
|
42
44
|
prepared,
|
|
43
45
|
summary,
|
|
44
46
|
description,
|
|
47
|
+
wakeOnCompletion,
|
|
45
48
|
timeoutMs,
|
|
46
49
|
maxOutputChars,
|
|
47
50
|
startedAt,
|
|
@@ -56,6 +59,7 @@ export async function handOffProcessJob({
|
|
|
56
59
|
prepared: ownedPrepared,
|
|
57
60
|
summary,
|
|
58
61
|
...(description === undefined ? {} : { description }),
|
|
62
|
+
...(wakeOnCompletion === undefined ? {} : { wakeOnCompletion }),
|
|
59
63
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
60
64
|
...(maxOutputChars === undefined ? {} : { maxOutputChars }),
|
|
61
65
|
launch(options = {}) {
|
|
@@ -89,7 +93,7 @@ export async function handOffProcessJob({
|
|
|
89
93
|
...(result.maxRuntimeMs === undefined ? {} : { max_runtime_ms: result.maxRuntimeMs }),
|
|
90
94
|
};
|
|
91
95
|
return {
|
|
92
|
-
text: `${BACKGROUND_START_GUIDANCE}\n${JSON.stringify(payload)}`,
|
|
96
|
+
text: `${wakeOnCompletion === false ? "Background process job started with wake_on_completion=false: its terminal lifecycle card will update, but this conversation will not receive a completion turn. Do not report the work as finished yet." : BACKGROUND_START_GUIDANCE}\n${JSON.stringify(payload)}`,
|
|
93
97
|
outcome: {
|
|
94
98
|
status: "ok",
|
|
95
99
|
code: "background_started",
|
|
@@ -147,6 +151,7 @@ const PUBLIC_BACKGROUND_START_FAILURES = Object.freeze({
|
|
|
147
151
|
process_job_cleanup_incomplete: "Process-job cleanup could not be confirmed.",
|
|
148
152
|
process_job_store_error: "Process-job storage failed.",
|
|
149
153
|
process_job_wake_failed: "Process-job wake delivery failed.",
|
|
154
|
+
process_job_wake_unknown: "Process-job wake delivery outcome is unknown; replay was suppressed.",
|
|
150
155
|
process_job_response_too_large: "The process-job response exceeded its size limit.",
|
|
151
156
|
process_job_invalid: "The process-job request is invalid.",
|
|
152
157
|
});
|
|
@@ -136,7 +136,7 @@ input.once("end", () => {
|
|
|
136
136
|
* or exceeds that cap.
|
|
137
137
|
*
|
|
138
138
|
* @param {{command: string, args?: string[], cwd?: string, env?: Record<string, string|undefined>}} commandSpec
|
|
139
|
-
* @param {{timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, input?: string|Buffer}} [options]
|
|
139
|
+
* @param {{timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, input?: string|Buffer, exactEnvironment?: boolean}} [options]
|
|
140
140
|
*/
|
|
141
141
|
export function runPreparedProcess(
|
|
142
142
|
commandSpec,
|
|
@@ -145,6 +145,7 @@ export function runPreparedProcess(
|
|
|
145
145
|
signal,
|
|
146
146
|
maxBufferBytes = DEFAULT_PROCESS_BUFFER_BYTES,
|
|
147
147
|
input,
|
|
148
|
+
exactEnvironment = false,
|
|
148
149
|
} = {},
|
|
149
150
|
) {
|
|
150
151
|
return startPreparedProcess(commandSpec, {
|
|
@@ -152,6 +153,7 @@ export function runPreparedProcess(
|
|
|
152
153
|
signal,
|
|
153
154
|
maxBufferBytes,
|
|
154
155
|
input,
|
|
156
|
+
exactEnvironment,
|
|
155
157
|
}).completion;
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -164,7 +166,15 @@ export function runPreparedProcess(
|
|
|
164
166
|
* descendant in the owned group is still alive.
|
|
165
167
|
*
|
|
166
168
|
* @param {{command: string, args?: string[], cwd?: string, env?: Record<string, string|undefined>}} commandSpec
|
|
167
|
-
*
|
|
169
|
+
* `outputMode` selects how output is handled. "buffer" (default) accumulates it
|
|
170
|
+
* under `maxBufferBytes` and terminates the process when that bound is crossed —
|
|
171
|
+
* the right contract for a job whose whole output is the result. "stream" hands
|
|
172
|
+
* every chunk to `onStdout`/`onStderr` and stores NEITHER, so an indefinitely
|
|
173
|
+
* long watch is never killed for producing output and the caller owns the only
|
|
174
|
+
* copy. Buffering stderr as well would hand a streaming caller two overlapping
|
|
175
|
+
* views of it: the runner's bounded PREFIX plus the caller's own tail.
|
|
176
|
+
*
|
|
177
|
+
* @param {{timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, input?: string|Buffer, waitForProcessGroup?: boolean, exactEnvironment?: boolean, outputMode?: "buffer"|"stream", onStdout?: (chunk: Buffer) => void, onStderr?: (chunk: Buffer) => void}} [options]
|
|
168
178
|
* For process jobs, `release()` is the persistence fence: the target cannot
|
|
169
179
|
* spawn until the host has durably recorded the returned ownership metadata.
|
|
170
180
|
* Foreground handles expose a harmless no-op release for one structural shape.
|
|
@@ -180,6 +190,7 @@ export function startPreparedProcess(
|
|
|
180
190
|
input,
|
|
181
191
|
waitForProcessGroup = false,
|
|
182
192
|
exactEnvironment = false,
|
|
193
|
+
outputMode = "buffer",
|
|
183
194
|
onStdout,
|
|
184
195
|
onStderr,
|
|
185
196
|
} = {},
|
|
@@ -436,10 +447,16 @@ export function startPreparedProcess(
|
|
|
436
447
|
});
|
|
437
448
|
}
|
|
438
449
|
|
|
439
|
-
function append(target, chunk, observe) {
|
|
450
|
+
function append(target, chunk, observe, mode = "buffer") {
|
|
440
451
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
441
452
|
try { observe?.(buffer); } catch { /* observers cannot break process ownership */ }
|
|
442
453
|
state.bytes += buffer.length;
|
|
454
|
+
if (mode === "discard") {
|
|
455
|
+
// Streamed output is the caller's to bound. Storing it, or killing the
|
|
456
|
+
// process once a cumulative byte total is crossed, would cap a watch's
|
|
457
|
+
// lifetime at its output volume rather than at its runtime budget.
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
443
460
|
const remaining = Math.max(0, maxBufferBytes - state.storedBytes);
|
|
444
461
|
if (remaining > 0) {
|
|
445
462
|
const stored = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
|
|
@@ -447,8 +464,8 @@ export function startPreparedProcess(
|
|
|
447
464
|
state.storedBytes += stored.length;
|
|
448
465
|
}
|
|
449
466
|
if (buffer.length > remaining) {
|
|
450
|
-
state.bufferExceeded = true;
|
|
451
467
|
state.truncated = true;
|
|
468
|
+
state.bufferExceeded = true;
|
|
452
469
|
terminate();
|
|
453
470
|
}
|
|
454
471
|
}
|
|
@@ -468,8 +485,11 @@ export function startPreparedProcess(
|
|
|
468
485
|
if (signal?.aborted) onAbort();
|
|
469
486
|
else signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
470
487
|
|
|
471
|
-
|
|
472
|
-
child.
|
|
488
|
+
const streaming = outputMode === "stream";
|
|
489
|
+
child.stdout?.on("data", (chunk) =>
|
|
490
|
+
append(stdout, chunk, onStdout, streaming ? "discard" : "buffer"));
|
|
491
|
+
child.stderr?.on("data", (chunk) =>
|
|
492
|
+
append(stderr, chunk, onStderr, streaming ? "discard" : "buffer"));
|
|
473
493
|
child.once("error", (error) => {
|
|
474
494
|
state.spawnError = error;
|
|
475
495
|
});
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// workspace — fallback for tool workdir resolution. Default: process.cwd().
|
|
19
19
|
// repoRoot — secondary allowed root (the host's installation root).
|
|
20
20
|
// Tool path-allowlist checks accept this in addition to workspace.
|
|
21
|
+
// additionalReadRoots — extra read-only roots for managed filesystem tools.
|
|
22
|
+
// additionalWriteRoots — extra read/write roots for managed filesystem tools.
|
|
21
23
|
// runId — used as the subdirectory under toolArtifactDir for tool output.
|
|
22
24
|
// toolArtifactDir — root for {dir}/tool-output/{runId}/{file} artifact writes
|
|
23
25
|
// from capChars/formatSearchLines. Null = no persistence.
|
|
@@ -52,6 +54,8 @@ import { DEFAULT_RUNTIME_BRAND, resolveRuntimeBrand } from "../../../runtime-bra
|
|
|
52
54
|
* @typedef {Object} ToolContext
|
|
53
55
|
* @property {string} [workspace]
|
|
54
56
|
* @property {string} [repoRoot]
|
|
57
|
+
* @property {ReadonlyArray<string>} [additionalReadRoots]
|
|
58
|
+
* @property {ReadonlyArray<string>} [additionalWriteRoots]
|
|
55
59
|
* @property {string} [runId]
|
|
56
60
|
* @property {string} [toolArtifactDir]
|
|
57
61
|
* @property {string} [ripgrepPath]
|
|
@@ -69,6 +73,8 @@ import { DEFAULT_RUNTIME_BRAND, resolveRuntimeBrand } from "../../../runtime-bra
|
|
|
69
73
|
const TOOL_CONTEXT_KEYS = /** @type {const} */ ([
|
|
70
74
|
"workspace",
|
|
71
75
|
"repoRoot",
|
|
76
|
+
"additionalReadRoots",
|
|
77
|
+
"additionalWriteRoots",
|
|
72
78
|
"runId",
|
|
73
79
|
"toolArtifactDir",
|
|
74
80
|
"ripgrepPath",
|
|
@@ -89,6 +95,8 @@ export function createToolContext(input = {}) {
|
|
|
89
95
|
const ctx = {
|
|
90
96
|
workspace: undefined,
|
|
91
97
|
repoRoot: undefined,
|
|
98
|
+
additionalReadRoots: undefined,
|
|
99
|
+
additionalWriteRoots: undefined,
|
|
92
100
|
runId: undefined,
|
|
93
101
|
toolArtifactDir: undefined,
|
|
94
102
|
ripgrepPath: undefined,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const MAX_INTERSTITIAL_SAMPLE_CHARS = 32 * 1024;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classify access and authentication interstitials without treating incidental
|
|
7
|
+
* words such as "captcha" or "access denied" as conclusive evidence.
|
|
8
|
+
*
|
|
9
|
+
* @param {{url?: string, text?: string, statusCode?: number}} input
|
|
10
|
+
* @returns {{code: "access_challenge"|"authentication_required", message: string}|undefined}
|
|
11
|
+
*/
|
|
12
|
+
export function classifyWebAccessInterstitial({ url, text, statusCode } = {}) {
|
|
13
|
+
const finalUrl = String(url || "");
|
|
14
|
+
const pathname = urlPathname(finalUrl);
|
|
15
|
+
const sample = normalizedSample(text);
|
|
16
|
+
|
|
17
|
+
const challengeArtifact = /\b(?:cf-chl-[\w-]+|cloudflare ray id|challenge-platform)\b/iu.test(sample);
|
|
18
|
+
const humanCheck = /\bverify (?:you are|that you are)(?: a)? human\b/iu.test(sample);
|
|
19
|
+
const browserCheck = /\bchecking your browser before accessing\b|\bunusual traffic from (?:your computer|this computer) network\b/iu.test(sample);
|
|
20
|
+
const securityVerification = /\bperforming security verification\b/iu.test(sample);
|
|
21
|
+
const javascriptCookieGate = /\benable javascript and cookies to continue\b/iu.test(sample);
|
|
22
|
+
const waitHeading = /\bjust a moment(?:\.{1,3})?\b/iu.test(sample);
|
|
23
|
+
const blockedAccess = /\baccess denied\b[\s\S]{0,240}\b(?:blocked|permission|reference|administrator)\b/iu.test(sample);
|
|
24
|
+
|
|
25
|
+
if (/\/(?:captcha|challenge)(?:\/|$)/iu.test(pathname)
|
|
26
|
+
|| challengeArtifact
|
|
27
|
+
|| humanCheck
|
|
28
|
+
|| browserCheck
|
|
29
|
+
|| blockedAccess
|
|
30
|
+
|| (securityVerification && javascriptCookieGate)
|
|
31
|
+
|| (waitHeading && (securityVerification || javascriptCookieGate))) {
|
|
32
|
+
return {
|
|
33
|
+
code: "access_challenge",
|
|
34
|
+
message: "Page presented an access challenge; no bypass was attempted.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (statusCode === 401 || statusCode === 407
|
|
39
|
+
|| /\/(?:login|signin|sign-in)(?:\/|$)/iu.test(pathname)
|
|
40
|
+
|| /\bauthentication required\b/iu.test(sample)
|
|
41
|
+
|| /\b(?:sign|log) in to continue\b/iu.test(sample)
|
|
42
|
+
|| (/\bsession (?:has )?expired\b/iu.test(sample) && /\b(?:sign|log) in\b/iu.test(sample))) {
|
|
43
|
+
return {
|
|
44
|
+
code: "authentication_required",
|
|
45
|
+
message: "Page requires authentication; no login was attempted.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function urlPathname(value) {
|
|
52
|
+
try { return new URL(value).pathname; }
|
|
53
|
+
catch { return ""; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {{url?: string, text?: string, statusCode?: number}} input
|
|
58
|
+
*/
|
|
59
|
+
export function assertNoWebAccessInterstitial(input) {
|
|
60
|
+
const classified = classifyWebAccessInterstitial(input);
|
|
61
|
+
if (classified) throw Object.assign(new Error(classified.message), { code: classified.code });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizedSample(value) {
|
|
65
|
+
return String(value || "")
|
|
66
|
+
.slice(0, MAX_INTERSTITIAL_SAMPLE_CHARS)
|
|
67
|
+
.replace(/<[^>]*>/gu, " ")
|
|
68
|
+
.replace(/\s+/gu, " ")
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
@@ -7,11 +7,37 @@ import { passthroughSandbox } from "../sandbox-seam.js";
|
|
|
7
7
|
import { runPreparedProcess } from "./shared/process-runner.js";
|
|
8
8
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
9
9
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
10
|
+
import { assertNoWebAccessInterstitial } from "./web-access-interstitial.js";
|
|
10
11
|
|
|
11
12
|
const BROWSER_TIMEOUT_MS = 20_000;
|
|
12
13
|
const BROWSER_CLOSE_TIMEOUT_MS = 5_000;
|
|
13
14
|
const BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
14
15
|
const MAX_BROWSER_NAMESPACE_CHARS = 16;
|
|
16
|
+
const BROWSER_HOST_ENV_KEYS = [
|
|
17
|
+
"COMSPEC", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME", "PATH", "PATHEXT",
|
|
18
|
+
"SHELL", "SystemRoot", "TEMP", "TMP", "TMPDIR", "USER", "WINDIR",
|
|
19
|
+
];
|
|
20
|
+
const BLOCKED_BROWSER_ENV_KEYS = [
|
|
21
|
+
"ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
22
|
+
"all_proxy", "http_proxy", "https_proxy", "no_proxy",
|
|
23
|
+
"AGENT_BROWSER_ACTION_POLICY", "AGENT_BROWSER_ALLOWED_DOMAINS", "AGENT_BROWSER_ALLOW_FILE_ACCESS",
|
|
24
|
+
"AGENT_BROWSER_ANNOTATE", "AGENT_BROWSER_ARGS", "AGENT_BROWSER_CDP", "AGENT_BROWSER_COLOR_SCHEME",
|
|
25
|
+
"AGENT_BROWSER_CONFIRM_ACTIONS", "AGENT_BROWSER_CONFIRM_INTERACTIVE", "AGENT_BROWSER_CONTENT_BOUNDARIES",
|
|
26
|
+
"AGENT_BROWSER_DEFAULT_TIMEOUT", "AGENT_BROWSER_DOWNLOAD_PATH", "AGENT_BROWSER_ENABLE",
|
|
27
|
+
"AGENT_BROWSER_ENCRYPTION_KEY", "AGENT_BROWSER_ENGINE", "AGENT_BROWSER_EXECUTABLE_PATH",
|
|
28
|
+
"AGENT_BROWSER_EXTENSIONS", "AGENT_BROWSER_HEADED", "AGENT_BROWSER_HIDE_SCROLLBARS",
|
|
29
|
+
"AGENT_BROWSER_IDLE_TIMEOUT_MS", "AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "AGENT_BROWSER_INIT_SCRIPTS",
|
|
30
|
+
"AGENT_BROWSER_IOS_DEVICE", "AGENT_BROWSER_IOS_UDID", "AGENT_BROWSER_MAX_OUTPUT",
|
|
31
|
+
"AGENT_BROWSER_NAMESPACE", "AGENT_BROWSER_NO_AUTO_DIALOG", "AGENT_BROWSER_NO_XVFB",
|
|
32
|
+
"AGENT_BROWSER_PLUGINS", "AGENT_BROWSER_PROFILE", "AGENT_BROWSER_PROVIDER", "AGENT_BROWSER_PROXY",
|
|
33
|
+
"AGENT_BROWSER_PROXY_BYPASS", "AGENT_BROWSER_RESTORE", "AGENT_BROWSER_RESTORE_CHECK_FN",
|
|
34
|
+
"AGENT_BROWSER_RESTORE_CHECK_TEXT", "AGENT_BROWSER_RESTORE_CHECK_URL", "AGENT_BROWSER_SANDBOX_VERSION",
|
|
35
|
+
"AGENT_BROWSER_SCREENSHOT_DIR", "AGENT_BROWSER_SCREENSHOT_FORMAT", "AGENT_BROWSER_SCREENSHOT_QUALITY",
|
|
36
|
+
"AGENT_BROWSER_SESSION", "AGENT_BROWSER_SESSION_NAME", "AGENT_BROWSER_SKILLS_DIR",
|
|
37
|
+
"AGENT_BROWSER_SNAPSHOT_ID", "AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_STATE",
|
|
38
|
+
"AGENT_BROWSER_STATE_EXPIRE_DAYS", "AGENT_BROWSER_STREAM_PORT", "AGENT_BROWSER_USER_AGENT",
|
|
39
|
+
"AGENT_BROWSER_WEBGPU",
|
|
40
|
+
];
|
|
15
41
|
|
|
16
42
|
/**
|
|
17
43
|
* Render one public page in a fresh anonymous agent-browser session.
|
|
@@ -42,6 +68,13 @@ export async function renderWithAgentBrowser(
|
|
|
42
68
|
let tempDir = null;
|
|
43
69
|
let unregister = () => {};
|
|
44
70
|
let closed = false;
|
|
71
|
+
const renderDeadlineAt = Date.now() + BROWSER_TIMEOUT_MS;
|
|
72
|
+
|
|
73
|
+
function remainingRenderMs() {
|
|
74
|
+
const remaining = renderDeadlineAt - Date.now();
|
|
75
|
+
if (remaining <= 0) throw Object.assign(new Error(`agent-browser timed out after ${BROWSER_TIMEOUT_MS}ms`), { code: "browser_render_failed" });
|
|
76
|
+
return remaining;
|
|
77
|
+
}
|
|
45
78
|
|
|
46
79
|
async function closeSession() {
|
|
47
80
|
if (closed) return;
|
|
@@ -85,59 +118,9 @@ export async function renderWithAgentBrowser(
|
|
|
85
118
|
args: [...baseArgs, ...commandArgs],
|
|
86
119
|
cwd: workspace,
|
|
87
120
|
env: {
|
|
88
|
-
|
|
89
|
-
// override inherited from the host. Empty strings are not safe here:
|
|
90
|
-
// agent-browser treats some of them (notably SESSION_NAME) as
|
|
91
|
-
// configured-but-invalid values.
|
|
92
|
-
AGENT_BROWSER_ACTION_POLICY: undefined,
|
|
93
|
-
AGENT_BROWSER_ALLOWED_DOMAINS: undefined,
|
|
94
|
-
AGENT_BROWSER_ANNOTATE: undefined,
|
|
95
|
-
AGENT_BROWSER_ARGS: undefined,
|
|
96
|
-
AGENT_BROWSER_COLOR_SCHEME: undefined,
|
|
97
|
-
AGENT_BROWSER_CONFIRM_ACTIONS: undefined,
|
|
98
|
-
AGENT_BROWSER_CONFIRM_INTERACTIVE: undefined,
|
|
99
|
-
AGENT_BROWSER_CONTENT_BOUNDARIES: undefined,
|
|
100
|
-
AGENT_BROWSER_DEFAULT_TIMEOUT: undefined,
|
|
101
|
-
AGENT_BROWSER_DOWNLOAD_PATH: undefined,
|
|
102
|
-
AGENT_BROWSER_ENABLE: undefined,
|
|
103
|
-
AGENT_BROWSER_ENCRYPTION_KEY: undefined,
|
|
104
|
-
AGENT_BROWSER_ENGINE: undefined,
|
|
105
|
-
AGENT_BROWSER_EXECUTABLE_PATH: undefined,
|
|
106
|
-
AGENT_BROWSER_EXTENSIONS: undefined,
|
|
107
|
-
AGENT_BROWSER_HEADED: undefined,
|
|
108
|
-
AGENT_BROWSER_HIDE_SCROLLBARS: undefined,
|
|
109
|
-
AGENT_BROWSER_IDLE_TIMEOUT_MS: undefined,
|
|
110
|
-
AGENT_BROWSER_INIT_SCRIPTS: undefined,
|
|
111
|
-
AGENT_BROWSER_IOS_DEVICE: undefined,
|
|
112
|
-
AGENT_BROWSER_IOS_UDID: undefined,
|
|
113
|
-
AGENT_BROWSER_MAX_OUTPUT: undefined,
|
|
114
|
-
AGENT_BROWSER_NAMESPACE: undefined,
|
|
115
|
-
AGENT_BROWSER_NO_AUTO_DIALOG: undefined,
|
|
116
|
-
AGENT_BROWSER_NO_XVFB: undefined,
|
|
117
|
-
AGENT_BROWSER_PLUGINS: undefined,
|
|
118
|
-
AGENT_BROWSER_PROFILE: undefined,
|
|
119
|
-
AGENT_BROWSER_PROVIDER: undefined,
|
|
120
|
-
AGENT_BROWSER_PROXY: undefined,
|
|
121
|
-
AGENT_BROWSER_PROXY_BYPASS: undefined,
|
|
122
|
-
AGENT_BROWSER_RESTORE: undefined,
|
|
123
|
-
AGENT_BROWSER_RESTORE_CHECK_FN: undefined,
|
|
124
|
-
AGENT_BROWSER_RESTORE_CHECK_TEXT: undefined,
|
|
125
|
-
AGENT_BROWSER_RESTORE_CHECK_URL: undefined,
|
|
126
|
-
AGENT_BROWSER_SCREENSHOT_DIR: undefined,
|
|
127
|
-
AGENT_BROWSER_SCREENSHOT_FORMAT: undefined,
|
|
128
|
-
AGENT_BROWSER_SCREENSHOT_QUALITY: undefined,
|
|
129
|
-
AGENT_BROWSER_SESSION: undefined,
|
|
130
|
-
AGENT_BROWSER_SESSION_NAME: undefined,
|
|
131
|
-
AGENT_BROWSER_SKILLS_DIR: undefined,
|
|
132
|
-
AGENT_BROWSER_SOCKET_DIR: undefined,
|
|
133
|
-
AGENT_BROWSER_STATE: undefined,
|
|
134
|
-
AGENT_BROWSER_STATE_EXPIRE_DAYS: undefined,
|
|
135
|
-
AGENT_BROWSER_STREAM_PORT: undefined,
|
|
136
|
-
AGENT_BROWSER_USER_AGENT: undefined,
|
|
137
|
-
AGENT_BROWSER_WEBGPU: undefined,
|
|
121
|
+
...isolatedBrowserEnvironment(),
|
|
138
122
|
AGENT_BROWSER_AUTO_CONNECT: "false",
|
|
139
123
|
AGENT_BROWSER_AUTOSAVE_INTERVAL_MS: "0",
|
|
140
|
-
AGENT_BROWSER_CDP: undefined,
|
|
141
124
|
AGENT_BROWSER_CONFIG: configPath,
|
|
142
125
|
AGENT_BROWSER_RESTORE_SAVE: "never",
|
|
143
126
|
NO_COLOR: "1",
|
|
@@ -145,10 +128,14 @@ export async function renderWithAgentBrowser(
|
|
|
145
128
|
},
|
|
146
129
|
});
|
|
147
130
|
try {
|
|
148
|
-
const result = await runPreparedProcess(
|
|
131
|
+
const result = await runPreparedProcess({
|
|
132
|
+
...prepared,
|
|
133
|
+
env: isolatedBrowserEnvironment(prepared.env, configPath),
|
|
134
|
+
}, {
|
|
149
135
|
timeoutMs,
|
|
150
136
|
signal: abortSignal,
|
|
151
137
|
maxBufferBytes: BROWSER_OUTPUT_BYTES,
|
|
138
|
+
exactEnvironment: true,
|
|
152
139
|
});
|
|
153
140
|
if (result.timedOut) throw new Error(`agent-browser timed out after ${timeoutMs}ms`);
|
|
154
141
|
if (result.aborted) throw new Error("agent-browser was aborted");
|
|
@@ -156,7 +143,7 @@ export async function renderWithAgentBrowser(
|
|
|
156
143
|
if (result.spawnError) throw result.spawnError;
|
|
157
144
|
if (result.signal) throw new Error(`agent-browser terminated by ${result.signal}`);
|
|
158
145
|
if (result.code !== 0) {
|
|
159
|
-
throw new Error(`agent-browser exited ${result.code}
|
|
146
|
+
throw new Error(`agent-browser exited ${result.code}`);
|
|
160
147
|
}
|
|
161
148
|
return String(result.stdout || "").trim();
|
|
162
149
|
} finally {
|
|
@@ -168,17 +155,55 @@ export async function renderWithAgentBrowser(
|
|
|
168
155
|
tempDir = await mkdtemp(join(workspace, ".mono-agent-web-"));
|
|
169
156
|
await writeFile(join(tempDir, "agent-browser.json"), "{}\n", { encoding: "utf8", mode: 0o600 });
|
|
170
157
|
unregister = registerCleanup?.(closeSession) ?? (() => {});
|
|
171
|
-
await run(["open", parsed.href]);
|
|
172
|
-
await run(["wait", "--load", "domcontentloaded"]);
|
|
173
|
-
const
|
|
158
|
+
await run(["open", parsed.href], remainingRenderMs());
|
|
159
|
+
await run(["wait", "--load", "domcontentloaded"], remainingRenderMs());
|
|
160
|
+
const finalUrlOutput = await run(["get", "url"], remainingRenderMs());
|
|
161
|
+
const finalUrl = validateFinalUrl(extractBrowserText(finalUrlOutput), parsed, sandbox, policy);
|
|
162
|
+
const output = await run(["read"], remainingRenderMs());
|
|
174
163
|
const text = extractBrowserText(output);
|
|
175
164
|
if (!text) throw new Error("agent-browser returned no readable rendered content");
|
|
176
|
-
|
|
165
|
+
assertNoWebAccessInterstitial({ url: finalUrl, text });
|
|
166
|
+
return { text, finalUrl };
|
|
177
167
|
} finally {
|
|
178
168
|
await closeSession();
|
|
179
169
|
}
|
|
180
170
|
}
|
|
181
171
|
|
|
172
|
+
function validateFinalUrl(value, requested, sandbox, policy) {
|
|
173
|
+
let finalUrl;
|
|
174
|
+
try { finalUrl = new URL(String(value || "").trim()); }
|
|
175
|
+
catch { throw Object.assign(new Error("agent-browser returned an invalid final URL"), { code: "browser_render_failed" }); }
|
|
176
|
+
if (!["http:", "https:"].includes(finalUrl.protocol) || finalUrl.username || finalUrl.password) {
|
|
177
|
+
throw Object.assign(new Error("agent-browser navigated to an unsupported final URL"), { code: "network_denied" });
|
|
178
|
+
}
|
|
179
|
+
const requestedHost = requested.hostname.toLowerCase();
|
|
180
|
+
const finalHost = finalUrl.hostname.toLowerCase();
|
|
181
|
+
if (!(finalHost === requestedHost || finalHost.endsWith(`.${requestedHost}`))
|
|
182
|
+
|| !sandbox.networkAllowsUrl(policy, finalUrl.href)) {
|
|
183
|
+
throw Object.assign(new Error("agent-browser final URL is outside the allowed domain policy"), { code: "network_denied" });
|
|
184
|
+
}
|
|
185
|
+
return finalUrl.href;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isolatedBrowserEnvironment(source = process.env, configPath) {
|
|
189
|
+
/** @type {Record<string, string|undefined>} */
|
|
190
|
+
const env = {};
|
|
191
|
+
for (const key of BROWSER_HOST_ENV_KEYS) {
|
|
192
|
+
const value = source?.[key];
|
|
193
|
+
if (value !== undefined) env[key] = value;
|
|
194
|
+
}
|
|
195
|
+
// Undefined means deletion to the sandbox seam. runPreparedProcess then
|
|
196
|
+
// receives this map as an exact environment, so omitted host variables
|
|
197
|
+
// cannot reappear during its ordinary process.env merge.
|
|
198
|
+
for (const key of BLOCKED_BROWSER_ENV_KEYS) env[key] = undefined;
|
|
199
|
+
env.AGENT_BROWSER_AUTO_CONNECT = "false";
|
|
200
|
+
env.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS = "0";
|
|
201
|
+
if (configPath !== undefined) env.AGENT_BROWSER_CONFIG = configPath;
|
|
202
|
+
env.AGENT_BROWSER_RESTORE_SAVE = "never";
|
|
203
|
+
env.NO_COLOR = "1";
|
|
204
|
+
return env;
|
|
205
|
+
}
|
|
206
|
+
|
|
182
207
|
function compactBrowserNamespace(value) {
|
|
183
208
|
const candidate = String(value || "").trim();
|
|
184
209
|
if (/^[A-Za-z0-9_-]+$/u.test(candidate) && candidate.length <= MAX_BROWSER_NAMESPACE_CHARS) {
|
|
@@ -206,7 +231,7 @@ function findText(value, depth = 0) {
|
|
|
206
231
|
return value.map((entry) => findText(entry, depth + 1)).filter(Boolean).join("\n").trim();
|
|
207
232
|
}
|
|
208
233
|
if (typeof value !== "object") return "";
|
|
209
|
-
for (const key of ["markdown", "content", "text", "result", "output", "data"]) {
|
|
234
|
+
for (const key of ["markdown", "content", "text", "url", "result", "output", "data"]) {
|
|
210
235
|
if (!(key in value)) continue;
|
|
211
236
|
const text = findText(value[key], depth + 1);
|
|
212
237
|
if (text) return text;
|