machine-bridge-mcp 3.0.0-beta.17 → 3.0.0-beta.21

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 (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -0
  3. package/SECURITY.md +9 -7
  4. package/browser-extension/browser-error-boundary.js +45 -0
  5. package/browser-extension/browser-operations.js +1 -1
  6. package/browser-extension/manifest.json +1 -1
  7. package/browser-extension/service-worker.js +9 -8
  8. package/docs/ARCHITECTURE.md +17 -15
  9. package/docs/AUDIT.md +30 -0
  10. package/docs/ENGINEERING.md +3 -3
  11. package/docs/GETTING_STARTED.md +1 -1
  12. package/docs/LOCAL_AUTHORIZATION.md +2 -2
  13. package/docs/LOCAL_AUTOMATION.md +1 -1
  14. package/docs/LOGGING.md +3 -1
  15. package/docs/MULTI_ACCOUNT.md +2 -2
  16. package/docs/OPERATIONS.md +13 -5
  17. package/docs/OVERVIEW.md +2 -2
  18. package/docs/PRIVACY.md +1 -1
  19. package/docs/TESTING.md +17 -13
  20. package/docs/THREAT_MODEL.md +13 -8
  21. package/docs/UPGRADING.md +19 -1
  22. package/package.json +1 -1
  23. package/scripts/coverage-check.mjs +12 -0
  24. package/src/local/account-admin.mjs +68 -1
  25. package/src/local/agent-skill-discovery.mjs +16 -5
  26. package/src/local/app-automation.mjs +27 -7
  27. package/src/local/cli-options.mjs +1 -1
  28. package/src/local/path-inspection.mjs +23 -0
  29. package/src/local/process-tree-ownership.mjs +17 -5
  30. package/src/local/process-tree.mjs +6 -2
  31. package/src/local/relay-connection.mjs +24 -11
  32. package/src/local/resource-operations.mjs +37 -8
  33. package/src/local/runtime-activation.mjs +16 -0
  34. package/src/local/runtime-capabilities.mjs +15 -3
  35. package/src/local/runtime.mjs +7 -4
  36. package/src/local/worker-deployment.mjs +16 -6
  37. package/src/local/workspace-file-service.mjs +29 -11
  38. package/src/worker/daemon-socket-attachment.ts +52 -0
  39. package/src/worker/daemon-sockets.ts +17 -48
  40. package/src/worker/durable-stream-calls.ts +129 -0
  41. package/src/worker/durable-stream-result.ts +22 -0
  42. package/src/worker/http.ts +37 -15
  43. package/src/worker/index.ts +143 -146
  44. package/src/worker/mcp-pending-call-expiry.ts +20 -0
  45. package/src/worker/mcp-pending-call-inspection.ts +33 -0
  46. package/src/worker/mcp-pending-call-records.ts +58 -0
  47. package/src/worker/mcp-pending-call-storage.ts +18 -0
  48. package/src/worker/mcp-pending-call-store.ts +285 -0
  49. package/src/worker/mcp-resumption-index.ts +41 -0
  50. package/src/worker/mcp-resumption-records.ts +8 -2
  51. package/src/worker/mcp-resumption.ts +84 -86
  52. package/src/worker/mcp-stream-dispatch.ts +42 -54
  53. package/src/worker/mcp-stream-proxy.ts +7 -69
  54. package/src/worker/mcp-stream-subscription.ts +98 -0
  55. package/src/worker/oauth-controller.ts +11 -2
  56. package/src/worker/oauth-refresh-exchange.ts +147 -0
  57. package/src/worker/oauth-refresh-families.ts +59 -17
  58. package/src/worker/oauth-state.ts +13 -2
  59. package/src/worker/oauth-token-derivation.ts +33 -0
  60. package/src/worker/oauth-token-issuance.ts +106 -0
  61. package/src/worker/oauth-tokens.ts +12 -184
  62. package/src/worker/observability.ts +19 -0
  63. package/src/worker/pending-admission.ts +15 -0
  64. package/src/worker/pending-call-contract.ts +4 -7
  65. package/src/worker/pending-calls.ts +4 -10
  66. package/src/worker/runtime-alarm-storage.ts +35 -0
  67. package/src/worker/runtime-alarm.ts +30 -20
  68. package/src/worker/websocket-protocol.ts +4 -0
  69. package/src/worker/worker-edge-guard.ts +81 -0
  70. package/src/worker/worker-edge-log.ts +63 -0
  71. package/src/worker/worker-entry.ts +50 -0
  72. package/src/worker/worker-metadata.ts +39 -0
  73. package/src/worker/worker-static-routes.ts +38 -50
  74. package/wrangler.jsonc +8 -0
@@ -2,6 +2,8 @@ import { DurableObject } from "cloudflare:workers";
2
2
  import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
3
3
  import relayContract from "../shared/relay-contract.json" with { type: "json" };
4
4
  import { PendingCallRegistrationError, PendingCallRegistry } from "./pending-calls.ts";
5
+ import { PendingAdmissionGate } from "./pending-admission.ts";
6
+ import type { PendingCallOutcome } from "./pending-call-contract.ts";
5
7
  import {
6
8
  DAEMON_LIVENESS_TIMEOUT_MS,
7
9
  DAEMON_READY_TIMEOUT_MS,
@@ -18,11 +20,12 @@ import { authorizeMcpRequest } from "./mcp-access.ts";
18
20
  import { handleMcpResumptionRequest } from "./mcp-resumption-http.ts";
19
21
  import {
20
22
  handleMcpStreamSubscribeRequest, mcpStreamDescriptorResponse, mcpStreamProxyMode,
21
- proxyMcpEventStream, sanitizeBridgeRequest,
22
23
  } from "./mcp-stream-proxy.ts";
23
24
  import { McpResumptionStore, McpStreamLimitError } from "./mcp-resumption.ts";
24
25
  import { McpStreamChannel } from "./mcp-stream-channel.ts";
25
26
  import { buildServerInfoResult, persistImmediateStreamOutcome, startEventDrivenStreamCall } from "./mcp-stream-dispatch.ts";
27
+ import { DurableStreamCallCoordinator } from "./durable-stream-calls.ts";
28
+ import { handleOuterWorkerFetch } from "./worker-entry.ts";
26
29
  import { daemonToolTimeoutMs } from "./tool-timeout.ts";
27
30
  import { WorkerObservability } from "./observability.ts";
28
31
  import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors.ts";
@@ -31,24 +34,23 @@ import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from ".
31
34
  import { OAuthController, type AuthorizedToken, type OAuthControllerEnv } from "./oauth-controller.ts";
32
35
  import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority.ts";
33
36
  import { serverInfoTool, workspaceTools } from "./tool-catalog.ts";
34
- import { OFFLINE_ACCESS_SCOPE, randomToken } from "./oauth-state.ts";
37
+ import { randomToken } from "./oauth-state.ts";
35
38
  import {
36
39
  HttpError, applyCors, baseUrl, corsPreflight, json, methodNotAllowed,
37
40
  parseJsonRequest, workerErrorClass,
38
41
  } from "./http.ts";
39
- import {
40
- durableObjectQuotaResponse, isDurableObjectQuotaError, respondWithoutDurableObject,
41
- } from "./worker-static-routes.ts";
42
+ import { authorizationServerMetadata } from "./worker-metadata.ts";
42
43
  import {
43
44
  asObject, isJsonRpcRequest, isJsonRpcResponse, requiredString, rpcError, rpcResult,
44
45
  sessionInstructionText, textToolResult, validateProtocolVersionHeader, type JsonRpcRequest,
45
46
  } from "./mcp-jsonrpc.ts";
46
47
  import {
47
- closeWebSocketQuietly, isObjectRecord, rejectDaemonMessage, sendWebSocketQuietly, trySendWebSocket,
48
+ closeWebSocketQuietly, daemonErrorCloseCode, isObjectRecord, rejectDaemonMessage,
49
+ sendWebSocketQuietly, trySendWebSocket,
48
50
  } from "./websocket-protocol.ts";
49
51
 
50
52
  const SERVER_NAME = String(serverMetadata.name);
51
- const SERVER_VERSION = "3.0.0-beta.17";
53
+ const SERVER_VERSION = "3.0.0-beta.21";
52
54
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
53
55
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
54
56
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
@@ -63,6 +65,7 @@ interface BridgeEnv extends OAuthControllerEnv {
63
65
  OAUTH_TOKEN_VERSION: string;
64
66
  MBM_WORKER_MAX_BODY_BYTES?: string;
65
67
  MBM_ALLOWED_ORIGINS?: string;
68
+ STATEFUL_RATE_LIMITER: RateLimit;
66
69
  }
67
70
 
68
71
  const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
@@ -74,17 +77,34 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
74
77
  private readonly daemonRegistry: DaemonSocketRegistry;
75
78
  private readonly streamChannel: McpStreamChannel;
76
79
  private readonly resumption: McpResumptionStore;
80
+ private readonly durableCalls: DurableStreamCallCoordinator;
81
+ private readonly pendingAdmission = new PendingAdmissionGate();
77
82
 
78
83
  constructor(ctx: DurableObjectState, env: BridgeEnv) {
79
84
  super(ctx, env);
80
- this.oauth = new OAuthController(ctx, env, SERVER_NAME, SERVER_VERSION);
85
+ this.oauth = new OAuthController(
86
+ ctx, env, SERVER_NAME, SERVER_VERSION,
87
+ (event) => this.observability.oauthRefreshEvent(event),
88
+ );
81
89
  this.daemonRegistry = new DaemonSocketRegistry(ctx);
82
90
  this.streamChannel = new McpStreamChannel(ctx, this.observability);
83
- this.resumption = new McpResumptionStore(ctx.storage, {}, (streamId, message) => this.streamChannel.publish(streamId, message));
91
+ this.resumption = new McpResumptionStore(
92
+ ctx.storage,
93
+ {},
94
+ (streamId, message) => this.streamChannel.publish(streamId, message),
95
+ (rows) => this.observability.streamStorageRowsWritten(rows),
96
+ );
97
+ this.durableCalls = new DurableStreamCallCoordinator(
98
+ this.resumption.calls,
99
+ this.daemonRegistry,
100
+ this.observability,
101
+ MAX_PENDING_CALLS,
102
+ (socket, message, closeReason, errorCode) => this.invalidateDaemonSocket(socket, message, closeReason, errorCode),
103
+ );
84
104
  }
85
105
 
86
106
  async fetch(request: Request): Promise<Response> {
87
- await this.pending.expireDue();
107
+ await this.expireOverdueCalls();
88
108
  const base = baseUrl(request);
89
109
  const extraOrigins = this.env.MBM_ALLOWED_ORIGINS ?? "";
90
110
  let response: Response;
@@ -97,31 +117,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
97
117
  private async handleRequest(request: Request, base: string): Promise<Response> {
98
118
  const url = new URL(request.url);
99
119
  try {
100
- if (url.pathname === "/") {
101
- if (request.method !== "GET") return methodNotAllowed("GET");
102
- return json({ ok: true, server: SERVER_NAME, version: SERVER_VERSION, mcp: `${base}/mcp` });
103
- }
104
- if (url.pathname === "/healthz") {
105
- if (request.method !== "GET") return methodNotAllowed("GET");
106
- return json({ ok: true, server: SERVER_NAME, version: SERVER_VERSION });
107
- }
108
- if (url.pathname === "/.well-known/mcp.json") {
109
- if (request.method !== "GET") return methodNotAllowed("GET");
110
- return json(this.mcpMetadata(base));
111
- }
112
- if (
113
- url.pathname === "/.well-known/oauth-authorization-server" ||
114
- url.pathname === "/.well-known/oauth-authorization-server/mcp" ||
115
- url.pathname === "/.well-known/openid-configuration" ||
116
- url.pathname === "/.well-known/openid-configuration/mcp"
117
- ) {
118
- if (request.method !== "GET") return methodNotAllowed("GET");
119
- return json(this.authorizationServerMetadata(base));
120
- }
121
- if (url.pathname === "/.well-known/oauth-protected-resource" || url.pathname === "/.well-known/oauth-protected-resource/mcp") {
122
- if (request.method !== "GET") return methodNotAllowed("GET");
123
- return json(this.protectedResourceMetadata(base));
124
- }
125
120
  if (url.pathname === "/admin/accounts") return await this.oauth.handleAccountAdmin(request, "accounts");
126
121
  if (url.pathname === "/admin/accounts/rotate-password") return await this.oauth.handleAccountAdmin(request, "rotate-password");
127
122
  if (url.pathname === "/admin/clients") return await this.oauth.handleClientAdmin(request);
@@ -152,7 +147,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
152
147
  }
153
148
 
154
149
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
155
- await this.pending.expireDue();
150
+ await this.expireOverdueCalls();
156
151
  if (this.streamChannel.isSubscriber(ws)) {
157
152
  this.streamChannel.rejectSubscriberMessage(ws);
158
153
  return;
@@ -242,10 +237,16 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
242
237
  const daemonPolicy = sanitizeDaemonPolicy(body.policy);
243
238
  const authenticatedAt = new Date().toISOString();
244
239
  const probeId = randomToken("probe");
240
+ const connectionId = socketAttachment.connectionId ?? "";
241
+ if (!connectionId) {
242
+ rejectDaemonMessage(ws, "missing_daemon_connection_identity", 1008, "daemon connection identity missing");
243
+ return;
244
+ }
245
245
  this.daemonRegistry.beginProbe(ws, {
246
246
  connectedAt: authenticatedAt,
247
247
  probeId,
248
248
  instanceId,
249
+ connectionId,
249
250
  policy: daemonPolicy,
250
251
  tools: sanitizeDaemonTools(body.tools, daemonPolicy),
251
252
  });
@@ -288,9 +289,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
288
289
  return;
289
290
  }
290
291
  const daemonInstanceId = readyAttachment.instanceId ?? "";
292
+ const connectionId = readyAttachment.connectionId ?? "";
293
+ if (!daemonInstanceId || !connectionId) {
294
+ await this.invalidateDaemonSocket(ws, "daemon readiness identity is incomplete", "daemon ready timeout", "daemon_ready_timeout");
295
+ return;
296
+ }
291
297
  const previousSockets = this.daemonRegistry.readyRoleSockets().filter((socket) => socket !== ws);
292
298
  const fallbackSocket = previousSockets.find((socket) => this.daemonRegistry.readyAttachment(socket)?.instanceId === daemonInstanceId);
293
- const reboundCallIds = this.pending.rebindInstance(daemonInstanceId, ws);
299
+ const reboundCallIds = [
300
+ ...this.pending.rebindInstance(daemonInstanceId, ws),
301
+ ...await this.durableCalls.rebind(daemonInstanceId, connectionId),
302
+ ];
294
303
  if (reboundCallIds.length > 0) {
295
304
  this.observability.event("info", "daemon.calls.rebound", { rebound_calls: reboundCallIds.length });
296
305
  }
@@ -299,7 +308,11 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
299
308
  ws.send(JSON.stringify({ type: "ready_ack", server: SERVER_NAME, version: SERVER_VERSION }));
300
309
  } catch {
301
310
  await this.invalidateDaemonSocket(ws, "daemon readiness acknowledgement failed", "daemon ready timeout", "daemon_ready_timeout");
302
- if (fallbackSocket?.readyState === WebSocket.OPEN) this.pending.rebindInstance(daemonInstanceId, fallbackSocket);
311
+ if (fallbackSocket?.readyState === WebSocket.OPEN) {
312
+ this.pending.rebindInstance(daemonInstanceId, fallbackSocket);
313
+ const fallbackConnectionId = this.daemonRegistry.readyAttachment(fallbackSocket)?.connectionId ?? "";
314
+ if (fallbackConnectionId) await this.durableCalls.rebind(daemonInstanceId, fallbackConnectionId);
315
+ }
303
316
  await this.scheduleRuntimeAlarm();
304
317
  return;
305
318
  }
@@ -324,10 +337,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
324
337
  }
325
338
 
326
339
  await this.touchDaemonSocket(ws);
327
- const matched = body.ok === false
328
- ? await this.pending.reject(body.id, daemonToolError(body.error), ws)
329
- : await this.pending.resolve(body.id, ws, body.result);
340
+ const outcome: PendingCallOutcome = body.ok === false
341
+ ? { ok: false, error: daemonToolError(body.error) }
342
+ : { ok: true, value: body.result };
343
+ let matched = outcome.ok
344
+ ? await this.pending.resolve(body.id, ws, outcome.value)
345
+ : await this.pending.reject(body.id, outcome.error, ws);
346
+ if (!matched && socketAttachment.connectionId) {
347
+ matched = await this.durableCalls.settle(body.id, socketAttachment.connectionId, outcome);
348
+ }
330
349
  if (!matched) this.observability.unmatchedResult();
350
+ else await this.scheduleRuntimeAlarm();
331
351
  }
332
352
 
333
353
  async webSocketClose(ws: WebSocket): Promise<void> {
@@ -390,20 +410,21 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
390
410
  if (body.method === "tools/call" && acceptsEventStream(request)) {
391
411
  if (proxyMode !== "prepare") return json(rpcError(body.id, -32603, "MCP stream proxy is unavailable"), 500);
392
412
  if (body.id === undefined || body.id === null) return json(rpcError(null, -32600, "tools/call requires a non-null request id"), 400);
413
+ const requestId = body.id;
393
414
  const streamId = randomToken("stream");
394
415
  try {
395
416
  await this.resumption.begin({
396
417
  streamId,
397
418
  tokenKey: access.authorized.tokenKey,
398
419
  sessionId,
399
- requestId: body.id,
420
+ requestId,
400
421
  });
401
422
  } catch (error) {
402
423
  if (error instanceof McpStreamLimitError) {
403
- return json(rpcError(body.id, -32004, error.message), 429);
424
+ return json(rpcError(requestId, -32004, error.message), 429);
404
425
  }
405
426
  this.observability.event("error", "mcp.stream.begin.failed", { error_class: workerErrorClass(error) });
406
- return json(rpcError(body.id, -32603, "Resumable stream storage is unavailable"), 503);
427
+ return json(rpcError(requestId, -32603, "Resumable stream storage is unavailable"), 503);
407
428
  }
408
429
 
409
430
  const params = asObject(body.params);
@@ -412,38 +433,40 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
412
433
  try {
413
434
  if (name === "server_info") {
414
435
  await persistImmediateStreamOutcome({
415
- resumption: this.resumption, observability: this.observability, streamId, requestId: body.id,
416
- outcome: { ok: true, value: this.serverInfoResult(base, access.authorized) },
436
+ resumption: this.resumption, observability: this.observability, streamId, requestId,
437
+ outcome: { ok: true, value: await this.serverInfoResult(base, access.authorized) },
417
438
  });
418
439
  } else {
419
440
  if (!workspaceTools.some((tool) => tool.name === name)) throw new Error(`unknown tool: ${name}`);
420
- if (!this.daemonToolEnabled(name)) throw new Error(`tool disabled by local daemon policy: ${name}`);
421
441
  if (!accountRoleAllowsTool(access.authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
422
442
  this.reclaimStaleDaemonSockets();
423
443
  const socket = this.daemonRegistry.readySockets()[0];
424
444
  if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
425
- const daemonInstanceId = this.daemonRegistry.readyAttachment(socket)?.instanceId ?? "";
426
- if (!daemonInstanceId) throw new WorkerToolError("unavailable", "local daemon connection is missing its instance identity", true);
427
- await startEventDrivenStreamCall({
428
- pending: this.pending, resumption: this.resumption, observability: this.observability,
429
- streamId, requestId: body.id, clientRequestKey: mcpClientRequestKey(access.authorized.tokenKey, sessionId, body.id),
430
- tool: name, arguments: args, socket, daemonInstanceId, timeoutMs: daemonToolTimeoutMs(name, args),
445
+ const attachment = this.daemonRegistry.readyAttachment(socket);
446
+ const daemonInstanceId = attachment?.instanceId ?? "";
447
+ const connectionId = attachment?.connectionId ?? "";
448
+ if (!daemonInstanceId || !connectionId) throw new WorkerToolError("unavailable", "local daemon connection is missing its relay identity", true);
449
+ if (!attachment?.tools?.includes(name)) throw new WorkerToolError("authorization_denied", `tool disabled by local daemon policy: ${name}`);
450
+ await this.pendingAdmission.run(() => startEventDrivenStreamCall({
451
+ resumption: this.resumption, observability: this.observability,
452
+ streamId, requestId, clientRequestKey: mcpClientRequestKey(access.authorized.tokenKey, sessionId, requestId),
453
+ tool: name, arguments: args, socket, daemonInstanceId, connectionId, timeoutMs: daemonToolTimeoutMs(name, args),
454
+ transientActiveCount: this.pending.size, maximumPendingCalls: MAX_PENDING_CALLS,
431
455
  authorization: {
432
456
  account_id: access.authorized.accountId, account_version: access.authorized.accountVersion,
433
457
  client_id: access.authorized.clientId, family_id: access.authorized.familyId, role: access.authorized.role,
434
458
  },
435
- onTimeout: (record) => this.daemonCallTimeout(record, name),
459
+ transform: name === "project_overview" ? {
460
+ kind: "project_overview", account_id: access.authorized.accountId,
461
+ account_version: access.authorized.accountVersion, role: access.authorized.role,
462
+ } : undefined,
436
463
  onSendFailure: () => this.invalidateDaemonSocket(socket, "failed to send daemon tool call", "daemon send failed"),
437
- transformResult: name === "project_overview"
438
- ? (value) => decorateProjectOverview(value, { accountId: access.authorized.accountId,
439
- accountVersion: access.authorized.accountVersion, role: access.authorized.role })
440
- : undefined,
441
- });
464
+ }));
442
465
  await this.scheduleRuntimeAlarm();
443
466
  }
444
467
  } catch (error) {
445
468
  await persistImmediateStreamOutcome({
446
- resumption: this.resumption, observability: this.observability, streamId, requestId: body.id,
469
+ resumption: this.resumption, observability: this.observability, streamId, requestId,
447
470
  outcome: { ok: false, error: error instanceof Error ? error : new Error("streamed tool call failed") },
448
471
  });
449
472
  }
@@ -466,9 +489,15 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
466
489
  const protocolVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested as typeof MCP_SUPPORTED_PROTOCOL_VERSIONS[number])
467
490
  ? requested
468
491
  : MCP_PROTOCOL_VERSION;
469
- const bootstrap = this.daemonToolEnabled("session_bootstrap")
470
- ? await this.callDaemonTool("session_bootstrap", { path: "." }, authorized).catch(() => null)
471
- : null;
492
+ let bootstrap = null;
493
+ if (this.daemonToolEnabled("session_bootstrap")) {
494
+ try {
495
+ bootstrap = await this.callDaemonTool("session_bootstrap", { path: "." }, authorized);
496
+ } catch {
497
+ // Initialization remains usable without optional local instructions, but the degradation is observable.
498
+ this.observability.recordError("session_bootstrap_failed");
499
+ }
500
+ }
472
501
  const localInstructions = sessionInstructionText(bootstrap);
473
502
  return rpcResult(request.id, {
474
503
  protocolVersion,
@@ -510,12 +539,13 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
510
539
  }
511
540
  return rpcError(request.id, -32601, `Method not found: ${request.method}`);
512
541
  }
513
- private serverInfoResult(base: string, authorized: AuthorizedToken): Record<string, unknown> {
514
- const { daemon, tools, authorization } = this.authorityContext(authorized);
542
+ private async serverInfoResult(base: string, authorized: AuthorizedToken): Promise<Record<string, unknown>> {
543
+ const { daemon, effectiveTools, advertisedTools, authorization } = this.authorityContext(authorized);
515
544
  return buildServerInfoResult({
516
545
  serverName: SERVER_NAME, serverVersion: SERVER_VERSION, base,
517
- oauth: this.authorizationServerMetadata(base), authorization, daemon, tools,
518
- pending: this.pending, daemonRegistry: this.daemonRegistry, observability: this.observability,
546
+ oauth: authorizationServerMetadata(base, SERVER_NAME), authorization, daemon,
547
+ effectiveTools, advertisedTools, pendingSnapshot: await this.durableCalls.snapshot(this.pending.snapshot()),
548
+ daemonRegistry: this.daemonRegistry, observability: this.observability,
519
549
  });
520
550
  }
521
551
  private async callTool(
@@ -527,7 +557,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
527
557
  ): Promise<unknown> {
528
558
  if (name === "server_info") return this.serverInfoResult(base, authorized);
529
559
  if (workspaceTools.some((tool) => tool.name === name)) {
530
- if (!this.daemonToolEnabled(name)) throw new Error(`tool disabled by local daemon policy: ${name}`);
531
560
  if (!accountRoleAllowsTool(authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
532
561
  const result = await this.callDaemonTool(name, args, authorized, requestKey);
533
562
  return name === "project_overview" ? decorateProjectOverview(result, { accountId: authorized.accountId,
@@ -547,18 +576,25 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
547
576
  const daemonAttachment = this.daemonRegistry.readyAttachment(socket);
548
577
  const daemonInstanceId = daemonAttachment?.instanceId ?? "";
549
578
  if (!daemonInstanceId) throw new WorkerToolError("unavailable", "local daemon connection is missing its instance identity", true);
579
+ if (!daemonAttachment?.tools?.includes(name)) throw new WorkerToolError("authorization_denied", `tool disabled by local daemon policy: ${name}`);
550
580
  const id = randomToken("call");
551
581
  const timeoutMs = daemonToolTimeoutMs(name, args);
552
- let result: Promise<unknown>;
582
+ let result!: Promise<unknown>;
553
583
  try {
554
- result = this.pending.register({
555
- id,
556
- socket,
557
- daemonInstanceId,
558
- clientRequestKey: requestKey,
559
- tool: name,
560
- timeoutMs,
561
- onTimeout: (record) => this.daemonCallTimeout(record, name),
584
+ await this.pendingAdmission.run(async () => {
585
+ const durableActive = await this.resumption.calls.activeCount();
586
+ if (this.pending.size + durableActive >= MAX_PENDING_CALLS) {
587
+ throw new WorkerToolError("limit_exceeded", "too many concurrent daemon tool calls", true);
588
+ }
589
+ result = this.pending.register({
590
+ id,
591
+ socket,
592
+ daemonInstanceId,
593
+ clientRequestKey: requestKey,
594
+ tool: name,
595
+ timeoutMs,
596
+ onTimeout: (record) => this.daemonCallTimeout(record, name),
597
+ });
562
598
  });
563
599
  } catch (error) {
564
600
  if (error instanceof PendingCallRegistrationError) {
@@ -603,10 +639,15 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
603
639
 
604
640
  private async cancelClientRequest(requestKey?: string): Promise<void> {
605
641
  if (!requestKey) return;
606
- await this.pending.cancelRequest(requestKey, (record) => {
642
+ const cancelledTransient = await this.pending.cancelRequest(requestKey, (record) => {
607
643
  if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
608
644
  return new WorkerToolError("cancelled", "tool call cancelled by client");
609
645
  });
646
+ if (cancelledTransient) {
647
+ await this.scheduleRuntimeAlarm();
648
+ return;
649
+ }
650
+ if (await this.durableCalls.cancel(requestKey)) await this.scheduleRuntimeAlarm();
610
651
  }
611
652
  private async acceptDaemonWebSocket(request: Request): Promise<Response> {
612
653
  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return new Response("Expected Upgrade: websocket", { status: 426 });
@@ -632,7 +673,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
632
673
  const [client, server] = Object.values(pair) as [WebSocket, WebSocket];
633
674
  this.ctx.acceptWebSocket(server);
634
675
  this.observability.socketCandidate();
635
- this.daemonRegistry.beginCandidate(server, challenge, preflight);
676
+ this.daemonRegistry.beginCandidate(server, challenge, preflight, randomToken("connection"));
636
677
  await this.scheduleRuntimeAlarm();
637
678
  const welcomed = trySendWebSocket(server, {
638
679
  type: "welcome",
@@ -655,16 +696,20 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
655
696
  }
656
697
 
657
698
  private allTools(role: AccountRole): Array<Record<string, unknown>> {
658
- const advertised = accountRoleToolNames(role, this.daemonAdvertisedTools());
699
+ const advertised = accountRoleToolNames(role, workspaceTools.map((tool) => tool.name));
659
700
  const localTools = workspaceTools.filter((tool) => advertised.has(tool.name));
660
701
  return [serverInfoTool, ...localTools].map((tool) => structuredClone(tool));
661
702
  }
703
+ private effectiveToolNames(role: AccountRole): string[] {
704
+ return ["server_info", ...accountRoleToolNames(role, this.daemonAdvertisedTools())];
705
+ }
662
706
  private authorityContext(authorized: AuthorizedToken) {
663
707
  const daemon = describeDaemonCeiling(this.daemonStatus(true));
664
- const tools = this.allTools(authorized.role).map((tool) => String(tool.name));
708
+ const advertisedTools = this.allTools(authorized.role).map((tool) => String(tool.name));
709
+ const effectiveTools = this.effectiveToolNames(authorized.role);
665
710
  const authorization = accountAuthoritySnapshot({ accountId: authorized.accountId, accountVersion: authorized.accountVersion,
666
- role: authorized.role, daemonPolicy: daemon.policy, effectiveTools: tools });
667
- return { daemon, tools, authorization };
711
+ role: authorized.role, daemonPolicy: daemon.policy, effectiveTools });
712
+ return { daemon, effectiveTools, advertisedTools, authorization };
668
713
  }
669
714
  private daemonToolEnabled(name: string): boolean {
670
715
  return this.daemonAdvertisedTools().has(name);
@@ -692,20 +737,22 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
692
737
  const cleanup = this.detachDaemonSocketCalls(ws, message);
693
738
  this.daemonRegistry.expire(ws);
694
739
  sendWebSocketQuietly(ws, { type: "error", error: errorCode });
695
- closeWebSocketQuietly(ws, 1008, closeReason);
740
+ closeWebSocketQuietly(ws, daemonErrorCloseCode(errorCode), closeReason);
696
741
  await cleanup;
697
742
  }
698
743
 
699
744
  private async detachDaemonSocketCalls(ws: WebSocket, message: string): Promise<number> {
700
745
  const attachment = this.daemonRegistry.attachment(ws);
701
- if (!attachment?.instanceId) {
746
+ if (!attachment?.instanceId || !attachment.connectionId) {
702
747
  return await this.pending.rejectSocket(ws, () => new WorkerToolError("unavailable", message, true));
703
748
  }
704
- return this.pending.detachSocket(
749
+ const transient = this.pending.detachSocket(
705
750
  ws,
706
751
  DAEMON_RECONNECT_GRACE_MS,
707
752
  () => new WorkerToolError("unavailable", `${message}; reconnect grace expired`, true),
708
753
  );
754
+ const durable = await this.durableCalls.detach(attachment.connectionId, DAEMON_RECONNECT_GRACE_MS);
755
+ return transient + durable;
709
756
  }
710
757
 
711
758
  private reclaimStaleDaemonSockets(now = Date.now()): void {
@@ -720,6 +767,11 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
720
767
  await processRuntimeAlarm(this.runtimeAlarmContext());
721
768
  }
722
769
 
770
+ private async expireOverdueCalls(): Promise<void> {
771
+ await this.pending.expireDue();
772
+ await this.durableCalls.expireDue();
773
+ }
774
+
723
775
  private async scheduleRuntimeAlarm(): Promise<void> {
724
776
  await scheduleRuntimeAlarm(this.runtimeAlarmContext());
725
777
  }
@@ -728,12 +780,15 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
728
780
  return {
729
781
  storage: this.ctx.storage,
730
782
  pending: this.pending,
783
+ durableCalls: this.resumption.calls,
784
+ expireDurableCall: (call: import("./mcp-pending-call-store.ts").PendingStreamCallView) => this.durableCalls.expire(call),
731
785
  daemonRegistry: this.daemonRegistry,
732
786
  invalidateDaemonSocket: (socket: WebSocket, message: string, closeReason: string, errorCode?: string) =>
733
787
  this.invalidateDaemonSocket(socket, message, closeReason, errorCode),
734
788
  onScheduleError: (error: unknown) => this.observability.event(
735
789
  "error", "runtime.alarm.schedule.failed", { error_class: workerErrorClass(error) },
736
790
  ),
791
+ onAlarmMutation: (action: "set" | "delete" | "noop") => this.observability.runtimeAlarmMutation(action),
737
792
  };
738
793
  }
739
794
 
@@ -760,42 +815,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
760
815
  };
761
816
  }
762
817
 
763
- private mcpMetadata(base: string): Record<string, unknown> {
764
- return {
765
- name: SERVER_NAME,
766
- version: SERVER_VERSION,
767
- protocolVersion: MCP_PROTOCOL_VERSION,
768
- protocolVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS],
769
- transport: { type: "streamable-http", url: `${base}/mcp` },
770
- auth: { type: "oauth", authorization_servers: [base] },
771
- };
772
- }
773
-
774
- private authorizationServerMetadata(base: string): Record<string, unknown> {
775
- return {
776
- issuer: base,
777
- authorization_endpoint: `${base}/oauth/authorize`,
778
- token_endpoint: `${base}/oauth/token`,
779
- registration_endpoint: `${base}/oauth/register`,
780
- response_types_supported: ["code"],
781
- grant_types_supported: ["authorization_code", "refresh_token"],
782
- token_endpoint_auth_methods_supported: ["none"],
783
- dpop_signing_alg_values_supported: ["ES256"],
784
- code_challenge_methods_supported: ["S256"],
785
- scopes_supported: [SERVER_NAME, OFFLINE_ACCESS_SCOPE],
786
- };
787
- }
788
-
789
- private protectedResourceMetadata(base: string): Record<string, unknown> {
790
- return {
791
- resource: `${base}/mcp`,
792
- authorization_servers: [base],
793
- scopes_supported: [SERVER_NAME, OFFLINE_ACCESS_SCOPE],
794
- bearer_methods_supported: ["header"],
795
- resource_name: SERVER_NAME,
796
- };
797
- }
798
-
799
818
  private bodyLimitBytes(): number {
800
819
  const parsed = Number.parseInt(this.env.MBM_WORKER_MAX_BODY_BYTES ?? "", 10);
801
820
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_BODY_BYTES;
@@ -804,30 +823,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
804
823
  }
805
824
 
806
825
  export default {
807
- async fetch(request: Request, env: BridgeEnv, ctx: ExecutionContext): Promise<Response> {
808
- const extraOrigins = env.MBM_ALLOWED_ORIGINS ?? "";
809
- const staticResponse = respondWithoutDurableObject(
810
- request,
811
- { server: SERVER_NAME, version: SERVER_VERSION },
812
- extraOrigins,
813
- );
814
- if (staticResponse) return staticResponse;
815
-
816
- try {
817
- const stub = env.BRIDGE.getByName("default");
818
- const streamed = await proxyMcpEventStream({
819
- request,
820
- bridge: stub,
821
- extraOrigins,
822
- ctx,
823
- });
824
- return streamed ?? stub.fetch(sanitizeBridgeRequest(request));
825
- } catch (error) {
826
- // Free-tier DO request exhaustion must not look like an opaque Worker crash.
827
- if (isDurableObjectQuotaError(error)) return durableObjectQuotaResponse(request, extraOrigins);
828
- throw error;
829
- }
830
- },
826
+ fetch: (request: Request, env: BridgeEnv, ctx: ExecutionContext) =>
827
+ handleOuterWorkerFetch(request, env, ctx, { server: SERVER_NAME, version: SERVER_VERSION }),
831
828
  } satisfies ExportedHandler<BridgeEnv>;
832
829
 
833
830
  function daemonInstanceId(value: unknown): string {
@@ -0,0 +1,20 @@
1
+ export function positiveDelay(value: unknown): number {
2
+ return Math.max(1, Math.floor(Number(value) || 1));
3
+ }
4
+
5
+ export function extendAttachedCallExpiry(
6
+ currentExpiry: number,
7
+ operationDeadline: number,
8
+ terminalRetentionMs: number,
9
+ ): number {
10
+ return Math.max(currentExpiry, operationDeadline + terminalRetentionMs);
11
+ }
12
+
13
+ export function extendDetachedCallExpiry(
14
+ currentExpiry: number,
15
+ reconnectDeadline: number,
16
+ remainingOperationMs: number,
17
+ terminalRetentionMs: number,
18
+ ): number {
19
+ return Math.max(currentExpiry, reconnectDeadline + remainingOperationMs + terminalRetentionMs);
20
+ }
@@ -0,0 +1,33 @@
1
+ import type { StreamIndexEntry } from "./mcp-resumption-records.ts";
2
+
3
+ export type PendingStreamCallSnapshot = {
4
+ active: number;
5
+ detached: number;
6
+ request_keys: number;
7
+ maximum: number;
8
+ oldest_ms: number;
9
+ by_tool: Record<string, number>;
10
+ };
11
+
12
+ export function activePendingCallCount(entries: StreamIndexEntry[]): number {
13
+ return entries.filter((entry) => entry.call).length;
14
+ }
15
+
16
+ export function pendingCallSnapshot(
17
+ entries: StreamIndexEntry[],
18
+ now: number,
19
+ maximum: number,
20
+ ): PendingStreamCallSnapshot {
21
+ const calls = entries.flatMap((entry) => entry.call ? [entry.call] : []);
22
+ const byTool = Object.create(null) as Record<string, number>;
23
+ let detached = 0;
24
+ let oldestMs = 0;
25
+ let requestKeys = 0;
26
+ for (const call of calls) {
27
+ byTool[call.tool] = (byTool[call.tool] ?? 0) + 1;
28
+ detached += Number(call.state === "detached");
29
+ requestKeys += Number(Boolean(call.client_request_key));
30
+ oldestMs = Math.max(oldestMs, now - call.started_at);
31
+ }
32
+ return { active: calls.length, detached, request_keys: requestKeys, maximum, oldest_ms: oldestMs, by_tool: byTool };
33
+ }
@@ -0,0 +1,58 @@
1
+ export type PendingStreamTransform = {
2
+ kind: "project_overview";
3
+ account_id: string;
4
+ account_version: number;
5
+ role: "owner" | "operator" | "editor" | "reviewer";
6
+ };
7
+
8
+ export type PendingStreamCall = {
9
+ call_id: string;
10
+ daemon_instance_id: string;
11
+ connection_id: string;
12
+ client_request_key?: string;
13
+ tool: string;
14
+ state: "attached" | "detached";
15
+ started_at: number;
16
+ operation_deadline_at: number;
17
+ remaining_timeout_ms: number;
18
+ reconnect_deadline_at?: number;
19
+ transform?: PendingStreamTransform;
20
+ };
21
+
22
+ const CALL_ID_PATTERN = /^call_[A-Za-z0-9_-]{43}$/;
23
+ const CONNECTION_ID_PATTERN = /^connection_[A-Za-z0-9_-]{43}$/;
24
+ const DAEMON_INSTANCE_PATTERN = /^daemon_[A-Za-z0-9_-]{16,96}$/;
25
+
26
+ export function validPendingStreamCall(value: unknown): value is PendingStreamCall {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
28
+ const call = value as Partial<PendingStreamCall>;
29
+ const transform = call.transform;
30
+ const validTransform = transform === undefined || (
31
+ transform.kind === "project_overview"
32
+ && typeof transform.account_id === "string"
33
+ && transform.account_id.length > 0
34
+ && transform.account_id.length <= 160
35
+ && Number.isSafeInteger(transform.account_version)
36
+ && transform.account_version! >= 1
37
+ && ["owner", "operator", "editor", "reviewer"].includes(String(transform.role))
38
+ );
39
+ return typeof call.call_id === "string" && CALL_ID_PATTERN.test(call.call_id)
40
+ && typeof call.daemon_instance_id === "string" && DAEMON_INSTANCE_PATTERN.test(call.daemon_instance_id)
41
+ && typeof call.connection_id === "string" && CONNECTION_ID_PATTERN.test(call.connection_id)
42
+ && (call.client_request_key === undefined || (
43
+ typeof call.client_request_key === "string"
44
+ && call.client_request_key.length > 0
45
+ && call.client_request_key.length <= 1024
46
+ ))
47
+ && typeof call.tool === "string" && call.tool.length > 0 && call.tool.length <= 128
48
+ && (call.state === "attached" || call.state === "detached")
49
+ && Number.isSafeInteger(call.started_at) && call.started_at! >= 0
50
+ && Number.isSafeInteger(call.operation_deadline_at) && call.operation_deadline_at! > call.started_at!
51
+ && Number.isSafeInteger(call.remaining_timeout_ms) && call.remaining_timeout_ms! >= 1
52
+ && (call.reconnect_deadline_at === undefined || (
53
+ Number.isSafeInteger(call.reconnect_deadline_at)
54
+ && call.reconnect_deadline_at! >= call.started_at!
55
+ ))
56
+ && (call.state === "attached" ? call.reconnect_deadline_at === undefined : call.reconnect_deadline_at !== undefined)
57
+ && validTransform;
58
+ }