machine-bridge-mcp 3.0.0-beta.20 → 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 +16 -0
- package/README.md +2 -0
- package/SECURITY.md +4 -4
- package/browser-extension/manifest.json +1 -1
- package/docs/ARCHITECTURE.md +11 -9
- package/docs/AUDIT.md +16 -0
- package/docs/ENGINEERING.md +1 -1
- package/docs/LOCAL_AUTHORIZATION.md +1 -1
- package/docs/LOGGING.md +3 -1
- package/docs/MULTI_ACCOUNT.md +1 -1
- package/docs/OPERATIONS.md +5 -3
- package/docs/OVERVIEW.md +1 -1
- package/docs/PRIVACY.md +1 -1
- package/docs/TESTING.md +9 -7
- package/docs/THREAT_MODEL.md +3 -2
- package/docs/UPGRADING.md +7 -1
- package/package.json +1 -1
- 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/runtime-activation.mjs +16 -0
- package/src/local/runtime.mjs +3 -1
- package/src/local/worker-deployment.mjs +16 -6
- 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/index.ts +120 -88
- 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 +80 -87
- package/src/worker/mcp-stream-dispatch.ts +42 -54
- 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 +29 -39
- package/src/worker/websocket-protocol.ts +4 -0
- package/src/worker/worker-entry.ts +50 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { PendingCallOutcome
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import type { PendingCallOutcome } from "./pending-call-contract.ts";
|
|
2
|
+
import type { JsonRpcMessage, McpResumptionStore } from "./mcp-resumption.ts";
|
|
3
|
+
import {
|
|
4
|
+
McpPendingCallConflictError,
|
|
5
|
+
McpPendingCallLimitError,
|
|
6
|
+
} from "./mcp-pending-call-store.ts";
|
|
7
|
+
import type { PendingStreamTransform } from "./mcp-pending-call-records.ts";
|
|
4
8
|
import { publicWorkerToolError, WorkerToolError } from "./errors.ts";
|
|
5
9
|
import { workerErrorClass } from "./http.ts";
|
|
6
10
|
import { rpcResult, textToolResult } from "./mcp-jsonrpc.ts";
|
|
7
|
-
import type { JsonRpcMessage, McpResumptionStore } from "./mcp-resumption.ts";
|
|
8
11
|
import { randomToken } from "./oauth-state.ts";
|
|
9
12
|
import type { WorkerObservability } from "./observability.ts";
|
|
10
13
|
|
|
@@ -17,7 +20,6 @@ export type StreamCallAuthorization = {
|
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
type StartStreamCallInput = {
|
|
20
|
-
pending: PendingCallRegistry;
|
|
21
23
|
resumption: McpResumptionStore;
|
|
22
24
|
observability: WorkerObservability;
|
|
23
25
|
streamId: string;
|
|
@@ -27,11 +29,13 @@ type StartStreamCallInput = {
|
|
|
27
29
|
arguments: Record<string, unknown>;
|
|
28
30
|
socket: WebSocket;
|
|
29
31
|
daemonInstanceId: string;
|
|
32
|
+
connectionId: string;
|
|
30
33
|
timeoutMs: number;
|
|
31
34
|
authorization: StreamCallAuthorization;
|
|
32
|
-
|
|
35
|
+
transientActiveCount: number;
|
|
36
|
+
maximumPendingCalls: number;
|
|
37
|
+
transform?: PendingStreamTransform;
|
|
33
38
|
onSendFailure: () => void | Promise<void>;
|
|
34
|
-
transformResult?: (value: unknown) => unknown;
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
type ImmediateOutcomeInput = {
|
|
@@ -50,9 +54,10 @@ export function buildServerInfoResult(input: {
|
|
|
50
54
|
oauth: Record<string, unknown>;
|
|
51
55
|
authorization: Record<string, any>;
|
|
52
56
|
daemon: Record<string, any>;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
effectiveTools: string[];
|
|
58
|
+
advertisedTools: string[];
|
|
59
|
+
pendingSnapshot: Record<string, unknown>;
|
|
60
|
+
daemonRegistry: import("./daemon-sockets.ts").DaemonSocketRegistry;
|
|
56
61
|
observability: WorkerObservability;
|
|
57
62
|
}): Record<string, unknown> {
|
|
58
63
|
const probing = input.daemonRegistry.probingSockets().length;
|
|
@@ -68,7 +73,7 @@ export function buildServerInfoResult(input: {
|
|
|
68
73
|
authority_summary: input.authorization.summary,
|
|
69
74
|
daemon: input.daemon,
|
|
70
75
|
worker: {
|
|
71
|
-
pending_calls: input.
|
|
76
|
+
pending_calls: input.pendingSnapshot,
|
|
72
77
|
daemon_candidates: candidates,
|
|
73
78
|
daemon_probes: probing,
|
|
74
79
|
sockets_live: {
|
|
@@ -79,14 +84,15 @@ export function buildServerInfoResult(input: {
|
|
|
79
84
|
},
|
|
80
85
|
observability: input.observability.snapshot(),
|
|
81
86
|
},
|
|
82
|
-
tools: input.
|
|
87
|
+
tools: input.effectiveTools,
|
|
83
88
|
tools_scope: "authenticated_account_effective_tools_before_host_filtering",
|
|
84
89
|
tool_delivery: {
|
|
85
90
|
full_profile_scope: "daemon-capability-ceiling-before-account-filtering",
|
|
86
91
|
daemon_advertised_tool_count: input.daemon.tool_count,
|
|
87
|
-
relay_advertised_tool_count: input.
|
|
88
|
-
effective_account_tool_count: input.
|
|
89
|
-
relay_advertised_scope: "
|
|
92
|
+
relay_advertised_tool_count: input.advertisedTools.length,
|
|
93
|
+
effective_account_tool_count: input.effectiveTools.length,
|
|
94
|
+
relay_advertised_scope: "stable_authenticated_account_catalog_before_host_filtering",
|
|
95
|
+
effective_scope: "live_daemon_and_account_intersection_before_host_filtering",
|
|
90
96
|
host_exposed_tools_known_to_server: false,
|
|
91
97
|
host_may_expose_subset: true,
|
|
92
98
|
},
|
|
@@ -95,30 +101,28 @@ export function buildServerInfoResult(input: {
|
|
|
95
101
|
|
|
96
102
|
export async function startEventDrivenStreamCall(input: StartStreamCallInput): Promise<void> {
|
|
97
103
|
const callId = randomToken("call");
|
|
98
|
-
const settle = createStreamSettlement({
|
|
99
|
-
resumption: input.resumption,
|
|
100
|
-
observability: input.observability,
|
|
101
|
-
streamId: input.streamId,
|
|
102
|
-
requestId: input.requestId,
|
|
103
|
-
tool: input.tool,
|
|
104
|
-
transformResult: input.transformResult,
|
|
105
|
-
});
|
|
106
104
|
try {
|
|
107
|
-
input.
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
await input.resumption.calls.activate({
|
|
106
|
+
streamId: input.streamId,
|
|
107
|
+
callId,
|
|
110
108
|
daemonInstanceId: input.daemonInstanceId,
|
|
109
|
+
connectionId: input.connectionId,
|
|
111
110
|
clientRequestKey: input.clientRequestKey,
|
|
112
111
|
tool: input.tool,
|
|
113
112
|
timeoutMs: input.timeoutMs,
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
transform: input.transform,
|
|
114
|
+
transientActiveCount: input.transientActiveCount,
|
|
115
|
+
maximumPendingCalls: input.maximumPendingCalls,
|
|
116
116
|
});
|
|
117
117
|
} catch (error) {
|
|
118
|
-
if (error instanceof
|
|
118
|
+
if (error instanceof McpPendingCallLimitError) {
|
|
119
|
+
throw new WorkerToolError("limit_exceeded", error.message, true);
|
|
120
|
+
}
|
|
121
|
+
if (error instanceof McpPendingCallConflictError) {
|
|
122
|
+
throw new WorkerToolError("conflict", error.message);
|
|
123
|
+
}
|
|
119
124
|
throw error;
|
|
120
125
|
}
|
|
121
|
-
input.resumption.activate(input.streamId);
|
|
122
126
|
input.observability.callStarted(input.tool);
|
|
123
127
|
try {
|
|
124
128
|
input.socket.send(JSON.stringify({
|
|
@@ -130,7 +134,9 @@ export async function startEventDrivenStreamCall(input: StartStreamCallInput): P
|
|
|
130
134
|
authorization: input.authorization,
|
|
131
135
|
}));
|
|
132
136
|
} catch {
|
|
133
|
-
|
|
137
|
+
const error = new WorkerToolError("network_error", "failed to send daemon tool call", true);
|
|
138
|
+
await input.resumption.calls.complete(callId, input.connectionId, streamTerminalMessage(input.requestId, { ok: false, error }));
|
|
139
|
+
input.observability.callFinished(input.tool, error.code);
|
|
134
140
|
await input.onSendFailure();
|
|
135
141
|
}
|
|
136
142
|
}
|
|
@@ -144,28 +150,7 @@ export async function persistImmediateStreamOutcome(input: ImmediateOutcomeInput
|
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
function
|
|
148
|
-
resumption: McpResumptionStore;
|
|
149
|
-
observability: WorkerObservability;
|
|
150
|
-
streamId: string;
|
|
151
|
-
requestId: string | number;
|
|
152
|
-
tool: string;
|
|
153
|
-
transformResult?: (value: unknown) => unknown;
|
|
154
|
-
}): (outcome: PendingCallOutcome) => Promise<void> {
|
|
155
|
-
return async (outcome) => {
|
|
156
|
-
const normalized = normalizeOutcome(outcome, input.transformResult);
|
|
157
|
-
const code = normalized.ok ? "" : publicWorkerToolError(normalized.error).code;
|
|
158
|
-
const message = streamTerminalMessage(input.requestId, normalized);
|
|
159
|
-
try {
|
|
160
|
-
await input.resumption.complete(input.streamId, message);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
input.observability.event("error", "mcp.stream.persist.failed", { error_class: workerErrorClass(error) });
|
|
163
|
-
}
|
|
164
|
-
input.observability.callFinished(input.tool, code);
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function streamTerminalMessage(
|
|
153
|
+
export function streamTerminalMessage(
|
|
169
154
|
requestId: string | number,
|
|
170
155
|
outcome: PendingCallOutcome,
|
|
171
156
|
transformResult?: (value: unknown) => unknown,
|
|
@@ -179,7 +164,10 @@ function streamTerminalMessage(
|
|
|
179
164
|
return message;
|
|
180
165
|
}
|
|
181
166
|
|
|
182
|
-
function normalizeOutcome(
|
|
167
|
+
export function normalizeOutcome(
|
|
168
|
+
outcome: PendingCallOutcome,
|
|
169
|
+
transformResult?: (value: unknown) => unknown,
|
|
170
|
+
): PendingCallOutcome {
|
|
183
171
|
if (!outcome.ok || !transformResult) return outcome;
|
|
184
172
|
try { return { ok: true, value: transformResult(outcome.value) }; }
|
|
185
173
|
catch (error) { return { ok: false, error: error instanceof Error ? error : new Error("stream result transformation failed") }; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class PendingAdmissionGate {
|
|
2
|
+
private tail: Promise<void> = Promise.resolve();
|
|
3
|
+
|
|
4
|
+
async run<T>(operation: () => T | Promise<T>): Promise<T> {
|
|
5
|
+
let release: () => void = () => {};
|
|
6
|
+
const predecessor = this.tail;
|
|
7
|
+
this.tail = new Promise<void>((resolve) => { release = resolve; });
|
|
8
|
+
await predecessor;
|
|
9
|
+
try {
|
|
10
|
+
return await operation();
|
|
11
|
+
} finally {
|
|
12
|
+
release();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -2,9 +2,10 @@ export type PendingCallOutcome =
|
|
|
2
2
|
| { ok: true; value: unknown }
|
|
3
3
|
| { ok: false; error: Error };
|
|
4
4
|
|
|
5
|
-
export type PendingCallSettlement =
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export type PendingCallSettlement = {
|
|
6
|
+
resolve: (value: unknown) => void;
|
|
7
|
+
reject: (error: Error) => void;
|
|
8
|
+
};
|
|
8
9
|
|
|
9
10
|
export interface PendingCallRecord {
|
|
10
11
|
id: string;
|
|
@@ -36,7 +37,3 @@ export interface RegisterPendingCall {
|
|
|
36
37
|
signal?: AbortSignal;
|
|
37
38
|
onAbort?: (record: PendingCallRecord) => Error;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
export interface RegisterEventPendingCall extends RegisterPendingCall {
|
|
41
|
-
settle: (outcome: PendingCallOutcome) => void | Promise<void>;
|
|
42
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PendingCallOutcome, PendingCallRecord, PendingCallSettlement,
|
|
1
|
+
import type { PendingCallOutcome, PendingCallRecord, PendingCallSettlement, RegisterPendingCall } from "./pending-call-contract.ts";
|
|
2
2
|
import { PendingCallDeadlines, type PendingCallDeadlineOptions } from "./pending-call-deadlines.ts";
|
|
3
3
|
|
|
4
4
|
export class PendingCallRegistrationError extends Error {
|
|
@@ -41,15 +41,10 @@ export class PendingCallRegistry {
|
|
|
41
41
|
let resolveResult!: (value: unknown) => void;
|
|
42
42
|
let rejectResult!: (error: Error) => void;
|
|
43
43
|
const result = new Promise<unknown>((resolve, reject) => { resolveResult = resolve; rejectResult = reject; });
|
|
44
|
-
this.add(input, {
|
|
44
|
+
this.add(input, { resolve: resolveResult, reject: rejectResult });
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
registerEvent(input: RegisterEventPendingCall): void {
|
|
49
|
-
this.assertCanRegister(input);
|
|
50
|
-
this.add(input, { kind: "event", settle: input.settle });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
48
|
resolve(id: string, socket: WebSocket, value: unknown): Promise<boolean> {
|
|
54
49
|
const record = this.byId.get(id);
|
|
55
50
|
return !record || record.socket !== socket ? Promise.resolve(false) : this.finish(id, { ok: true, value });
|
|
@@ -101,7 +96,7 @@ export class PendingCallRegistry {
|
|
|
101
96
|
|
|
102
97
|
snapshot(): { active: number; detached: number; request_keys: number; maximum: number; oldest_ms: number; by_tool: Record<string, number> } {
|
|
103
98
|
const now = this.deadlines.now();
|
|
104
|
-
const byTool
|
|
99
|
+
const byTool = Object.create(null) as Record<string, number>;
|
|
105
100
|
let detached = 0;
|
|
106
101
|
let oldestMs = 0;
|
|
107
102
|
for (const record of this.byId.values()) {
|
|
@@ -161,8 +156,7 @@ export class PendingCallRegistry {
|
|
|
161
156
|
private async finish(id: string, outcome: PendingCallOutcome): Promise<boolean> {
|
|
162
157
|
const record = this.take(id);
|
|
163
158
|
if (!record) return false;
|
|
164
|
-
|
|
165
|
-
else await record.settlement.settle(outcome);
|
|
159
|
+
outcome.ok ? record.settlement.resolve(outcome.value) : record.settlement.reject(outcome.error);
|
|
166
160
|
return true;
|
|
167
161
|
}
|
|
168
162
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface AlarmStorage {
|
|
2
|
+
getAlarm(): Promise<number | null>;
|
|
3
|
+
setAlarm(scheduledTime: number | Date): Promise<void>;
|
|
4
|
+
deleteAlarm(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function writeEarliestRuntimeAlarm(input: {
|
|
8
|
+
storage: AlarmStorage;
|
|
9
|
+
nextDeadline: number;
|
|
10
|
+
now: number;
|
|
11
|
+
onError: (error: unknown) => void;
|
|
12
|
+
onMutation?: (action: "set" | "delete" | "noop") => void;
|
|
13
|
+
}): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const current = await input.storage.getAlarm();
|
|
16
|
+
if (!Number.isFinite(input.nextDeadline)) {
|
|
17
|
+
if (current === null) {
|
|
18
|
+
input.onMutation?.("noop");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await input.storage.deleteAlarm();
|
|
22
|
+
input.onMutation?.("delete");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const target = Math.max(input.now, input.nextDeadline);
|
|
26
|
+
if (current !== null && current > input.now && current <= target) {
|
|
27
|
+
input.onMutation?.("noop");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await input.storage.setAlarm(target);
|
|
31
|
+
input.onMutation?.("set");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
input.onError(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -4,14 +4,10 @@ import {
|
|
|
4
4
|
daemonReadyDeadlineMs,
|
|
5
5
|
} from "./daemon-liveness.ts";
|
|
6
6
|
import type { DaemonSocketRegistry } from "./daemon-sockets.ts";
|
|
7
|
+
import type { McpPendingCallStore, PendingStreamCallView } from "./mcp-pending-call-store.ts";
|
|
7
8
|
import type { PendingCallRegistry } from "./pending-calls.ts";
|
|
8
9
|
import { closeWebSocketQuietly, sendWebSocketQuietly } from "./websocket-protocol.ts";
|
|
9
|
-
|
|
10
|
-
interface AlarmStorage {
|
|
11
|
-
getAlarm(): Promise<number | null>;
|
|
12
|
-
setAlarm(scheduledTime: number | Date): Promise<void>;
|
|
13
|
-
deleteAlarm(): Promise<void>;
|
|
14
|
-
}
|
|
10
|
+
import { writeEarliestRuntimeAlarm, type AlarmStorage } from "./runtime-alarm-storage.ts";
|
|
15
11
|
|
|
16
12
|
type InvalidateDaemonSocket = (
|
|
17
13
|
socket: WebSocket,
|
|
@@ -23,6 +19,8 @@ type InvalidateDaemonSocket = (
|
|
|
23
19
|
interface RuntimeAlarmContext {
|
|
24
20
|
storage: AlarmStorage;
|
|
25
21
|
pending: PendingCallRegistry;
|
|
22
|
+
durableCalls?: Pick<McpPendingCallStore, "due" | "nextDeadlineDelayMs">;
|
|
23
|
+
expireDurableCall?: (call: PendingStreamCallView) => Promise<void>;
|
|
26
24
|
daemonRegistry: DaemonSocketRegistry;
|
|
27
25
|
invalidateDaemonSocket: InvalidateDaemonSocket;
|
|
28
26
|
onScheduleError: (error: unknown) => void;
|
|
@@ -31,7 +29,10 @@ interface RuntimeAlarmContext {
|
|
|
31
29
|
|
|
32
30
|
export async function processRuntimeAlarm(context: RuntimeAlarmContext, now = Date.now()): Promise<void> {
|
|
33
31
|
await context.pending.expireDue();
|
|
34
|
-
|
|
32
|
+
if (context.durableCalls && context.expireDurableCall) {
|
|
33
|
+
for (const call of await context.durableCalls.due(now)) await context.expireDurableCall(call);
|
|
34
|
+
}
|
|
35
|
+
let nextDeadline = await pendingAlarmDeadline(context, now);
|
|
35
36
|
for (const socket of context.daemonRegistry.candidateSockets()) {
|
|
36
37
|
const attachment = context.daemonRegistry.attachment(socket);
|
|
37
38
|
const connectedAt = Date.parse(attachment?.connectedAt ?? "");
|
|
@@ -67,11 +68,17 @@ export async function processRuntimeAlarm(context: RuntimeAlarmContext, now = Da
|
|
|
67
68
|
}
|
|
68
69
|
nextDeadline = Math.min(nextDeadline, deadline);
|
|
69
70
|
}
|
|
70
|
-
await
|
|
71
|
+
await writeEarliestRuntimeAlarm({
|
|
72
|
+
storage: context.storage,
|
|
73
|
+
nextDeadline,
|
|
74
|
+
now,
|
|
75
|
+
onError: context.onScheduleError,
|
|
76
|
+
onMutation: context.onAlarmMutation,
|
|
77
|
+
});
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export async function scheduleRuntimeAlarm(context: RuntimeAlarmContext, now = Date.now()): Promise<void> {
|
|
74
|
-
let nextDeadline = pendingAlarmDeadline(context
|
|
81
|
+
let nextDeadline = await pendingAlarmDeadline(context, now);
|
|
75
82
|
for (const socket of context.daemonRegistry.candidateSockets()) {
|
|
76
83
|
const connectedAt = Date.parse(context.daemonRegistry.attachment(socket)?.connectedAt ?? "");
|
|
77
84
|
if (!Number.isFinite(connectedAt)) {
|
|
@@ -103,37 +110,20 @@ export async function scheduleRuntimeAlarm(context: RuntimeAlarmContext, now = D
|
|
|
103
110
|
}
|
|
104
111
|
nextDeadline = Math.min(nextDeadline, deadline);
|
|
105
112
|
}
|
|
106
|
-
await
|
|
113
|
+
await writeEarliestRuntimeAlarm({
|
|
114
|
+
storage: context.storage,
|
|
115
|
+
nextDeadline,
|
|
116
|
+
now,
|
|
117
|
+
onError: context.onScheduleError,
|
|
118
|
+
onMutation: context.onAlarmMutation,
|
|
119
|
+
});
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
function pendingAlarmDeadline(
|
|
110
|
-
const
|
|
122
|
+
async function pendingAlarmDeadline(context: RuntimeAlarmContext, now: number): Promise<number> {
|
|
123
|
+
const transientDelay = context.pending.nextDeadlineDelayMs();
|
|
124
|
+
const durableDelay = context.durableCalls
|
|
125
|
+
? await context.durableCalls.nextDeadlineDelayMs()
|
|
126
|
+
: Number.POSITIVE_INFINITY;
|
|
127
|
+
const delay = Math.min(transientDelay, durableDelay);
|
|
111
128
|
return Number.isFinite(delay) ? now + Math.max(1, Math.ceil(delay)) : Number.POSITIVE_INFINITY;
|
|
112
129
|
}
|
|
113
|
-
|
|
114
|
-
async function writeRuntimeAlarm(context: RuntimeAlarmContext, nextDeadline: number, now: number): Promise<void> {
|
|
115
|
-
try {
|
|
116
|
-
const current = await context.storage.getAlarm();
|
|
117
|
-
if (!Number.isFinite(nextDeadline)) {
|
|
118
|
-
if (current === null) {
|
|
119
|
-
context.onAlarmMutation?.("noop");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
await context.storage.deleteAlarm();
|
|
123
|
-
context.onAlarmMutation?.("delete");
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const target = Math.max(now, nextDeadline);
|
|
128
|
-
// An earlier future alarm is safe: it will re-evaluate current liveness and
|
|
129
|
-
// pending deadlines. Do not rewrite it on every heartbeat merely to move it later.
|
|
130
|
-
if (current !== null && current > now && current <= target) {
|
|
131
|
-
context.onAlarmMutation?.("noop");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
await context.storage.setAlarm(target);
|
|
135
|
-
context.onAlarmMutation?.("set");
|
|
136
|
-
} catch (error) {
|
|
137
|
-
context.onScheduleError(error);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -21,6 +21,10 @@ export function sendWebSocketQuietly(ws: WebSocket, value: unknown): void {
|
|
|
21
21
|
trySendWebSocket(ws, value);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function daemonErrorCloseCode(errorCode: string): number {
|
|
25
|
+
return errorCode === "daemon_transport_error" || errorCode === "daemon_liveness_timeout" ? 1012 : 1008;
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
export function closeWebSocketQuietly(ws: WebSocket, code?: number, reason?: string): void {
|
|
25
29
|
try {
|
|
26
30
|
ws.close(code, reason);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
proxyMcpEventStream,
|
|
3
|
+
sanitizeBridgeRequest,
|
|
4
|
+
} from "./mcp-stream-proxy.ts";
|
|
5
|
+
import { respondWithoutDurableObject } from "./worker-static-routes.ts";
|
|
6
|
+
import { createThrottledEdgeLogger } from "./worker-edge-log.ts";
|
|
7
|
+
import {
|
|
8
|
+
admitStatefulRequest,
|
|
9
|
+
durableObjectQuotaResponse,
|
|
10
|
+
isDurableObjectQuotaError,
|
|
11
|
+
outerWorkerErrorClass,
|
|
12
|
+
workerGatewayErrorResponse,
|
|
13
|
+
} from "./worker-edge-guard.ts";
|
|
14
|
+
|
|
15
|
+
type OuterBridgeStub = { fetch(request: Request): Promise<Response> };
|
|
16
|
+
type OuterBridgeNamespace = { getByName(name: string): OuterBridgeStub };
|
|
17
|
+
|
|
18
|
+
export interface OuterWorkerEnv {
|
|
19
|
+
BRIDGE: OuterBridgeNamespace;
|
|
20
|
+
STATEFUL_RATE_LIMITER: RateLimit;
|
|
21
|
+
MBM_ALLOWED_ORIGINS?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const logOuterFetchFailure = createThrottledEdgeLogger();
|
|
25
|
+
|
|
26
|
+
export async function handleOuterWorkerFetch(
|
|
27
|
+
request: Request,
|
|
28
|
+
env: OuterWorkerEnv,
|
|
29
|
+
ctx: ExecutionContext,
|
|
30
|
+
identity: { server: string; version: string },
|
|
31
|
+
): Promise<Response> {
|
|
32
|
+
const extraOrigins = env.MBM_ALLOWED_ORIGINS ?? "";
|
|
33
|
+
const staticResponse = respondWithoutDurableObject(request, identity, extraOrigins);
|
|
34
|
+
if (staticResponse) return staticResponse;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const limited = await admitStatefulRequest(request, env.STATEFUL_RATE_LIMITER, extraOrigins);
|
|
38
|
+
if (limited) return limited;
|
|
39
|
+
const stub = env.BRIDGE.getByName("default");
|
|
40
|
+
const streamed = await proxyMcpEventStream({ request, bridge: stub, extraOrigins, ctx });
|
|
41
|
+
return streamed ?? stub.fetch(sanitizeBridgeRequest(request));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isDurableObjectQuotaError(error)) return durableObjectQuotaResponse(request, extraOrigins);
|
|
44
|
+
logOuterFetchFailure("error", "outer.fetch.failed", {
|
|
45
|
+
path: new URL(request.url).pathname,
|
|
46
|
+
error_class: outerWorkerErrorClass(error),
|
|
47
|
+
});
|
|
48
|
+
return workerGatewayErrorResponse(request, extraOrigins);
|
|
49
|
+
}
|
|
50
|
+
}
|