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.
- package/CHANGELOG.md +50 -0
- package/README.md +2 -0
- package/SECURITY.md +9 -7
- package/browser-extension/browser-error-boundary.js +45 -0
- package/browser-extension/browser-operations.js +1 -1
- package/browser-extension/manifest.json +1 -1
- package/browser-extension/service-worker.js +9 -8
- package/docs/ARCHITECTURE.md +17 -15
- package/docs/AUDIT.md +30 -0
- package/docs/ENGINEERING.md +3 -3
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/LOCAL_AUTHORIZATION.md +2 -2
- package/docs/LOCAL_AUTOMATION.md +1 -1
- package/docs/LOGGING.md +3 -1
- package/docs/MULTI_ACCOUNT.md +2 -2
- package/docs/OPERATIONS.md +13 -5
- package/docs/OVERVIEW.md +2 -2
- package/docs/PRIVACY.md +1 -1
- package/docs/TESTING.md +17 -13
- package/docs/THREAT_MODEL.md +13 -8
- package/docs/UPGRADING.md +19 -1
- package/package.json +1 -1
- package/scripts/coverage-check.mjs +12 -0
- package/src/local/account-admin.mjs +68 -1
- package/src/local/agent-skill-discovery.mjs +16 -5
- package/src/local/app-automation.mjs +27 -7
- package/src/local/cli-options.mjs +1 -1
- package/src/local/path-inspection.mjs +23 -0
- package/src/local/process-tree-ownership.mjs +17 -5
- package/src/local/process-tree.mjs +6 -2
- package/src/local/relay-connection.mjs +24 -11
- package/src/local/resource-operations.mjs +37 -8
- package/src/local/runtime-activation.mjs +16 -0
- package/src/local/runtime-capabilities.mjs +15 -3
- package/src/local/runtime.mjs +7 -4
- package/src/local/worker-deployment.mjs +16 -6
- package/src/local/workspace-file-service.mjs +29 -11
- package/src/worker/daemon-socket-attachment.ts +52 -0
- package/src/worker/daemon-sockets.ts +17 -48
- package/src/worker/durable-stream-calls.ts +129 -0
- package/src/worker/durable-stream-result.ts +22 -0
- package/src/worker/http.ts +37 -15
- package/src/worker/index.ts +143 -146
- package/src/worker/mcp-pending-call-expiry.ts +20 -0
- package/src/worker/mcp-pending-call-inspection.ts +33 -0
- package/src/worker/mcp-pending-call-records.ts +58 -0
- package/src/worker/mcp-pending-call-storage.ts +18 -0
- package/src/worker/mcp-pending-call-store.ts +285 -0
- package/src/worker/mcp-resumption-index.ts +41 -0
- package/src/worker/mcp-resumption-records.ts +8 -2
- package/src/worker/mcp-resumption.ts +84 -86
- package/src/worker/mcp-stream-dispatch.ts +42 -54
- package/src/worker/mcp-stream-proxy.ts +7 -69
- package/src/worker/mcp-stream-subscription.ts +98 -0
- package/src/worker/oauth-controller.ts +11 -2
- package/src/worker/oauth-refresh-exchange.ts +147 -0
- package/src/worker/oauth-refresh-families.ts +59 -17
- package/src/worker/oauth-state.ts +13 -2
- package/src/worker/oauth-token-derivation.ts +33 -0
- package/src/worker/oauth-token-issuance.ts +106 -0
- package/src/worker/oauth-tokens.ts +12 -184
- package/src/worker/observability.ts +19 -0
- package/src/worker/pending-admission.ts +15 -0
- package/src/worker/pending-call-contract.ts +4 -7
- package/src/worker/pending-calls.ts +4 -10
- package/src/worker/runtime-alarm-storage.ts +35 -0
- package/src/worker/runtime-alarm.ts +30 -20
- package/src/worker/websocket-protocol.ts +4 -0
- package/src/worker/worker-edge-guard.ts +81 -0
- package/src/worker/worker-edge-log.ts +63 -0
- package/src/worker/worker-entry.ts +50 -0
- package/src/worker/worker-metadata.ts +39 -0
- package/src/worker/worker-static-routes.ts +38 -50
- package/wrangler.jsonc +8 -0
package/src/worker/index.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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 =
|
|
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)
|
|
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
|
|
328
|
-
?
|
|
329
|
-
:
|
|
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
|
|
420
|
+
requestId,
|
|
400
421
|
});
|
|
401
422
|
} catch (error) {
|
|
402
423
|
if (error instanceof McpStreamLimitError) {
|
|
403
|
-
return json(rpcError(
|
|
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(
|
|
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
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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,
|
|
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:
|
|
518
|
-
|
|
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
|
|
582
|
+
let result!: Promise<unknown>;
|
|
553
583
|
try {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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,
|
|
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
|
|
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
|
|
667
|
-
return { daemon,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
808
|
-
|
|
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
|
+
}
|