machine-bridge-mcp 0.8.1 → 0.9.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 +28 -0
- package/CONTRIBUTING.md +12 -4
- package/README.md +38 -2
- package/SECURITY.md +10 -0
- package/docs/AGENT_CONTEXT.md +206 -0
- package/docs/ARCHITECTURE.md +11 -2
- package/docs/ENGINEERING.md +6 -0
- package/docs/LOGGING.md +9 -7
- package/docs/OPERATIONS.md +3 -3
- package/docs/TESTING.md +5 -3
- package/package.json +6 -4
- package/src/local/agent-context.mjs +708 -0
- package/src/local/cli.mjs +22 -0
- package/src/local/relay-connection.mjs +100 -17
- package/src/local/runtime.mjs +51 -3
- package/src/local/service.mjs +1 -1
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +172 -0
- package/src/worker/index.ts +23 -5
- package/tsconfig.json +2 -0
package/src/local/cli.mjs
CHANGED
|
@@ -337,6 +337,10 @@ async function prepareStartMode(args, state, logger) {
|
|
|
337
337
|
|
|
338
338
|
function reportExistingDaemon(args, state, owner, logger) {
|
|
339
339
|
const pid = owner?.pid ? `pid ${owner.pid}` : "unknown pid";
|
|
340
|
+
if (isIdempotentDaemonOnlyStart(args)) {
|
|
341
|
+
logger.debug?.("local daemon already running; daemon-only start completed as an idempotent no-op", { owner_pid_known: Boolean(owner?.pid) });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
340
344
|
logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
|
|
341
345
|
if (args.json) {
|
|
342
346
|
printStartJson(state, {
|
|
@@ -354,6 +358,24 @@ function reportExistingDaemon(args, state, owner, logger) {
|
|
|
354
358
|
});
|
|
355
359
|
}
|
|
356
360
|
|
|
361
|
+
function isIdempotentDaemonOnlyStart(args) {
|
|
362
|
+
if (!args.daemonOnly || args.json) return false;
|
|
363
|
+
return !Boolean(
|
|
364
|
+
args.profile
|
|
365
|
+
|| args.execMode
|
|
366
|
+
|| args.rotateSecrets
|
|
367
|
+
|| args.forceWorker
|
|
368
|
+
|| args.workerName
|
|
369
|
+
|| args.noWrite
|
|
370
|
+
|| args.noExec
|
|
371
|
+
|| args.fullEnv
|
|
372
|
+
|| args.unrestrictedPaths
|
|
373
|
+
|| args.absolutePaths
|
|
374
|
+
|| args.printMcpCredentials
|
|
375
|
+
|| args.printCredentials
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
357
379
|
async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
|
|
358
380
|
let runtime = null;
|
|
359
381
|
try {
|
|
@@ -3,10 +3,13 @@ import { classifyOperationalError } from "./log.mjs";
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25_000;
|
|
5
5
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
6
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
|
|
6
7
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
7
8
|
const DEFAULT_OUTAGE_WARN_AFTER_MS = 10_000;
|
|
8
9
|
const DEFAULT_OUTAGE_WARN_REPEAT_MS = 60_000;
|
|
10
|
+
const DEFAULT_OUTAGE_WARN_MAX_REPEAT_MS = 15 * 60_000;
|
|
9
11
|
const MAX_CLOSE_REASON_CHARS = 128;
|
|
12
|
+
const MAX_PROTOCOL_ERROR_CODE_CHARS = 64;
|
|
10
13
|
|
|
11
14
|
const DEFAULT_SCHEDULER = Object.freeze({
|
|
12
15
|
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
@@ -35,9 +38,14 @@ export class RelayConnection {
|
|
|
35
38
|
this.maxPayload = boundedPositiveInteger(options.maxPayload, 8 * 1024 * 1024);
|
|
36
39
|
this.heartbeatIntervalMs = boundedPositiveInteger(options.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
37
40
|
this.heartbeatTimeoutMs = boundedPositiveInteger(options.heartbeatTimeoutMs, DEFAULT_HEARTBEAT_TIMEOUT_MS);
|
|
41
|
+
this.connectTimeoutMs = boundedPositiveInteger(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
38
42
|
this.handshakeTimeoutMs = boundedPositiveInteger(options.handshakeTimeoutMs, DEFAULT_HANDSHAKE_TIMEOUT_MS);
|
|
39
43
|
this.outageWarnAfterMs = boundedPositiveInteger(options.outageWarnAfterMs, DEFAULT_OUTAGE_WARN_AFTER_MS);
|
|
40
44
|
this.outageWarnRepeatMs = boundedPositiveInteger(options.outageWarnRepeatMs, DEFAULT_OUTAGE_WARN_REPEAT_MS);
|
|
45
|
+
this.outageWarnMaxRepeatMs = Math.max(
|
|
46
|
+
this.outageWarnRepeatMs,
|
|
47
|
+
boundedPositiveInteger(options.outageWarnMaxRepeatMs, DEFAULT_OUTAGE_WARN_MAX_REPEAT_MS),
|
|
48
|
+
);
|
|
41
49
|
|
|
42
50
|
this.closed = true;
|
|
43
51
|
this.socket = null;
|
|
@@ -47,12 +55,14 @@ export class RelayConnection {
|
|
|
47
55
|
this.lastInboundAt = 0;
|
|
48
56
|
this.reconnectAttempt = 0;
|
|
49
57
|
this.reconnectTimer = null;
|
|
58
|
+
this.connectTimer = null;
|
|
50
59
|
this.heartbeatTimer = null;
|
|
51
60
|
this.handshakeTimer = null;
|
|
52
61
|
this.outageWarnTimer = null;
|
|
53
62
|
this.outageStartedAt = 0;
|
|
54
63
|
this.outageAttempts = 0;
|
|
55
64
|
this.outageNoticeEmitted = false;
|
|
65
|
+
this.outageWarningCount = 0;
|
|
56
66
|
this.lastOutageWarnAt = 0;
|
|
57
67
|
this.lastCloseCategory = "connection_interrupted";
|
|
58
68
|
this.lastTransportErrorClass = "";
|
|
@@ -77,6 +87,7 @@ export class RelayConnection {
|
|
|
77
87
|
this.closed = true;
|
|
78
88
|
this.ready = false;
|
|
79
89
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
90
|
+
this.clearTimer("connectTimer", "clearTimeout");
|
|
80
91
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
81
92
|
this.clearTimer("reconnectTimer", "clearTimeout");
|
|
82
93
|
this.clearTimer("outageWarnTimer", "clearTimeout");
|
|
@@ -128,7 +139,8 @@ export class RelayConnection {
|
|
|
128
139
|
} else if (this.outageStartedAt > 0) {
|
|
129
140
|
const outageMs = Math.max(0, this.connectedAt - this.outageStartedAt);
|
|
130
141
|
if (this.outageNoticeEmitted) {
|
|
131
|
-
this.logger.info?.(
|
|
142
|
+
this.logger.info?.(`remote relay connection restored after ${formatDuration(outageMs)} (${formatAttempts(this.outageAttempts)})`);
|
|
143
|
+
this.logger.debug?.("remote relay outage recovery details", {
|
|
132
144
|
outage_seconds: roundSeconds(outageMs),
|
|
133
145
|
attempts: this.outageAttempts,
|
|
134
146
|
});
|
|
@@ -150,6 +162,20 @@ export class RelayConnection {
|
|
|
150
162
|
return true;
|
|
151
163
|
}
|
|
152
164
|
|
|
165
|
+
handleServerError(message = {}) {
|
|
166
|
+
const errorCode = sanitizeProtocolErrorCode(message?.error);
|
|
167
|
+
this.logger.debug?.("remote relay reported a protocol error", { error_code: errorCode });
|
|
168
|
+
if (errorCode === "daemon_hello_timeout" && !this.ready) {
|
|
169
|
+
const socket = this.socket;
|
|
170
|
+
if (this.closed || !socket) return true;
|
|
171
|
+
this.pendingCloseCategory = "relay_handshake_timeout";
|
|
172
|
+
terminateSocket(socket);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
this.failPermanently("relay_protocol_error");
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
connect() {
|
|
154
180
|
if (this.closed || this.socket) return;
|
|
155
181
|
const wsUrl = `${this.workerUrl.replace(/^http/i, "ws")}/daemon/ws`;
|
|
@@ -167,8 +193,17 @@ export class RelayConnection {
|
|
|
167
193
|
return;
|
|
168
194
|
}
|
|
169
195
|
this.socket = socket;
|
|
196
|
+
this.clearTimer("connectTimer", "clearTimeout");
|
|
197
|
+
this.connectTimer = this.scheduler.setTimeout(() => {
|
|
198
|
+
if (this.socket !== socket || this.closed || this.isSocketOpen(socket)) return;
|
|
199
|
+
this.logger.debug?.("remote relay transport connection timed out", { timeout_ms: this.connectTimeoutMs });
|
|
200
|
+
this.pendingCloseCategory = "relay_connect_timeout";
|
|
201
|
+
terminateSocket(socket);
|
|
202
|
+
}, this.connectTimeoutMs);
|
|
203
|
+
this.connectTimer?.unref?.();
|
|
170
204
|
|
|
171
205
|
socket.on("open", () => {
|
|
206
|
+
this.clearTimer("connectTimer", "clearTimeout");
|
|
172
207
|
if (this.socket !== socket || this.closed) {
|
|
173
208
|
try { socket.close(1000, "stale daemon connection"); } catch {}
|
|
174
209
|
return;
|
|
@@ -204,6 +239,7 @@ export class RelayConnection {
|
|
|
204
239
|
const wasReady = this.ready;
|
|
205
240
|
this.socket = null;
|
|
206
241
|
this.ready = false;
|
|
242
|
+
this.clearTimer("connectTimer", "clearTimeout");
|
|
207
243
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
208
244
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
209
245
|
const reasonText = sanitizeCloseReason(reason);
|
|
@@ -265,6 +301,7 @@ export class RelayConnection {
|
|
|
265
301
|
this.closed = true;
|
|
266
302
|
this.ready = false;
|
|
267
303
|
this.socket = null;
|
|
304
|
+
this.clearTimer("connectTimer", "clearTimeout");
|
|
268
305
|
this.clearTimer("heartbeatTimer", "clearInterval");
|
|
269
306
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
270
307
|
this.clearTimer("reconnectTimer", "clearTimeout");
|
|
@@ -286,7 +323,8 @@ export class RelayConnection {
|
|
|
286
323
|
reject(error);
|
|
287
324
|
return;
|
|
288
325
|
}
|
|
289
|
-
this.logger.error?.(message
|
|
326
|
+
this.logger.error?.(message);
|
|
327
|
+
this.logger.debug?.("remote relay fatal details", { category, cause: relayCloseUserCause(category) });
|
|
290
328
|
queueMicrotask(() => {
|
|
291
329
|
try { this.onFatal(error); } catch (callbackError) {
|
|
292
330
|
this.logger.error?.("relay fatal callback failed", { error_class: classifyOperationalError(callbackError) });
|
|
@@ -299,7 +337,6 @@ export class RelayConnection {
|
|
|
299
337
|
this.recordOutage(category);
|
|
300
338
|
const delay = this.reconnectDelay(this.reconnectAttempt++);
|
|
301
339
|
this.scheduleOutageWarning();
|
|
302
|
-
this.maybeRepeatOutageWarning();
|
|
303
340
|
this.logger.debug?.("scheduling daemon reconnect", { delay_ms: delay, attempt: this.outageAttempts });
|
|
304
341
|
this.reconnectTimer = this.scheduler.setTimeout(() => {
|
|
305
342
|
this.reconnectTimer = null;
|
|
@@ -352,31 +389,39 @@ export class RelayConnection {
|
|
|
352
389
|
}
|
|
353
390
|
|
|
354
391
|
scheduleOutageWarning() {
|
|
355
|
-
if (this.
|
|
356
|
-
const
|
|
357
|
-
|
|
392
|
+
if (this.outageWarnTimer || this.outageStartedAt === 0 || this.closed || this.ready) return;
|
|
393
|
+
const dueAt = this.outageNoticeEmitted
|
|
394
|
+
? this.lastOutageWarnAt + this.nextOutageWarningDelay()
|
|
395
|
+
: this.outageStartedAt + this.outageWarnAfterMs;
|
|
396
|
+
const delay = Math.max(0, dueAt - this.now());
|
|
358
397
|
this.outageWarnTimer = this.scheduler.setTimeout(() => {
|
|
359
398
|
this.outageWarnTimer = null;
|
|
360
399
|
if (this.closed || this.ready || this.outageStartedAt === 0) return;
|
|
361
400
|
this.emitOutageWarning();
|
|
401
|
+
this.scheduleOutageWarning();
|
|
362
402
|
}, delay);
|
|
363
403
|
this.outageWarnTimer?.unref?.();
|
|
364
404
|
}
|
|
365
405
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.emitOutageWarning();
|
|
406
|
+
nextOutageWarningDelay() {
|
|
407
|
+
const exponent = Math.max(0, Math.min(this.outageWarningCount - 1, 20));
|
|
408
|
+
return Math.min(this.outageWarnRepeatMs * (2 ** exponent), this.outageWarnMaxRepeatMs);
|
|
370
409
|
}
|
|
371
410
|
|
|
372
411
|
emitOutageWarning() {
|
|
373
412
|
const outageMs = Math.max(0, this.now() - this.outageStartedAt);
|
|
374
413
|
this.outageNoticeEmitted = true;
|
|
414
|
+
this.outageWarningCount += 1;
|
|
375
415
|
this.lastOutageWarnAt = this.now();
|
|
376
|
-
this.
|
|
416
|
+
const cause = relayCloseUserCause(this.lastCloseCategory);
|
|
417
|
+
const action = outageMs >= 5 * 60_000
|
|
418
|
+
? " If this persists, check internet access and the deployed Worker."
|
|
419
|
+
: "";
|
|
420
|
+
this.logger.warn?.(`remote relay unavailable for ${formatDuration(outageMs)}; reconnecting automatically (${formatAttempts(this.outageAttempts)}; ${cause}).${action}`);
|
|
421
|
+
this.logger.debug?.("remote relay outage details", {
|
|
377
422
|
outage_seconds: roundSeconds(outageMs),
|
|
378
423
|
attempts: this.outageAttempts,
|
|
379
|
-
cause
|
|
424
|
+
cause,
|
|
380
425
|
...(this.lastTransportErrorClass ? { error_class: this.lastTransportErrorClass } : {}),
|
|
381
426
|
});
|
|
382
427
|
}
|
|
@@ -386,6 +431,7 @@ export class RelayConnection {
|
|
|
386
431
|
this.outageStartedAt = 0;
|
|
387
432
|
this.outageAttempts = 0;
|
|
388
433
|
this.outageNoticeEmitted = false;
|
|
434
|
+
this.outageWarningCount = 0;
|
|
389
435
|
this.lastOutageWarnAt = 0;
|
|
390
436
|
this.lastCloseCategory = "connection_interrupted";
|
|
391
437
|
this.lastTransportErrorClass = "";
|
|
@@ -406,10 +452,15 @@ export class RelayConnection {
|
|
|
406
452
|
|
|
407
453
|
export function relayCloseCategory(code, reason = "") {
|
|
408
454
|
const numeric = Number(code);
|
|
409
|
-
|
|
455
|
+
const reasonText = String(reason || "");
|
|
456
|
+
if (isSupersededClose(numeric, reasonText)) return "superseded";
|
|
457
|
+
if (numeric === 1008 && reasonText === "daemon hello timeout") return "relay_handshake_timeout";
|
|
458
|
+
if (numeric === 1008 && ["stale daemon candidate", "expired daemon candidate"].includes(reasonText)) return "relay_restarting_or_unavailable";
|
|
459
|
+
if (numeric === 1008 && ["daemon hello required", "missing daemon attachment", "invalid daemon candidate timestamp"].includes(reasonText)) return "relay_protocol_error";
|
|
410
460
|
if (numeric === 1000) return "normal_close";
|
|
411
461
|
if (numeric === 1001 || numeric === 1012 || numeric === 1013) return "relay_restarting_or_unavailable";
|
|
412
462
|
if (numeric === 1006) return "connection_interrupted";
|
|
463
|
+
if (numeric === 1002) return "relay_protocol_error";
|
|
413
464
|
if (numeric === 1007) return "invalid_transport_payload";
|
|
414
465
|
if (numeric === 1008) return "relay_policy_rejected";
|
|
415
466
|
if (numeric === 1009) return "message_too_large";
|
|
@@ -421,13 +472,12 @@ function relayFatalMessage(category) {
|
|
|
421
472
|
if (category === "relay_protocol_mismatch") {
|
|
422
473
|
return "remote relay identity or version does not match this daemon; upgrade and redeploy both components";
|
|
423
474
|
}
|
|
475
|
+
if (category === "relay_protocol_error") {
|
|
476
|
+
return "remote relay protocol error; upgrade and redeploy both components, then restart the daemon";
|
|
477
|
+
}
|
|
424
478
|
return "remote relay rejected the daemon connection; verify credentials or redeploy the Worker";
|
|
425
479
|
}
|
|
426
480
|
|
|
427
|
-
function relayOutageWarningMessage() {
|
|
428
|
-
return "remote relay is unavailable; automatic reconnection is still in progress";
|
|
429
|
-
}
|
|
430
|
-
|
|
431
481
|
function relayCloseUserCause(category) {
|
|
432
482
|
const causes = {
|
|
433
483
|
connection_interrupted: "connection interrupted",
|
|
@@ -436,9 +486,11 @@ function relayCloseUserCause(category) {
|
|
|
436
486
|
relay_internal_error: "relay internal error",
|
|
437
487
|
relay_protocol_mismatch: "relay identity or version mismatch",
|
|
438
488
|
relay_authentication_failed: "relay authentication failed",
|
|
489
|
+
relay_connect_timeout: "relay connection attempt timed out",
|
|
439
490
|
relay_handshake_timeout: "relay authentication acknowledgement timed out",
|
|
440
491
|
relay_heartbeat_timeout: "relay stopped responding",
|
|
441
492
|
relay_transport_error: "relay transport error",
|
|
493
|
+
relay_protocol_error: "relay protocol error",
|
|
442
494
|
invalid_transport_payload: "invalid transport payload",
|
|
443
495
|
message_too_large: "message exceeded the relay limit",
|
|
444
496
|
normal_close: "connection closed",
|
|
@@ -512,6 +564,37 @@ function boundedPositiveInteger(value, fallback) {
|
|
|
512
564
|
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
513
565
|
}
|
|
514
566
|
|
|
567
|
+
function sanitizeProtocolErrorCode(value) {
|
|
568
|
+
const code = String(value || "unknown_error").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_PROTOCOL_ERROR_CODE_CHARS);
|
|
569
|
+
return code || "unknown_error";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function formatAttempts(value) {
|
|
573
|
+
const attempts = Math.max(1, Math.floor(Number(value) || 1));
|
|
574
|
+
return `${attempts} reconnect attempt${attempts === 1 ? "" : "s"}`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function formatDuration(milliseconds) {
|
|
578
|
+
let seconds = Math.max(1, Math.round(Number(milliseconds) / 1000));
|
|
579
|
+
const units = [
|
|
580
|
+
["day", 86_400],
|
|
581
|
+
["hour", 3_600],
|
|
582
|
+
["minute", 60],
|
|
583
|
+
["second", 1],
|
|
584
|
+
];
|
|
585
|
+
const parts = [];
|
|
586
|
+
for (const [label, size] of units) {
|
|
587
|
+
if (seconds < size && parts.length === 0) continue;
|
|
588
|
+
const amount = Math.floor(seconds / size);
|
|
589
|
+
if (amount > 0) {
|
|
590
|
+
parts.push(`${amount} ${label}${amount === 1 ? "" : "s"}`);
|
|
591
|
+
seconds -= amount * size;
|
|
592
|
+
}
|
|
593
|
+
if (parts.length === 2) break;
|
|
594
|
+
}
|
|
595
|
+
return parts.join(" ") || "1 second";
|
|
596
|
+
}
|
|
597
|
+
|
|
515
598
|
function roundSeconds(milliseconds) {
|
|
516
599
|
return Math.max(1, Math.round(milliseconds / 1000));
|
|
517
600
|
}
|
package/src/local/runtime.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { classifyOperationalError } from "./log.mjs";
|
|
|
14
14
|
import { ManagedJobManager } from "./managed-jobs.mjs";
|
|
15
15
|
import { generateRegisteredSshKey } from "./resource-operations.mjs";
|
|
16
16
|
import { expandHome } from "./state.mjs";
|
|
17
|
+
import { AgentContextManager } from "./agent-context.mjs";
|
|
17
18
|
|
|
18
19
|
export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
19
20
|
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
@@ -27,6 +28,11 @@ const SLOW_TOOL_CALL_MS = 30_000;
|
|
|
27
28
|
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
28
29
|
server_info: (runtime) => runtime.runtimeInfo(),
|
|
29
30
|
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
31
|
+
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
32
|
+
list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
|
|
33
|
+
load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
|
|
34
|
+
list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
|
|
35
|
+
run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
|
|
30
36
|
list_roots: (runtime) => runtime.listRoots(),
|
|
31
37
|
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
32
38
|
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
|
|
@@ -101,6 +107,13 @@ export class LocalRuntime {
|
|
|
101
107
|
displayPath: (value) => this.displayPath(value),
|
|
102
108
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
103
109
|
});
|
|
110
|
+
this.agentContextManager = new AgentContextManager({
|
|
111
|
+
workspace: this.workspace,
|
|
112
|
+
policy: this.policy,
|
|
113
|
+
displayPath: (value) => this.displayPath(value),
|
|
114
|
+
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
115
|
+
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
116
|
+
});
|
|
104
117
|
this.relay = createRelayConnection(this, {
|
|
105
118
|
workerUrl: remoteWorkerUrl,
|
|
106
119
|
secret: remoteSecret,
|
|
@@ -176,12 +189,16 @@ export class LocalRuntime {
|
|
|
176
189
|
async handleMessage(raw) {
|
|
177
190
|
let message;
|
|
178
191
|
try { message = JSON.parse(raw); } catch {
|
|
179
|
-
this.
|
|
192
|
+
this.handleRelayProtocolViolation("invalid_server_json");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!isPlainRecord(message)) {
|
|
196
|
+
this.handleRelayProtocolViolation("invalid_server_message");
|
|
180
197
|
return;
|
|
181
198
|
}
|
|
182
199
|
if (this.handleRelayControlMessage(message)) return;
|
|
183
200
|
if (message.type !== "tool_call") {
|
|
184
|
-
this.
|
|
201
|
+
this.handleRelayProtocolViolation("unexpected_server_message_type");
|
|
185
202
|
return;
|
|
186
203
|
}
|
|
187
204
|
await this.handleRelayToolCall(message);
|
|
@@ -197,6 +214,10 @@ export class LocalRuntime {
|
|
|
197
214
|
return true;
|
|
198
215
|
}
|
|
199
216
|
if (message.type === "pong") return true;
|
|
217
|
+
if (message.type === "error") {
|
|
218
|
+
this.relay?.handleServerError(message);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
200
221
|
if (message.type === "cancel_call") {
|
|
201
222
|
if (typeof message.id === "string") this.cancelCall(message.id, "remote cancellation");
|
|
202
223
|
return true;
|
|
@@ -204,6 +225,14 @@ export class LocalRuntime {
|
|
|
204
225
|
return false;
|
|
205
226
|
}
|
|
206
227
|
|
|
228
|
+
handleRelayProtocolViolation(errorCode) {
|
|
229
|
+
if (this.relay) {
|
|
230
|
+
this.relay.handleServerError({ type: "error", error: errorCode });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.logger.error?.("remote relay protocol error; upgrade and redeploy both components, then restart the daemon");
|
|
234
|
+
}
|
|
235
|
+
|
|
207
236
|
async handleRelayToolCall(message) {
|
|
208
237
|
const envelope = normalizeRelayToolCall(message);
|
|
209
238
|
if (!envelope.ok) {
|
|
@@ -717,6 +746,25 @@ export class LocalRuntime {
|
|
|
717
746
|
return this.runProcess(argv[0], argv.slice(1), clampInt(args.timeout_seconds, 120, 1, 600) * 1000, false, 512 * 1024, context, cwd);
|
|
718
747
|
}
|
|
719
748
|
|
|
749
|
+
async runLocalCommand(args, context = {}) {
|
|
750
|
+
if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_local_command is disabled by daemon policy");
|
|
751
|
+
const command = await this.agentContextManager.resolveLocalCommand(args, context);
|
|
752
|
+
const argv = validateArgv(command.argv);
|
|
753
|
+
const cwd = await this.resolveExistingPath(command.cwd);
|
|
754
|
+
if (!(await stat(cwd)).isDirectory()) throw new Error("registered command cwd is not a directory");
|
|
755
|
+
const requestedTimeout = args.timeout_seconds === undefined
|
|
756
|
+
? command.timeoutSeconds
|
|
757
|
+
: clampInt(args.timeout_seconds, command.timeoutSeconds, 1, 600);
|
|
758
|
+
const timeoutSeconds = Math.min(requestedTimeout, command.timeoutSeconds);
|
|
759
|
+
const result = await this.runProcess(argv[0], argv.slice(1), timeoutSeconds * 1000, false, 512 * 1024, context, cwd);
|
|
760
|
+
return {
|
|
761
|
+
name: command.name,
|
|
762
|
+
cwd: this.displayPath(cwd),
|
|
763
|
+
timeout_seconds: timeoutSeconds,
|
|
764
|
+
...result,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
720
768
|
async execCommand(command, timeoutSeconds, context = {}) {
|
|
721
769
|
if (this.policy.execMode !== "shell") throw new Error("exec_command requires shell execution mode");
|
|
722
770
|
if (!command || typeof command !== "string") throw new Error("command is required");
|
|
@@ -1154,7 +1202,7 @@ function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, on
|
|
|
1154
1202
|
function handleRelayData(runtime, data) {
|
|
1155
1203
|
const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
1156
1204
|
if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
|
|
1157
|
-
runtime.
|
|
1205
|
+
runtime.handleRelayProtocolViolation("server_message_too_large");
|
|
1158
1206
|
return;
|
|
1159
1207
|
}
|
|
1160
1208
|
return runtime.handleMessage(raw);
|
package/src/local/service.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
|
9
9
|
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
10
10
|
const WINDOWS_TASK = "MachineBridgeMCP";
|
|
11
11
|
const SERVICE_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
12
|
-
const AUTOSTART_LOG_SCHEMA_VERSION =
|
|
12
|
+
const AUTOSTART_LOG_SCHEMA_VERSION = 3;
|
|
13
13
|
|
|
14
14
|
function serviceRun(command, args) {
|
|
15
15
|
return run(command, args, {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
],
|
|
9
9
|
"instructions": [
|
|
10
10
|
"You are connected to a local workspace through machine-bridge-mcp.",
|
|
11
|
+
"For substantive workspace tasks, call agent_context for the relevant target path before editing or executing. Apply returned instruction files in precedence order, load only relevant local skills, and prefer registered local commands for repeatable workflows.",
|
|
11
12
|
"Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
|
|
12
13
|
"Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
|
|
13
14
|
"Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
|
|
@@ -31,6 +31,178 @@
|
|
|
31
31
|
"additionalProperties": false
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
"name": "agent_context",
|
|
36
|
+
"title": "Load agent context",
|
|
37
|
+
"description": "Discover Codex-compatible global and root-to-target instruction precedence, progressively disclosed local skills, and registered commands for a target path.",
|
|
38
|
+
"availability": "always",
|
|
39
|
+
"annotations": {
|
|
40
|
+
"readOnlyHint": true,
|
|
41
|
+
"destructiveHint": false,
|
|
42
|
+
"idempotentHint": true,
|
|
43
|
+
"openWorldHint": false
|
|
44
|
+
},
|
|
45
|
+
"inputSchema": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"path": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"default": ".",
|
|
51
|
+
"maxLength": 32768
|
|
52
|
+
},
|
|
53
|
+
"include_instruction_content": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": true
|
|
56
|
+
},
|
|
57
|
+
"max_skills": {
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 1,
|
|
60
|
+
"maximum": 500,
|
|
61
|
+
"default": 100
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "list_local_skills",
|
|
69
|
+
"title": "List local skills",
|
|
70
|
+
"description": "Search bounded SKILL.md or skill.md bundles from Codex-compatible and explicitly configured skill roots without executing them.",
|
|
71
|
+
"availability": "always",
|
|
72
|
+
"annotations": {
|
|
73
|
+
"readOnlyHint": true,
|
|
74
|
+
"destructiveHint": false,
|
|
75
|
+
"idempotentHint": true,
|
|
76
|
+
"openWorldHint": false
|
|
77
|
+
},
|
|
78
|
+
"inputSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"path": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"default": ".",
|
|
84
|
+
"maxLength": 32768
|
|
85
|
+
},
|
|
86
|
+
"query": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"default": "",
|
|
89
|
+
"maxLength": 1000
|
|
90
|
+
},
|
|
91
|
+
"max_results": {
|
|
92
|
+
"type": "integer",
|
|
93
|
+
"minimum": 1,
|
|
94
|
+
"maximum": 500,
|
|
95
|
+
"default": 100
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "load_local_skill",
|
|
103
|
+
"title": "Load local skill",
|
|
104
|
+
"description": "Load one discovered local skill entrypoint and a bounded relative file inventory. Loading returns instructions and never executes skill files implicitly.",
|
|
105
|
+
"availability": "always",
|
|
106
|
+
"annotations": {
|
|
107
|
+
"readOnlyHint": true,
|
|
108
|
+
"destructiveHint": false,
|
|
109
|
+
"idempotentHint": true,
|
|
110
|
+
"openWorldHint": false
|
|
111
|
+
},
|
|
112
|
+
"inputSchema": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"skill": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"minLength": 1,
|
|
118
|
+
"maxLength": 32768
|
|
119
|
+
},
|
|
120
|
+
"path": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"default": ".",
|
|
123
|
+
"maxLength": 32768
|
|
124
|
+
},
|
|
125
|
+
"max_files": {
|
|
126
|
+
"type": "integer",
|
|
127
|
+
"minimum": 1,
|
|
128
|
+
"maximum": 500,
|
|
129
|
+
"default": 200
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"required": [
|
|
133
|
+
"skill"
|
|
134
|
+
],
|
|
135
|
+
"additionalProperties": false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "list_local_commands",
|
|
140
|
+
"title": "List registered local commands",
|
|
141
|
+
"description": "List commands registered by hierarchical .machine-bridge/agent.json manifests for the selected target path.",
|
|
142
|
+
"availability": "always",
|
|
143
|
+
"annotations": {
|
|
144
|
+
"readOnlyHint": true,
|
|
145
|
+
"destructiveHint": false,
|
|
146
|
+
"idempotentHint": true,
|
|
147
|
+
"openWorldHint": false
|
|
148
|
+
},
|
|
149
|
+
"inputSchema": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"path": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"default": ".",
|
|
155
|
+
"maxLength": 32768
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"additionalProperties": false
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "run_local_command",
|
|
163
|
+
"title": "Run registered local command",
|
|
164
|
+
"description": "Execute a named command registered in .machine-bridge/agent.json as a direct argv process without shell parsing. Manifest timeout and argument policy remain authoritative.",
|
|
165
|
+
"availability": "direct-exec",
|
|
166
|
+
"annotations": {
|
|
167
|
+
"readOnlyHint": false,
|
|
168
|
+
"destructiveHint": true,
|
|
169
|
+
"idempotentHint": false,
|
|
170
|
+
"openWorldHint": true
|
|
171
|
+
},
|
|
172
|
+
"inputSchema": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"properties": {
|
|
175
|
+
"name": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"minLength": 1,
|
|
178
|
+
"maxLength": 64
|
|
179
|
+
},
|
|
180
|
+
"path": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"default": ".",
|
|
183
|
+
"maxLength": 32768
|
|
184
|
+
},
|
|
185
|
+
"args": {
|
|
186
|
+
"type": "array",
|
|
187
|
+
"items": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"maxLength": 65536
|
|
190
|
+
},
|
|
191
|
+
"maxItems": 64,
|
|
192
|
+
"default": []
|
|
193
|
+
},
|
|
194
|
+
"timeout_seconds": {
|
|
195
|
+
"type": "integer",
|
|
196
|
+
"minimum": 1,
|
|
197
|
+
"maximum": 600
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": [
|
|
201
|
+
"name"
|
|
202
|
+
],
|
|
203
|
+
"additionalProperties": false
|
|
204
|
+
}
|
|
205
|
+
},
|
|
34
206
|
{
|
|
35
207
|
"name": "list_roots",
|
|
36
208
|
"title": "List workspace roots",
|