machine-bridge-mcp 0.12.1 → 0.13.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/CHANGELOG.md +37 -0
- package/README.md +6 -4
- package/SECURITY.md +5 -1
- package/browser-extension/manifest.json +2 -2
- package/docs/AGENT_CONTEXT.md +16 -6
- package/docs/ARCHITECTURE.md +14 -6
- package/docs/AUDIT.md +25 -1
- package/docs/CLIENTS.md +1 -1
- package/docs/ENGINEERING.md +5 -4
- package/docs/LOCAL_AUTOMATION.md +1 -1
- package/docs/LOGGING.md +3 -1
- package/docs/OPERATIONS.md +6 -1
- package/docs/PRIVACY.md +8 -3
- package/docs/RELEASING.md +5 -5
- package/docs/TESTING.md +11 -9
- package/package.json +6 -2
- package/scripts/github-release.mjs +28 -0
- package/scripts/privacy-check.mjs +150 -2
- package/scripts/release-ci.mjs +18 -0
- package/src/local/agent-context.mjs +75 -18
- package/src/local/app-automation.mjs +15 -1
- package/src/local/atomic-fs.mjs +14 -2
- package/src/local/browser-bridge.mjs +3 -1
- package/src/local/capability-observer.mjs +56 -0
- package/src/local/default-instructions.mjs +21 -91
- package/src/local/network-proxy.mjs +34 -0
- package/src/local/process-sessions.mjs +7 -0
- package/src/local/project-package.mjs +234 -0
- package/src/local/relay-connection.mjs +26 -0
- package/src/local/runtime.mjs +21 -20
- package/src/shared/tool-catalog.json +4 -4
- package/src/worker/index.ts +1 -1
|
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import { lstat, open, opendir } from "node:fs/promises";
|
|
4
4
|
import { join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { packageScriptDisplayCommand, readProjectPackageMetadata, safeVersionValue } from "./project-package.mjs";
|
|
5
6
|
|
|
6
|
-
const MAX_METADATA_FILE_BYTES = 1024 * 1024;
|
|
7
7
|
const MAX_PROJECT_CONTEXT_BYTES = 16 * 1024;
|
|
8
8
|
const MAX_SCRIPT_NAMES = 24;
|
|
9
9
|
const MAX_WORKFLOW_FILES = 20;
|
|
@@ -77,14 +77,6 @@ const DOCUMENT_FILES = Object.freeze([
|
|
|
77
77
|
"docs/TESTING.md",
|
|
78
78
|
]);
|
|
79
79
|
|
|
80
|
-
const LOCKFILES = Object.freeze([
|
|
81
|
-
["package-lock.json", "npm"],
|
|
82
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
83
|
-
["yarn.lock", "yarn"],
|
|
84
|
-
["bun.lock", "bun"],
|
|
85
|
-
["bun.lockb", "bun"],
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
80
|
const CI_FILES = Object.freeze([
|
|
89
81
|
".gitlab-ci.yml",
|
|
90
82
|
"azure-pipelines.yml",
|
|
@@ -155,52 +147,34 @@ export async function discoverAutomaticProjectInstruction({
|
|
|
155
147
|
}
|
|
156
148
|
|
|
157
149
|
async function readPackageFacts(root, throwIfCancelled) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (await isRegularNonSymlink(join(root, name))) lockfiles.push({ name, manager });
|
|
164
|
-
}
|
|
165
|
-
if (!packageText) {
|
|
166
|
-
const lines = lockfiles.length
|
|
167
|
-
? [`- JavaScript lockfiles detected without readable root package metadata: ${lockfiles.map((item) => `\`${item.name}\``).join(", ")}. Inspect before installing dependencies.`]
|
|
150
|
+
const metadata = await readProjectPackageMetadata(root, throwIfCancelled);
|
|
151
|
+
if (!metadata.detected) return { detected: false, lines: [] };
|
|
152
|
+
if (metadata.packageState === "missing") {
|
|
153
|
+
const lines = metadata.lockfiles.length
|
|
154
|
+
? [`- JavaScript lockfiles detected without readable root package metadata: ${metadata.lockfiles.map((item) => `\`${item.name}\``).join(", ")}. Inspect before installing dependencies.`]
|
|
168
155
|
: [];
|
|
169
|
-
return { detected: lockfiles.length > 0, lines };
|
|
156
|
+
return { detected: metadata.lockfiles.length > 0, lines };
|
|
170
157
|
}
|
|
171
|
-
|
|
172
|
-
let parsed;
|
|
173
|
-
try {
|
|
174
|
-
parsed = JSON.parse(packageText);
|
|
175
|
-
} catch {
|
|
158
|
+
if (metadata.packageState === "invalid-json") {
|
|
176
159
|
return { detected: true, lines: ["- A root `package.json` exists but is not valid JSON. Do not infer package commands until it is repaired or understood."] };
|
|
177
160
|
}
|
|
178
|
-
if (
|
|
161
|
+
if (metadata.packageState === "invalid-root") {
|
|
162
|
+
return { detected: true, lines: ["- A root `package.json` exists but is not a JSON object."] };
|
|
163
|
+
}
|
|
179
164
|
|
|
180
165
|
const lines = [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
? Object.entries(parsed.scripts).filter(([name, value]) => validScriptName(name) && typeof value === "string").map(([name]) => name)
|
|
189
|
-
: [];
|
|
190
|
-
if (scripts.length) {
|
|
191
|
-
const selected = prioritizeScriptNames(scripts).slice(0, MAX_SCRIPT_NAMES);
|
|
192
|
-
const commands = selected.map((name) => formatPackageScriptCommand(managerName, name));
|
|
193
|
-
const suffix = scripts.length > selected.length ? `; ${scripts.length - selected.length} additional script(s) omitted` : "";
|
|
166
|
+
if (metadata.declaredManager) lines.push(`- Declared package manager: \`${escapeInlineCode(metadata.declaredManager)}\`.`);
|
|
167
|
+
if (metadata.lockfiles.length === 1) lines.push(`- Package lockfile: \`${metadata.lockfiles[0].name}\`. Preserve it and use the matching package manager.`);
|
|
168
|
+
if (metadata.lockfiles.length > 1) lines.push(`- Multiple JavaScript lockfiles are present: ${metadata.lockfiles.map((item) => `\`${item.name}\``).join(", ")}. Do not choose or rewrite one automatically; inspect project guidance first.`);
|
|
169
|
+
if (metadata.scripts.length) {
|
|
170
|
+
const selected = metadata.scripts.slice(0, MAX_SCRIPT_NAMES);
|
|
171
|
+
const commands = selected.map((name) => packageScriptDisplayCommand(metadata.managerName, name));
|
|
172
|
+
const suffix = metadata.scripts.length > selected.length ? `; ${metadata.scripts.length - selected.length} additional script(s) omitted` : "";
|
|
194
173
|
lines.push(`- Declared package scripts (names only; bodies are not injected): ${commands.map((command) => `\`${escapeInlineCode(command)}\``).join(", ")}${suffix}.`);
|
|
195
174
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.map(([name, value]) => [name, safeVersionValue(value)])
|
|
200
|
-
.filter(([name, value]) => /^[A-Za-z0-9_.-]{1,40}$/.test(name) && value)
|
|
201
|
-
.slice(0, 10)
|
|
202
|
-
.map(([name, value]) => `\`${escapeInlineCode(name)} ${escapeInlineCode(value)}\``);
|
|
203
|
-
if (engines.length) lines.push(`- Declared runtime constraints: ${engines.join(", ")}.`);
|
|
175
|
+
if (metadata.engines.length) {
|
|
176
|
+
const engines = metadata.engines.map(([name, value]) => `\`${escapeInlineCode(name)} ${escapeInlineCode(value)}\``);
|
|
177
|
+
lines.push(`- Declared runtime constraints: ${engines.join(", ")}.`);
|
|
204
178
|
}
|
|
205
179
|
return { detected: true, lines };
|
|
206
180
|
}
|
|
@@ -275,50 +249,6 @@ function virtualInstruction(source, content, precedence, scope) {
|
|
|
275
249
|
};
|
|
276
250
|
}
|
|
277
251
|
|
|
278
|
-
function packageManagerName(value) {
|
|
279
|
-
if (!value) return "";
|
|
280
|
-
const match = /^(npm|pnpm|yarn|bun)(?:@|$)/.exec(value.trim());
|
|
281
|
-
return match?.[1] || "";
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function uniqueLockManager(lockfiles) {
|
|
285
|
-
const managers = [...new Set(lockfiles.map((item) => item.manager))];
|
|
286
|
-
return managers.length === 1 ? managers[0] : "";
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function formatPackageScriptCommand(manager, name) {
|
|
290
|
-
if (manager === "pnpm") return `pnpm run ${name}`;
|
|
291
|
-
if (manager === "yarn") return `yarn ${name}`;
|
|
292
|
-
if (manager === "bun") return `bun run ${name}`;
|
|
293
|
-
return `npm run ${name}`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function prioritizeScriptNames(names) {
|
|
297
|
-
const preferred = ["check", "test", "lint", "typecheck", "build", "format", "verify", "ci", "prepack", "start", "dev"];
|
|
298
|
-
const rank = new Map(preferred.map((name, index) => [name, index]));
|
|
299
|
-
return [...new Set(names)].sort((left, right) => {
|
|
300
|
-
const leftRank = rank.has(left) ? rank.get(left) : preferred.length;
|
|
301
|
-
const rightRank = rank.has(right) ? rank.get(right) : preferred.length;
|
|
302
|
-
return leftRank - rightRank || left.localeCompare(right);
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function validScriptName(value) {
|
|
307
|
-
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9:._/-]{0,119}$/.test(value);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function safePackageManager(value) {
|
|
311
|
-
if (typeof value !== "string") return "";
|
|
312
|
-
const normalized = value.trim();
|
|
313
|
-
return /^(?:npm|pnpm|yarn|bun)@[A-Za-z0-9][A-Za-z0-9.+_-]{0,90}$/.test(normalized) ? normalized : "";
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function safeVersionValue(value) {
|
|
317
|
-
if (typeof value !== "string") return "";
|
|
318
|
-
const normalized = safeSingleLine(value, 120);
|
|
319
|
-
return normalized && /^[A-Za-z0-9][A-Za-z0-9 ._*+<>=~^|&!/-]{0,119}$/.test(normalized) ? normalized : "";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
252
|
function skippableMetadataError(error) {
|
|
323
253
|
return ["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP", "EBUSY"].includes(error?.code);
|
|
324
254
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
2
|
+
import { getProxyForUrl } from "proxy-from-env";
|
|
3
|
+
|
|
4
|
+
export function proxyAgentForWebSocket(webSocketUrl, proxyResolver = getProxyForUrl) {
|
|
5
|
+
const target = new URL(String(webSocketUrl));
|
|
6
|
+
if (target.protocol !== "ws:" && target.protocol !== "wss:") throw new Error("relay WebSocket URL must use ws or wss");
|
|
7
|
+
const proxyLookupUrl = new URL(target);
|
|
8
|
+
proxyLookupUrl.protocol = target.protocol === "wss:" ? "https:" : "http:";
|
|
9
|
+
const proxyValue = String(proxyResolver(proxyLookupUrl.href) || "").trim();
|
|
10
|
+
if (!proxyValue) return { agent: null, mode: "direct" };
|
|
11
|
+
let proxyUrl;
|
|
12
|
+
try {
|
|
13
|
+
proxyUrl = new URL(proxyValue);
|
|
14
|
+
} catch {
|
|
15
|
+
throw proxyConfigurationError("relay proxy configuration is not a valid URL");
|
|
16
|
+
}
|
|
17
|
+
if (!new Set(["http:", "https:"]).has(proxyUrl.protocol)) {
|
|
18
|
+
throw proxyConfigurationError("relay proxy must use HTTP or HTTPS");
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return {
|
|
22
|
+
agent: new HttpsProxyAgent(proxyUrl),
|
|
23
|
+
mode: "proxy",
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
throw proxyConfigurationError("relay proxy configuration could not be initialized");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function proxyConfigurationError(message) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = "relay_proxy_configuration";
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
@@ -253,6 +253,13 @@ export function terminateProcessTree(child, signal) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
export function terminateProcessTreeWithEscalation(child, options = {}) {
|
|
257
|
+
const graceMs = Number.isFinite(Number(options.graceMs)) ? Math.max(0, Number(options.graceMs)) : 2000;
|
|
258
|
+
const schedule = typeof options.setTimeout === "function" ? options.setTimeout : setTimeout;
|
|
259
|
+
terminateProcessTree(child, "SIGTERM");
|
|
260
|
+
return schedule(() => terminateProcessTree(child, "SIGKILL"), graceMs);
|
|
261
|
+
}
|
|
262
|
+
|
|
256
263
|
function waitForSpawn(child) {
|
|
257
264
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
258
265
|
const onSpawn = () => { cleanup(); resolvePromise(); };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_METADATA_FILE_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_PACKAGE_COMMANDS = 64;
|
|
7
|
+
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
8
|
+
|
|
9
|
+
const PACKAGE_SCRIPT_INTENTS = Object.freeze({
|
|
10
|
+
check: "check verify validate validation test lint typecheck 完整测试 检查 验证 测试 校验 审查",
|
|
11
|
+
verify: "check verify validate validation test 检查 验证 测试 校验",
|
|
12
|
+
validate: "check verify validate validation test 检查 验证 测试 校验",
|
|
13
|
+
test: "test tests testing unit integration 完整测试 测试 单元测试 集成测试",
|
|
14
|
+
lint: "lint static analysis code quality 检查 静态检查 代码质量",
|
|
15
|
+
typecheck: "typecheck type check typescript 类型检查",
|
|
16
|
+
build: "build compile bundle package 构建 编译 打包",
|
|
17
|
+
compile: "build compile 构建 编译",
|
|
18
|
+
format: "format formatting 格式化",
|
|
19
|
+
start: "start run launch 启动 运行",
|
|
20
|
+
dev: "develop development start server 开发 启动 服务",
|
|
21
|
+
serve: "serve server start 服务 启动",
|
|
22
|
+
audit: "audit security vulnerability 审计 安全 漏洞",
|
|
23
|
+
deploy: "deploy deployment 部署",
|
|
24
|
+
release: "release publish 发布",
|
|
25
|
+
publish: "publish release 发布",
|
|
26
|
+
pack: "pack package 打包",
|
|
27
|
+
prepack: "pack package 打包",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const LOCKFILES = Object.freeze([
|
|
31
|
+
["package-lock.json", "npm"],
|
|
32
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
33
|
+
["yarn.lock", "yarn"],
|
|
34
|
+
["bun.lock", "bun"],
|
|
35
|
+
["bun.lockb", "bun"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export async function readProjectPackageMetadata(root, throwIfCancelled = () => {}) {
|
|
39
|
+
const packagePath = join(root, "package.json");
|
|
40
|
+
const packageText = await readOptionalRegularUtf8(packagePath, MAX_METADATA_FILE_BYTES);
|
|
41
|
+
const lockfiles = [];
|
|
42
|
+
for (const [name, manager] of LOCKFILES) {
|
|
43
|
+
throwIfCancelled();
|
|
44
|
+
if (await isRegularNonSymlink(join(root, name))) lockfiles.push({ name, manager });
|
|
45
|
+
}
|
|
46
|
+
if (!packageText) {
|
|
47
|
+
return {
|
|
48
|
+
detected: lockfiles.length > 0,
|
|
49
|
+
packagePath,
|
|
50
|
+
packageState: "missing",
|
|
51
|
+
declaredManager: "",
|
|
52
|
+
managerName: uniqueLockManager(lockfiles),
|
|
53
|
+
lockfiles,
|
|
54
|
+
scripts: [],
|
|
55
|
+
engines: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(packageText);
|
|
62
|
+
} catch {
|
|
63
|
+
return invalidPackageMetadata(packagePath, lockfiles, "invalid-json");
|
|
64
|
+
}
|
|
65
|
+
if (!isPlainRecord(parsed)) return invalidPackageMetadata(packagePath, lockfiles, "invalid-root");
|
|
66
|
+
|
|
67
|
+
const declaredManager = safePackageManager(parsed.packageManager);
|
|
68
|
+
const scripts = isPlainRecord(parsed.scripts)
|
|
69
|
+
? Object.entries(parsed.scripts)
|
|
70
|
+
.filter(([name, value]) => validScriptName(name) && typeof value === "string")
|
|
71
|
+
.map(([name]) => name)
|
|
72
|
+
: [];
|
|
73
|
+
const engines = isPlainRecord(parsed.engines)
|
|
74
|
+
? Object.entries(parsed.engines)
|
|
75
|
+
.map(([name, value]) => [name, safeVersionValue(value)])
|
|
76
|
+
.filter(([name, value]) => /^[A-Za-z0-9_.-]{1,40}$/.test(name) && value)
|
|
77
|
+
.slice(0, 10)
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
const inferredManager = packageManagerName(declaredManager) || uniqueLockManager(lockfiles);
|
|
81
|
+
return {
|
|
82
|
+
detected: true,
|
|
83
|
+
packagePath,
|
|
84
|
+
packageState: "valid",
|
|
85
|
+
declaredManager,
|
|
86
|
+
managerName: inferredManager || (lockfiles.length === 0 ? "npm" : ""),
|
|
87
|
+
lockfiles,
|
|
88
|
+
scripts: prioritizeScriptNames(scripts),
|
|
89
|
+
engines,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function packageScriptCommand(manager, script, platform = process.platform, commandShell = process.env.ComSpec || "cmd.exe") {
|
|
94
|
+
const executable = PACKAGE_MANAGERS.has(manager) ? manager : "npm";
|
|
95
|
+
if (!validScriptName(script)) throw new Error("package script name is invalid");
|
|
96
|
+
if (platform === "win32") return [commandShell, "/d", "/s", "/c", `${executable} run ${script}`];
|
|
97
|
+
return [executable, "run", script];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function packageScriptDisplayCommand(manager, script) {
|
|
101
|
+
const executable = PACKAGE_MANAGERS.has(manager) ? manager : "npm";
|
|
102
|
+
if (!validScriptName(script)) throw new Error("package script name is invalid");
|
|
103
|
+
return `${executable} run ${script}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function automaticPackageCommands(metadata, cwd, platform = process.platform) {
|
|
107
|
+
if (metadata?.packageState !== "valid" || !metadata.managerName || !metadata.scripts?.length) return new Map();
|
|
108
|
+
const commands = new Map();
|
|
109
|
+
for (const script of metadata.scripts.slice(0, MAX_PACKAGE_COMMANDS)) {
|
|
110
|
+
const base = normalizedCommandName(script);
|
|
111
|
+
if (!base) continue;
|
|
112
|
+
let name = `package.${base}`;
|
|
113
|
+
let suffix = 2;
|
|
114
|
+
while (commands.has(name)) name = `package.${base}.${suffix++}`;
|
|
115
|
+
commands.set(name, {
|
|
116
|
+
name,
|
|
117
|
+
description: `Run the declared package script '${script}' using the detected package manager.`,
|
|
118
|
+
argv: packageScriptCommand(metadata.managerName, script, platform),
|
|
119
|
+
cwd,
|
|
120
|
+
timeoutSeconds: 600,
|
|
121
|
+
allowExtraArgs: false,
|
|
122
|
+
source: metadata.packagePath,
|
|
123
|
+
sourceType: "automatic-package-script",
|
|
124
|
+
searchTerms: packageScriptSearchTerms(script),
|
|
125
|
+
script,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return commands;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function safeVersionValue(value) {
|
|
132
|
+
if (typeof value !== "string") return "";
|
|
133
|
+
const normalized = safeSingleLine(value, 120);
|
|
134
|
+
return normalized && /^[A-Za-z0-9][A-Za-z0-9 ._*+<>=~^|&!/-]{0,119}$/.test(normalized) ? normalized : "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
function packageScriptSearchTerms(script) {
|
|
139
|
+
const normalized = String(script).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
140
|
+
const root = normalized.split("-")[0];
|
|
141
|
+
return PACKAGE_SCRIPT_INTENTS[root] || "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function invalidPackageMetadata(packagePath, lockfiles, packageState) {
|
|
145
|
+
return {
|
|
146
|
+
detected: true,
|
|
147
|
+
packagePath,
|
|
148
|
+
packageState,
|
|
149
|
+
declaredManager: "",
|
|
150
|
+
managerName: uniqueLockManager(lockfiles),
|
|
151
|
+
lockfiles,
|
|
152
|
+
scripts: [],
|
|
153
|
+
engines: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function packageManagerName(value) {
|
|
158
|
+
if (!value) return "";
|
|
159
|
+
const match = /^(npm|pnpm|yarn|bun)(?:@|$)/.exec(value.trim());
|
|
160
|
+
return match?.[1] || "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function uniqueLockManager(lockfiles) {
|
|
164
|
+
const managers = [...new Set(lockfiles.map((item) => item.manager))];
|
|
165
|
+
return managers.length === 1 ? managers[0] : "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function prioritizeScriptNames(names) {
|
|
169
|
+
const preferred = ["check", "test", "lint", "typecheck", "build", "format", "verify", "ci", "prepack", "start", "dev"];
|
|
170
|
+
const rank = new Map(preferred.map((name, index) => [name, index]));
|
|
171
|
+
return [...new Set(names)].sort((left, right) => {
|
|
172
|
+
const leftRank = rank.has(left) ? rank.get(left) : preferred.length;
|
|
173
|
+
const rightRank = rank.has(right) ? rank.get(right) : preferred.length;
|
|
174
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validScriptName(value) {
|
|
179
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9:._/-]{0,119}$/.test(value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizedCommandName(value) {
|
|
183
|
+
const normalized = String(value).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
184
|
+
return /^[a-z][a-z0-9._-]{0,47}$/.test(normalized) ? normalized : "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function safePackageManager(value) {
|
|
188
|
+
if (typeof value !== "string") return "";
|
|
189
|
+
const normalized = value.trim();
|
|
190
|
+
return /^(?:npm|pnpm|yarn|bun)@[A-Za-z0-9][A-Za-z0-9.+_-]{0,90}$/.test(normalized) ? normalized : "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function isRegularNonSymlink(filePath) {
|
|
194
|
+
const info = await lstat(filePath).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
195
|
+
return Boolean(info && !info.isSymbolicLink() && info.isFile());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readOptionalRegularUtf8(filePath, maxBytes) {
|
|
199
|
+
const info = await lstat(filePath).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
200
|
+
if (!info || info.isSymbolicLink() || !info.isFile() || info.size > maxBytes) return null;
|
|
201
|
+
const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0)).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
202
|
+
if (!handle) return null;
|
|
203
|
+
try {
|
|
204
|
+
const current = await handle.stat();
|
|
205
|
+
if (!current.isFile() || current.size > maxBytes) return null;
|
|
206
|
+
const buffer = Buffer.alloc(current.size);
|
|
207
|
+
let offset = 0;
|
|
208
|
+
while (offset < buffer.length) {
|
|
209
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
210
|
+
if (!bytesRead) break;
|
|
211
|
+
offset += bytesRead;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset));
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
await handle.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function skippableMetadataError(error) {
|
|
224
|
+
return ["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP", "EBUSY"].includes(error?.code);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function safeSingleLine(value, maxLength) {
|
|
228
|
+
if (typeof value !== "string") return "";
|
|
229
|
+
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isPlainRecord(value) {
|
|
233
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
234
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { classifyOperationalError } from "./log.mjs";
|
|
3
|
+
import { proxyAgentForWebSocket } from "./network-proxy.mjs";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25_000;
|
|
5
6
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
@@ -35,6 +36,8 @@ export class RelayConnection {
|
|
|
35
36
|
this.scheduler = options.scheduler || DEFAULT_SCHEDULER;
|
|
36
37
|
this.now = typeof options.now === "function" ? options.now : Date.now;
|
|
37
38
|
this.reconnectDelay = typeof options.reconnectDelay === "function" ? options.reconnectDelay : reconnectDelay;
|
|
39
|
+
this.proxyAgentForUrl = typeof options.proxyAgentForUrl === "function" ? options.proxyAgentForUrl : proxyAgentForWebSocket;
|
|
40
|
+
this.networkRoute = "unresolved";
|
|
38
41
|
this.maxPayload = boundedPositiveInteger(options.maxPayload, 8 * 1024 * 1024);
|
|
39
42
|
this.heartbeatIntervalMs = boundedPositiveInteger(options.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
40
43
|
this.heartbeatTimeoutMs = boundedPositiveInteger(options.heartbeatTimeoutMs, DEFAULT_HEARTBEAT_TIMEOUT_MS);
|
|
@@ -72,6 +75,16 @@ export class RelayConnection {
|
|
|
72
75
|
this.connectedOnceReject = null;
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
status() {
|
|
79
|
+
return {
|
|
80
|
+
ready: this.ready,
|
|
81
|
+
closed: this.closed,
|
|
82
|
+
network_route: this.networkRoute,
|
|
83
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
84
|
+
outage_active: this.outageStartedAt > 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
start() {
|
|
76
89
|
if (!this.closed && this.connectedOnce) return this.connectedOnce;
|
|
77
90
|
this.closed = false;
|
|
@@ -182,11 +195,20 @@ export class RelayConnection {
|
|
|
182
195
|
this.logger.debug?.("connecting to remote relay", { endpoint: redactUrl(wsUrl), attempt: this.reconnectAttempt + 1 });
|
|
183
196
|
let socket;
|
|
184
197
|
try {
|
|
198
|
+
const proxy = this.proxyAgentForUrl(wsUrl);
|
|
199
|
+
this.networkRoute = proxy?.agent ? "proxy" : "direct";
|
|
185
200
|
socket = new this.WebSocketClass(wsUrl, {
|
|
186
201
|
headers: { "X-Bridge-Token": this.secret },
|
|
187
202
|
maxPayload: this.maxPayload,
|
|
203
|
+
...(proxy?.agent ? { agent: proxy.agent } : {}),
|
|
188
204
|
});
|
|
205
|
+
this.logger.debug?.("remote relay network route selected", { route: this.networkRoute });
|
|
189
206
|
} catch (error) {
|
|
207
|
+
if (error?.code === "relay_proxy_configuration") {
|
|
208
|
+
this.networkRoute = "invalid-proxy-configuration";
|
|
209
|
+
this.failPermanently("relay_proxy_configuration");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
190
212
|
this.lastTransportErrorClass = classifyOperationalError(error);
|
|
191
213
|
this.logger.debug?.("remote relay connection could not be created", { error_class: this.lastTransportErrorClass });
|
|
192
214
|
this.scheduleReconnect("connection_interrupted");
|
|
@@ -475,6 +497,9 @@ function relayFatalMessage(category) {
|
|
|
475
497
|
if (category === "relay_protocol_error") {
|
|
476
498
|
return "remote relay protocol error; upgrade and redeploy both components, then restart the daemon";
|
|
477
499
|
}
|
|
500
|
+
if (category === "relay_proxy_configuration") {
|
|
501
|
+
return "remote relay proxy configuration is invalid; check HTTP_PROXY, HTTPS_PROXY, and NO_PROXY";
|
|
502
|
+
}
|
|
478
503
|
return "remote relay rejected the daemon connection; verify credentials or redeploy the Worker";
|
|
479
504
|
}
|
|
480
505
|
|
|
@@ -491,6 +516,7 @@ function relayCloseUserCause(category) {
|
|
|
491
516
|
relay_heartbeat_timeout: "relay stopped responding",
|
|
492
517
|
relay_transport_error: "relay transport error",
|
|
493
518
|
relay_protocol_error: "relay protocol error",
|
|
519
|
+
relay_proxy_configuration: "relay proxy configuration invalid",
|
|
494
520
|
invalid_transport_payload: "invalid transport payload",
|
|
495
521
|
message_too_large: "message exceeded the relay limit",
|
|
496
522
|
normal_close: "connection closed",
|
package/src/local/runtime.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } fro
|
|
|
7
7
|
import { RelayConnection } from "./relay-connection.mjs";
|
|
8
8
|
import { applyUpdateHunks, parsePatchEnvelope } from "./patch.mjs";
|
|
9
9
|
import { executionEnv, workspaceShellCommand } from "./shell.mjs";
|
|
10
|
-
import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validateArgv } from "./process-sessions.mjs";
|
|
10
|
+
import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, terminateProcessTreeWithEscalation, validateArgv } from "./process-sessions.mjs";
|
|
11
11
|
export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
|
|
12
12
|
import { allToolNames, assertCanonicalFullPolicy, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, POLICY_PROFILES, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
|
|
13
13
|
import { classifyOperationalError } from "./log.mjs";
|
|
@@ -17,6 +17,7 @@ import { expandHome } from "./state.mjs";
|
|
|
17
17
|
import { AgentContextManager } from "./agent-context.mjs";
|
|
18
18
|
import { AppAutomationManager } from "./app-automation.mjs";
|
|
19
19
|
import { BrowserBridgeManager } from "./browser-bridge.mjs";
|
|
20
|
+
import { CapabilityObserver } from "./capability-observer.mjs";
|
|
20
21
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
21
22
|
|
|
22
23
|
export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
@@ -85,7 +86,7 @@ export function runtimeToolHandlerNames() {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export class LocalRuntime {
|
|
88
|
-
constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true }) {
|
|
89
|
+
constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {} }) {
|
|
89
90
|
const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
|
|
90
91
|
const remoteSecret = secret || "";
|
|
91
92
|
this.workspaceInput = resolve(workspace || process.cwd());
|
|
@@ -100,6 +101,7 @@ export class LocalRuntime {
|
|
|
100
101
|
this.callProcesses = new Map();
|
|
101
102
|
this.cancelledCalls = new Set();
|
|
102
103
|
this.mutationQueue = Promise.resolve();
|
|
104
|
+
this.capabilityObserver = new CapabilityObserver();
|
|
103
105
|
this.runtimeDir = createRuntimeDir();
|
|
104
106
|
if (typeof jobRoot !== "string" || !jobRoot.trim()) throw new Error("persistent managed-job root is required");
|
|
105
107
|
this.managedJobManager = new ManagedJobManager({
|
|
@@ -139,6 +141,7 @@ export class LocalRuntime {
|
|
|
139
141
|
const readResourceText = (name) => this.readLocalResourceText(name);
|
|
140
142
|
const readResourceBinary = (name) => this.readLocalResourceBinary(name);
|
|
141
143
|
this.appAutomationManager = new AppAutomationManager({
|
|
144
|
+
...applicationAutomation,
|
|
142
145
|
policy: this.policy,
|
|
143
146
|
displayPath: (value) => this.displayPath(value),
|
|
144
147
|
runProcess,
|
|
@@ -198,9 +201,11 @@ export class LocalRuntime {
|
|
|
198
201
|
per_tool_events: "debug-only",
|
|
199
202
|
default_logs_include_tool_failures: false,
|
|
200
203
|
tool_arguments_or_results_logged: false,
|
|
204
|
+
capability_routing: this.capabilityObserver.snapshot(),
|
|
201
205
|
},
|
|
202
206
|
runtime: {
|
|
203
207
|
environment: this.policy.minimalEnv ? "isolated-minimal" : "full-parent",
|
|
208
|
+
relay: this.relay?.status?.() || null,
|
|
204
209
|
runtime_dir: this.policy.exposeAbsolutePaths ? this.runtimeDir : "<private-runtime-dir>",
|
|
205
210
|
process_sessions: this.processSessionManager.status(),
|
|
206
211
|
managed_jobs: this.managedJobManager.status(),
|
|
@@ -323,14 +328,8 @@ export class LocalRuntime {
|
|
|
323
328
|
this.cancelledCalls.add(callId);
|
|
324
329
|
this.processSessionManager.notifyCancellation();
|
|
325
330
|
this.browserBridgeManager?.cancelCall(callId);
|
|
326
|
-
for (const child of this.callProcesses.get(callId) || []) terminateProcessTree(child, "SIGTERM");
|
|
327
331
|
const children = [...(this.callProcesses.get(callId) || [])];
|
|
328
|
-
|
|
329
|
-
const timer = setTimeout(() => {
|
|
330
|
-
for (const child of children) if (this.activeProcesses.has(child)) terminateProcessTree(child, "SIGKILL");
|
|
331
|
-
}, 2000);
|
|
332
|
-
timer.unref?.();
|
|
333
|
-
}
|
|
332
|
+
for (const child of children) terminateProcessTreeWithEscalation(child);
|
|
334
333
|
this.logger.debug?.("tool call cancellation requested", { call_id: shortCallId(callId), reason });
|
|
335
334
|
}
|
|
336
335
|
|
|
@@ -351,6 +350,7 @@ export class LocalRuntime {
|
|
|
351
350
|
gitRoot: git.code === 0 ? this.displayPath(git.stdout.trim()) : "",
|
|
352
351
|
policy: this.policy,
|
|
353
352
|
tools: ["server_info", ...this.tools()],
|
|
353
|
+
capabilityRouting: this.capabilityObserver.snapshot(),
|
|
354
354
|
topLevel: top.entries || [],
|
|
355
355
|
};
|
|
356
356
|
}
|
|
@@ -795,13 +795,14 @@ export class LocalRuntime {
|
|
|
795
795
|
status_tool: "browser_status",
|
|
796
796
|
} : null,
|
|
797
797
|
};
|
|
798
|
+
this.capabilityObserver.recordBootstrap(bootstrap);
|
|
798
799
|
return bootstrap;
|
|
799
800
|
}
|
|
800
801
|
|
|
801
802
|
async resolveTaskCapabilities(args = {}, context = {}) {
|
|
802
803
|
const result = await this.agentContextManager.resolveTaskCapabilities(args, context);
|
|
803
804
|
const task = String(args.task || "");
|
|
804
|
-
if (
|
|
805
|
+
if (this.policy.profile === "full") {
|
|
805
806
|
const applications = await this.appAutomationManager.listApplications({ query: "", max_results: 500 }, context).catch(() => ({ applications: [] }));
|
|
806
807
|
const lower = task.toLowerCase();
|
|
807
808
|
result.application_matches = applications.applications
|
|
@@ -813,7 +814,12 @@ export class LocalRuntime {
|
|
|
813
814
|
} else {
|
|
814
815
|
result.application_matches = [];
|
|
815
816
|
}
|
|
817
|
+
if (result.application_matches.length) {
|
|
818
|
+
result.recommended_tools = [...new Set([...result.recommended_tools, "list_local_applications", "open_local_application", "inspect_local_application", "operate_local_application"])];
|
|
819
|
+
}
|
|
816
820
|
result.browser_backend = this.policy.profile === "full" ? { tool: "browser_status", existing_profile: true, extension_bridge: true } : null;
|
|
821
|
+
result.routing_observability = "Call server_info or project_overview to verify that bootstrap and task capability resolution reached the local runtime.";
|
|
822
|
+
this.capabilityObserver.recordResolution(task, result);
|
|
817
823
|
return result;
|
|
818
824
|
}
|
|
819
825
|
|
|
@@ -873,12 +879,9 @@ export class LocalRuntime {
|
|
|
873
879
|
|
|
874
880
|
terminateActiveProcesses(signal = "SIGTERM", escalate = false) {
|
|
875
881
|
const children = [...this.activeProcesses];
|
|
876
|
-
for (const child of children)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
for (const child of children) if (this.activeProcesses.has(child)) terminateProcessTree(child, "SIGKILL");
|
|
880
|
-
}, 2000);
|
|
881
|
-
timer.unref?.();
|
|
882
|
+
for (const child of children) {
|
|
883
|
+
if (escalate && signal !== "SIGKILL") terminateProcessTreeWithEscalation(child);
|
|
884
|
+
else terminateProcessTree(child, signal);
|
|
882
885
|
}
|
|
883
886
|
}
|
|
884
887
|
|
|
@@ -911,14 +914,12 @@ export class LocalRuntime {
|
|
|
911
914
|
let killTimer = null;
|
|
912
915
|
const timer = setTimeout(() => {
|
|
913
916
|
timedOut = true;
|
|
914
|
-
|
|
915
|
-
killTimer = setTimeout(() => terminateProcessTree(child, "SIGKILL"), 2000);
|
|
916
|
-
killTimer.unref?.();
|
|
917
|
+
killTimer = terminateProcessTreeWithEscalation(child);
|
|
917
918
|
}, timeoutMs);
|
|
918
919
|
timer.unref?.();
|
|
919
920
|
const cleanup = () => {
|
|
920
921
|
clearTimeout(timer);
|
|
921
|
-
if (killTimer) clearTimeout(killTimer);
|
|
922
|
+
if (killTimer && !timedOut) clearTimeout(killTimer);
|
|
922
923
|
this.activeProcesses.delete(child);
|
|
923
924
|
if (context.callId) {
|
|
924
925
|
const set = this.callProcesses.get(context.callId);
|