@mono-agent/agent-runtime 0.4.0 → 0.5.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 +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +227 -45
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +21 -10
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -35
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
import { readToolRuntime
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { readToolRuntime } from "./runtime-context.js";
|
|
4
|
+
import { resolveSandboxPolicy } from "./tool-context.js";
|
|
5
|
+
|
|
6
|
+
// Every read here falls back to the module-default context when no per-instance
|
|
7
|
+
// ToolContext is threaded (`ctx ?? readToolRuntime()`), so hosts that only call
|
|
8
|
+
// the deep-path configureToolRuntime keep their historical behavior.
|
|
9
|
+
function configured(ctx) {
|
|
10
|
+
const { workspace, repoRoot } = ctx ?? readToolRuntime();
|
|
7
11
|
return { workspace, repoRoot };
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
export function workspaceRoot(workdir) {
|
|
11
|
-
const { workspace, repoRoot } = configured();
|
|
14
|
+
export function workspaceRoot(workdir, ctx) {
|
|
15
|
+
const { workspace, repoRoot } = configured(ctx);
|
|
12
16
|
return resolve(workdir || workspace || repoRoot || process.cwd());
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export function resolveToolPath(path, workdir) {
|
|
19
|
+
export function resolveToolPath(path, workdir, ctx) {
|
|
16
20
|
if (!path || typeof path !== "string") return path;
|
|
17
|
-
return resolve(isAbsolute(path) ? path : resolve(workspaceRoot(workdir), path));
|
|
21
|
+
return resolve(isAbsolute(path) ? path : resolve(workspaceRoot(workdir, ctx), path));
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export function isPathAllowed(path, workdir, options = {}) {
|
|
@@ -26,25 +30,27 @@ export function isWritablePathAllowed(path, workdir, options = {}) {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function isPathAllowedFor(path, workdir, access, options) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
33
|
+
const ctx = options.ctx;
|
|
34
|
+
const r = resolveToolPath(path, workdir, ctx);
|
|
35
|
+
const policy = resolveSandboxPolicy(ctx ?? readToolRuntime(), options.sandboxPolicy);
|
|
31
36
|
if (policy) {
|
|
32
37
|
const field = access === "write" ? policy.writableRoots : policy.readableRoots;
|
|
33
38
|
return insideSandboxRoots(Array.isArray(field) ? field : [], r)
|
|
34
|
-
&& (access !== "write" || !sandboxDeniesWrite(policy, r));
|
|
39
|
+
&& (access !== "write" || !sandboxDeniesWrite(policy, r, ctx));
|
|
35
40
|
}
|
|
36
|
-
const { workspace, repoRoot } = configured();
|
|
41
|
+
const { workspace, repoRoot } = configured(ctx);
|
|
37
42
|
return insideLegacyRoots([workdir, workspace, repoRoot, process.cwd(), "/tmp"], r);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function isWorkdirAllowed(workdir, options = {}) {
|
|
41
46
|
if (!workdir) return true;
|
|
47
|
+
const ctx = options.ctx;
|
|
42
48
|
const r = resolve(workdir);
|
|
43
|
-
const policy = resolveSandboxPolicy(options.sandboxPolicy);
|
|
49
|
+
const policy = resolveSandboxPolicy(ctx ?? readToolRuntime(), options.sandboxPolicy);
|
|
44
50
|
if (policy) {
|
|
45
51
|
return insideSandboxRoots(Array.isArray(policy.readableRoots) ? policy.readableRoots : [], r);
|
|
46
52
|
}
|
|
47
|
-
const { workspace, repoRoot } = configured();
|
|
53
|
+
const { workspace, repoRoot } = configured(ctx);
|
|
48
54
|
return insideLegacyRoots([workspace, repoRoot, process.cwd(), "/tmp"], r);
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -95,21 +101,21 @@ function realTargetPath(target) {
|
|
|
95
101
|
return resolved;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
function sandboxDeniesWrite(policy, target) {
|
|
104
|
+
function sandboxDeniesWrite(policy, target, ctx) {
|
|
99
105
|
const patterns = Array.isArray(policy.denyWrite) ? policy.denyWrite : [];
|
|
100
106
|
if (patterns.length === 0) return false;
|
|
101
107
|
const candidates = [...new Set([resolve(target), realTargetPath(target)])];
|
|
102
108
|
return candidates.some((candidate) =>
|
|
103
|
-
patterns.some((pattern) => denyWritePatternMatches(policy, pattern, candidate)));
|
|
109
|
+
patterns.some((pattern) => denyWritePatternMatches(policy, pattern, candidate, ctx)));
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
function denyWritePatternMatches(policy, pattern, target) {
|
|
112
|
+
function denyWritePatternMatches(policy, pattern, target, ctx) {
|
|
107
113
|
if (!pattern || typeof pattern !== "string") return false;
|
|
108
114
|
const normalizedPattern = normalizeMatchPath(pattern);
|
|
109
115
|
if (isAbsolute(pattern)) {
|
|
110
116
|
return globPatternMatches(normalizedPattern, normalizeMatchPath(target));
|
|
111
117
|
}
|
|
112
|
-
const root = resolve(policy.root || workspaceRoot());
|
|
118
|
+
const root = resolve(policy.root || workspaceRoot(undefined, ctx));
|
|
113
119
|
const rel = relative(root, resolve(target));
|
|
114
120
|
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return false;
|
|
115
121
|
return globPatternMatches(stripDotSlash(normalizedPattern), normalizeMatchPath(rel));
|
|
@@ -9,18 +9,31 @@ import {
|
|
|
9
9
|
} from "./constants.js";
|
|
10
10
|
import { boundedInt } from "./dedup.js";
|
|
11
11
|
import { writeToolArtifact } from "./output-truncation.js";
|
|
12
|
-
import {
|
|
12
|
+
import { readToolRuntime } from "./runtime-context.js";
|
|
13
13
|
|
|
14
14
|
const requireFromHere = createRequire(import.meta.url);
|
|
15
15
|
|
|
16
16
|
// Lazy so the message respects whatever runtimeBrand the host configured.
|
|
17
|
-
export function ripgrepMissingMessage() {
|
|
18
|
-
const brand =
|
|
17
|
+
export function ripgrepMissingMessage(ctx) {
|
|
18
|
+
const brand = (ctx ?? readToolRuntime()).runtimeBrand;
|
|
19
19
|
return `Error: ripgrep (rg) is not available. Configure ripgrepPath via configureToolRuntime() or install ripgrep on PATH; run \`${brand.doctorCommand}\` for details.`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Mutable cache of the resolved ripgrep binary path. Stored on an object so
|
|
23
23
|
// callers can read the latest value without re-importing the module.
|
|
24
|
+
//
|
|
25
|
+
// KNOWN cross-runtime sharing (pre-existing, out of scope for the per-instance
|
|
26
|
+
// ToolContext work): this cache is MODULE-LEVEL, not per-ToolContext. The first
|
|
27
|
+
// resolveRgPath call to resolve a non-undefined value wins process-wide, so two
|
|
28
|
+
// createRuntime instances configured with DIFFERENT ripgrepPath values share
|
|
29
|
+
// whichever binary was resolved first (a later instance's ripgrepPath is
|
|
30
|
+
// ignored unless it passes `{refresh: true}`). Unlike workspace/repoRoot/
|
|
31
|
+
// sandbox/brand — which are fully isolated per instance via ToolContext — the
|
|
32
|
+
// ripgrep binary path is effectively global. In practice every runtime in a
|
|
33
|
+
// process resolves the same vendored/PATH binary, so this is benign; it is
|
|
34
|
+
// documented (and asserted as a known-shared cache in the two-runtimes
|
|
35
|
+
// isolation test) rather than fixed, since a per-instance rg cache would be a
|
|
36
|
+
// larger change with no real-world payoff today.
|
|
24
37
|
export const cachedRgPath = { value: undefined };
|
|
25
38
|
|
|
26
39
|
function vendoredRgPath() {
|
|
@@ -51,9 +64,12 @@ function rgFromPath() {
|
|
|
51
64
|
return null;
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @param {{refresh?: boolean, ctx?: any}} [options]
|
|
69
|
+
*/
|
|
70
|
+
export function resolveRgPath({ refresh = false, ctx } = {}) {
|
|
55
71
|
if (!refresh && cachedRgPath.value !== undefined) return cachedRgPath.value;
|
|
56
|
-
const { ripgrepPath } = readToolRuntime();
|
|
72
|
+
const { ripgrepPath } = ctx ?? readToolRuntime();
|
|
57
73
|
if (ripgrepPath) {
|
|
58
74
|
cachedRgPath.value = existsSync(ripgrepPath) ? ripgrepPath : null;
|
|
59
75
|
} else {
|
|
@@ -76,12 +92,17 @@ export function normalizeGlobPattern(pattern) {
|
|
|
76
92
|
return raw || "**/*";
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @param {any} rawLines
|
|
97
|
+
* @param {{label?: string, noMatches?: string, maxLines?: number, maxChars?: number, offset?: number, ctx?: any}} [options]
|
|
98
|
+
*/
|
|
79
99
|
export function formatSearchLines(rawLines, {
|
|
80
100
|
label,
|
|
81
101
|
noMatches,
|
|
82
102
|
maxLines = DEFAULT_MAX_SEARCH_LINES,
|
|
83
103
|
maxChars = DEFAULT_MAX_SEARCH_CHARS,
|
|
84
104
|
offset = 0,
|
|
105
|
+
ctx,
|
|
85
106
|
} = {}) {
|
|
86
107
|
const lines = Array.isArray(rawLines) ? rawLines.filter(Boolean) : String(rawLines || "").trim().split("\n").filter(Boolean);
|
|
87
108
|
if (!lines.length) return noMatches;
|
|
@@ -99,7 +120,7 @@ export function formatSearchLines(rawLines, {
|
|
|
99
120
|
}
|
|
100
121
|
if (kept.length === slice.length) return kept.join("\n");
|
|
101
122
|
const fullText = lines.join("\n");
|
|
102
|
-
const artifact = writeToolArtifact(label, fullText);
|
|
123
|
+
const artifact = writeToolArtifact(label, fullText, ctx);
|
|
103
124
|
const suffix = [
|
|
104
125
|
`[truncated ${label || "search"} result: showing ${kept.length} of ${total} lines after excluding generated/vendor paths.`,
|
|
105
126
|
start ? `Offset ${start} was applied.` : null,
|
|
@@ -109,12 +130,17 @@ export function formatSearchLines(rawLines, {
|
|
|
109
130
|
return `${kept.join("\n")}\n\n${suffix}`;
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} text
|
|
135
|
+
* @param {{label?: string, noMatches?: string, maxLines?: number, maxChars?: number, offset?: number, ctx?: any}} [options]
|
|
136
|
+
*/
|
|
112
137
|
export function capLines(text, {
|
|
113
138
|
label,
|
|
114
139
|
noMatches,
|
|
115
140
|
maxLines = DEFAULT_MAX_SEARCH_LINES,
|
|
116
141
|
maxChars = DEFAULT_MAX_SEARCH_CHARS,
|
|
117
142
|
offset = 0,
|
|
143
|
+
ctx,
|
|
118
144
|
} = {}) {
|
|
119
145
|
return formatSearchLines(String(text || "").trim().split("\n").filter(Boolean), {
|
|
120
146
|
label,
|
|
@@ -122,6 +148,7 @@ export function capLines(text, {
|
|
|
122
148
|
maxLines,
|
|
123
149
|
maxChars,
|
|
124
150
|
offset,
|
|
151
|
+
ctx,
|
|
125
152
|
});
|
|
126
153
|
}
|
|
127
154
|
|
|
@@ -1,69 +1,79 @@
|
|
|
1
|
-
//
|
|
2
|
-
// The host configures this once at worker boot; internal
|
|
3
|
-
// modules (output-truncation, ripgrep, path-resolver, pi-bridge) read from
|
|
4
|
-
// it instead of reaching into process.env.
|
|
1
|
+
// Module-level DEFAULT tool context — the back-compat layer over tool-context.js.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
3
|
+
// Historically this file held the one-per-process tool-runtime singleton. That
|
|
4
|
+
// assumed one task per process, which is false in a long-lived multi-channel
|
|
5
|
+
// host: two `createRuntime` instances clobbered each other's workspace /
|
|
6
|
+
// repoRoot / sandboxPolicy / brand. The per-instance ToolContext (tool-context.js)
|
|
7
|
+
// fixes that — `createRuntime` now builds its own context and threads it to
|
|
8
|
+
// bridges as `options.toolContext`.
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
// This module survives as the DEFAULT context every internal read site falls
|
|
11
|
+
// back to when no per-instance context was threaded (`ctx ?? readToolRuntime()`).
|
|
12
|
+
// Hosts that only call the deep-path `configureToolRuntime` (e.g. worklab's
|
|
13
|
+
// worker.js / doctor.js) get byte-for-byte identical behavior to before: they
|
|
14
|
+
// configure this default context and the tools read it.
|
|
15
|
+
//
|
|
16
|
+
// The public exports are unchanged: configureToolRuntime / readToolRuntime /
|
|
17
|
+
// readRuntimeBrand / resetToolRuntime (plus the internal resolveSandboxPolicy
|
|
18
|
+
// default-context convenience). See tool-context.js for the recognized keys.
|
|
19
|
+
|
|
20
|
+
// @ts-check
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
createToolContext,
|
|
24
|
+
resetToolContext,
|
|
25
|
+
updateToolContext,
|
|
26
|
+
resolveSandboxPolicy as resolveContextSandboxPolicy,
|
|
27
|
+
} from "./tool-context.js";
|
|
28
|
+
import { DEFAULT_RUNTIME_BRAND } from "../../../runtime-brand.js";
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
/** @typedef {import('./tool-context.js').ToolContext} ToolContext */
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {ToolContext} ToolRuntimeContext
|
|
33
|
+
* Back-compat alias for the historical name.
|
|
34
|
+
*/
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
repoRoot: undefined,
|
|
30
|
-
runId: undefined,
|
|
31
|
-
toolArtifactDir: undefined,
|
|
32
|
-
ripgrepPath: undefined,
|
|
33
|
-
qaOutputDir: undefined,
|
|
34
|
-
sandboxPolicy: undefined,
|
|
35
|
-
runtimeBrand: { ...DEFAULT_RUNTIME_BRAND },
|
|
36
|
-
};
|
|
36
|
+
/** @type {ToolContext} */
|
|
37
|
+
const defaultContext = createToolContext({});
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {Partial<ToolContext>} [next]
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
38
43
|
export function configureToolRuntime(next = {}) {
|
|
39
|
-
|
|
40
|
-
if (key === "runtimeBrand") continue;
|
|
41
|
-
if (key in next) context[key] = next[key];
|
|
42
|
-
}
|
|
43
|
-
if (next.runtimeBrand !== undefined) {
|
|
44
|
-
context.runtimeBrand = resolveRuntimeBrand(next.runtimeBrand);
|
|
45
|
-
}
|
|
44
|
+
updateToolContext(defaultContext, next);
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @returns {ToolContext}
|
|
49
|
+
*/
|
|
48
50
|
export function readToolRuntime() {
|
|
49
|
-
return
|
|
51
|
+
return defaultContext;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// Default-context convenience: resolves a request policy against the module
|
|
55
|
+
// default context. Internal read sites that carry a per-instance context call
|
|
56
|
+
// tool-context.js's `resolveSandboxPolicy(ctx, requestPolicy)` directly with
|
|
57
|
+
// `ctx ?? readToolRuntime()`; this wrapper preserves the historical
|
|
58
|
+
// single-argument signature for the default path.
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('../../sandbox-seam.js').SandboxPolicy} [requestPolicy]
|
|
61
|
+
* @returns {import('../../sandbox-seam.js').SandboxPolicy|undefined}
|
|
62
|
+
*/
|
|
56
63
|
export function resolveSandboxPolicy(requestPolicy = undefined) {
|
|
57
|
-
|
|
58
|
-
return merged && merged.mode !== "off" ? merged : undefined;
|
|
64
|
+
return resolveContextSandboxPolicy(defaultContext, requestPolicy);
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @returns {import('../../../runtime-brand.js').RuntimeBrand}
|
|
69
|
+
*/
|
|
61
70
|
export function readRuntimeBrand() {
|
|
62
|
-
return
|
|
71
|
+
return defaultContext.runtimeBrand || { ...DEFAULT_RUNTIME_BRAND };
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
65
77
|
export function resetToolRuntime() {
|
|
66
|
-
|
|
67
|
-
context[key] = key === "runtimeBrand" ? { ...DEFAULT_RUNTIME_BRAND } : undefined;
|
|
68
|
-
}
|
|
78
|
+
resetToolContext(defaultContext);
|
|
69
79
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Per-runtime-instance tool context.
|
|
2
|
+
//
|
|
3
|
+
// A ToolContext carries the configuration the kernel's internal tool helpers
|
|
4
|
+
// (output-truncation, ripgrep, path-resolver, pi-bridge, the tool impls) need
|
|
5
|
+
// to resolve workdirs, artifact paths, the ripgrep binary, sandbox policy, and
|
|
6
|
+
// host-specific brand strings. `createRuntime` builds ONE context per runtime
|
|
7
|
+
// instance and threads it to every bridge via `options.toolContext`; tools read
|
|
8
|
+
// from it instead of a process-global. This replaces the former one-per-process
|
|
9
|
+
// singleton (see runtime-context.js) that clobbered its fields when a
|
|
10
|
+
// long-lived host created more than one runtime.
|
|
11
|
+
//
|
|
12
|
+
// This module is pure: it owns no module-level state. The default/back-compat
|
|
13
|
+
// singleton lives in runtime-context.js, which builds its own ToolContext with
|
|
14
|
+
// createToolContext and mutates it with updateToolContext. Every internal read
|
|
15
|
+
// site falls back to that default via `ctx ?? readToolRuntime()`.
|
|
16
|
+
//
|
|
17
|
+
// Recognized keys:
|
|
18
|
+
// workspace — fallback for tool workdir resolution. Default: process.cwd().
|
|
19
|
+
// repoRoot — secondary allowed root (the host's installation root).
|
|
20
|
+
// Tool path-allowlist checks accept this in addition to workspace.
|
|
21
|
+
// runId — used as the subdirectory under toolArtifactDir for tool output.
|
|
22
|
+
// toolArtifactDir — root for {dir}/tool-output/{runId}/{file} artifact writes
|
|
23
|
+
// from capChars/formatSearchLines. Null = no persistence.
|
|
24
|
+
// ripgrepPath — absolute path to the ripgrep binary. When unset, falls
|
|
25
|
+
// back to vendored binary, then PATH lookup.
|
|
26
|
+
// qaOutputDir — fallback for normalizeMcpToolParams when the per-call
|
|
27
|
+
// runArtifactDir isn't supplied.
|
|
28
|
+
// sandboxPolicy — optional strict filesystem/process/network sandbox policy.
|
|
29
|
+
// sandboxEngine — optional concrete engine handed to the injected sandbox
|
|
30
|
+
// implementation when it prepares subprocess commands.
|
|
31
|
+
// sandbox — RuntimeSandbox implementation the policy is enforced
|
|
32
|
+
// through (see ../../sandbox-seam.js). Always resolved,
|
|
33
|
+
// like runtimeBrand: defaults to passthroughSandbox when
|
|
34
|
+
// a host doesn't inject a real one.
|
|
35
|
+
// runtimeBrand — resolved RuntimeBrand object (see runtime-brand.js).
|
|
36
|
+
// Internal helpers read it to stamp host-specific names
|
|
37
|
+
// (MCP client name, transcript schema id, doctor command).
|
|
38
|
+
|
|
39
|
+
// @ts-check
|
|
40
|
+
|
|
41
|
+
import { passthroughSandbox } from "../../sandbox-seam.js";
|
|
42
|
+
import { DEFAULT_RUNTIME_BRAND, resolveRuntimeBrand } from "../../../runtime-brand.js";
|
|
43
|
+
|
|
44
|
+
/** @typedef {import('../../../runtime-brand.js').RuntimeBrand} RuntimeBrand */
|
|
45
|
+
/** @typedef {import('../../sandbox-seam.js').SandboxPolicy} SandboxPolicy */
|
|
46
|
+
/** @typedef {import('../../sandbox-seam.js').RuntimeSandboxEngine} RuntimeSandboxEngine */
|
|
47
|
+
/** @typedef {import('../../sandbox-seam.js').RuntimeSandbox} RuntimeSandbox */
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} ToolContext
|
|
51
|
+
* @property {string} [workspace]
|
|
52
|
+
* @property {string} [repoRoot]
|
|
53
|
+
* @property {string} [runId]
|
|
54
|
+
* @property {string} [toolArtifactDir]
|
|
55
|
+
* @property {string} [ripgrepPath]
|
|
56
|
+
* @property {string} [qaOutputDir]
|
|
57
|
+
* @property {SandboxPolicy} [sandboxPolicy]
|
|
58
|
+
* @property {RuntimeSandboxEngine} [sandboxEngine]
|
|
59
|
+
* @property {RuntimeSandbox} sandbox
|
|
60
|
+
* @property {RuntimeBrand} runtimeBrand
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
// The data keys (everything except the always-resolved runtimeBrand). A fixed
|
|
64
|
+
// list — not `Object.keys(ctx)` — so an unknown key on `next` is ignored, matching
|
|
65
|
+
// the historical configureToolRuntime contract.
|
|
66
|
+
const TOOL_CONTEXT_KEYS = /** @type {const} */ ([
|
|
67
|
+
"workspace",
|
|
68
|
+
"repoRoot",
|
|
69
|
+
"runId",
|
|
70
|
+
"toolArtifactDir",
|
|
71
|
+
"ripgrepPath",
|
|
72
|
+
"qaOutputDir",
|
|
73
|
+
"sandboxPolicy",
|
|
74
|
+
"sandboxEngine",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a fresh ToolContext from a partial input. `runtimeBrand` is always
|
|
79
|
+
* resolved (to the defaults when absent); the remaining keys default to
|
|
80
|
+
* undefined and are copied from `input` when present.
|
|
81
|
+
* @param {Partial<ToolContext>} [input]
|
|
82
|
+
* @returns {ToolContext}
|
|
83
|
+
*/
|
|
84
|
+
export function createToolContext(input = {}) {
|
|
85
|
+
/** @type {ToolContext} */
|
|
86
|
+
const ctx = {
|
|
87
|
+
workspace: undefined,
|
|
88
|
+
repoRoot: undefined,
|
|
89
|
+
runId: undefined,
|
|
90
|
+
toolArtifactDir: undefined,
|
|
91
|
+
ripgrepPath: undefined,
|
|
92
|
+
qaOutputDir: undefined,
|
|
93
|
+
sandboxPolicy: undefined,
|
|
94
|
+
sandboxEngine: undefined,
|
|
95
|
+
sandbox: passthroughSandbox,
|
|
96
|
+
runtimeBrand: { ...DEFAULT_RUNTIME_BRAND },
|
|
97
|
+
};
|
|
98
|
+
return updateToolContext(ctx, input);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Mutate an existing ToolContext in place with the keys present on `next`,
|
|
103
|
+
* leaving untouched keys as-is. Mutating in place (rather than returning a
|
|
104
|
+
* copy) is deliberate: `createRuntime` threads a single context object to its
|
|
105
|
+
* bridges and `configureTools` updates that same reference so subsequent runs
|
|
106
|
+
* observe the change — the instance-scoped equivalent of the old global
|
|
107
|
+
* configureToolRuntime.
|
|
108
|
+
* @param {ToolContext} ctx
|
|
109
|
+
* @param {Partial<ToolContext>} [next]
|
|
110
|
+
* @returns {ToolContext}
|
|
111
|
+
*/
|
|
112
|
+
export function updateToolContext(ctx, next = {}) {
|
|
113
|
+
for (const key of TOOL_CONTEXT_KEYS) {
|
|
114
|
+
// Per-key assignment across a heterogeneous record: TS can't correlate the
|
|
115
|
+
// looked-up value type with `key` across two independent indexed accesses
|
|
116
|
+
// (a known structural limitation, not a real type hazard here).
|
|
117
|
+
if (key in next) ctx[key] = /** @type {any} */ (next)[key];
|
|
118
|
+
}
|
|
119
|
+
if (next.sandbox !== undefined) {
|
|
120
|
+
ctx.sandbox = next.sandbox;
|
|
121
|
+
}
|
|
122
|
+
if (next.runtimeBrand !== undefined) {
|
|
123
|
+
ctx.runtimeBrand = resolveRuntimeBrand(next.runtimeBrand);
|
|
124
|
+
}
|
|
125
|
+
return ctx;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reset every data key to undefined and the brand back to the defaults.
|
|
130
|
+
* @param {ToolContext} ctx
|
|
131
|
+
* @returns {ToolContext}
|
|
132
|
+
*/
|
|
133
|
+
export function resetToolContext(ctx) {
|
|
134
|
+
for (const key of TOOL_CONTEXT_KEYS) {
|
|
135
|
+
ctx[key] = /** @type {any} */ (undefined);
|
|
136
|
+
}
|
|
137
|
+
ctx.sandbox = passthroughSandbox;
|
|
138
|
+
ctx.runtimeBrand = { ...DEFAULT_RUNTIME_BRAND };
|
|
139
|
+
return ctx;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Single source of truth for the sandbox policy a tool call runs under.
|
|
143
|
+
// Merging (rather than letting the per-call option shadow the context policy)
|
|
144
|
+
// keeps the guarantee monotonic (I13): a request-scoped policy can tighten the
|
|
145
|
+
// host-configured policy but never weaken or disable it. The merge itself is
|
|
146
|
+
// delegated to `ctx.sandbox.mergePolicies` (defaulting to passthroughSandbox)
|
|
147
|
+
// so the actual algorithm is host-injected, not hard-coded here.
|
|
148
|
+
/**
|
|
149
|
+
* @param {ToolContext|undefined} ctx
|
|
150
|
+
* @param {SandboxPolicy} [requestPolicy]
|
|
151
|
+
* @returns {SandboxPolicy|undefined}
|
|
152
|
+
*/
|
|
153
|
+
export function resolveSandboxPolicy(ctx, requestPolicy = undefined) {
|
|
154
|
+
const sandbox = ctx?.sandbox ?? passthroughSandbox;
|
|
155
|
+
const merged = sandbox.mergePolicies(ctx?.sandboxPolicy ?? undefined, requestPolicy ?? undefined);
|
|
156
|
+
return merged && merged.mode !== "off" ? merged : undefined;
|
|
157
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_MAX_TOOL_OUTPUT_CHARS } from "./shared/constants.js";
|
|
2
2
|
import { capChars } from "./shared/output-truncation.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
4
|
+
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
5
|
+
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
5
6
|
|
|
6
7
|
const FETCH_TIMEOUT_MS = 15000;
|
|
7
8
|
const MAX_REDIRECTS = 5;
|
|
@@ -21,9 +22,13 @@ function fetchRetryDelay(ms) {
|
|
|
21
22
|
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number}} params
|
|
27
|
+
* @param {{sandboxPolicy?: any, ctx?: any, retryDelaysMs?: number[]}} [options]
|
|
28
|
+
*/
|
|
24
29
|
export async function webFetchToolImpl(
|
|
25
30
|
{ url, headers = {}, max_output_chars },
|
|
26
|
-
{ sandboxPolicy, retryDelaysMs = DEFAULT_FETCH_RETRY_DELAYS_MS } = {},
|
|
31
|
+
{ sandboxPolicy, ctx, retryDelaysMs = DEFAULT_FETCH_RETRY_DELAYS_MS } = {},
|
|
27
32
|
) {
|
|
28
33
|
const maxChars = Number(max_output_chars) || DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
29
34
|
let parsed;
|
|
@@ -31,17 +36,23 @@ export async function webFetchToolImpl(
|
|
|
31
36
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
32
37
|
return "Error: WebFetch only supports http(s) URLs.";
|
|
33
38
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
39
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
40
|
+
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
41
|
+
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
42
|
+
if (!sandbox.networkAllowsUrl(policy, parsed.href)) return "Error: Network access denied by sandbox policy.";
|
|
36
43
|
const requestHeaders = { "User-Agent": "AgentRuntime/0.1", ...headers };
|
|
37
|
-
|
|
44
|
+
// `policy.network` can be absent (a hand-built, non-real-mode policy — the
|
|
45
|
+
// networkAllowsUrl gate above already denied any real-mode policy missing
|
|
46
|
+
// it); treat that as unrestricted rather than dereferencing `.mode` on
|
|
47
|
+
// undefined.
|
|
48
|
+
const restricted = policy !== undefined && policy.network !== undefined && policy.network.mode !== "all";
|
|
38
49
|
const maxRetries = Array.isArray(retryDelaysMs) ? retryDelaysMs.length : 0;
|
|
39
50
|
|
|
40
51
|
let lastErrorMessage = "request failed";
|
|
41
52
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
42
53
|
try {
|
|
43
54
|
const resp = restricted
|
|
44
|
-
? await fetchCheckingRedirects(parsed, requestHeaders, policy)
|
|
55
|
+
? await fetchCheckingRedirects(parsed, requestHeaders, policy, sandbox)
|
|
45
56
|
: await fetch(url, { headers: requestHeaders, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
46
57
|
if (typeof resp === "string") return resp; // policy/redirect error — not retryable
|
|
47
58
|
// Transient server errors (5xx) are worth one more attempt.
|
|
@@ -53,7 +64,7 @@ export async function webFetchToolImpl(
|
|
|
53
64
|
}
|
|
54
65
|
const text = await resp.text();
|
|
55
66
|
if (!resp.ok) return `HTTP ${resp.status}: ${text.slice(0, 500)}`;
|
|
56
|
-
return capChars(text, { label: "WebFetch", maxChars });
|
|
67
|
+
return capChars(text, { label: "WebFetch", maxChars, ctx });
|
|
57
68
|
} catch (err) {
|
|
58
69
|
lastErrorMessage = err.message;
|
|
59
70
|
if (isTransientFetchError(err) && attempt < maxRetries) {
|
|
@@ -69,7 +80,7 @@ export async function webFetchToolImpl(
|
|
|
69
80
|
// fetch() follows redirects transparently, which would let an allowed host
|
|
70
81
|
// bounce the request to a denied one — follow them manually and re-check the
|
|
71
82
|
// policy on every hop. Custom headers only travel to the original origin.
|
|
72
|
-
async function fetchCheckingRedirects(initialUrl, headers, policy) {
|
|
83
|
+
async function fetchCheckingRedirects(initialUrl, headers, policy, sandbox) {
|
|
73
84
|
let current = initialUrl;
|
|
74
85
|
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
75
86
|
const sameOrigin = current.origin === initialUrl.origin;
|
|
@@ -85,7 +96,7 @@ async function fetchCheckingRedirects(initialUrl, headers, policy) {
|
|
|
85
96
|
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
86
97
|
return "Error: WebFetch only supports http(s) URLs.";
|
|
87
98
|
}
|
|
88
|
-
if (!
|
|
99
|
+
if (!sandbox.networkAllowsUrl(policy, next.href)) {
|
|
89
100
|
return "Error: Network access denied by sandbox policy (redirect).";
|
|
90
101
|
}
|
|
91
102
|
try { await resp.body?.cancel(); } catch { /* best-effort */ }
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
2
|
+
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
3
|
+
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @param {{query: string, limit?: number}} params
|
|
7
|
+
* @param {{sandboxPolicy?: any, ctx?: any}} [options]
|
|
8
|
+
*/
|
|
9
|
+
export async function webSearchToolImpl({ query, limit = 5 }, { sandboxPolicy, ctx } = {}) {
|
|
5
10
|
const max = Math.min(Math.max(Number(limit) || 5, 1), 10);
|
|
6
11
|
const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
7
|
-
|
|
12
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
13
|
+
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
14
|
+
if (!sandbox.networkAllowsUrl(resolveSandboxPolicy(resolvedCtx, sandboxPolicy), url)) return "Error: Network access denied by sandbox policy.";
|
|
8
15
|
const resp = await fetch(url, {
|
|
9
16
|
headers: { "User-Agent": "Mozilla/5.0 AgentRuntime/0.1" },
|
|
10
17
|
signal: AbortSignal.timeout(15000),
|
package/src/agent/tools/write.js
CHANGED
|
@@ -3,9 +3,13 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { MAX_WRITE_BYTES } from "./shared/constants.js";
|
|
4
4
|
import { isWritablePathAllowed, resolveToolPath } from "./shared/path-resolver.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {{file_path: string, content?: string, workdir?: string}} params
|
|
8
|
+
* @param {{sandboxPolicy?: any, ctx?: any}} [options]
|
|
9
|
+
*/
|
|
10
|
+
export async function writeToolImpl({ file_path, content, workdir }, { sandboxPolicy, ctx } = {}) {
|
|
11
|
+
const target = resolveToolPath(file_path, workdir, ctx);
|
|
12
|
+
if (!isWritablePathAllowed(target, workdir, { sandboxPolicy, ctx })) return `Error: Path not allowed: ${file_path}`;
|
|
9
13
|
const bytes = Buffer.byteLength(content || "", "utf8");
|
|
10
14
|
if (bytes > MAX_WRITE_BYTES) return `Error: Content too large (${bytes} bytes)`;
|
|
11
15
|
mkdirSync(dirname(target), { recursive: true });
|
package/src/agent/transcript.js
CHANGED
|
@@ -84,6 +84,17 @@ function summarizeTurn(turn, { maxChars = DEFAULT_TURN_SUMMARY_CHARS } = {}) {
|
|
|
84
84
|
// is nothing usable to resume from. Older turns beyond `verbatimTurns` are
|
|
85
85
|
// summarized into a one-paragraph snippet; only the trailing `verbatimTurns`
|
|
86
86
|
// keep their full assistant text + tool detail.
|
|
87
|
+
/**
|
|
88
|
+
* @param {Array<*>} events
|
|
89
|
+
* @param {Object} [options]
|
|
90
|
+
* @param {number} [options.maxTurns]
|
|
91
|
+
* @param {number} [options.verbatimTurns]
|
|
92
|
+
* @param {number} [options.maxChars]
|
|
93
|
+
* @param {number} [options.toolResultChars]
|
|
94
|
+
* @param {number} [options.assistantTextChars]
|
|
95
|
+
* @param {number} [options.turnSummaryChars]
|
|
96
|
+
* @param {import('../runtime-brand.js').RuntimeBrand} [options.runtimeBrand]
|
|
97
|
+
*/
|
|
87
98
|
export function buildTranscriptTailSnapshot(events, {
|
|
88
99
|
maxTurns = DEFAULT_MAX_TURNS,
|
|
89
100
|
verbatimTurns = DEFAULT_VERBATIM_TURNS,
|
|
@@ -91,6 +102,10 @@ export function buildTranscriptTailSnapshot(events, {
|
|
|
91
102
|
toolResultChars = DEFAULT_TOOL_RESULT_CHARS,
|
|
92
103
|
assistantTextChars = DEFAULT_ASSISTANT_TEXT_CHARS,
|
|
93
104
|
turnSummaryChars = DEFAULT_TURN_SUMMARY_CHARS,
|
|
105
|
+
// The resolved RuntimeBrand for the schema id. The router (the only caller)
|
|
106
|
+
// threads the instance brand; absent (deep/default callers) it falls back to
|
|
107
|
+
// the module-default context brand.
|
|
108
|
+
runtimeBrand,
|
|
94
109
|
} = {}) {
|
|
95
110
|
if (!Array.isArray(events) || events.length === 0) return null;
|
|
96
111
|
const turns = [];
|
|
@@ -154,7 +169,7 @@ export function buildTranscriptTailSnapshot(events, {
|
|
|
154
169
|
turn_index: turns.length - tailWindow.length + idx + 1,
|
|
155
170
|
summary: summarizeTurn(turn, { maxChars: turnSummaryChars }),
|
|
156
171
|
}));
|
|
157
|
-
const brand = readRuntimeBrand();
|
|
172
|
+
const brand = runtimeBrand ?? readRuntimeBrand();
|
|
158
173
|
const snapshot = {
|
|
159
174
|
schema: `${brand.schemaPrefix}.transcript-tail.v1`,
|
|
160
175
|
captured_at: Date.now(),
|