machine-bridge-mcp 1.2.9 → 1.2.11

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +1 -1
  4. package/browser-extension/manifest.json +2 -2
  5. package/docs/ARCHITECTURE.md +15 -8
  6. package/docs/AUDIT.md +22 -0
  7. package/docs/LOGGING.md +4 -2
  8. package/docs/MULTI_ACCOUNT.md +2 -2
  9. package/docs/OPERATIONS.md +5 -4
  10. package/docs/RELEASING.md +1 -1
  11. package/docs/TESTING.md +5 -1
  12. package/docs/TOOL_REFERENCE.md +4 -4
  13. package/package.json +6 -2
  14. package/scripts/check-plan.mjs +3 -0
  15. package/scripts/check-runner.mjs +75 -0
  16. package/scripts/coverage-check.mjs +1 -1
  17. package/scripts/github-backlog.mjs +116 -0
  18. package/scripts/github-push.mjs +4 -0
  19. package/scripts/release-acceptance.mjs +29 -13
  20. package/scripts/run-checks.mjs +11 -21
  21. package/src/local/bounded-output.mjs +20 -3
  22. package/src/local/errors.mjs +5 -1
  23. package/src/local/execution-limits.mjs +6 -1
  24. package/src/local/process-execution.mjs +94 -14
  25. package/src/local/process-output-stream.mjs +82 -0
  26. package/src/local/process-result-projection.mjs +25 -0
  27. package/src/local/process-sessions.mjs +37 -51
  28. package/src/local/relay-call-recovery.mjs +148 -0
  29. package/src/local/relay-connection.mjs +5 -0
  30. package/src/local/runtime-relay.mjs +16 -0
  31. package/src/local/runtime.mjs +60 -39
  32. package/src/local/secure-file.mjs +24 -4
  33. package/src/local/tools.mjs +4 -9
  34. package/src/shared/result-projection.d.mts +6 -0
  35. package/src/shared/result-projection.json +4 -0
  36. package/src/shared/result-projection.mjs +50 -0
  37. package/src/shared/server-metadata.json +1 -0
  38. package/src/shared/tool-catalog.json +4 -4
  39. package/src/worker/daemon-sockets.ts +10 -1
  40. package/src/worker/errors.ts +18 -4
  41. package/src/worker/index.ts +45 -9
  42. package/src/worker/mcp-jsonrpc.ts +4 -2
  43. package/src/worker/pending-call-contract.ts +26 -0
  44. package/src/worker/pending-calls.ts +41 -25
  45. package/tsconfig.local.json +1 -0
@@ -8,12 +8,14 @@ const WORKER_ERROR_CODES = new Set([
8
8
  export class WorkerToolError extends Error {
9
9
  readonly code: string;
10
10
  readonly retryable: boolean;
11
+ readonly details?: Record<string, unknown>;
11
12
 
12
- constructor(code: string, message: string, retryable = false) {
13
+ constructor(code: string, message: string, retryable = false, details?: Record<string, unknown>) {
13
14
  super(message);
14
15
  this.name = "WorkerToolError";
15
16
  this.code = normalizeCode(code);
16
17
  this.retryable = retryable;
18
+ this.details = details;
17
19
  }
18
20
  }
19
21
 
@@ -23,11 +25,19 @@ export function daemonToolError(value: unknown): WorkerToolError {
23
25
  const message = typeof input.message === "string" && input.message
24
26
  ? input.message.slice(0, 2000)
25
27
  : "daemon tool failed";
26
- return new WorkerToolError(code, message, input.retryable === true);
28
+ return new WorkerToolError(
29
+ code, message, input.retryable === true,
30
+ isRecord(input.details) ? input.details : undefined,
31
+ );
27
32
  }
28
33
 
29
- export function publicWorkerToolError(error: unknown): { code: string; message: string; retryable: boolean } {
30
- if (error instanceof WorkerToolError) return { code: error.code, message: error.message, retryable: error.retryable };
34
+ export function publicWorkerToolError(error: unknown): { code: string; message: string; retryable: boolean; details?: Record<string, unknown> } {
35
+ if (error instanceof WorkerToolError) {
36
+ return {
37
+ code: error.code, message: error.message, retryable: error.retryable,
38
+ ...(error.details ? { details: error.details } : {}),
39
+ };
40
+ }
31
41
  return { code: "execution_failed", message: "tool execution failed", retryable: false };
32
42
  }
33
43
 
@@ -36,6 +46,10 @@ function normalizeCode(value: unknown): string {
36
46
  return WORKER_ERROR_CODES.has(code) ? code : "execution_failed";
37
47
  }
38
48
 
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
51
+ }
52
+
39
53
  function asObject(value: unknown): Record<string, unknown> {
40
54
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
41
55
  }
@@ -34,13 +34,14 @@ import {
34
34
  } from "./websocket-protocol.ts";
35
35
 
36
36
  const SERVER_NAME = String(serverMetadata.name);
37
- const SERVER_VERSION = "1.2.9";
37
+ const SERVER_VERSION = "1.2.11";
38
38
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
39
39
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
40
40
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
41
41
  const MAX_BODY_BYTES = 16 * 1024 * 1024;
42
42
  const MAX_PENDING_CALLS = 32;
43
43
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
44
+ const DAEMON_RECONNECT_GRACE_MS = 30_000;
44
45
 
45
46
  interface BridgeEnv extends OAuthControllerEnv {
46
47
  BRIDGE: DurableObjectNamespace<BridgeRoom>;
@@ -179,11 +180,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
179
180
  return;
180
181
  }
181
182
  const daemonPolicy = sanitizeDaemonPolicy(body.policy);
183
+ const instanceId = daemonInstanceId(body.instance_id);
184
+ if (!instanceId) {
185
+ rejectDaemonMessage(ws, "invalid_daemon_instance", 1002, "daemon instance id required");
186
+ return;
187
+ }
182
188
  const authenticatedAt = new Date().toISOString();
183
189
  const probeId = randomToken("probe");
184
190
  this.daemonRegistry.beginProbe(ws, {
185
191
  connectedAt: authenticatedAt,
186
192
  probeId,
193
+ instanceId,
187
194
  policy: daemonPolicy,
188
195
  tools: sanitizeDaemonTools(body.tools, daemonPolicy),
189
196
  });
@@ -218,11 +225,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
218
225
  return;
219
226
  }
220
227
  const readyAt = new Date().toISOString();
221
- if (!this.daemonRegistry.promote(ws, readyAt)) {
228
+ const readyAttachment = this.daemonRegistry.promote(ws, readyAt);
229
+ if (!readyAttachment) {
222
230
  rejectDaemonMessage(ws, "invalid_relay_readiness_state", 1002, "invalid daemon readiness state");
223
231
  return;
224
232
  }
233
+ const reboundCallIds = this.pending.rebindInstance(readyAttachment.instanceId ?? "", ws);
234
+ if (reboundCallIds.length > 0) {
235
+ this.observability.event("info", "daemon.calls.rebound", { rebound_calls: reboundCallIds.length });
236
+ }
225
237
  try {
238
+ ws.send(JSON.stringify({ type: "resume_calls", ids: reboundCallIds }));
226
239
  ws.send(JSON.stringify({ type: "ready_ack", server: SERVER_NAME, version: SERVER_VERSION }));
227
240
  } catch {
228
241
  this.invalidateDaemonSocket(ws, "daemon readiness acknowledgement failed", "daemon ready timeout", "daemon_ready_timeout");
@@ -263,7 +276,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
263
276
 
264
277
  private async cleanupDaemonSocket(ws: WebSocket, message: string): Promise<void> {
265
278
  this.observability.socketDisconnected();
266
- this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
279
+ this.detachDaemonSocketCalls(ws, message);
267
280
  await this.scheduleSocketAlarms();
268
281
  }
269
282
 
@@ -428,6 +441,9 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
428
441
  this.reclaimStaleDaemonSockets();
429
442
  const socket = this.daemonRegistry.readySockets()[0];
430
443
  if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
444
+ const daemonAttachment = this.daemonRegistry.readyAttachment(socket);
445
+ const daemonInstanceId = daemonAttachment?.instanceId ?? "";
446
+ if (!daemonInstanceId) throw new WorkerToolError("unavailable", "local daemon connection is missing its instance identity", true);
431
447
  const id = randomToken("call");
432
448
  const timeoutMs = daemonToolTimeoutMs(name, args);
433
449
  let result: Promise<unknown>;
@@ -435,20 +451,23 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
435
451
  result = this.pending.register({
436
452
  id,
437
453
  socket,
454
+ daemonInstanceId,
438
455
  clientRequestKey: requestKey,
439
456
  tool: name,
440
457
  timeoutMs,
441
458
  onTimeout: (record) => {
442
- sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
443
- const silentForMs = Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(record.socket));
444
- if (!Number.isFinite(silentForMs) || silentForMs > 45_000) {
459
+ if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
460
+ const silentForMs = record.socket
461
+ ? Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(record.socket))
462
+ : 0;
463
+ if (record.socket && (!Number.isFinite(silentForMs) || silentForMs > 45_000)) {
445
464
  this.invalidateDaemonSocket(record.socket, "daemon became unresponsive", "daemon liveness timeout");
446
465
  }
447
466
  return new WorkerToolError("timeout", `daemon tool timed out: ${name}`, true);
448
467
  },
449
468
  signal,
450
469
  onAbort: (record) => {
451
- sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
470
+ if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
452
471
  return new WorkerToolError("cancelled", "MCP client stopped waiting for the tool result");
453
472
  },
454
473
  });
@@ -482,7 +501,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
482
501
  private cancelClientRequest(requestKey?: string): void {
483
502
  if (!requestKey) return;
484
503
  this.pending.cancelRequest(requestKey, (record) => {
485
- sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
504
+ if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
486
505
  return new WorkerToolError("cancelled", "tool call cancelled by client");
487
506
  });
488
507
  }
@@ -542,12 +561,24 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
542
561
  closeReason: string,
543
562
  errorCode = "daemon_liveness_timeout",
544
563
  ): void {
564
+ this.detachDaemonSocketCalls(ws, message);
545
565
  this.daemonRegistry.expire(ws);
546
- this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
547
566
  sendWebSocketQuietly(ws, { type: "error", error: errorCode });
548
567
  closeWebSocketQuietly(ws, 1008, closeReason);
549
568
  }
550
569
 
570
+ private detachDaemonSocketCalls(ws: WebSocket, message: string): number {
571
+ const attachment = this.daemonRegistry.attachment(ws);
572
+ if (!attachment?.instanceId) {
573
+ return this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
574
+ }
575
+ return this.pending.detachSocket(
576
+ ws,
577
+ DAEMON_RECONNECT_GRACE_MS,
578
+ () => new WorkerToolError("unavailable", `${message}; reconnect grace expired`, true),
579
+ );
580
+ }
581
+
551
582
  private reclaimStaleDaemonSockets(now = Date.now()): void {
552
583
  for (const socket of this.daemonRegistry.readyRoleSockets()) {
553
584
  const deadline = daemonLivenessDeadlineMs(this.daemonRegistry.readyAttachment(socket));
@@ -697,3 +728,8 @@ export default {
697
728
  return stub.fetch(request);
698
729
  },
699
730
  } satisfies ExportedHandler<BridgeEnv>;
731
+
732
+ function daemonInstanceId(value: unknown): string {
733
+ if (typeof value !== "string" || !/^daemon_[A-Za-z0-9_-]{16,96}$/.test(value)) return "";
734
+ return value;
735
+ }
@@ -1,3 +1,4 @@
1
+ import { projectMcpResult } from "../shared/result-projection.mjs";
1
2
  const JSONRPC_VERSION = "2.0";
2
3
  const MAX_SESSION_INSTRUCTION_BYTES = 3 * 1024 * 1024;
3
4
 
@@ -48,11 +49,12 @@ export function textToolResult(value: unknown, isError = false): Record<string,
48
49
  return result;
49
50
  }
50
51
  }
52
+ const projection = projectMcpResult(value);
51
53
  const result: Record<string, unknown> = {
52
- content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }],
54
+ content: [{ type: "text", text: projection.text }],
53
55
  isError,
54
56
  };
55
- if (value && typeof value === "object" && !Array.isArray(value)) result.structuredContent = value;
57
+ if (projection.structuredContent) result.structuredContent = projection.structuredContent;
56
58
  return result;
57
59
  }
58
60
 
@@ -0,0 +1,26 @@
1
+ export interface PendingCallRecord {
2
+ id: string;
3
+ socket?: WebSocket;
4
+ daemonInstanceId?: string;
5
+ reconnectTimeout?: ReturnType<typeof setTimeout>;
6
+ clientRequestKey?: string;
7
+ tool: string;
8
+ startedAt: number;
9
+ timeout: ReturnType<typeof setTimeout>;
10
+ resolve: (value: unknown) => void;
11
+ reject: (error: Error) => void;
12
+ signal?: AbortSignal;
13
+ abortHandler?: () => void;
14
+ }
15
+
16
+ export interface RegisterPendingCall {
17
+ id: string;
18
+ socket: WebSocket;
19
+ daemonInstanceId?: string;
20
+ clientRequestKey?: string;
21
+ tool: string;
22
+ timeoutMs: number;
23
+ onTimeout: (record: PendingCallRecord) => Error;
24
+ signal?: AbortSignal;
25
+ onAbort?: (record: PendingCallRecord) => Error;
26
+ }
@@ -1,3 +1,5 @@
1
+ import type { PendingCallRecord, RegisterPendingCall } from "./pending-call-contract.ts";
2
+
1
3
  export class PendingCallRegistrationError extends Error {
2
4
  readonly code: "conflict" | "limit_exceeded";
3
5
  readonly retryable: boolean;
@@ -10,30 +12,6 @@ export class PendingCallRegistrationError extends Error {
10
12
  }
11
13
  }
12
14
 
13
- export interface PendingCallRecord {
14
- id: string;
15
- socket: WebSocket;
16
- clientRequestKey?: string;
17
- tool: string;
18
- startedAt: number;
19
- timeout: ReturnType<typeof setTimeout>;
20
- resolve: (value: unknown) => void;
21
- reject: (error: Error) => void;
22
- signal?: AbortSignal;
23
- abortHandler?: () => void;
24
- }
25
-
26
- interface RegisterPendingCall {
27
- id: string;
28
- socket: WebSocket;
29
- clientRequestKey?: string;
30
- tool: string;
31
- timeoutMs: number;
32
- onTimeout: (record: PendingCallRecord) => Error;
33
- signal?: AbortSignal;
34
- onAbort?: (record: PendingCallRecord) => Error;
35
- }
36
-
37
15
  export class PendingCallRegistry {
38
16
  private readonly maximum: number;
39
17
  private readonly byId = new Map<string, PendingCallRecord>();
@@ -78,6 +56,7 @@ export class PendingCallRegistry {
78
56
  const record: PendingCallRecord = {
79
57
  id: input.id,
80
58
  socket: input.socket,
59
+ daemonInstanceId: input.daemonInstanceId,
81
60
  clientRequestKey: input.clientRequestKey,
82
61
  tool: String(input.tool || "unknown"),
83
62
  startedAt,
@@ -126,16 +105,52 @@ export class PendingCallRegistry {
126
105
  return ids.length;
127
106
  }
128
107
 
129
- snapshot(): { active: number; request_keys: number; maximum: number; oldest_ms: number; by_tool: Record<string, number> } {
108
+ detachSocket(
109
+ socket: WebSocket,
110
+ graceMs: number,
111
+ createError: (record: PendingCallRecord) => Error,
112
+ ): number {
113
+ const records = [...this.byId.values()].filter((record) => record.socket === socket);
114
+ const delay = Math.max(1, Math.floor(Number(graceMs) || 1));
115
+ for (const record of records) {
116
+ record.socket = undefined;
117
+ if (record.reconnectTimeout) clearTimeout(record.reconnectTimeout);
118
+ record.reconnectTimeout = setTimeout(() => {
119
+ const current = this.byId.get(record.id);
120
+ if (!current || current.socket) return;
121
+ const expired = this.take(record.id);
122
+ if (expired) expired.reject(createError(expired));
123
+ }, delay);
124
+ }
125
+ return records.length;
126
+ }
127
+
128
+ rebindInstance(daemonInstanceId: string, socket: WebSocket): string[] {
129
+ if (!daemonInstanceId) return [];
130
+ const rebound: string[] = [];
131
+ for (const record of this.byId.values()) {
132
+ if (record.socket || record.daemonInstanceId !== daemonInstanceId) continue;
133
+ if (record.reconnectTimeout) clearTimeout(record.reconnectTimeout);
134
+ record.reconnectTimeout = undefined;
135
+ record.socket = socket;
136
+ rebound.push(record.id);
137
+ }
138
+ return rebound;
139
+ }
140
+
141
+ snapshot(): { active: number; detached: number; request_keys: number; maximum: number; oldest_ms: number; by_tool: Record<string, number> } {
130
142
  const now = performance.now();
131
143
  const byTool: Record<string, number> = {};
144
+ let detached = 0;
132
145
  let oldestMs = 0;
133
146
  for (const record of this.byId.values()) {
134
147
  byTool[record.tool] = (byTool[record.tool] || 0) + 1;
148
+ if (!record.socket) detached += 1;
135
149
  oldestMs = Math.max(oldestMs, now - record.startedAt);
136
150
  }
137
151
  return {
138
152
  active: this.byId.size,
153
+ detached,
139
154
  request_keys: this.byRequestKey.size,
140
155
  maximum: this.maximum,
141
156
  oldest_ms: oldestMs,
@@ -147,6 +162,7 @@ export class PendingCallRegistry {
147
162
  const record = this.byId.get(id);
148
163
  if (!record) return undefined;
149
164
  clearTimeout(record.timeout);
165
+ if (record.reconnectTimeout) clearTimeout(record.reconnectTimeout);
150
166
  if (record.signal && record.abortHandler) record.signal.removeEventListener("abort", record.abortHandler);
151
167
  this.byId.delete(id);
152
168
  if (record.clientRequestKey && this.byRequestKey.get(record.clientRequestKey) === id) {
@@ -24,6 +24,7 @@
24
24
  "src/local/call-registry.mjs",
25
25
  "src/local/policy.mjs",
26
26
  "src/local/runtime-paths.mjs",
27
+ "src/local/relay-call-recovery.mjs",
27
28
  "src/local/agent-context-projection.mjs",
28
29
  "src/local/agent-skill-discovery.mjs",
29
30
  "src/local/agent-text-file.mjs",