machine-bridge-mcp 0.3.3 → 0.4.2
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 +61 -0
- package/README.md +158 -171
- package/SECURITY.md +92 -40
- package/docs/ARCHITECTURE.md +126 -59
- package/docs/CLIENTS.md +125 -0
- package/docs/OPERATIONS.md +83 -0
- package/docs/RELEASING.md +62 -0
- package/docs/TESTING.md +51 -0
- package/package.json +18 -8
- package/src/local/cli.mjs +152 -51
- package/src/local/daemon.mjs +620 -306
- package/src/local/patch.mjs +140 -0
- package/src/local/process-sessions.mjs +352 -0
- package/src/local/service.mjs +5 -16
- package/src/local/shell.mjs +22 -5
- package/src/local/state.mjs +1 -1
- package/src/local/stdio.mjs +194 -0
- package/src/local/tools.mjs +96 -0
- package/src/shared/tool-catalog.json +638 -0
- package/src/worker/index.ts +151 -162
- package/tsconfig.json +4 -2
- package/src/local/self-test.mjs +0 -227
package/src/local/self-test.mjs
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { run } from "./shell.mjs";
|
|
5
|
-
import { parseArgs, validateCommandOptions, validatePositionals } from "./cli.mjs";
|
|
6
|
-
import { daemonSelfTest } from "./daemon.mjs";
|
|
7
|
-
import { formatFields, redactSecret, sanitizeLogText } from "./log.mjs";
|
|
8
|
-
import { systemdQuote, trimAutostartLogs } from "./service.mjs";
|
|
9
|
-
import { acquireDaemonLock, acquireStartupLock, ensureWorkerSecrets, loadState, previewSecret, redactState, removeStateRoot, saveState, selectedWorkspace, setSelectedWorkspace, validateStateRootForRemoval } from "./state.mjs";
|
|
10
|
-
|
|
11
|
-
await daemonSelfTest();
|
|
12
|
-
await stateSelfTest();
|
|
13
|
-
await cliSelfTest();
|
|
14
|
-
await logSelfTest();
|
|
15
|
-
await serviceSelfTest();
|
|
16
|
-
await shellSelfTest();
|
|
17
|
-
await workerSourceSelfTest();
|
|
18
|
-
console.log("local daemon/state/cli/log/service/worker self-test ok");
|
|
19
|
-
|
|
20
|
-
async function stateSelfTest() {
|
|
21
|
-
const stateRoot = await mkdtemp(join(tmpdir(), "mbm-state-test-"));
|
|
22
|
-
const workspace = await mkdtemp(join(tmpdir(), "mbm-state-workspace-"));
|
|
23
|
-
try {
|
|
24
|
-
setSelectedWorkspace(workspace, stateRoot);
|
|
25
|
-
if (selectedWorkspace(stateRoot) !== workspace) throw new Error("selected workspace was not persisted");
|
|
26
|
-
const state = loadState(workspace, { stateDir: stateRoot });
|
|
27
|
-
if (state.schemaVersion !== 2) throw new Error("unexpected state schema version");
|
|
28
|
-
ensureWorkerSecrets(state, { rotateSecrets: true });
|
|
29
|
-
const lock = acquireDaemonLock(state);
|
|
30
|
-
if (!lock.acquired) throw new Error("first daemon lock acquisition failed");
|
|
31
|
-
try {
|
|
32
|
-
const duplicate = acquireDaemonLock(state);
|
|
33
|
-
if (duplicate.acquired) throw new Error("duplicate daemon lock acquisition should fail");
|
|
34
|
-
if (duplicate.owner?.pid !== process.pid) throw new Error("duplicate daemon lock owner was not reported");
|
|
35
|
-
} finally {
|
|
36
|
-
lock.release();
|
|
37
|
-
}
|
|
38
|
-
const relock = acquireDaemonLock(state);
|
|
39
|
-
if (!relock.acquired) throw new Error("daemon lock was not released");
|
|
40
|
-
relock.release();
|
|
41
|
-
|
|
42
|
-
const startup = acquireStartupLock(state);
|
|
43
|
-
if (!startup.acquired) throw new Error("startup lock acquisition failed");
|
|
44
|
-
const duplicateStartup = acquireStartupLock(state);
|
|
45
|
-
if (duplicateStartup.acquired) throw new Error("duplicate startup lock acquisition should fail");
|
|
46
|
-
startup.release();
|
|
47
|
-
const startupAgain = acquireStartupLock(state);
|
|
48
|
-
if (!startupAgain.acquired) throw new Error("startup lock was not released");
|
|
49
|
-
startupAgain.release();
|
|
50
|
-
|
|
51
|
-
const redacted = redactState(state);
|
|
52
|
-
if (redacted.worker.oauthPassword !== "<redacted>") throw new Error("oauthPassword was not fully redacted");
|
|
53
|
-
if (redacted.worker.daemonSecret !== "<redacted>") throw new Error("daemonSecret was not fully redacted");
|
|
54
|
-
if (redacted.worker.oauthTokenVersion !== "<redacted>") throw new Error("oauthTokenVersion was not fully redacted");
|
|
55
|
-
if (previewSecret(state.worker.oauthPassword) !== "<redacted>") throw new Error("previewSecret did not fully redact secret");
|
|
56
|
-
if (redactSecret(state.worker.daemonSecret) !== "<redacted>") throw new Error("redactSecret did not fully redact secret");
|
|
57
|
-
|
|
58
|
-
state.localApi = {
|
|
59
|
-
apiKey: "old-local-api-key",
|
|
60
|
-
upstreamUrl: "https://api.example.test/v1",
|
|
61
|
-
upstreamKey: "old-upstream-key",
|
|
62
|
-
upstreamModel: "old-upstream-model",
|
|
63
|
-
};
|
|
64
|
-
saveState(state);
|
|
65
|
-
const profileEntries = await readdir(state.paths.profileDir);
|
|
66
|
-
if (profileEntries.some(name => name.endsWith(".tmp"))) throw new Error("atomic state write left a temporary file");
|
|
67
|
-
const migrated = loadState(workspace, { stateDir: stateRoot });
|
|
68
|
-
if ("localApi" in migrated) throw new Error("legacy local API state was not removed");
|
|
69
|
-
|
|
70
|
-
await writeFile(state.paths.statePath, "{not-json", "utf8");
|
|
71
|
-
const recovered = loadState(workspace, { stateDir: stateRoot });
|
|
72
|
-
if (recovered.workspace.path !== workspace) throw new Error("corrupt state recovery failed");
|
|
73
|
-
const backups = (await readdir(state.paths.profileDir)).filter(name => name.startsWith("state.json.corrupt-"));
|
|
74
|
-
if (backups.length !== 1) throw new Error("corrupt state backup was not retained exactly once");
|
|
75
|
-
const safeRemoval = validateStateRootForRemoval(stateRoot);
|
|
76
|
-
if (!safeRemoval.exists || safeRemoval.root !== state.paths.stateRoot) throw new Error("safe state root validation failed");
|
|
77
|
-
|
|
78
|
-
const legacyStateRoot = await mkdtemp(join(tmpdir(), "mbm-legacy-state-"));
|
|
79
|
-
try {
|
|
80
|
-
await writeFile(join(legacyStateRoot, ".machine-bridge-mcp-state"), "machine-bridge-mcp state root\n", "utf8");
|
|
81
|
-
const legacyState = loadState(workspace, { stateDir: legacyStateRoot });
|
|
82
|
-
const marker = JSON.parse(await readFile(join(legacyState.paths.stateRoot, ".machine-bridge-mcp-state"), "utf8"));
|
|
83
|
-
if (marker.app !== "machine-bridge-mcp" || marker.schema !== 1) throw new Error("legacy state marker was not migrated");
|
|
84
|
-
removeStateRoot(legacyStateRoot);
|
|
85
|
-
} finally {
|
|
86
|
-
await rm(legacyStateRoot, { recursive: true, force: true }).catch(() => {});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const lookalike = await mkdtemp(join(tmpdir(), "mbm-lookalike-state-"));
|
|
90
|
-
try {
|
|
91
|
-
await import("node:fs/promises").then(({ mkdir }) => mkdir(join(lookalike, "profiles")));
|
|
92
|
-
expectThrow(() => loadState(workspace, { stateDir: lookalike }), "does not contain recognizable");
|
|
93
|
-
} finally {
|
|
94
|
-
await rm(lookalike, { recursive: true, force: true }).catch(() => {});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const unrelated = await mkdtemp(join(tmpdir(), "mbm-unrelated-test-"));
|
|
98
|
-
try {
|
|
99
|
-
await writeFile(join(unrelated, "keep.txt"), "do not delete", "utf8");
|
|
100
|
-
expectThrow(() => validateStateRootForRemoval(unrelated), "unrelated entries");
|
|
101
|
-
if (!(await stat(join(unrelated, "keep.txt"))).isFile()) throw new Error("unsafe state root validation modified unrelated data");
|
|
102
|
-
} finally {
|
|
103
|
-
await rm(unrelated, { recursive: true, force: true }).catch(() => {});
|
|
104
|
-
}
|
|
105
|
-
} finally {
|
|
106
|
-
try { removeStateRoot(stateRoot); } catch { await rm(stateRoot, { recursive: true, force: true }).catch(() => {}); }
|
|
107
|
-
await rm(workspace, { recursive: true, force: true }).catch(() => {});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function cliSelfTest() {
|
|
112
|
-
const parsed = parseArgs(["--no-write", "/tmp/example", "--unrestricted-paths=false", "--worker-name", "mbm-test"]);
|
|
113
|
-
if (parsed.noWrite !== true || parsed._[0] !== "/tmp/example") throw new Error("boolean option consumed positional workspace");
|
|
114
|
-
if (parsed.unrestrictedPaths !== false || parsed.workerName !== "mbm-test") throw new Error("CLI option parsing failed");
|
|
115
|
-
expectThrow(() => parseArgs(["--unknown-option"]), "Unknown option");
|
|
116
|
-
expectThrow(() => parseArgs(["--workspace"]), "requires a value");
|
|
117
|
-
expectThrow(() => parseArgs(["--quiet", "--quiet"]), "Duplicate option");
|
|
118
|
-
expectThrow(() => parseArgs(["--quiet=maybe"]), "expects true or false");
|
|
119
|
-
expectThrow(() => validatePositionals("start", { _: ["one", "two"] }), "at most one positional");
|
|
120
|
-
expectThrow(() => validatePositionals("start", { _: ["one"], workspace: "two" }), "both positionally");
|
|
121
|
-
expectThrow(() => validatePositionals("uninstall", { _: ["unexpected"] }), "does not accept positional");
|
|
122
|
-
expectThrow(() => validateCommandOptions("uninstall", { _: [], workspace: "/tmp/project" }), "not valid for uninstall");
|
|
123
|
-
expectThrow(() => validateCommandOptions("doctor", { _: [], fullEnv: true }), "not valid for doctor");
|
|
124
|
-
validateCommandOptions("start", { _: [], unrestrictedPaths: true, noExec: true });
|
|
125
|
-
validatePositionals("workspace", { _: ["set", "/tmp/project"] });
|
|
126
|
-
validatePositionals("service", { _: ["install", "/tmp/project"] });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function logSelfTest() {
|
|
130
|
-
const rendered = formatFields({
|
|
131
|
-
token: "mcp_at_should-not-appear",
|
|
132
|
-
nested: { password: "secret", message: "mcp_password_abcdef\nforged" },
|
|
133
|
-
authorization: "Bearer abcdefghijklmnopqrstuvwxyz",
|
|
134
|
-
});
|
|
135
|
-
if (rendered.includes("should-not-appear") || rendered.includes("password_abcdef") || rendered.includes("Bearer abcdef")) {
|
|
136
|
-
throw new Error("structured log secret redaction failed");
|
|
137
|
-
}
|
|
138
|
-
if (rendered.includes("\nforged")) throw new Error("structured log newline injection was not escaped");
|
|
139
|
-
if (sanitizeLogText("ok\n[error] forged").includes("\n[error]")) throw new Error("log message newline injection was not escaped");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function serviceSelfTest() {
|
|
143
|
-
const stateRoot = await mkdtemp(join(tmpdir(), "mbm-service-test-"));
|
|
144
|
-
try {
|
|
145
|
-
const logs = join(stateRoot, "logs");
|
|
146
|
-
await writeFile(join(stateRoot, "placeholder"), "", "utf8");
|
|
147
|
-
await import("node:fs/promises").then(({ mkdir }) => mkdir(logs, { recursive: true }));
|
|
148
|
-
const file = join(logs, "daemon.err.log");
|
|
149
|
-
await writeFile(file, "x".repeat(4096), "utf8");
|
|
150
|
-
trimAutostartLogs(stateRoot, { maxBytes: 2048, keepBytes: 1024 });
|
|
151
|
-
if ((await stat(file)).size > 1024) throw new Error("autostart log trimming failed");
|
|
152
|
-
const quoted = systemdQuote("path with space/%value'\n");
|
|
153
|
-
if (!quoted.startsWith('"') || !quoted.includes("%%") || !quoted.includes("\\n")) throw new Error("systemd argument quoting failed");
|
|
154
|
-
} finally {
|
|
155
|
-
await rm(stateRoot, { recursive: true, force: true }).catch(() => {});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function shellSelfTest() {
|
|
160
|
-
const result = await run(process.execPath, ["-e", "process.stdout.write('x'.repeat(4096)); process.stderr.write('y'.repeat(4096));"], {
|
|
161
|
-
capture: true,
|
|
162
|
-
maxOutputBytes: 1024,
|
|
163
|
-
});
|
|
164
|
-
if (result.code !== 0 || !result.stdout.includes("[truncated") || !result.stderr.includes("[truncated")) {
|
|
165
|
-
throw new Error("bounded shell capture failed");
|
|
166
|
-
}
|
|
167
|
-
const timedOut = await run(process.execPath, ["-e", "setTimeout(() => {}, 30000)"], {
|
|
168
|
-
capture: true,
|
|
169
|
-
allowFailure: true,
|
|
170
|
-
timeoutMs: 50,
|
|
171
|
-
});
|
|
172
|
-
if (timedOut.code !== 124 || !timedOut.stderr.includes("timed out")) throw new Error("shell timeout handling failed");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function workerSourceSelfTest() {
|
|
176
|
-
const source = await readFile(new URL("../worker/index.ts", import.meta.url), "utf8");
|
|
177
|
-
const unawaitedAsyncRoutes = [
|
|
178
|
-
"return this.registerClient(request);",
|
|
179
|
-
"return this.authorizeSubmit(request, base);",
|
|
180
|
-
"return this.exchangeToken(request, base);",
|
|
181
|
-
"return this.acceptDaemonWebSocket(request);",
|
|
182
|
-
"return this.handleMcp(request, base);",
|
|
183
|
-
].filter(snippet => source.includes(snippet));
|
|
184
|
-
if (unawaitedAsyncRoutes.length) {
|
|
185
|
-
throw new Error(`Worker async routes must be awaited so HttpError is caught: ${unawaitedAsyncRoutes.join(", ")}`);
|
|
186
|
-
}
|
|
187
|
-
for (const required of [
|
|
188
|
-
"MAX_PENDING_CALLS",
|
|
189
|
-
"MAX_DAEMON_MESSAGE_BYTES",
|
|
190
|
-
"withOAuthLock",
|
|
191
|
-
"oauthQueue",
|
|
192
|
-
"AUTH_FAILURE_LIMIT",
|
|
193
|
-
"OAUTH_BODY_LIMIT_BYTES",
|
|
194
|
-
"pending.socket !== ws",
|
|
195
|
-
"isJsonRpcId(candidate.id)",
|
|
196
|
-
"pruneRecordByExpiry(store.tokens, MAX_OAUTH_TOKENS)",
|
|
197
|
-
"A valid PKCE S256 challenge is required.",
|
|
198
|
-
"hmac-sha256:",
|
|
199
|
-
"DAEMON_HELLO_TIMEOUT_MS",
|
|
200
|
-
"async alarm()",
|
|
201
|
-
"storage.setAlarm",
|
|
202
|
-
'role: "candidate"',
|
|
203
|
-
'role: "expired"',
|
|
204
|
-
"daemon_hello_timeout",
|
|
205
|
-
"replaced by authenticated daemon",
|
|
206
|
-
]) {
|
|
207
|
-
if (!source.includes(required)) throw new Error(`Worker hardening guard missing: ${required}`);
|
|
208
|
-
}
|
|
209
|
-
for (const removed of [
|
|
210
|
-
"/api/mcp/sampling",
|
|
211
|
-
"/api/daemon/status",
|
|
212
|
-
"sampling/createMessage",
|
|
213
|
-
'request.headers.get("User-Agent")',
|
|
214
|
-
]) {
|
|
215
|
-
if (source.includes(removed)) throw new Error(`obsolete or public-sensitive Worker route remains: ${removed}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function expectThrow(callback, pattern) {
|
|
220
|
-
try {
|
|
221
|
-
callback();
|
|
222
|
-
} catch (error) {
|
|
223
|
-
if (String(error?.message || error).includes(pattern)) return;
|
|
224
|
-
throw error;
|
|
225
|
-
}
|
|
226
|
-
throw new Error(`expected throw containing: ${pattern}`);
|
|
227
|
-
}
|