machine-bridge-mcp 3.0.0-beta.15 → 3.0.0-beta.17
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 +26 -0
- package/browser-extension/manifest.json +1 -1
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/AUDIT.md +15 -1
- package/docs/ENGINEERING.md +1 -1
- package/docs/OPERATIONS.md +10 -2
- package/docs/TESTING.md +1 -1
- package/package.json +1 -1
- package/src/local/managed-job-plan.mjs +2 -2
- package/src/worker/daemon-sockets.ts +9 -13
- package/src/worker/index.ts +76 -88
- package/src/worker/mcp-resumption.ts +7 -9
- package/src/worker/mcp-stream-channel.ts +125 -0
- package/src/worker/mcp-stream-proxy.ts +78 -50
- package/src/worker/observability.ts +17 -0
- package/src/worker/pending-call-contract.ts +2 -0
- package/src/worker/pending-call-deadlines.ts +15 -1
- package/src/worker/pending-calls.ts +28 -11
- package/src/worker/runtime-alarm.ts +119 -0
- package/src/worker/worker-static-routes.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0-beta.17 - 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Serve `/healthz`, `/`, and CORS preflight from the outer Worker so activation and doctor checks no longer consume Durable Object free-tier request volume. Durable Object free-tier exhaustion now returns a structured `503 durable_object_quota_exceeded` instead of Cloudflare error 1101.
|
|
8
|
+
|
|
9
|
+
### Durable Object stream request amplification fix
|
|
10
|
+
|
|
11
|
+
- Replace the outer Worker's time-proportional internal Durable Object poll loop with a fixed two-request terminal path: one authenticated descriptor `prepare`, then one hibernatable WebSocket `subscribe`.
|
|
12
|
+
- Add `mcp-stream-channel.ts` so `BridgeRoom` accepts a single stream subscriber through `DurableObjectState.acceptWebSocket()`, replaces stale resume subscribers, rechecks storage after registration to close the completion race, and pushes exactly one terminal JSON-RPC message.
|
|
13
|
+
- Persist-ready notifications are fire-and-forget from `McpResumptionStore`; if persistence fails, the current online subscriber can still receive the transient terminal result while recovery storage keeps failure semantics.
|
|
14
|
+
- Keep daemon candidate cleanup from treating stream-subscriber sockets as daemon candidates, and reject client-to-DO data on receive-only stream subscribers.
|
|
15
|
+
- Fix the outer subscription waiter so invalid terminal payloads reject instead of leaving the SSE completion Promise permanently unsettled.
|
|
16
|
+
- Extend deterministic infrastructure coverage for the fixed two-request budget, obsolete poll-mode rejection, subscriber replacement, registration races, immediate-completion paths, protocol errors, and non-daemon socket isolation. Update architecture, engineering, testing, audit, and operations contracts to describe subscribe push delivery instead of short pending/terminal polls.
|
|
17
|
+
|
|
18
|
+
## 3.0.0-beta.16 - 2026-07-25
|
|
19
|
+
|
|
20
|
+
### Pending-call recovery and verified handover
|
|
21
|
+
|
|
22
|
+
- Separate the upstream MCP host/connector shard-mapper incident from Machine Bridge evidence. The exact temporary-keyspace error never appeared in Worker or daemon diagnostics and did not increment Worker server-error counters, so it is documented as an external boundary failure with unknown platform ownership rather than misclassified as a local daemon, OAuth, Git, or Cloudflare defect.
|
|
23
|
+
- Close the Machine Bridge failure-amplification path discovered after recovery. Pending calls now retain monotonic operation and reconnect deadlines, schedule the earliest deadline through the Durable Object alarm, and run a compensating overdue sweep on every HTTP/WebSocket event. In-memory timers remain the fast path; a transient alarm-storage error is observable without converting already-dispatched work into a false terminal failure.
|
|
24
|
+
- Make verified same-instance daemon handover atomic with respect to in-flight calls. Both attached and detached records move to the replacement before the incumbent closes, the complete `resume_calls` set is sent, remaining operation timeout is preserved, and failed replacement acknowledgement restores ownership to a still-open incumbent.
|
|
25
|
+
- Add deterministic disabled-timer deadline tests, direct runtime-alarm scheduling/failure tests, and a real Wrangler/workerd race regression that connects a same-instance replacement while the incumbent still owns an active call. The call remains active rather than detached and completes through the verified replacement.
|
|
26
|
+
- Use null-prototype dictionaries for managed-job `env` and `env_resources`, so valid variable names such as `__proto__`, `constructor`, `toString`, and `valueOf` remain ordinary own data instead of mutating JavaScript object prototypes. Add behavior coverage without weakening duplicate-variable rejection.
|
|
27
|
+
- Correct architecture and operations documentation that still described Durable Object `waitUntil` ownership or direct rejection during socket replacement. Document the three deadline enforcement paths, stale-pending diagnosis, host/connector internal-storage error triage, and the exact test evidence.
|
|
28
|
+
|
|
3
29
|
## 3.0.0-beta.15 - 2026-07-25
|
|
4
30
|
|
|
5
31
|
### Event-driven streamed-call settlement
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "3.0.0-beta.
|
|
33
|
+
"version_name": "3.0.0-beta.17",
|
|
34
34
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
|
|
35
35
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -106,17 +106,17 @@ The stdio server implements newline-delimited JSON-RPC over stdin/stdout. It neg
|
|
|
106
106
|
|
|
107
107
|
### Cloudflare Worker and Durable Object
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Public `/healthz`, `/`, and CORS preflight are answered by the outer Worker without Durable Object requests, so activation and doctor checks do not consume free-tier DO volume. All other requests route to one named Durable Object. It owns:
|
|
110
110
|
|
|
111
111
|
- OAuth clients, authorization codes, hashed access-token records, an independently versioned hashed refresh-token store, and throttling metadata;
|
|
112
112
|
- one active end-to-end-verified daemon WebSocket plus bounded candidate and probing sockets;
|
|
113
113
|
- policy/tool metadata attached to the active socket;
|
|
114
|
-
- a bounded in-memory map of pending daemon calls;
|
|
114
|
+
- a bounded in-memory map of pending daemon calls, with monotonic operation/reconnect deadlines projected onto Durable Object alarms and rechecked at every event boundary;
|
|
115
115
|
- bounded resumable MCP delivery metadata and terminal responses for recently disconnected SSE clients.
|
|
116
116
|
|
|
117
|
-
`BridgeRoom` owns Durable Object routing, MCP authorization/dispatch, daemon WebSocket lifecycle, pending relay-call composition, cancellation, and resumable state. `mcp-stream-proxy.ts` owns the outer-Worker transport adapter: it strips public internal-control headers, obtains a bounded authenticated descriptor, creates the client-facing SSE stream, and
|
|
117
|
+
`BridgeRoom` owns Durable Object routing, MCP authorization/dispatch, daemon WebSocket lifecycle, pending relay-call composition, cancellation, and resumable state. `mcp-stream-proxy.ts` owns the outer-Worker transport adapter: it strips public internal-control headers, obtains a bounded authenticated descriptor, creates the client-facing SSE stream, and waits for the terminal result through one authenticated internal WebSocket subscription. `mcp-stream-channel.ts` owns Durable Object subscriber registration, single-subscriber replacement, and hibernation-safe terminal push. `mcp-access.ts` owns shared Bearer/DPoP authorization for POST and recovery GET. `mcp-resumption-http.ts` owns recovery routing and signed session/protocol binding and returns descriptors rather than a long-lived response. `mcp-resumption.ts` owns stream admission, transaction ordering, immediate pending/terminal polls, expiry, replay, and lifecycle state; `mcp-resumption-records.ts` owns the compact metadata index, terminal-message bounds, serialization, and SHA-256 integrity metadata. `mcp-stream.ts` owns SSE sequence-zero/sequence-one framing and heartbeats. `mcp-jsonrpc.ts` owns JSON-RPC shape validation, result/error framing, MCP tool-result projection, session-instruction bounds, and protocol-header validation. `websocket-protocol.ts` owns record validation plus best-effort send/close/rejection helpers. `OAuthController` owns OAuth-store pruning, registration throttling, authorization submission, account-admin routing, token exchange, access-token verification, and the serialization queue for OAuth mutations. Worker-internal TypeScript imports use explicit `.ts` specifiers and JSON import attributes, so the same modules are directly executable under the pinned Node runtime and bundled by Wrangler.
|
|
118
118
|
|
|
119
|
-
The Worker verifies OAuth, validates MCP envelopes and optional protocol headers, converts `tools/call` into WebSocket messages, correlates explicit cancellation by access-token hash, signed MCP session, and JSON-RPC ID, and formats text/structured/image results. An HTTP/SSE disconnect is transport disposal, not MCP cancellation. For streamed daemon tools, `BridgeRoom` commits a recovery record, registers an event-settled pending call, sends the daemon envelope, and immediately returns an internal descriptor without retaining a terminal Promise. The later WebSocket result, explicit cancellation, timeout, send failure, or reconnect-grace expiry persists the terminal JSON-RPC envelope. JSON-only calls retain the ordinary Promise path. The outer Worker sends `stream:0`, heartbeats, and `stream:1`;
|
|
119
|
+
The Worker verifies OAuth, validates MCP envelopes and optional protocol headers, converts `tools/call` into WebSocket messages, correlates explicit cancellation by access-token hash, signed MCP session, and JSON-RPC ID, and formats text/structured/image results. An HTTP/SSE disconnect is transport disposal, not MCP cancellation. For streamed daemon tools, `BridgeRoom` commits a recovery record, registers an event-settled pending call, sends the daemon envelope, and immediately returns an internal descriptor without retaining a terminal Promise. The later WebSocket result, explicit cancellation, timeout, send failure, or reconnect-grace expiry persists the terminal JSON-RPC envelope. JSON-only calls retain the ordinary Promise path. The outer Worker sends `stream:0`, heartbeats, and `stream:1`; the Durable Object accepts one hibernatable subscriber WebSocket per stream and pushes the terminal JSON-RPC envelope once. Authenticated `GET /mcp` with `Last-Event-ID` resumes only the original OAuth-token/MCP-session stream; POST always creates new work. Public requests cannot select internal descriptor/subscribe modes because the outer boundary removes those headers before forwarding. At most 64 records and 1.5 MiB of terminal JSON per record are retained for two minutes. The recovery store tracks active stream identifiers, not live Promises; a transient terminal map is used only when persistence fails. If the Durable Object restarts with a pending record but no active owner, recovery reports that side effects may have occurred and requires reconciliation before retry. It has no local filesystem or process API.
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
### Daemon device authentication
|
|
@@ -172,7 +172,7 @@ Remote OAuth binds each code, access token, and refresh token to a named Machine
|
|
|
172
172
|
7. The MCP client initializes against the sole current protocol version; an obsolete client must upgrade rather than enter a legacy execution path. The Worker returns a stateless HMAC-bound `MCP-Session-Id`, and later request/cancellation correlation is scoped by OAuth token, MCP session, JSON-RPC id type, and id value. Two clients may therefore reuse the same JSON-RPC id concurrently without collision. Sessionless POSTs remain independent and are not inserted into a token-global cancellation index. When the daemon advertises `session_bootstrap`, the Worker requests bounded local instructions and appends them to the initialization result; failure degrades to static instructions.
|
|
173
173
|
8. A new daemon first authenticates as a bounded `probing` socket. The Worker sends a random `relay_probe`; the local runtime returns it through the normal session-bound result-delivery path; only the matching result produces `ready_ack`, promotion to the active daemon, and safe replacement of an incumbent connection.
|
|
174
174
|
9. `tools/list` is derived only from the active end-to-end-verified daemon; without one, only `server_info` is advertised.
|
|
175
|
-
10. `tools/call` receives a random relay call ID and is bound to the current daemon socket, that daemon process's ephemeral instance identifier, and the authenticated client request key. When the client accepts `text/event-stream`,
|
|
175
|
+
10. `tools/call` receives a random relay call ID and is bound to the current daemon socket, that daemon process's ephemeral instance identifier, and the authenticated client request key. When the client accepts `text/event-stream`, `BridgeRoom` commits recovery state, registers an event-settled pending call, sends the daemon envelope, and returns a bounded descriptor immediately; the outer Worker owns the SSE priming frame, keepalives, and one internal terminal subscription. No unresolved terminal Promise or Durable Object `waitUntil` owns the dispatch. JSON-only clients retain the single terminal response.
|
|
176
176
|
11. The runtime validates policy and arguments, executes the tool, and returns a bounded result. Closing or losing the HTTP response stream only makes that stream unwritable; it is not an MCP cancellation and does not remove the pending request.
|
|
177
177
|
12. If the socket remains ready, the Durable Object accepts the result only from that socket. If it drops, the Worker detaches the pending call for at most two minutes and accepts completion only after a replacement socket with the same daemon-process identifier has passed the end-to-end readiness probe. The local runtime preserves the operation and queues a completion over the same shared interval. The Worker pauses the record's remaining normal deadline while detached and resumes it after same-instance rebinding, so connected calls are not granted an unconditional recovery extension.
|
|
178
178
|
13. Only a matching session-scoped `notifications/cancelled` request removes the pending indexes and sends best-effort cancellation to a connected daemon. Local completion that races with explicit cancellation is discarded. On every readiness handover, the Worker first sends an authoritative bounded `resume_calls` set; the runtime cancels active calls and queued results absent from that set before accepting `ready_ack`. A request explicitly cancelled while disconnected therefore cannot be revived by a fast reconnect.
|
|
@@ -255,7 +255,7 @@ Reconnect uses bounded exponential backoff with jitter. Brief self-healing inter
|
|
|
255
255
|
|
|
256
256
|
The Worker stores socket transitions in `DaemonSocketRegistry`: `candidate` before hello, `probing` after authentication, `daemon` only after the end-to-end result probe, and `expired` after terminal failure. Durable Object alarms enforce separate hello, readiness, and steady-state liveness deadlines across hibernation. A healthy incumbent remains active while a replacement is probed; a malformed, silent, incompatible, or identity-mismatched replacement is closed without displacing it. Only a verified candidate receives `ready_ack` and then replaces the old socket. Ready daemons stay live only while inbound traffic refreshes `lastSeenAt`; silent half-open or hibernation-restored sockets are reclaimed instead of advertising `daemon.connected` while tool calls time out.
|
|
257
257
|
|
|
258
|
-
Each daemon process generates a random bounded `instance_id` at startup and includes it in every reconnect hello. Pending calls normally retain their assigned socket. On an unexpected socket loss, only those records are detached and the shared two-minute relay contract bounds recovery.
|
|
258
|
+
Each daemon process generates a random bounded `instance_id` at startup and includes it in every reconnect hello. Pending calls normally retain their assigned socket. On an unexpected socket loss, only those records are detached and the shared two-minute relay contract bounds recovery. During verified same-instance handover, the Worker transfers both already-detached calls and still-attached calls from the incumbent socket to the replacement before closing the incumbent; this prevents the asynchronous close event from creating a detached call after the only rebind pass. If replacement acknowledgement fails, ownership is restored to the still-open same-instance incumbent. Another process cannot inherit or resolve these calls. The local runtime mirrors that state machine by preserving active calls and completed-result envelopes until relay readiness returns. Before `ready_ack`, the Worker sends the exact IDs that still have remote waiters; the runtime cancels everything else and only then replays retained results through the verified socket. Grace expiry restores the terminal behavior: reject remote waiters, cancel local ordinary calls, terminate process trees, and discard undeliverable results. The shared execution envelope remains independent of reconnect grace. Worker operation countdown is paused while detached or transferred and resumed with its remaining budget, so handover cannot reset the normal timeout. In-memory timers are only the fast path: the earliest monotonic pending deadline is also scheduled as a Durable Object alarm, and every HTTP or WebSocket event performs a compensating overdue scan. A transient alarm-storage failure is observable but does not turn an already-dispatched operation into a false terminal failure; the next event scan remains the bounded recovery path. This does not make calls durable across daemon restart or machine failure; managed jobs remain the separate durable mechanism.
|
|
259
259
|
|
|
260
260
|
## Persistence
|
|
261
261
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Security and privacy audit notes
|
|
2
2
|
|
|
3
|
+
## 2026-07-25 version 3.0.0-beta.16 pending-call recovery and boundary audit
|
|
4
|
+
|
|
5
|
+
The reported incident exposed two separate failure domains. The host/connector layer returned `No shard mapper found` for a temporary high-replication backfill keyspace and then rejected even minimal MCP calls. The exact text did not exist in Machine Bridge source, deployed Worker events, daemon logs, or local command output; Worker HTTP server-error counters remained unchanged. The original shard-routing failure therefore occurred before or outside the deployed Worker/daemon boundary. The evidence is insufficient to identify the owner of that upstream temporary store, so this audit does not misattribute it to Cloudflare, the local daemon, OAuth, or Git. The host path later recovered without credential rotation or state deletion.
|
|
6
|
+
|
|
7
|
+
Independent inspection after recovery found a real Machine Bridge amplification defect: two calls remained active and detached beyond the shared two-minute reconnect grace. Pending operation and reconnect expiry used only in-memory `setTimeout` callbacks. Event-driven streamed calls intentionally return from the initiating Durable Object event, so a timer alone was not a valid cross-event lifecycle guarantee. The Worker now records monotonic operation/reconnect deadlines, exposes the earliest remaining duration to one combined Durable Object alarm, and performs an overdue scan at every HTTP and WebSocket event boundary. Timer callbacks remain the low-latency path. Alarm-storage failure emits only a bounded coarse event and does not falsely fail work that was already dispatched; the next event scan is the compensating path.
|
|
8
|
+
|
|
9
|
+
A second race existed during verified daemon replacement. The previous implementation rebound only already-detached calls, then closed the incumbent socket. Calls still attached to that socket were detached later by its asynchronous close callback, after the only rebind pass, and could remain orphaned. Same-instance promotion now transfers both attached and detached calls to the candidate before closing the incumbent, sends the complete authoritative `resume_calls` set, preserves the remaining normal timeout, and restores ownership to a still-open incumbent if replacement acknowledgement fails. Different daemon instances remain unable to inherit calls.
|
|
10
|
+
|
|
11
|
+
The broader review found a separate prototype-sensitive input defect in managed-job environment maps. POSIX/Windows-compatible variable validation permits names such as `__proto__`, `constructor`, `toString`, and `valueOf`, but validation accumulated them into ordinary JavaScript objects. Assigning `__proto__` invoked the legacy prototype setter rather than creating an own environment entry. Both plain and resource-backed environment maps now use null-prototype dictionaries, with stage/inspection regression coverage for all prototype-shaped keys and the existing duplicate-variable denial.
|
|
12
|
+
|
|
13
|
+
Documentation had also drifted: the remote lifecycle still claimed streamed work used Durable Object `waitUntil`, despite beta.15 explicitly removing and architecture tests forbidding that design. Architecture, operations, testing, changelog, and this audit now describe event-settled dispatch, alarm-plus-sweep deadline enforcement, atomic same-instance handover, upstream-host diagnosis, and prototype-safe environment maps. Complete and production dependency audits report zero vulnerabilities; privacy history, critical-module coverage, Worker dry-run, deterministic lifecycle tests, managed-job integration, type checks, lint, and real Wrangler OAuth/MCP integration pass before candidate preparation.
|
|
14
|
+
|
|
15
|
+
These source changes do not claim to repair the upstream shard mapper or prove its platform owner. They prevent a transient external failure or socket replacement from leaving permanent pending-call occupancy inside Machine Bridge. No Worker deployment, daemon/service replacement, global installation, credential rotation, push, tag, npm publication, or GitHub Release is performed by the source audit itself. Exact beta.16 candidate activation and owner-machine verification remain mandatory.
|
|
16
|
+
|
|
3
17
|
## 2026-07-25 version 3.0.0-beta.15 event-lifecycle audit
|
|
4
18
|
|
|
5
19
|
The exact beta.14 candidate was activated by the repository owner. The Worker, launchd daemon, private runtime path, activation record, candidate checksum, `status`, and `doctor` all converged on `3.0.0-beta.14`; the globally installed beta.12 package remained the explicit rollback baseline. A random temporary owner account and OAuth client then exercised the production protocol and were removed after the run. Sequence zero, token/session isolation, disconnect recovery, terminal sequence one, and empty acknowledgement replay all passed.
|
|
@@ -18,7 +32,7 @@ The exact beta.13 tarball was activated through the owner command and converged
|
|
|
18
32
|
|
|
19
33
|
The same live run exposed a release-blocking production scheduling defect that the local Wrangler integration had not represented. While `BridgeRoom` directly returned an open SSE response, later requests routed to the same Durable Object—including `server_info` and the authoritative session-scoped `notifications/cancelled` notification—did not enter until the stream ended. The daemon call eventually terminated through its own boundary, but Worker observability did not record a successful cancellation. Beta.13 therefore has no acceptance record, is not pushed or published, and is explicitly blocked.
|
|
20
34
|
|
|
21
|
-
Beta.14 separates client transport ownership from durable state ownership. The outer stateless Worker creates the public SSE stream. `BridgeRoom` performs OAuth/DPoP authorization, signed MCP-session validation, stream admission, daemon dispatch, explicit cancellation, and terminal persistence, then returns a small internal descriptor. The outer Worker uses
|
|
35
|
+
Beta.14 separates client transport ownership from durable state ownership. The outer stateless Worker creates the public SSE stream. `BridgeRoom` performs OAuth/DPoP authorization, signed MCP-session validation, stream admission, daemon dispatch, explicit cancellation, and terminal persistence, then returns a small internal descriptor. The outer Worker uses one service-binding WebSocket subscription after the authenticated descriptor: pending streams hibernate under `acceptWebSocket()`, terminal state is pushed once, and missing or expired state fails closed. No internal request remains open in the Durable Object for the life of a long tool call. Publicly supplied internal-control headers are stripped before every service-binding forward, so callers cannot select the unauthenticated internal subscribe path.
|
|
22
36
|
|
|
23
37
|
The real Wrangler regression keeps the original SSE response open, confirms a concurrent `server_info` sees the pending call, sends `notifications/cancelled`, observes the matching daemon `cancel_call`, and receives a cancelled terminal result on the original stream. Existing disconnect/recovery, wrong-session rejection, sequence-one acknowledgement, CORS, persistence faults, capacity, integrity, and oversized-message tests remain in force. This correction requires a new exact beta.14 candidate, owner activation, and repeated live verification; beta.13 activation evidence cannot be reused.
|
|
24
38
|
|
package/docs/ENGINEERING.md
CHANGED
|
@@ -21,7 +21,7 @@ This document records project-wide decisions that must survive individual fixes,
|
|
|
21
21
|
15. **Ambiguous health is not permission to repeat a remote write.** A successful Wrangler deployment is recorded before secondary health verification. Timeout, proxy, TLS, network, and temporary service failures preserve the deployment fingerprint and fail for diagnosis; only bounded evidence of a stale identity/version permits automatic same-name redeployment. Changing the Worker name is an explicit remote-resource transition, not a retry strategy.
|
|
22
22
|
16. **Execution continuity and delivery continuity are separate proof obligations.** Keeping work alive after a client transport closes is insufficient unless the same authenticated principal can recover a terminal result or a durable handle. Fresh requests and replay endpoints must remain separate so recovery cannot accidentally duplicate a non-idempotent operation.
|
|
23
23
|
17. **Remote compound commands are not persistence evidence.** When a remote edit and a long test share one relay call, a transport interruption can obscure whether the edit completed. High-impact writes must be followed by an independent read of stable anchors or a Git diff before tests and conclusions rely on them.
|
|
24
|
-
18. **Durable state owners do not retain cross-event terminal Promises.** A Durable Object that must accept cancellation, status, or recovery requests cannot retain the public SSE response, an internal request waiting for completion, or an unresolved Promise owned by the initiating fetch event. The outer Worker owns streaming; streamed daemon calls are registered and returned immediately, then settled by later WebSocket, cancellation, timeout, send-failure, or reconnect-expiry events. Descriptor
|
|
24
|
+
18. **Durable state owners do not retain cross-event terminal Promises.** A Durable Object that must accept cancellation, status, or recovery requests cannot retain the public SSE response, an internal request waiting for completion, or an unresolved Promise owned by the initiating fetch event. The outer Worker owns streaming; streamed daemon calls are registered and returned immediately, then settled by later WebSocket, cancellation, timeout, send-failure, or reconnect-expiry events. Descriptor requests remain short; terminal delivery uses one authenticated hibernatable WebSocket subscription. Both are admitted only on the internal service-binding path and are unreachable through caller-supplied internal headers.
|
|
25
25
|
|
|
26
26
|
A proposed change that conflicts with an invariant requires an explicit owner decision and corresponding documentation update. It must not be hidden inside an unrelated refactor.
|
|
27
27
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -12,6 +12,8 @@ machine-mcp service status
|
|
|
12
12
|
|
|
13
13
|
### Worker deployment and health convergence
|
|
14
14
|
|
|
15
|
+
`/healthz` and `/` are answered by the outer Worker and do not consume Durable Object request volume. If MCP or daemon routes return `503 durable_object_quota_exceeded` (or Cloudflare 1101 with Durable Objects free-tier exhaustion in Worker tails), wait for the daily UTC free-tier reset or move the account off the free DO plan; do not treat that as a failed script deploy when `/healthz` still reports the expected version.
|
|
16
|
+
|
|
15
17
|
Wrangler upload and public health verification are two separate observations. Once Wrangler reports a successful deployment and supplies the `workers.dev` URL, Machine Bridge immediately records that URL together with the exact deployment fingerprint and package version. It then verifies `/healthz` through the standard `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` environment route. If that secondary probe times out or encounters a proxy, TLS, network, or temporary HTTP 5xx failure, startup stops with an actionable error, but the successful deployment evidence remains. The next ordinary start verifies the same Worker and does not repeat the upload.
|
|
16
18
|
|
|
17
19
|
Automatic redeployment is limited to bounded health evidence that the recorded endpoint is genuinely stale: a persistent package-version mismatch, an unexpected Machine Bridge identity, or a persistent `404`/`410`. Unreachability is not proof of absence. `--force-worker` remains the explicit override when an operator deliberately wants an upload despite matching state.
|
|
@@ -51,12 +53,18 @@ A successful diagnostic result applies only to that probe. An MCP host can still
|
|
|
51
53
|
|
|
52
54
|
Machine Bridge supports concurrent calls: the Worker admits up to 32 pending daemon calls and the local runtime admits up to 16 active tool calls. These are capacity limits, not a single global execution queue. Each successful MCP initialization receives a signed `MCP-Session-Id`; JSON-RPC ids and cancellation are scoped to that session, so separate chat windows may reuse the same numeric ids safely even when they share one OAuth account and token.
|
|
53
55
|
|
|
54
|
-
`server_info.worker.pending_calls` reports `active`, `detached`, `request_keys`, `maximum`, `oldest_ms`, and `by_tool`. `worker.sockets_live` separately reports `authenticated`, `probing`, `ready`, and `candidates`; only `ready` sockets contribute to `daemon.connected` and tool advertisement. A nonzero `active` count means work is in flight, not that the bridge is locked. `detached > 0` means a daemon socket was lost and those requests are inside the bounded two-minute same-instance reconnect window. Calls for simple reads and probes should continue while another independent process call runs. Only explicit session-scoped MCP cancellation, timeout, or reconnect-grace expiry removes the pending record and its request key; an HTTP response disconnect is not cancellation. A daemon-socket closure detaches only calls assigned to that socket; the same daemon process can reclaim them after completing readiness, while another process cannot. Grace expiry rejects the request and cancels the local ordinary operation. Refreshing a chat page is not the recovery mechanism and should not be required.
|
|
56
|
+
`server_info.worker.pending_calls` reports `active`, `detached`, `request_keys`, `maximum`, `oldest_ms`, and `by_tool`. `worker.sockets_live` separately reports `authenticated`, `probing`, `ready`, and `candidates`; only `ready` sockets contribute to `daemon.connected` and tool advertisement. A nonzero `active` count means work is in flight, not that the bridge is locked. `detached > 0` means a daemon socket was lost and those requests are inside the bounded two-minute same-instance reconnect window. Calls for simple reads and probes should continue while another independent process call runs. Only explicit session-scoped MCP cancellation, timeout, or reconnect-grace expiry removes the pending record and its request key; an HTTP response disconnect is not cancellation. A daemon-socket closure detaches only calls assigned to that socket; the same daemon process can reclaim them after completing readiness, while another process cannot. A verified same-instance replacement transfers both detached and still-attached calls before the incumbent closes. Normal and reconnect deadlines have three enforcement paths: monotonic in-event timers, a Durable Object alarm, and an overdue sweep at the next HTTP/WebSocket event. Therefore `detached > 0` with `oldest_ms` materially beyond the two-minute grace is a lifecycle defect rather than normal recovery. Grace expiry rejects the request and cancels the local ordinary operation. Refreshing a chat page is not the recovery mechanism and should not be required.
|
|
55
57
|
|
|
56
|
-
For Streamable HTTP clients such as ChatGPT that advertise `text/event-stream`, the outer Worker returns an immediate sequence-zero SSE event identifier and a keepalive comment every ten seconds until the terminal sequence-one JSON-RPC result. `BridgeRoom` never owns the long-lived public stream or an unresolved terminal Promise. Stream initiation commits recovery state, registers the daemon call, sends it, and returns a descriptor; a later WebSocket result, explicit cancellation, timeout, send failure, or reconnect-grace expiry writes the terminal result.
|
|
58
|
+
For Streamable HTTP clients such as ChatGPT that advertise `text/event-stream`, the outer Worker returns an immediate sequence-zero SSE event identifier and a keepalive comment every ten seconds until the terminal sequence-one JSON-RPC result. `BridgeRoom` never owns the long-lived public stream or an unresolved terminal Promise. Stream initiation commits recovery state, registers the daemon call, sends it, and returns a descriptor; a later WebSocket result, explicit cancellation, timeout, send failure, or reconnect-grace expiry writes the terminal result. One internal hibernatable WebSocket subscription therefore coexists with concurrent `server_info`, recovery, and session-scoped `notifications/cancelled` requests while SSE remains open, without creating a request per poll interval. Caller-supplied internal stream headers are removed at the public boundary. If the client or an intermediary closes the stream, Machine Bridge keeps the bounded operation alive; only `notifications/cancelled` carries cancellation semantics. A compatible host resumes the original stream with authenticated `GET /mcp`, the original `MCP-Session-Id`, and `Last-Event-ID`; it must not repeat the POST. Recovery records are token/session-bound, retained for at most two minutes, limited to 64 streams, and persist at most 1.5 MiB of terminal JSON. Error `-32002` means the online result exceeded the replay budget; `-32003` means the Worker restarted before it could persist a terminal result and the operation may already have produced side effects; reconcile state before retrying. Error `-32005` means stored replay data failed integrity validation.
|
|
57
59
|
|
|
58
60
|
`server_info.worker.observability.calls.unmatched_results` counts results that reached the Worker after their pending record was already removed. A small increase can accompany cancellation or timeout races, especially during mixed-version upgrade convergence; sustained growth together with old pending calls indicates incompatible components or a lifecycle defect. The counter contains no tool arguments or result data.
|
|
59
61
|
|
|
62
|
+
### MCP host or connector internal-storage errors
|
|
63
|
+
|
|
64
|
+
An error naming an internal shard mapper, temporary keyspace, backfill store, or connector database is not automatically a Machine Bridge Worker or daemon error. During the beta.15 incident, the exact error text was absent from repository source, Worker events, daemon logs, and local process output; Worker HTTP `server_error` counters also did not increase, while the host temporarily failed even `server_info`. That evidence places the original failure before or outside the deployed Worker/daemon boundary, but it does not identify which upstream platform component owned the temporary store. Do not rotate OAuth/device credentials, delete local state, or restart a healthy daemon solely because of such a message.
|
|
65
|
+
|
|
66
|
+
After the host path recovers, run `server_info`, `machine-mcp doctor`, and `machine-mcp service status`. Compare Worker `requests.server_error`, pending-call age, ready socket count, daemon PID/start time, and local logs. If the upstream text never appears locally and Worker server errors remain unchanged, report the host/connector incident separately. If pending calls remain older than their operation or reconnect deadline, that is a Machine Bridge lifecycle issue and should be investigated independently rather than attributed to the upstream shard error.
|
|
67
|
+
|
|
60
68
|
### Relay interruption messages
|
|
61
69
|
|
|
62
70
|
A reconnect warning is evidence of a transport outage, not proof that the daemon process exited. Compare daemon PID/process start, `connected_at`, `last_seen_at`, `runtime.relay.last_disconnected_at`, close category/code, and outage count. A system VPN/TUN may remain shown as connected while its internal route is unavailable; Machine Bridge reports that route only as `system-network-stack` with application-proxy scope. The reconnect schedule now tops out at fifteen seconds.
|
package/docs/TESTING.md
CHANGED
|
@@ -159,4 +159,4 @@ The stdio integration test also sends an oversized line, verifies bounded reject
|
|
|
159
159
|
|
|
160
160
|
`npm run mcp-resumption:test` directly exercises stream cursor parsing, OAuth-token/MCP-session isolation, immediate pending/terminal polls, active and completed replay, Worker-restart ambiguity, result-size fallback, SHA-256 tamper detection, transient persistence failure, expiry, capacity, and completed-record eviction.
|
|
161
161
|
|
|
162
|
-
`npm run worker-runtime-infrastructure:test` verifies outer-Worker stream ownership, stripping of caller-supplied internal headers, bounded descriptor/
|
|
162
|
+
`npm run worker-runtime-infrastructure:test` verifies outer-Worker stream ownership, stripping of caller-supplied internal headers, bounded descriptor/subscribe adaptation, fixed two-request Durable Object budgets, sequence-zero/sequence-one framing, subscription-error closure, subscriber replacement, registration races, non-daemon socket isolation, and the shared two-minute/64-stream/1.5-MiB contract. It also models the production event-lifecycle boundary: event-mode registration must return without a terminal Promise or early settlement, while later success, daemon rejection, explicit cancellation, timeout, send failure, result transformation, persistence failure, and same-instance reconnect each produce one terminal result and remove pending indexes. Deadline tests deliberately use a scheduler that never fires callbacks, advance the monotonic clock, and prove that event-boundary sweeps expire both attached operation deadlines and detached reconnect deadlines without leaking request keys. A direct runtime-alarm coordinator test verifies earliest-pending scheduling, alarm removal when no deadline remains, event-entry expiry before rescheduling, and bounded reporting when Durable Object alarm storage fails. The same suite also proves that direct same-instance handover transfers an attached call and preserves its remaining timeout budget. `npm run worker:integration-test` performs the real Wrangler path: keep SSE open while a concurrent `server_info` succeeds and explicit cancellation reaches the matching daemon call; connect a verified same-instance replacement while the incumbent still owns an in-flight call and prove transfer occurs before incumbent close; disconnect after sequence zero; reject another session; recover with GET plus `Last-Event-ID`; and prove a sequence-one acknowledgement is not delivered twice. Managed-job integration treats `__proto__`, `constructor`, `toString`, and `valueOf` environment/resource-map keys as ordinary own data while retaining duplicate-key rejection. Static architecture checks forbid a stream-initiation `dispatchJsonRpc` Promise, `resumption.attach`, Durable Object `waitUntil`, or Promise-valued recovery state. The parser accumulates complete SSE events and does not assume network chunk boundaries. CORS coverage requires both `DPoP` and `Last-Event-ID`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.17",
|
|
4
4
|
"description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -125,7 +125,7 @@ function validateEnv(value, label) {
|
|
|
125
125
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
126
126
|
const entries = Object.entries(value);
|
|
127
127
|
if (entries.length > 64) throw new Error(`${label} has too many entries`);
|
|
128
|
-
const out =
|
|
128
|
+
const out = Object.create(null);
|
|
129
129
|
for (const [key, raw] of entries) {
|
|
130
130
|
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
|
|
131
131
|
out[key] = boundedString(raw, 16 * 1024, `${label}.${key}`);
|
|
@@ -138,7 +138,7 @@ function validateEnvResources(value, label) {
|
|
|
138
138
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
139
139
|
const entries = Object.entries(value);
|
|
140
140
|
if (entries.length > 32) throw new Error(`${label} has too many entries`);
|
|
141
|
-
const out =
|
|
141
|
+
const out = Object.create(null);
|
|
142
142
|
for (const [key, raw] of entries) {
|
|
143
143
|
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
|
|
144
144
|
out[key] = validateResourceName(raw);
|
|
@@ -24,7 +24,8 @@ interface WebSocketContext {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export class DaemonSocketRegistry {
|
|
27
|
-
|
|
27
|
+
private readonly context: WebSocketContext;
|
|
28
|
+
constructor(context: WebSocketContext) { this.context = context; }
|
|
28
29
|
|
|
29
30
|
attachment(socket: WebSocket): DaemonAttachment | undefined {
|
|
30
31
|
const raw = socket.deserializeAttachment();
|
|
@@ -57,7 +58,10 @@ export class DaemonSocketRegistry {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
nonReadySockets(): WebSocket[] {
|
|
60
|
-
return this.context.getWebSockets().filter((socket) =>
|
|
61
|
+
return this.context.getWebSockets().filter((socket) => {
|
|
62
|
+
const role = this.attachment(socket)?.role;
|
|
63
|
+
return Boolean(role && role !== "daemon" && socket.readyState === WebSocket.OPEN);
|
|
64
|
+
});
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
beginCandidate(
|
|
@@ -81,13 +85,8 @@ export class DaemonSocketRegistry {
|
|
|
81
85
|
|
|
82
86
|
beginProbe(socket: WebSocket, values: { connectedAt: string; probeId: string; instanceId: string; policy: DaemonPolicy; tools: string[] }): void {
|
|
83
87
|
socket.serializeAttachment({
|
|
84
|
-
role: "probing",
|
|
85
|
-
|
|
86
|
-
lastSeenAt: values.connectedAt,
|
|
87
|
-
probeId: values.probeId,
|
|
88
|
-
instanceId: values.instanceId,
|
|
89
|
-
policy: values.policy,
|
|
90
|
-
tools: values.tools,
|
|
88
|
+
role: "probing", connectedAt: values.connectedAt, lastSeenAt: values.connectedAt,
|
|
89
|
+
probeId: values.probeId, instanceId: values.instanceId, policy: values.policy, tools: values.tools,
|
|
91
90
|
} satisfies DaemonAttachment);
|
|
92
91
|
}
|
|
93
92
|
|
|
@@ -112,10 +111,7 @@ export class DaemonSocketRegistry {
|
|
|
112
111
|
const attachment = this.attachment(socket);
|
|
113
112
|
if (!attachment) return;
|
|
114
113
|
socket.serializeAttachment({
|
|
115
|
-
role: "expired",
|
|
116
|
-
connectedAt: attachment.connectedAt,
|
|
117
|
-
lastSeenAt: attachment.lastSeenAt,
|
|
118
|
-
instanceId: attachment.instanceId,
|
|
114
|
+
role: "expired", connectedAt: attachment.connectedAt, lastSeenAt: attachment.lastSeenAt, instanceId: attachment.instanceId,
|
|
119
115
|
} satisfies DaemonAttachment);
|
|
120
116
|
}
|
|
121
117
|
|