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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
43
43
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
44
44
|
"syntax": "node scripts/syntax-check.mjs",
|
|
45
|
-
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run catalog:test && npm run agent-context:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
45
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run catalog:test && npm run agent-context:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
46
46
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
47
47
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
48
48
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"full-access:test": "node tests/full-access-test.mjs",
|
|
56
56
|
"atomic-fs:test": "node tests/atomic-fs-test.mjs",
|
|
57
57
|
"privacy:check": "node scripts/privacy-check.mjs",
|
|
58
|
+
"privacy:history": "node scripts/privacy-check.mjs --history",
|
|
58
59
|
"package:test": "node tests/package-test.mjs",
|
|
59
60
|
"release-impact:check": "node scripts/release-impact-check.mjs",
|
|
60
61
|
"release-impact:test": "node tests/release-impact-test.mjs",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"secure-file:test": "node tests/secure-file-test.mjs",
|
|
66
67
|
"architecture:test": "node tests/architecture-test.mjs",
|
|
67
68
|
"release-state:test": "node tests/release-state-test.mjs",
|
|
69
|
+
"release-ci:test": "node tests/release-ci-test.mjs",
|
|
68
70
|
"agent-context:test": "node tests/agent-context-test.mjs",
|
|
69
71
|
"browser-bridge:test": "node tests/browser-bridge-test.mjs",
|
|
70
72
|
"app-automation:test": "node tests/app-automation-test.mjs",
|
|
@@ -73,6 +75,8 @@
|
|
|
73
75
|
"app-automation:live-test": "node tests/app-automation-test.mjs --live-macos"
|
|
74
76
|
},
|
|
75
77
|
"dependencies": {
|
|
78
|
+
"https-proxy-agent": "9.1.0",
|
|
79
|
+
"proxy-from-env": "2.1.0",
|
|
76
80
|
"wrangler": "4.110.0",
|
|
77
81
|
"ws": "8.21.0"
|
|
78
82
|
},
|
|
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { runNetworkCommand } from "./network-retry.mjs";
|
|
13
|
+
import { requireSuccessfulCiRun } from "./release-ci.mjs";
|
|
13
14
|
import { tagSyncError } from "./release-state.mjs";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
|
|
@@ -124,6 +125,31 @@ function remoteTagCommit(tag) {
|
|
|
124
125
|
return (peeled ?? direct)?.[0] ?? null;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function assertSuccessfulCi(head) {
|
|
129
|
+
const text = outputNetwork("gh", [
|
|
130
|
+
"run",
|
|
131
|
+
"list",
|
|
132
|
+
"--workflow",
|
|
133
|
+
".github/workflows/ci.yml",
|
|
134
|
+
"--commit",
|
|
135
|
+
head,
|
|
136
|
+
"--event",
|
|
137
|
+
"push",
|
|
138
|
+
"--limit",
|
|
139
|
+
"20",
|
|
140
|
+
"--json",
|
|
141
|
+
"databaseId,status,conclusion,headSha,event,createdAt,url",
|
|
142
|
+
]);
|
|
143
|
+
let runs;
|
|
144
|
+
try { runs = JSON.parse(text); }
|
|
145
|
+
catch { fail("GitHub Actions did not return valid JSON"); }
|
|
146
|
+
let run;
|
|
147
|
+
try { run = requireSuccessfulCiRun(runs, head); }
|
|
148
|
+
catch (error) { fail(String(error?.message || error)); }
|
|
149
|
+
console.log(`GitHub Actions CI succeeded for ${head} (run ${run.databaseId}).`);
|
|
150
|
+
return run;
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
function releaseInfo(tag) {
|
|
128
154
|
const result = runNetwork(
|
|
129
155
|
"gh",
|
|
@@ -148,6 +174,7 @@ function assertCoreSync({ requireReleaseAsset }) {
|
|
|
148
174
|
if (head !== originMain) {
|
|
149
175
|
fail(`HEAD ${head} does not match origin/main ${originMain}`);
|
|
150
176
|
}
|
|
177
|
+
assertSuccessfulCi(head);
|
|
151
178
|
|
|
152
179
|
const localCommit = localTagCommit(tag);
|
|
153
180
|
const localTagError = tagSyncError({ scope: "local", tag, head, commit: localCommit });
|
|
@@ -275,6 +302,7 @@ function publishCurrent() {
|
|
|
275
302
|
}
|
|
276
303
|
runNetwork("git", ["push", "origin", "HEAD:main"]);
|
|
277
304
|
}
|
|
305
|
+
assertSuccessfulCi(head);
|
|
278
306
|
|
|
279
307
|
const existingLocal = localTagCommit(tag);
|
|
280
308
|
if (existingLocal && existingLocal !== head) {
|
|
@@ -5,6 +5,11 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
7
|
const selfPath = "scripts/privacy-check.mjs";
|
|
8
|
+
const requestedOptions = new Set(process.argv.slice(2));
|
|
9
|
+
for (const option of requestedOptions) {
|
|
10
|
+
if (option !== "--history") throw new Error(`unknown privacy-check option: ${option}`);
|
|
11
|
+
}
|
|
12
|
+
const scanHistory = requestedOptions.has("--history");
|
|
8
13
|
const candidates = collectCandidateFiles(root);
|
|
9
14
|
const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
|
|
10
15
|
const findings = [];
|
|
@@ -47,6 +52,8 @@ for (const relativePath of candidates) {
|
|
|
47
52
|
scanDenylist(relativePath, text, denylist, findings);
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
const historySummaryCounts = scanHistory ? scanReachableHistory(root, denylist, findings) : { blobs: 0, commits: 0 };
|
|
56
|
+
|
|
50
57
|
if (findings.length) {
|
|
51
58
|
for (const finding of findings.slice(0, 100)) {
|
|
52
59
|
process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
|
|
@@ -56,7 +63,8 @@ if (findings.length) {
|
|
|
56
63
|
process.exit(1);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
const historySummary = scanHistory ? `; ${historySummaryCounts.blobs} reachable history blobs; ${historySummaryCounts.commits} commit messages` : "";
|
|
67
|
+
process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries${historySummary})\n`);
|
|
60
68
|
|
|
61
69
|
function collectCandidateFiles(directory) {
|
|
62
70
|
try {
|
|
@@ -87,6 +95,136 @@ function collectCandidateFiles(directory) {
|
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
function scanReachableHistory(directory, entries, out) {
|
|
99
|
+
let listing;
|
|
100
|
+
try {
|
|
101
|
+
listing = execFileSync("git", ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
|
|
102
|
+
encoding: "buffer",
|
|
103
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("privacy history scan could not enumerate reachable Git objects");
|
|
108
|
+
}
|
|
109
|
+
let records;
|
|
110
|
+
try { records = new TextDecoder("utf-8", { fatal: true }).decode(listing).split("\0").filter(Boolean); }
|
|
111
|
+
catch { throw new Error("privacy history scan encountered a non-UTF-8 Git object path"); }
|
|
112
|
+
const objectPaths = new Map();
|
|
113
|
+
let objectHash = "";
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
if (/^[0-9a-f]{40,64}$/.test(record)) {
|
|
116
|
+
objectHash = record;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!record.startsWith("path=") || !objectHash) {
|
|
120
|
+
objectHash = "";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const relativePath = record.slice(5);
|
|
124
|
+
if (relativePath && !relativePath.includes("\0")) {
|
|
125
|
+
if (!objectPaths.has(objectHash)) objectPaths.set(objectHash, new Set());
|
|
126
|
+
objectPaths.get(objectHash).add(relativePath);
|
|
127
|
+
}
|
|
128
|
+
objectHash = "";
|
|
129
|
+
}
|
|
130
|
+
const hashes = [...objectPaths.keys()];
|
|
131
|
+
let metadata;
|
|
132
|
+
try {
|
|
133
|
+
metadata = execFileSync("git", ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
|
|
134
|
+
input: `${hashes.join("\n")}\n`,
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
137
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
}).trim().split(/\r?\n/);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error("privacy history scan could not classify reachable Git objects");
|
|
141
|
+
}
|
|
142
|
+
const blobSizes = new Map();
|
|
143
|
+
for (const line of metadata) {
|
|
144
|
+
const match = /^([0-9a-f]{40,64}) blob ([0-9]+)$/.exec(line);
|
|
145
|
+
if (match) blobSizes.set(match[1], Number(match[2]));
|
|
146
|
+
}
|
|
147
|
+
let scanned = 0;
|
|
148
|
+
for (const [hash, paths] of objectPaths) {
|
|
149
|
+
if (!blobSizes.has(hash)) continue;
|
|
150
|
+
const relativePaths = [...paths].sort();
|
|
151
|
+
const reportPath = relativePaths[0] || "unknown";
|
|
152
|
+
const contentPath = relativePaths.find((relativePath) => relativePath !== selfPath) || reportPath;
|
|
153
|
+
for (const relativePath of relativePaths) {
|
|
154
|
+
if (relativePath === ".privacy-denylist") continue;
|
|
155
|
+
const historicalPath = `history/${hash.slice(0, 12)}/${relativePath}`;
|
|
156
|
+
scanDenylistPath(historicalPath, entries, out);
|
|
157
|
+
scanSensitivePath(historicalPath, out);
|
|
158
|
+
}
|
|
159
|
+
if (blobSizes.get(hash) > 5 * 1024 * 1024) {
|
|
160
|
+
out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file exceeds privacy scanner size limit and requires manual review" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let buffer;
|
|
164
|
+
try {
|
|
165
|
+
buffer = execFileSync("git", ["-C", directory, "cat-file", "blob", hash], {
|
|
166
|
+
encoding: "buffer",
|
|
167
|
+
maxBuffer: 6 * 1024 * 1024,
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file content could not be read" });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
scanned += 1;
|
|
175
|
+
const historicalPath = `history/${hash.slice(0, 12)}/${contentPath}`;
|
|
176
|
+
if (buffer.includes(0)) {
|
|
177
|
+
out.push({ path: historicalPath, line: 1, rule: "binary file in reachable Git history requires manual review" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let historicalText;
|
|
181
|
+
try { historicalText = new TextDecoder("utf-8", { fatal: true }).decode(buffer); }
|
|
182
|
+
catch {
|
|
183
|
+
out.push({ path: historicalPath, line: 1, rule: "non-UTF-8 file in reachable Git history requires manual review" });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (contentPath !== selfPath) scanBuiltIn(historicalPath, historicalText, out);
|
|
187
|
+
if (relativePaths.some((relativePath) => path.basename(relativePath).toLowerCase() === ".npmrc")) scanNpmrc(historicalPath, historicalText, out);
|
|
188
|
+
scanDenylist(historicalPath, historicalText, entries, out);
|
|
189
|
+
}
|
|
190
|
+
const commitCount = scanReachableCommitMessages(directory, entries, out);
|
|
191
|
+
return { blobs: scanned, commits: commitCount };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scanReachableCommitMessages(directory, entries, out) {
|
|
195
|
+
let output;
|
|
196
|
+
try {
|
|
197
|
+
output = execFileSync("git", ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
|
|
198
|
+
encoding: "buffer",
|
|
199
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
200
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error("privacy history scan could not read reachable commit messages");
|
|
204
|
+
}
|
|
205
|
+
let records;
|
|
206
|
+
try { records = new TextDecoder("utf-8", { fatal: true }).decode(output).split("\0"); }
|
|
207
|
+
catch { throw new Error("privacy history scan encountered a non-UTF-8 commit message"); }
|
|
208
|
+
let count = 0;
|
|
209
|
+
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
210
|
+
const hash = records[index].trim();
|
|
211
|
+
const message = records[index + 1];
|
|
212
|
+
if (!/^[0-9a-f]{40,64}$/.test(hash)) continue;
|
|
213
|
+
const reportPath = `history/commit/${hash.slice(0, 12)}/message`;
|
|
214
|
+
const scannedMessage = removeKnownPublicAutomationTrailers(message);
|
|
215
|
+
scanBuiltIn(reportPath, scannedMessage, out);
|
|
216
|
+
scanDenylist(reportPath, scannedMessage, entries, out);
|
|
217
|
+
count += 1;
|
|
218
|
+
}
|
|
219
|
+
return count;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function removeKnownPublicAutomationTrailers(message) {
|
|
223
|
+
return String(message).split(/\r?\n/).filter((line) => {
|
|
224
|
+
return !/^(?:Signed-off-by|Co-authored-by):\s+dependabot\[bot\]\s+<[^>\s]+@github\.com>\s*$/i.test(line);
|
|
225
|
+
}).join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
90
228
|
function redactReportPath(relativePath, entries) {
|
|
91
229
|
let shown = String(relativePath);
|
|
92
230
|
for (const entry of entries) {
|
|
@@ -112,7 +250,6 @@ function scanBuiltIn(relativePath, text, out) {
|
|
|
112
250
|
["live payment API key", /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g],
|
|
113
251
|
["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
|
|
114
252
|
["JWT-like bearer token", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g],
|
|
115
|
-
["URL with embedded credentials", /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi],
|
|
116
253
|
["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
|
|
117
254
|
["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
|
|
118
255
|
];
|
|
@@ -122,6 +259,11 @@ function scanBuiltIn(relativePath, text, out) {
|
|
|
122
259
|
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
|
|
123
260
|
}
|
|
124
261
|
}
|
|
262
|
+
const credentialUrl = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@([^\s/"'<>]+)/gi;
|
|
263
|
+
for (const match of text.matchAll(credentialUrl)) {
|
|
264
|
+
if (isReservedExampleHost(stripHostPort(match[1]))) continue;
|
|
265
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "URL with embedded credentials" });
|
|
266
|
+
}
|
|
125
267
|
const emailLike = /\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b/gi;
|
|
126
268
|
for (const match of text.matchAll(emailLike)) {
|
|
127
269
|
if (isReservedExampleHost(match[1]) || String(match[1]).toLowerCase().endsWith(".noreply.github.com")) continue;
|
|
@@ -215,6 +357,12 @@ function hasNearbySshContext(text, offset) {
|
|
|
215
357
|
return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
|
|
216
358
|
}
|
|
217
359
|
|
|
360
|
+
function stripHostPort(host) {
|
|
361
|
+
const value = String(host || "");
|
|
362
|
+
if (value.startsWith("[")) return value.slice(1, value.indexOf("]") > 0 ? value.indexOf("]") : undefined);
|
|
363
|
+
return value.replace(/:[0-9]+$/, "");
|
|
364
|
+
}
|
|
365
|
+
|
|
218
366
|
function isReservedExampleHost(host) {
|
|
219
367
|
const value = String(host || "").toLowerCase().replace(/\.$/, "");
|
|
220
368
|
return value === "localhost"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function requireSuccessfulCiRun(runs, head) {
|
|
2
|
+
if (!Array.isArray(runs)) throw new Error("GitHub Actions response is not an array");
|
|
3
|
+
if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
|
|
4
|
+
const matching = runs
|
|
5
|
+
.filter((run) => run && run.headSha === head && run.event === "push")
|
|
6
|
+
.sort((left, right) => String(right.createdAt || "").localeCompare(String(left.createdAt || "")));
|
|
7
|
+
const run = matching[0];
|
|
8
|
+
if (!run) {
|
|
9
|
+
throw new Error(`no push-triggered CI run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
|
|
10
|
+
}
|
|
11
|
+
if (run.status !== "completed") {
|
|
12
|
+
throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
|
|
13
|
+
}
|
|
14
|
+
if (run.conclusion !== "success") {
|
|
15
|
+
throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun CI before release`);
|
|
16
|
+
}
|
|
17
|
+
return run;
|
|
18
|
+
}
|
|
@@ -3,6 +3,7 @@ import { constants as fsConstants } from "node:fs";
|
|
|
3
3
|
import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { createBuiltinInstruction, discoverAutomaticProjectInstruction } from "./default-instructions.mjs";
|
|
6
|
+
import { automaticPackageCommands, readProjectPackageMetadata } from "./project-package.mjs";
|
|
6
7
|
|
|
7
8
|
const CONFIG_RELATIVE_PATH = join(".machine-bridge", "agent.json");
|
|
8
9
|
const GLOBAL_CONFIG_RELATIVE_PATH = join(".config", "machine-bridge-mcp", "agent.json");
|
|
@@ -24,6 +25,35 @@ const MAX_COMMAND_ARGV = 128;
|
|
|
24
25
|
const MAX_COMMAND_ARGUMENT_BYTES = 256 * 1024;
|
|
25
26
|
const COMMAND_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
26
27
|
const TOKEN_STOP_WORDS = new Set(["the", "and", "for", "with", "from", "this", "that", "use", "using", "into", "of", "to", "a", "an", "in", "on", "is", "are", "or", "及", "和", "的", "了", "在", "用", "使用", "进行", "根据"]);
|
|
28
|
+
const ENGLISH_TOKEN_CANONICAL = new Map([
|
|
29
|
+
["created", "create"], ["creating", "create"], ["creation", "create"], ["creator", "create"],
|
|
30
|
+
["improved", "improve"], ["improving", "improve"], ["improvement", "improve"],
|
|
31
|
+
["installed", "install"], ["installing", "install"], ["installation", "install"], ["installer", "install"],
|
|
32
|
+
["searched", "search"], ["searching", "search"], ["finder", "find"],
|
|
33
|
+
["documentation", "docs"], ["document", "docs"], ["documents", "docs"],
|
|
34
|
+
["validated", "verify"], ["validate", "verify"], ["validation", "verify"], ["verification", "verify"],
|
|
35
|
+
["factcheck", "verify"], ["fact-check", "verify"], ["testing", "test"], ["tests", "test"],
|
|
36
|
+
["building", "build"], ["built", "build"], ["deployment", "deploy"], ["deployed", "deploy"],
|
|
37
|
+
]);
|
|
38
|
+
const HAN_TOKEN_ALIASES = Object.freeze([
|
|
39
|
+
[/创建|新建|编写/u, ["create", "creator"]],
|
|
40
|
+
[/改进|优化|更新|维护/u, ["improve", "update"]],
|
|
41
|
+
[/技能/u, ["skill"]],
|
|
42
|
+
[/安装/u, ["install", "installer"]],
|
|
43
|
+
[/部署/u, ["deploy", "deployment"]],
|
|
44
|
+
[/查找|搜索|检索/u, ["search", "find"]],
|
|
45
|
+
[/最新|当前/u, ["latest", "current"]],
|
|
46
|
+
[/官方/u, ["official"]],
|
|
47
|
+
[/文档|资料/u, ["docs", "documentation"]],
|
|
48
|
+
[/事实核查|核查|验证|校验/u, ["verify", "factcheck"]],
|
|
49
|
+
[/测试/u, ["test"]],
|
|
50
|
+
[/前端/u, ["frontend"]],
|
|
51
|
+
[/设计/u, ["design"]],
|
|
52
|
+
[/邮件|邮箱/u, ["email"]],
|
|
53
|
+
[/浏览器|网页|网站/u, ["browser", "web"]],
|
|
54
|
+
[/性能/u, ["performance"]],
|
|
55
|
+
[/安全|漏洞/u, ["security", "audit"]],
|
|
56
|
+
]);
|
|
27
57
|
const CONFIG_KEYS = new Set(["version", "builtin_instructions", "automatic_project_context", "model_instructions_file", "instruction_files", "instruction_max_bytes", "skill_roots", "commands"]);
|
|
28
58
|
const COMMAND_KEYS = new Set(["description", "argv", "cwd", "timeout_seconds", "allow_extra_args"]);
|
|
29
59
|
|
|
@@ -117,7 +147,7 @@ export class AgentContextManager {
|
|
|
117
147
|
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
|
|
118
148
|
.slice(0, maxSkills);
|
|
119
149
|
const commandMatches = [...state.commands.values()]
|
|
120
|
-
.map((command) => ({ command, score: relevanceScore(task,
|
|
150
|
+
.map((command) => ({ command, score: relevanceScore(task, commandMatchText(command), command.name) }))
|
|
121
151
|
.filter((item) => item.score > 0)
|
|
122
152
|
.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
|
|
123
153
|
.slice(0, 20);
|
|
@@ -127,7 +157,11 @@ export class AgentContextManager {
|
|
|
127
157
|
const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
|
|
128
158
|
selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
|
|
129
159
|
}
|
|
130
|
-
const recommendedTools = recommendTools(task,
|
|
160
|
+
const recommendedTools = recommendTools(task, {
|
|
161
|
+
commandsAvailable: state.commands.size > 0,
|
|
162
|
+
commandRelevant: (commandMatches[0]?.score || 0) >= 3,
|
|
163
|
+
skillRelevant: Boolean(selected),
|
|
164
|
+
});
|
|
131
165
|
const refresh = capabilityFingerprint(state, discovered.skills);
|
|
132
166
|
return {
|
|
133
167
|
task,
|
|
@@ -239,7 +273,7 @@ export class AgentContextManager {
|
|
|
239
273
|
scopeRoot,
|
|
240
274
|
instructionFiles: [...DEFAULT_INSTRUCTION_FILES],
|
|
241
275
|
instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
|
|
242
|
-
skillRoots: defaultSkillRoots(directories, this.home, this.policy.unrestrictedPaths === true),
|
|
276
|
+
skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, this.policy.unrestrictedPaths === true),
|
|
243
277
|
commands: new Map(),
|
|
244
278
|
builtinInstructionsEnabled: true,
|
|
245
279
|
automaticProjectContextEnabled: true,
|
|
@@ -253,6 +287,9 @@ export class AgentContextManager {
|
|
|
253
287
|
instructionsTruncated: false,
|
|
254
288
|
};
|
|
255
289
|
|
|
290
|
+
const packageMetadata = await readProjectPackageMetadata(scopeRoot, () => this.throwIfCancelled(context));
|
|
291
|
+
state.commands = automaticPackageCommands(packageMetadata, scopeRoot);
|
|
292
|
+
|
|
256
293
|
if (this.home) {
|
|
257
294
|
const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
|
|
258
295
|
const config = await readOptionalConfig(globalConfig, this.home, false);
|
|
@@ -488,9 +525,14 @@ function directoriesBetween(root, leaf) {
|
|
|
488
525
|
return result.reverse();
|
|
489
526
|
}
|
|
490
527
|
|
|
491
|
-
function defaultSkillRoots(directories, home, unrestricted) {
|
|
492
|
-
const roots = [
|
|
528
|
+
function defaultSkillRoots(directories, home, codexHome, unrestricted) {
|
|
529
|
+
const roots = [];
|
|
530
|
+
for (const directory of [...directories].reverse()) {
|
|
531
|
+
roots.push(resolve(directory, ".agents", "skills"));
|
|
532
|
+
roots.push(resolve(directory, ".codex", "skills"));
|
|
533
|
+
}
|
|
493
534
|
if (unrestricted && home) roots.push(resolve(home, ".agents", "skills"));
|
|
535
|
+
if (unrestricted && codexHome) roots.push(resolve(codexHome, "skills"));
|
|
494
536
|
if (unrestricted && process.platform !== "win32") roots.push(resolve("/etc/codex/skills"));
|
|
495
537
|
return [...new Set(roots)];
|
|
496
538
|
}
|
|
@@ -718,13 +760,19 @@ function capabilityFingerprint(state, skills) {
|
|
|
718
760
|
}));
|
|
719
761
|
}
|
|
720
762
|
|
|
763
|
+
function commandMatchText(command) {
|
|
764
|
+
return `${command.name} ${command.description} ${command.argv.join(" ")} ${command.searchTerms || ""}`;
|
|
765
|
+
}
|
|
766
|
+
|
|
721
767
|
function relevanceScore(task, candidate, identity = "") {
|
|
722
768
|
const taskTokens = tokenize(task);
|
|
723
769
|
const candidateTokens = tokenize(candidate);
|
|
724
770
|
if (!taskTokens.size || !candidateTokens.size) return 0;
|
|
771
|
+
const identityTokens = tokenize(identity);
|
|
725
772
|
let score = 0;
|
|
726
773
|
for (const token of taskTokens) {
|
|
727
774
|
if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
|
|
775
|
+
if (identityTokens.has(token)) score += token.length >= 6 ? 4 : 3;
|
|
728
776
|
}
|
|
729
777
|
const taskComparable = comparableText(task);
|
|
730
778
|
const candidateComparable = comparableText(candidate);
|
|
@@ -747,33 +795,40 @@ function tokenize(value) {
|
|
|
747
795
|
const tokens = new Set();
|
|
748
796
|
for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
|
|
749
797
|
const token = raw.replace(/^[.-]+|[.-]+$/g, "");
|
|
750
|
-
|
|
751
|
-
for (const part of token.split(/[._-]+/))
|
|
752
|
-
if (part.length >= 2 && !TOKEN_STOP_WORDS.has(part)) tokens.add(part);
|
|
753
|
-
}
|
|
798
|
+
addToken(tokens, token);
|
|
799
|
+
for (const part of token.split(/[._-]+/)) addToken(tokens, part);
|
|
754
800
|
}
|
|
755
801
|
for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
|
|
756
|
-
|
|
802
|
+
addToken(tokens, sequence);
|
|
757
803
|
const minimumSize = sequence.length === 1 ? 1 : 2;
|
|
758
804
|
for (let size = minimumSize; size <= Math.min(3, sequence.length); size += 1) {
|
|
759
|
-
for (let index = 0; index + size <= sequence.length; index += 1)
|
|
760
|
-
const token = sequence.slice(index, index + size);
|
|
761
|
-
if (!TOKEN_STOP_WORDS.has(token)) tokens.add(token);
|
|
762
|
-
}
|
|
805
|
+
for (let index = 0; index + size <= sequence.length; index += 1) addToken(tokens, sequence.slice(index, index + size));
|
|
763
806
|
}
|
|
764
807
|
}
|
|
808
|
+
for (const [pattern, aliases] of HAN_TOKEN_ALIASES) {
|
|
809
|
+
if (!pattern.test(text)) continue;
|
|
810
|
+
for (const alias of aliases) addToken(tokens, alias);
|
|
811
|
+
}
|
|
765
812
|
return tokens;
|
|
766
813
|
}
|
|
767
814
|
|
|
768
|
-
function
|
|
815
|
+
function addToken(tokens, raw) {
|
|
816
|
+
const token = String(raw || "").replace(/^[.-]+|[.-]+$/g, "");
|
|
817
|
+
if (token.length < 2 || TOKEN_STOP_WORDS.has(token)) return;
|
|
818
|
+
tokens.add(token);
|
|
819
|
+
const canonical = ENGLISH_TOKEN_CANONICAL.get(token);
|
|
820
|
+
if (canonical) tokens.add(canonical);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function recommendTools(task, { commandsAvailable, commandRelevant, skillRelevant }) {
|
|
769
824
|
const lower = task.toLowerCase();
|
|
770
825
|
const tools = ["agent_context"];
|
|
771
|
-
if (
|
|
772
|
-
if (
|
|
826
|
+
if (skillRelevant) tools.push("load_local_skill");
|
|
827
|
+
if (commandRelevant) tools.push("run_local_command");
|
|
773
828
|
if (/browser|chrome|edge|brave|网页|浏览器|表单|网站/.test(lower)) tools.push("browser_status", "browser_list_tabs", "browser_inspect_page", "browser_action", "browser_fill_form");
|
|
774
829
|
if (/app|application|gui|window|应用|软件|窗口|界面/.test(lower)) tools.push("list_local_applications", "inspect_local_application", "operate_local_application");
|
|
775
830
|
if (/git|commit|branch|diff|仓库|提交|分支/.test(lower)) tools.push("git_status", "git_diff");
|
|
776
|
-
if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(
|
|
831
|
+
if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(commandsAvailable ? "run_local_command" : "run_process");
|
|
777
832
|
if (/file|code|source|edit|write|文件|代码|源码|修改|写入/.test(lower)) tools.push("read_file", "search_text", "edit_file", "apply_patch");
|
|
778
833
|
return [...new Set(tools)];
|
|
779
834
|
}
|
|
@@ -828,6 +883,8 @@ function publicCommands(commands, displayPath) {
|
|
|
828
883
|
timeout_seconds: command.timeoutSeconds,
|
|
829
884
|
allow_extra_args: command.allowExtraArgs,
|
|
830
885
|
source: displayPath(command.source),
|
|
886
|
+
source_type: command.sourceType || "agent-config",
|
|
887
|
+
...(command.script ? { package_script: command.script } : {}),
|
|
831
888
|
}));
|
|
832
889
|
}
|
|
833
890
|
|
|
@@ -9,9 +9,10 @@ const MAX_UI_ELEMENTS = 500;
|
|
|
9
9
|
const MAX_UI_DEPTH = 12;
|
|
10
10
|
const MAX_TEXT_CHARS = 4000;
|
|
11
11
|
const APP_NAME_PATTERN = /^[^\0\r\n]{1,300}$/;
|
|
12
|
+
const DEFAULT_APPLICATION_CACHE_MS = 30_000;
|
|
12
13
|
|
|
13
14
|
export class AppAutomationManager {
|
|
14
|
-
constructor({ policy, displayPath, runProcess, readResourceText, throwIfCancelled = () => {}, platform = process.platform, home = homedir(), applicationRoots = null }) {
|
|
15
|
+
constructor({ policy, displayPath, runProcess, readResourceText, throwIfCancelled = () => {}, platform = process.platform, home = homedir(), applicationRoots = null, applicationCacheMs = DEFAULT_APPLICATION_CACHE_MS, now = () => Date.now() }) {
|
|
15
16
|
this.policy = policy || {};
|
|
16
17
|
this.displayPath = displayPath;
|
|
17
18
|
this.runProcess = runProcess;
|
|
@@ -20,6 +21,9 @@ export class AppAutomationManager {
|
|
|
20
21
|
this.platform = platform;
|
|
21
22
|
this.home = home;
|
|
22
23
|
this.applicationRoots = applicationRoots;
|
|
24
|
+
this.applicationCacheMs = Math.max(0, Number(applicationCacheMs) || 0);
|
|
25
|
+
this.now = now;
|
|
26
|
+
this.applicationCache = null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
capabilities() {
|
|
@@ -57,6 +61,16 @@ export class AppAutomationManager {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
async discoverApplications(context = {}) {
|
|
64
|
+
const now = this.now();
|
|
65
|
+
if (this.applicationCache && now - this.applicationCache.createdAt < this.applicationCacheMs) {
|
|
66
|
+
return this.applicationCache.result;
|
|
67
|
+
}
|
|
68
|
+
const result = await this.scanApplications(context);
|
|
69
|
+
this.applicationCache = { createdAt: this.now(), result };
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async scanApplications(context = {}) {
|
|
60
74
|
if (this.platform === "darwin") {
|
|
61
75
|
return listMacApplications(this.applicationRoots || ["/Applications", "/System/Applications", join(this.home, "Applications")], context, this.throwIfCancelled);
|
|
62
76
|
}
|
package/src/local/atomic-fs.mjs
CHANGED
|
@@ -5,8 +5,11 @@ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
|
5
5
|
|
|
6
6
|
export function replaceFileSync(source, target, options = {}) {
|
|
7
7
|
const rename = typeof options.rename === "function" ? options.rename : renameSync;
|
|
8
|
-
const
|
|
8
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : sleepSync;
|
|
9
|
+
const random = typeof options.random === "function" ? options.random : Math.random;
|
|
10
|
+
const attempts = clampInteger(options.attempts, 16, 1, 64);
|
|
9
11
|
const baseDelayMs = clampInteger(options.baseDelayMs, 15, 0, 1000);
|
|
12
|
+
const maxDelayMs = Math.max(baseDelayMs, clampInteger(options.maxDelayMs, 250, 0, 2000));
|
|
10
13
|
let lastError;
|
|
11
14
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
12
15
|
try {
|
|
@@ -15,7 +18,7 @@ export function replaceFileSync(source, target, options = {}) {
|
|
|
15
18
|
} catch (error) {
|
|
16
19
|
lastError = error;
|
|
17
20
|
if (!isTransientReplaceError(error) || attempt === attempts) throw error;
|
|
18
|
-
|
|
21
|
+
sleep(retryDelayMs(attempt, baseDelayMs, maxDelayMs, random));
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
throw lastError || new Error("atomic file replacement failed");
|
|
@@ -25,6 +28,15 @@ export function isTransientReplaceError(error) {
|
|
|
25
28
|
return TRANSIENT_REPLACE_ERRORS.has(String(error?.code || ""));
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
function retryDelayMs(attempt, baseDelayMs, maxDelayMs, random) {
|
|
32
|
+
if (baseDelayMs <= 0 || maxDelayMs <= 0) return 0;
|
|
33
|
+
const exponential = Math.min(baseDelayMs * (2 ** Math.min(attempt - 1, 5)), maxDelayMs);
|
|
34
|
+
const randomValue = Number(random());
|
|
35
|
+
const normalizedRandom = Number.isFinite(randomValue) ? Math.min(Math.max(randomValue, 0), 1) : 0;
|
|
36
|
+
const jitter = Math.floor(exponential * 0.5 * normalizedRandom);
|
|
37
|
+
return Math.min(exponential + jitter, maxDelayMs);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
function sleepSync(milliseconds) {
|
|
29
41
|
if (milliseconds <= 0) return;
|
|
30
42
|
Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
|
|
@@ -355,7 +355,9 @@ export class BrowserBridgeManager {
|
|
|
355
355
|
if (settled) return;
|
|
356
356
|
settled = true;
|
|
357
357
|
clearTimeout(timer);
|
|
358
|
-
if (!ok)
|
|
358
|
+
if (!ok) {
|
|
359
|
+
try { ws.terminate(); } catch { try { ws.close(); } catch {} }
|
|
360
|
+
}
|
|
359
361
|
resolvePromise(ok);
|
|
360
362
|
};
|
|
361
363
|
ws.on("message", (data) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export class CapabilityObserver {
|
|
4
|
+
constructor(now = () => Date.now(), taskFingerprintKey = randomBytes(32)) {
|
|
5
|
+
this.now = now;
|
|
6
|
+
this.taskFingerprintKey = Buffer.from(taskFingerprintKey);
|
|
7
|
+
this.bootstrapCount = 0;
|
|
8
|
+
this.resolutionCount = 0;
|
|
9
|
+
this.lastBootstrap = null;
|
|
10
|
+
this.lastResolution = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
recordBootstrap(result) {
|
|
14
|
+
this.bootstrapCount += 1;
|
|
15
|
+
this.lastBootstrap = {
|
|
16
|
+
observed_at: timestamp(this.now()),
|
|
17
|
+
target: result?.target || ".",
|
|
18
|
+
instruction_bytes: Buffer.byteLength(String(result?.instructions || "")),
|
|
19
|
+
builtin_loaded: Boolean(result?.builtin_instructions),
|
|
20
|
+
automatic_project_context_loaded: Boolean(result?.automatic_project_context),
|
|
21
|
+
global_model_instructions_loaded: Boolean(result?.model_instructions_file),
|
|
22
|
+
refresh_fingerprint: String(result?.capability_refresh?.instruction_and_command_fingerprint || ""),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
recordResolution(task, result) {
|
|
27
|
+
this.resolutionCount += 1;
|
|
28
|
+
this.lastResolution = {
|
|
29
|
+
observed_at: timestamp(this.now()),
|
|
30
|
+
task_fingerprint: createHmac("sha256", this.taskFingerprintKey).update(String(task || "")).digest("hex"),
|
|
31
|
+
target: result?.target || ".",
|
|
32
|
+
selected_skill: result?.selected_skill?.name || null,
|
|
33
|
+
matched_skills: Array.isArray(result?.skill_matches) ? result.skill_matches.length : 0,
|
|
34
|
+
matched_commands: Array.isArray(result?.command_matches) ? result.command_matches.length : 0,
|
|
35
|
+
matched_applications: Array.isArray(result?.application_matches) ? result.application_matches.length : 0,
|
|
36
|
+
recommended_tools: Array.isArray(result?.recommended_tools) ? [...result.recommended_tools] : [],
|
|
37
|
+
refresh_fingerprint: String(result?.refresh?.fingerprint || ""),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
snapshot() {
|
|
42
|
+
return {
|
|
43
|
+
bootstrap_observed: this.bootstrapCount > 0,
|
|
44
|
+
bootstrap_count: this.bootstrapCount,
|
|
45
|
+
task_resolution_observed: this.resolutionCount > 0,
|
|
46
|
+
task_resolution_count: this.resolutionCount,
|
|
47
|
+
last_bootstrap: this.lastBootstrap ? structuredClone(this.lastBootstrap) : null,
|
|
48
|
+
last_task_resolution: this.lastResolution ? structuredClone(this.lastResolution) : null,
|
|
49
|
+
enforcement_boundary: "The server can discover, rank, load, and report capabilities; the MCP host decides whether to call the resolver or recommended tools.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function timestamp(milliseconds) {
|
|
55
|
+
return new Date(Number(milliseconds)).toISOString();
|
|
56
|
+
}
|