machine-bridge-mcp 3.0.0-beta.16 → 3.0.0-beta.20
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 +49 -0
- package/SECURITY.md +6 -4
- 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 +10 -10
- package/docs/AUDIT.md +15 -1
- package/docs/ENGINEERING.md +3 -3
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/LOCAL_AUTHORIZATION.md +1 -1
- package/docs/LOCAL_AUTOMATION.md +1 -1
- package/docs/MULTI_ACCOUNT.md +1 -1
- package/docs/OPERATIONS.md +11 -3
- package/docs/OVERVIEW.md +1 -1
- package/docs/TESTING.md +10 -8
- package/docs/THREAT_MODEL.md +10 -6
- package/docs/UPGRADING.md +12 -0
- 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/resource-operations.mjs +37 -8
- package/src/local/runtime-capabilities.mjs +15 -3
- package/src/local/runtime.mjs +4 -3
- package/src/local/workspace-file-service.mjs +29 -11
- package/src/worker/daemon-sockets.ts +9 -13
- package/src/worker/http.ts +37 -15
- package/src/worker/index.ts +70 -79
- package/src/worker/mcp-resumption.ts +18 -15
- package/src/worker/mcp-stream-channel.ts +125 -0
- package/src/worker/mcp-stream-proxy.ts +22 -56
- 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 +36 -0
- package/src/worker/runtime-alarm.ts +22 -2
- package/src/worker/worker-edge-guard.ts +81 -0
- package/src/worker/worker-edge-log.ts +63 -0
- package/src/worker/worker-metadata.ts +39 -0
- package/src/worker/worker-static-routes.ts +48 -0
- package/wrangler.jsonc +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0-beta.20 - 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Rewrite the bounded Worker error-cause traversal with an explicit object type guard and `WeakSet<object>` cycle tracking. This preserves the eight-level/cycle-safe classification behavior while eliminating the CodeQL `js/comparison-between-incompatible-types` finding; the existing cyclic-cause regression test continues to enforce non-duplication.
|
|
8
|
+
|
|
9
|
+
## 3.0.0-beta.19 - 2026-07-26
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Restore the documented `account revoke-client CLIENT_ID` CLI command. The action was implemented end to end, but its positional-argument limit was omitted, so every valid client ID was rejected as an extra positional argument before the signed administration request could be sent. Add direct parser regression coverage for both `account clients` and `account revoke-client`.
|
|
14
|
+
|
|
15
|
+
## 3.0.0-beta.18 - 2026-07-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Prevent intermittent hosted-client account loss during refresh-token rotation. A consumed refresh token may now recover at most two same-client, same-resource, same-scope, same-DPoP retries inside a 30-second concurrency window. Both retries reproduce the original deployment-keyed HMAC replacement pair without creating another credential branch or extending expiration; retries beyond that bound return `temporarily_unavailable`, while replay after the window still revokes the complete family. Schema-2 refresh state migrates in place to schema 3.
|
|
20
|
+
- Normalize unexpected outer-Worker failures to a structured retryable `502 worker_gateway_error` instead of allowing `scriptThrewException`/Cloudflare 1101 to surface as a generic account connection failure. Logged error classes contain only error names/codes, never exception messages.
|
|
21
|
+
- Retry an internal terminal WebSocket subscription with bounded delays after transport closure or retryable 429/5xx responses. Normal streamed calls retain the fixed two-request Durable Object path; failure recovery is capped at three subscription attempts.
|
|
22
|
+
- Stop reading request bodies immediately after a declared or observed size violation instead of draining attacker-controlled bytes. Permission and I/O failures during write-path and workspace traversal checks now propagate rather than being misclassified as missing files.
|
|
23
|
+
- Make partial application and skill discovery explicit through bounded path-projected warnings and coarse error classes. Optional `session_bootstrap` failure remains non-fatal but is now visible in Worker observability.
|
|
24
|
+
- Bound account-administration responses to one MiB, cancel oversized bodies, and require successful replies to be JSON objects. Generated SSH key registration now attempts both cleanup targets and reports incomplete rollback instead of silently leaving an unregistered private key.
|
|
25
|
+
- Add a fixed browser-extension error boundary, remove raw debugger details from successful fallback results, and enforce the 32-operation concurrency ceiling independently inside the extension. Error-cause inspection is cycle-aware and capped at eight levels.
|
|
26
|
+
|
|
27
|
+
### Quota and deployment hardening
|
|
28
|
+
|
|
29
|
+
- Serve all public discovery metadata and unknown-path 404 responses in the outer Worker. Only an exact stateful route-and-method allowlist can reach the rate limiter and Durable Object; invalid methods are rejected at the stateless edge.
|
|
30
|
+
- Add a Cloudflare Rate Limiting binding before Durable Object dispatch. Binding failure is fail-open because it is a quota guard rather than an authorization boundary; OAuth, session, and role checks remain inside the Durable Object.
|
|
31
|
+
- Coalesce Durable Object alarms: an already scheduled earlier alarm is reused instead of being rewritten on every daemon heartbeat, and empty alarm state avoids redundant deletes.
|
|
32
|
+
- Report refresh outcomes, estimated resumable-stream row writes, and alarm set/delete/no-op counters in Worker observability. Regression tests hold a normal stream to four storage-row writes before expiry cleanup.
|
|
33
|
+
- Rate-limit repeated edge degradation logs and report suppressed-event counts, while redacting sensitive field names. Remove duplicate `waitUntil` registration for one streamed terminal operation.
|
|
34
|
+
- Add hard critical-coverage thresholds for every new OAuth, stream, metadata, quota, edge-logging, and filesystem-state module rather than relying only on line-count architecture checks.
|
|
35
|
+
- Split OAuth refresh exchange, token issuance, terminal subscription, public metadata, and edge quota guards into focused modules rather than raising architecture limits.
|
|
36
|
+
|
|
37
|
+
## 3.0.0-beta.17 - 2026-07-26
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- 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.
|
|
42
|
+
|
|
43
|
+
### Durable Object stream request amplification fix
|
|
44
|
+
|
|
45
|
+
- 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`.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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.
|
|
48
|
+
- Keep daemon candidate cleanup from treating stream-subscriber sockets as daemon candidates, and reject client-to-DO data on receive-only stream subscribers.
|
|
49
|
+
- Fix the outer subscription waiter so invalid terminal payloads reject instead of leaving the SSE completion Promise permanently unsettled.
|
|
50
|
+
- 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.
|
|
51
|
+
|
|
3
52
|
## 3.0.0-beta.16 - 2026-07-25
|
|
4
53
|
|
|
5
54
|
### Pending-call recovery and verified handover
|
package/SECURITY.md
CHANGED
|
@@ -132,7 +132,7 @@ Version 3 has no long-lived `ACCOUNT_ADMIN_SECRET`. Account and OAuth-client adm
|
|
|
132
132
|
- timestamp;
|
|
133
133
|
- random nonce.
|
|
134
134
|
|
|
135
|
-
The Worker verifies the root certificate, session signature, bounded timestamp, body hash, and nonce replay state. Nonce capacity fails closed instead of evicting live replay markers.
|
|
135
|
+
The Worker verifies the root certificate, session signature, bounded timestamp, body hash, and nonce replay state. Nonce capacity fails closed instead of evicting live replay markers. The local administration client accepts at most one MiB, cancels oversized responses, and requires every successful non-empty reply to be a JSON object; malformed success cannot be mistaken for an empty valid result.
|
|
136
136
|
|
|
137
137
|
Account passwords are generated 256-bit tokens. The Worker stores independent salted verifiers, not plaintext passwords. A generated password is printed once by the command that creates or rotates it.
|
|
138
138
|
|
|
@@ -174,6 +174,8 @@ The browser broker:
|
|
|
174
174
|
|
|
175
175
|
These controls do not make web content trustworthy. Pages may contain prompt injection, deceptive labels, hidden consequences, changing UI state, inaccessible cross-origin frames, or high-value authenticated sessions. Machine Bridge cannot prove the extension is loaded in an isolated profile.
|
|
176
176
|
|
|
177
|
+
The extension independently caps active operations at 32. Its public error boundary returns only fixed or allowlisted guidance; raw Chrome, DevTools, page, URL, selector, filesystem-path, account-shaped, and credential-shaped exception text is not forwarded to a remote client. Successful trusted-input fallback likewise reports a fixed reason rather than the local debugger failure.
|
|
178
|
+
|
|
177
179
|
After trusted input dispatch begins, an ambiguous failure is reported as unknown outcome and is not automatically replayed.
|
|
178
180
|
|
|
179
181
|
Local resources may be injected without returning their bytes through MCP, but the destination page or application still receives them. Screenshots and page source can themselves contain secrets.
|
|
@@ -192,15 +194,15 @@ Sensitive and persistence targets are owner-only. Generic remote file tools cann
|
|
|
192
194
|
|
|
193
195
|
Direct processes use argv without shell parsing. Shell expansion is available only through the explicit shell tool.
|
|
194
196
|
|
|
195
|
-
Process counts, stdin, output, timeouts, retained sessions, and tool-call concurrency are bounded. Timeout, cancellation,
|
|
197
|
+
Process counts, stdin, output, timeouts, retained sessions, and tool-call concurrency are bounded. Timeout, explicit cancellation, reconnect-grace expiry, runtime shutdown, and non-recoverable daemon replacement use process-tree termination with bounded graceful and forced phases. A transient relay or HTTP/SSE disconnect is not cancellation.
|
|
196
198
|
|
|
197
|
-
Interactive process sessions die
|
|
199
|
+
Interactive process sessions die when their owning runtime stops or is replaced; an ordinary same-process relay reconnect does not itself destroy them. Retained output sessions and process control are bound to account, account version, OAuth client, and refresh family.
|
|
198
200
|
|
|
199
201
|
The daemon does not enforce universal CPU, memory, disk, or network quotas. An authorized owner process can still exhaust host or external resources.
|
|
200
202
|
|
|
201
203
|
## Local resources and managed jobs
|
|
202
204
|
|
|
203
|
-
Registered resources store canonical paths and bounded metadata, not file contents. Private resource files require restrictive permissions on Unix-like systems unless explicitly overridden.
|
|
205
|
+
Registered resources store canonical paths and bounded metadata, not file contents. Private resource files require restrictive permissions on Unix-like systems unless explicitly overridden. Generated SSH private-key bytes are never returned through MCP. If state registration fails after a new key pair is created, both files are removed; failure to complete that rollback is surfaced as a compound error rather than silently leaving an unregistered private key.
|
|
204
206
|
|
|
205
207
|
At job acceptance, referenced resources are reopened, bounded, hashed, and copied into a private runtime area. Changed or unavailable resources fail closed. Environment injection may be visible to same-user process inspection; private file-path substitution or stdin is generally safer.
|
|
206
208
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const SAFE_EXACT = new Set([
|
|
3
|
+
"unsupported browser tab action",
|
|
4
|
+
"no active browser tab",
|
|
5
|
+
"navigate requires url",
|
|
6
|
+
"trusted input is unavailable for this action",
|
|
7
|
+
"trusted input currently requires the top frame; use input_mode=dom for a subframe",
|
|
8
|
+
"this page cannot be scripted by a browser extension",
|
|
9
|
+
"page automation module is unavailable",
|
|
10
|
+
"browser request cancelled",
|
|
11
|
+
"tab closed during navigation wait",
|
|
12
|
+
"element reference is stale; inspect the page again",
|
|
13
|
+
"element did not become geometrically stable before timeout",
|
|
14
|
+
"matched element is not a file input",
|
|
15
|
+
"no form or submit control found",
|
|
16
|
+
"matched element is not associated with a form",
|
|
17
|
+
"select option was not found",
|
|
18
|
+
"trusted input requires a valid tab",
|
|
19
|
+
"trusted input target has no usable viewport point",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function publicError(error) {
|
|
23
|
+
const message = String(error?.message || error || "");
|
|
24
|
+
if (message.startsWith("unknown browser method:")) return "unknown browser method";
|
|
25
|
+
if (message.startsWith("invalid CSS selector:")) return "invalid CSS selector";
|
|
26
|
+
if (/^selector matched \d+ elements;/.test(message)) return "selector matched multiple elements; use ref or index to disambiguate";
|
|
27
|
+
if (message.startsWith("browser wait timed out")) return "browser wait timed out";
|
|
28
|
+
if (message.startsWith("trusted browser input may have been partially dispatched")) {
|
|
29
|
+
return "trusted browser input may have been partially dispatched; the action outcome is unknown. Inspect the page before retrying.";
|
|
30
|
+
}
|
|
31
|
+
if (message.startsWith("form submission failed after")) {
|
|
32
|
+
return "form submission failed after partial changes; inspect the page before retrying";
|
|
33
|
+
}
|
|
34
|
+
if (message.startsWith("element was not actionable before timeout")) return "element was not actionable before timeout";
|
|
35
|
+
if (message.startsWith("element was not clickable before timeout")) return "element was not clickable before timeout";
|
|
36
|
+
if (message.startsWith("unsupported element action:")) return "unsupported element action";
|
|
37
|
+
if (message.startsWith("unsupported key modifier:") || message.startsWith("unsupported key:")) return "unsupported keyboard input";
|
|
38
|
+
return SAFE_EXACT.has(message) ? message : "browser operation failed";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Object.defineProperty(globalThis, "__machineBridgeBrowserErrorBoundary", {
|
|
42
|
+
value: Object.freeze({ publicError }),
|
|
43
|
+
configurable: false,
|
|
44
|
+
});
|
|
45
|
+
})();
|
|
@@ -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.20",
|
|
34
34
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
|
|
35
35
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
importScripts("devtools-input.js", "browser-operations.js");
|
|
2
|
-
|
|
1
|
+
importScripts("browser-error-boundary.js", "devtools-input.js", "browser-operations.js");
|
|
3
2
|
let socket = null;
|
|
4
3
|
let reconnectTimer = null;
|
|
5
4
|
let reconnectAttempt = 0;
|
|
6
5
|
const MAX_RESULT_BYTES = 7 * 1024 * 1024;
|
|
7
6
|
const BROWSER_EXTENSION_PROTOCOL = 3;
|
|
8
7
|
const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
8
|
+
const MAX_ACTIVE_REQUESTS = 32;
|
|
9
9
|
const activeRequests = new Map();
|
|
10
|
-
|
|
11
10
|
chrome.runtime.onInstalled.addListener(() => { ensureReconnectAlarm(); void connectFromStorage(); });
|
|
12
11
|
chrome.runtime.onStartup.addListener(() => { ensureReconnectAlarm(); void connectFromStorage(); });
|
|
13
12
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
@@ -21,7 +20,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
21
20
|
return true;
|
|
22
21
|
});
|
|
23
22
|
chrome.action.onClicked.addListener((tab) => void handleActionClick(tab));
|
|
24
|
-
|
|
25
23
|
async function handleActionClick(tab) {
|
|
26
24
|
if (tab?.id && parsePairingPage(tab.url)) {
|
|
27
25
|
await confirmRepairFromTab(tab);
|
|
@@ -31,12 +29,10 @@ async function handleActionClick(tab) {
|
|
|
31
29
|
const pairUrl = pairingUrlFromEndpoint(current.endpoint) || "http://127.0.0.1:39393/pair";
|
|
32
30
|
await chrome.tabs.create({ url: pairUrl });
|
|
33
31
|
}
|
|
34
|
-
|
|
35
32
|
function pairingUrlFromEndpoint(endpoint) {
|
|
36
33
|
const parsed = parseBrokerEndpoint(endpoint);
|
|
37
34
|
return parsed ? `http://127.0.0.1:${parsed.port}/pair` : "";
|
|
38
35
|
}
|
|
39
|
-
|
|
40
36
|
function parsePairingPage(value) {
|
|
41
37
|
let parsed;
|
|
42
38
|
try { parsed = new URL(String(value || "")); } catch { return null; }
|
|
@@ -299,6 +295,12 @@ async function handleMessage(ws, raw, onReady = () => {}) {
|
|
|
299
295
|
closeSocketQuietly(ws, 1002, "duplicate browser request id");
|
|
300
296
|
return;
|
|
301
297
|
}
|
|
298
|
+
if (activeRequests.size >= MAX_ACTIVE_REQUESTS) {
|
|
299
|
+
if (!sendResponse(ws, message.id, false, null, "too many concurrent browser requests")) {
|
|
300
|
+
closeSocketQuietly(ws, 1011, "browser overload response delivery failed");
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
302
304
|
const state = { cancelled: false, timeoutMs: browserOperations().boundedRequestTimeout(message.timeout_ms), socket: ws };
|
|
303
305
|
activeRequests.set(message.id, state);
|
|
304
306
|
try {
|
|
@@ -309,7 +311,7 @@ async function handleMessage(ws, raw, onReady = () => {}) {
|
|
|
309
311
|
closeSocketQuietly(ws, 1011, "browser response delivery failed");
|
|
310
312
|
}
|
|
311
313
|
} catch (error) {
|
|
312
|
-
if (!state.cancelled && !sendResponse(ws, message.id, false, null,
|
|
314
|
+
if (!state.cancelled && !sendResponse(ws, message.id, false, null, globalThis.__machineBridgeBrowserErrorBoundary.publicError(error))) {
|
|
313
315
|
closeSocketQuietly(ws, 1011, "browser error delivery failed");
|
|
314
316
|
}
|
|
315
317
|
} finally {
|
|
@@ -336,7 +338,6 @@ function sendResponse(ws, id, ok, result, error = "") {
|
|
|
336
338
|
return sendSocketQuietly(ws, payload);
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
|
|
340
341
|
function browserOperations() {
|
|
341
342
|
const api = globalThis.__machineBridgeBrowserOperations;
|
|
342
343
|
if (!api || typeof api.dispatch !== "function") throw new Error("browser operations module is unavailable");
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -64,7 +64,7 @@ See [Session instructions, skills, commands, and capability discovery](AGENT_CON
|
|
|
64
64
|
|
|
65
65
|
`BrowserBridgeManager` owns only connection orchestration for the loopback HTTP/WebSocket broker, owner/client failover, routed requests, cancellation, extension replacement, and start/stop generation control. Every asynchronous startup boundary rechecks the generation so a listener or upstream socket cannot appear after `stop()` has invalidated that start. `browser-operation-service.mjs` owns MCP-facing browser argument normalization, resource-backed values/uploads, form semantics, screenshot conversion, and status presentation. Extension version/capability parsing lives in the strict checked `browser-extension-protocol.mjs`; pairing files and local HTML/Host/Origin helpers live in `browser-pairing-store.mjs`. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
|
|
66
66
|
|
|
67
|
-
The packaged Manifest V3 extension runs in the user's existing Chromium profile. Its service worker is limited to pairing, transport, acknowledged protocol readiness, cancellation, and response routing. Fixed `browser-operations.js` owns tab lifecycle, aggregate frame/source budgets, waits, screenshots, and input-backend selection; fixed `page-automation.js` is injected into selected frames for snapshot-version-2 semantics, stable refs, bounded DOM/text traversal, actionability checks, open-Shadow-DOM traversal, structured DOM operations, multi-field forms, and resource-backed file inputs. Fixed `devtools-input.js` exposes only bounded mouse, keyboard, and text sequences through the Chromium debugger API; callers cannot select CDP methods. Trusted sessions attach for one action and detach in `finally`; DOM fallback is allowed only before any DevTools Input dispatch, preventing duplicate side effects after an ambiguous command failure. Protocol 3 requires bidirectional `hello`/`hello_ack` plus exact packaged-version and capability equality; pairing state and replacement are committed only after validation, so an invalid candidate cannot displace or overwrite the working configuration. The broker validates loopback hostnames, canonical extension IDs, matching pairing/broker ports, bearer subprotocols, message sizes, concurrency, and deadlines. Pairing material is owner-only and omitted from MCP/log output.
|
|
67
|
+
The packaged Manifest V3 extension runs in the user's existing Chromium profile. Its service worker is limited to pairing, transport, acknowledged protocol readiness, cancellation, bounded request lifecycle, and response routing. It independently caps active operations at 32 even though the local broker enforces the same ceiling. Fixed `browser-error-boundary.js` maps only an allowlist of actionable messages to the remote protocol; unclassified Chrome, DevTools, page, selector, URL, path, and account-shaped exception text becomes the fixed `browser operation failed` result. Fixed `browser-operations.js` owns tab lifecycle, aggregate frame/source budgets, waits, screenshots, and input-backend selection; successful trusted-input fallback reports a fixed reason rather than the local debugger exception. Fixed `page-automation.js` is injected into selected frames for snapshot-version-2 semantics, stable refs, bounded DOM/text traversal, actionability checks, open-Shadow-DOM traversal, structured DOM operations, multi-field forms, and resource-backed file inputs. Fixed `devtools-input.js` exposes only bounded mouse, keyboard, and text sequences through the Chromium debugger API; callers cannot select CDP methods. Trusted sessions attach for one action and detach in `finally`; DOM fallback is allowed only before any DevTools Input dispatch, preventing duplicate side effects after an ambiguous command failure. Protocol 3 requires bidirectional `hello`/`hello_ack` plus exact packaged-version and capability equality; pairing state and replacement are committed only after validation, so an invalid candidate cannot displace or overwrite the working configuration. The broker validates loopback hostnames, canonical extension IDs, matching pairing/broker ports, bearer subprotocols, message sizes, concurrency, and deadlines. Pairing material is owner-only and omitted from MCP/log output.
|
|
68
68
|
|
|
69
69
|
See [Local application and browser automation](LOCAL_AUTOMATION.md).
|
|
70
70
|
|
|
@@ -106,7 +106,7 @@ 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`, `/`, discovery metadata, CORS preflight, and unknown-path 404s are answered by the outer Worker without Durable Object requests. Only an exact stateful route allowlist passes a burst guard and reaches 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;
|
|
@@ -114,16 +114,16 @@ All requests for a deployed Worker route to one named Durable Object. It owns:
|
|
|
114
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
|
|
117
|
+
`BridgeRoom` owns stateful routing, MCP authorization/dispatch, daemon WebSocket lifecycle, pending relay-call composition, cancellation, and resumable state. `worker-static-routes.ts` and `worker-metadata.ts` own stateless public routing; `worker-edge-guard.ts` owns the burst guard and structured quota/gateway failures. `mcp-stream-proxy.ts` owns public SSE adaptation, while `mcp-stream-subscription.ts` owns bounded terminal-subscription retries and payload validation. `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
|
|
123
123
|
|
|
124
124
|
The local daemon uses a long-term P-256 device root to certify a 24-hour ephemeral session key. Worker deployment receives only the root public JWK. The default provider on every platform is an owner-only portable private JWK. macOS can instead use non-exportable Secure Enclave material only when `MBM_MACOS_TRUST_BROKER` identifies an app-like broker whose Apple signature, Team ID, canonical non-symlink executable path with no group/other write access, provisioning-backed Keychain capability, and actual create/delete key probe all validate. The enrolled state binds that broker identity, Keychain tag, and public key. One root signature at daemon startup certifies the in-memory session key. Every WebSocket attempt then carries a short-lived preflight signed by the session key and bound to Worker origin, package version, nonce, timestamp, and the root certificate. The nonce is consumed once through bounded Durable Object state. After upgrade, the Worker issues a fresh challenge and accepts tools only after a second session signature binds the challenge, origin, package version, daemon instance ID, and timestamp. Reconnects reuse the in-memory key without touching the root. End-to-end readiness still requires an ordinary relay probe before a verified candidate replaces the incumbent.
|
|
125
125
|
|
|
126
|
-
The primary OAuth store separates trusted client registrations and named accounts from authorization codes and access-token records. A separate versioned Durable Object key owns refresh-token families, consumed-token markers, and family revocation. A `client_id` identifies an MCP application and redirect URIs; account records identify the authorized human or service identity. Codes and tokens bind client ID, account ID, account version, role, scope, resource, deployment token version, family identity, and expiration where applicable. Only hashes of bearer tokens are persisted. Access tokens last fifteen minutes; refresh tokens rotate
|
|
126
|
+
The primary OAuth store separates trusted client registrations and named accounts from authorization codes and access-token records. A separate versioned Durable Object key owns refresh-token families, consumed-token markers, and family revocation. A `client_id` identifies an MCP application and redirect URIs; account records identify the authorized human or service identity. Codes and tokens bind client ID, account ID, account version, role, scope, resource, deployment token version, family identity, and expiration where applicable. Only hashes of bearer tokens are persisted. Access tokens last fifteen minutes; refresh tokens rotate and expire after fourteen idle days or thirty absolute family days. An identity-equivalent retry may reproduce the same deployment-keyed HMAC replacement pair at most twice inside a 30-second concurrency window without extending its expiration; retry-budget exhaustion is throttled, while reuse after that window revokes the complete family, including active access tokens. The Worker carries account, client, and refresh-family identity with every relayed call so long-lived runtime objects cannot cross principals. One bridge-specific Durable Object and one local runtime remain the normal topology for a workspace/trust domain; see [MULTI_ACCOUNT.md](MULTI_ACCOUNT.md).
|
|
127
127
|
|
|
128
128
|
The daemon attachment deliberately omits workspace path/name/hash and process ID. Explicit authenticated tools may return workspace metadata according to local path-display policy.
|
|
129
129
|
|
|
@@ -168,11 +168,11 @@ Remote OAuth binds each code, access token, and refresh token to a named Machine
|
|
|
168
168
|
3. The Worker validates authorization parameters before displaying a password form.
|
|
169
169
|
4. The user verifies client name and redirect URI and enters a Machine Bridge account name and password.
|
|
170
170
|
5. The Worker creates a five-minute code bound to client, redirect, resource, normalized scope, and PKCE challenge.
|
|
171
|
-
6. A valid verifier exchanges the one-time code for an expiring access token and refresh token; only their hashes are stored. A refresh request is bound to the original public client, account, scope, resource,
|
|
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.
|
|
171
|
+
6. A valid verifier exchanges the one-time code for an expiring access token and refresh token; only their hashes are stored. A refresh request is bound to the original public client, account, scope, resource, account version/role, deployment token version, and optional DPoP key. Rotation derives one replacement pair from the consumed token and the private deployment token version, then permits at most two identity-equivalent responses with that exact pair during a 30-second concurrency window; over-budget retries are throttled, and replay after the window revokes the family.
|
|
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 and increments the bounded `session_bootstrap_failed` observability counter.
|
|
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`, `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 terminal
|
|
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.
|
|
@@ -231,9 +231,9 @@ The default `full` profile passes the complete parent environment. Isolated envi
|
|
|
231
231
|
|
|
232
232
|
Large object results use `structuredContent` as the authoritative representation. The human text mirror is complete only below the shared 16 KiB projection threshold; above it, both local stdio and the Worker return the same compact byte-count/field summary instead of serializing the object a second time. `process-result-projection.mjs`, `process-output-stream.mjs`, and the shared result projector keep lifecycle, byte retention, and MCP presentation as separate boundaries.
|
|
233
233
|
|
|
234
|
-
Startup-lock waits, daemon takeover, process-session reads, managed-job recovery handoff, browser/page waits, application-cache freshness, and in-memory duration metrics use monotonic elapsed time, so wall-clock correction cannot extend or prematurely terminate their configured duration. Persisted timestamps and retention/credential expiry continue to use wall time. Process sessions retain bounded byte buffers with monotonic offsets, accept bounded stdin, support short output/exit waits, and are capped per runtime. Valid UTF-8 is returned as text; byte slices that are not valid UTF-8 also include lossless base64 data. Head/tail previews trim incomplete UTF-8 boundary code points instead of introducing replacement characters. Session IDs are random. Running sessions are killed on runtime stop
|
|
234
|
+
Startup-lock waits, daemon takeover, process-session reads, managed-job recovery handoff, browser/page waits, application-cache freshness, and in-memory duration metrics use monotonic elapsed time, so wall-clock correction cannot extend or prematurely terminate their configured duration. Persisted timestamps and retention/credential expiry continue to use wall time. Process sessions retain bounded byte buffers with monotonic offsets, accept bounded stdin, support short output/exit waits, and are capped per runtime. Valid UTF-8 is returned as text; byte slices that are not valid UTF-8 also include lossless base64 data. Head/tail previews trim incomplete UTF-8 boundary code points instead of introducing replacement characters. Session IDs are random. Running sessions are killed on runtime stop or non-recoverable daemon replacement; a transient same-process relay disconnect enters bounded recovery instead of being treated as cancellation.
|
|
235
235
|
|
|
236
|
-
Child processes run in a separate process group where supported. Timeout, cancellation,
|
|
236
|
+
Child processes run in a separate process group where supported. Timeout, explicit cancellation, reconnect-grace expiry, runtime shutdown, and non-recoverable replacement send termination to process trees, with a referenced forced-escalation timer that remains alive even when the direct child exits before a resistant descendant. Windows uses tree-aware task termination.
|
|
237
237
|
|
|
238
238
|
Managed jobs use the same argv/environment primitives but a different lifecycle. Each job is capped at 16 main and 16 finally steps, 50 retained jobs, 64 registered resources, 8 MiB of referenced resource bytes, 512 KiB of temporary-file content, and bounded per-step output. They are non-interactive. Resource paths/stdin/environment are injected only inside the runner. Exact resource output redaction is defense in depth; discard capture is the strong option when a command may echo credentials.
|
|
239
239
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Security and privacy audit notes
|
|
2
2
|
|
|
3
|
+
## 2026-07-26 version 3.0.0-beta.18 account-continuity and quota audit
|
|
4
|
+
|
|
5
|
+
The repeated hosted-client message `We couldn't connect your account. Please try again.` occurred while the Worker, Durable Object, daemon, Wrangler login, and end-to-end doctor probes were healthy. The remaining credential path had a destructive concurrency assumption: refresh tokens rotated once, and any immediate second use revoked the complete family—including the replacement credentials just returned to a concurrent request. A network retry or two refreshes racing through the same hosted connector could therefore convert a recoverable response race into total account loss. Beta.18 records a bounded source snapshot for 30 seconds and uses a deployment-keyed, domain-separated HMAC to reproduce the original replacement pair for at most two identity-equivalent retry responses. Retries neither create another credential branch nor extend expiration; the Worker returns a retryable 429 after that budget, and retains whole-family revocation for replay after the grace window. Client, resource, scope, account/version/role, deployment token version, and DPoP binding are revalidated on every retry.
|
|
6
|
+
|
|
7
|
+
The broader quota review found that beta.17 removed time-proportional stream polling but still forwarded public discovery and random paths to the Durable Object, rewrote liveness alarms on every heartbeat, exposed Preview URLs, and rethrew outer failures as platform exceptions. Beta.18 moves all stateless metadata and 404s to the outer Worker, adds a stateful-route rate-limit binding, coalesces alarm writes, exposes logical storage-write/alarm counters, retries terminal subscription transport failure within a hard bound, converts unexpected edge failures to a privacy-safe structured 502, and keeps the automatically provisioned `workers.dev` endpoint as the complete default deployment path.
|
|
8
|
+
|
|
9
|
+
A second fault-directed pass found issues outside the original incident scope. Bounded request readers retained at most the configured bytes but continued draining the rest of an oversized stream, allowing attacker-controlled duration work. Invalid methods on known stateful routes reached the rate limiter and Durable Object before returning 405. Several write-path and recursive-walk checks treated every `lstat`/`opendir` error as “missing,” so permission or I/O failure could be evaluated under a false path state. Repeated platform degradation also emitted one log per request, and newly split security modules were absent from the critical-coverage gate. The final implementation cancels body readers as soon as a declared or observed limit is crossed, rejects invalid methods before stateful dispatch, distinguishes only `ENOENT` from real filesystem failure, exposes partial application/skill discovery warnings, suppresses repeated edge logs with a reported duplicate count, removes an unreachable revoked-family branch and duplicate stream `waitUntil`, and establishes coverage floors for every new boundary module.
|
|
10
|
+
|
|
11
|
+
A third pass across local administration, sensitive-resource rollback, and the browser extension found additional boundary defects. The account administration client accepted an unbounded response and silently converted invalid successful JSON into an empty object. Newly generated SSH key files could remain unregistered if state persistence and best-effort cleanup both failed, because cleanup errors were discarded. The browser extension returned arbitrary local/page exception text and debugger fallback details to the remote caller, relied only on the broker for its concurrency ceiling, and the edge error classifier followed cyclic `cause` chains without a depth limit. Skill-symlink warnings also risked embedding absolute paths through native filesystem messages. These paths now use bounded strict JSON, complete two-file key rollback with compound failure, a fixed browser error boundary and fallback reason, an independent 32-request extension ceiling, bounded cycle-aware error traversal, and coarse path-projected discovery warnings.
|
|
12
|
+
|
|
13
|
+
Tests cover schema-2 migration, two idempotent concurrent refresh retries that reproduce the original access/refresh pair, retry-budget exhaustion without family revocation, post-grace whole-family revocation, DPoP proof replay and client/resource/scope isolation, static-route/unknown-path/invalid-method DO bypass, rate-limit denial and fail-open binding outage, nested/cyclic/deep quota classification, sanitized and throttled gateway logs, immediate oversized-body cancellation, bounded strict administration responses, complete and incomplete SSH rollback, fail-closed filesystem state inspection, privacy-safe partial discovery reporting, fixed browser-error projection and independent concurrency, fixed normal stream request/write budgets, single keepalive registration, subscription retry bounds, and alarm set/no-op/delete behavior. This source audit does not claim that an in-Worker rate limiter can enforce the account-wide Workers Free daily limit against distributed traffic; it also records the deployment-wide per-location bucket as a localized availability trade-off.
|
|
14
|
+
|
|
15
|
+
No live Worker deployment, daemon replacement, npm publication, tag, push, or acceptance is implied by this source audit. The exact beta.18 candidate must be owner-activated and observed before acceptance.
|
|
16
|
+
|
|
3
17
|
## 2026-07-25 version 3.0.0-beta.16 pending-call recovery and boundary audit
|
|
4
18
|
|
|
5
19
|
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.
|
|
@@ -32,7 +46,7 @@ The exact beta.13 tarball was activated through the owner command and converged
|
|
|
32
46
|
|
|
33
47
|
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.
|
|
34
48
|
|
|
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
|
|
49
|
+
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.
|
|
36
50
|
|
|
37
51
|
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.
|
|
38
52
|
|
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
|
|
|
@@ -63,7 +63,7 @@ Rules:
|
|
|
63
63
|
- Adapters may translate data but should not duplicate policy or schemas.
|
|
64
64
|
- Every protocol control message emitted by one side must be explicitly accepted, rejected, or version-gated by the other side, with an end-to-end contract test covering the message name and semantics.
|
|
65
65
|
- State transitions are explicit; readiness is not inferred from a lower-level event. An open WebSocket is not authenticated until `hello_ack`, and authenticated transport is not service readiness until an end-to-end result probe has returned on the same relay session. Pre-ready work and premature readiness acknowledgements fail closed.
|
|
66
|
-
- Every externally controlled input is bounded before expensive allocation, traversal, parsing, storage, or execution.
|
|
66
|
+
- Every externally controlled input is bounded before expensive allocation, traversal, parsing, storage, or execution. A byte limit constrains bytes actually consumed and duration work, not only the subset retained in memory; once a declared or observed bound is crossed, cancel or close the source immediately.
|
|
67
67
|
- Externally controlled string keys must not use prototype-chain membership or truthiness on ordinary objects. Use `Map`, `Set`, `Object.hasOwn`, or null-prototype records for command dispatch, enums, ACLs, form fields, registries, and other key-addressed contracts.
|
|
68
68
|
- Repository text must not contain invisible ASCII controls other than tab, CR, and LF; architecture tests enforce this even when JavaScript syntax remains valid.
|
|
69
69
|
- Persistent mutations use owner-only files, bounded no-follow reads, flushed atomic replacement, and integrity checks appropriate to the data.
|
|
@@ -72,7 +72,7 @@ Rules:
|
|
|
72
72
|
- Exclusive locks use the shared complete-before-visible hard-link claim. Reclamation requires process identity plus a matching file snapshot/token; do not unlink a path merely because an earlier read looked stale.
|
|
73
73
|
- Service providers normalize success/failure to one result contract. Definition removal follows the shared platform-stop → verified-daemon-stop → remove order.
|
|
74
74
|
- Retry is limited to classified transient failures. Authentication, authorization, validation, integrity, and policy errors fail immediately.
|
|
75
|
-
- Cleanup-only catches may be best effort, but primary failures must not be silently discarded.
|
|
75
|
+
- Cleanup-only catches may be best effort, but primary failures must not be silently discarded. Rollback of newly created credentials, keys, or other sensitive artifacts is part of the primary integrity result: incomplete rollback must be reported explicitly after attempting every cleanup target.
|
|
76
76
|
- New work should not increase an already broad orchestration module when the behavior has an independent lifecycle or test surface. Extract the domain first.
|
|
77
77
|
|
|
78
78
|
`runtime.mjs` owns local tool semantics. `relay-connection.mjs` owns authenticated relay connection lifecycle. The CLI orchestrates them; it must not become the second implementation of either.
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -170,7 +170,7 @@ The first start performs these operations:
|
|
|
170
170
|
|
|
171
171
|
The foreground command remains attached to the terminal. Keep it running while testing. The remote Worker cannot execute local tools when no authenticated daemon is connected.
|
|
172
172
|
|
|
173
|
-
The Worker name is a persistent workspace identity, not a retry counter. A successful Wrangler upload is recorded before health verification, so a later timeout does not require a new name and does not make the next start upload again. Supplying a different `--worker-name` for an initialized workspace requires `--force-worker` because it intentionally creates/replaces a separate Cloudflare Worker identity.
|
|
173
|
+
The Worker name is a persistent workspace identity, not a retry counter. A successful Wrangler upload is recorded before health verification, so a later timeout does not require a new name and does not make the next start upload again. Supplying a different `--worker-name` for an initialized workspace requires `--force-worker` because it intentionally creates/replaces a separate Cloudflare Worker identity. The printed `workers.dev` MCP URL is the standard public endpoint and requires no separately owned domain.
|
|
174
174
|
|
|
175
175
|
To run only in the background after setup:
|
|
176
176
|
|
|
@@ -36,7 +36,7 @@ Dynamic OAuth registration creates an untrusted client record. The first success
|
|
|
36
36
|
- the account role;
|
|
37
37
|
- the OAuth client ID.
|
|
38
38
|
|
|
39
|
-
A client cannot silently switch to another account. Account disablement, role changes, password rotation, client revocation, token-version rotation, and refresh-token replay invalidate the relevant credentials.
|
|
39
|
+
A client cannot silently switch to another account. Account disablement, role changes, password rotation, client revocation, token-version rotation, and refresh-token replay outside the bounded concurrent-refresh window invalidate the relevant credentials.
|
|
40
40
|
|
|
41
41
|
Inspect trusted clients locally:
|
|
42
42
|
|
package/docs/LOCAL_AUTOMATION.md
CHANGED
|
@@ -93,7 +93,7 @@ Menu-bar and menu subtrees are not recursively expanded by default. This keeps m
|
|
|
93
93
|
|
|
94
94
|
## Capability discovery and automatic selection
|
|
95
95
|
|
|
96
|
-
`resolve_task_capabilities` rescans instruction files, skills, explicit/automatic package commands, and relevant local automation metadata on every call. It ranks matching skills and commands, optionally loads the best skill, and compares every canonical-full task with cached installed-application names, so a task that directly names an app does not need generic “app/window” wording. Application inventory is refreshed after a bounded cache interval.
|
|
96
|
+
`resolve_task_capabilities` rescans instruction files, skills, explicit/automatic package commands, and relevant local automation metadata on every call. It ranks matching skills and commands, optionally loads the best skill, and compares every canonical-full task with cached installed-application names, so a task that directly names an app does not need generic “app/window” wording. Application inventory is refreshed after a bounded cache interval. Per-root discovery failures are returned as bounded `warnings`, and capability resolution reports `application_discovery.available`, warning count, truncation, and a coarse error class instead of silently treating an unreadable inventory as an empty successful scan.
|
|
97
97
|
|
|
98
98
|
This is the strongest reliable server-side automation boundary available through MCP: discovery, refresh, ranking, and progressive skill loading are automatic. The MCP host still owns the model loop and decides whether a recommended tool is exposed, approved, or invoked. Machine Bridge cannot force ChatGPT web or another host to make a call that the host declines.
|
|
99
99
|
|
package/docs/MULTI_ACCOUNT.md
CHANGED
|
@@ -107,7 +107,7 @@ Authorization codes bind:
|
|
|
107
107
|
|
|
108
108
|
Access and refresh tokens additionally bind to the deployment token version and refresh-family ID. Token values are stored as SHA-256 lookup keys.
|
|
109
109
|
|
|
110
|
-
Access tokens last fifteen minutes. Refresh tokens rotate on every use, have a fourteen-day idle limit and thirty-day family limit, and leave bounded replay markers.
|
|
110
|
+
Access tokens last fifteen minutes. Refresh tokens rotate on every use, have a fourteen-day idle limit and thirty-day family limit, and leave bounded replay markers. To tolerate a lost response or concurrent hosted-client refresh, the same consumed token may return the same HMAC-derived replacement pair at most twice during a 30-second window. These retries do not create new credential branches or extend expiration. Further in-window attempts are rate-limited; reuse after that window revokes the complete family, including active access tokens.
|
|
111
111
|
|
|
112
112
|
A refresh request also verifies that the client remains bound to the current account version and role.
|
|
113
113
|
|
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.
|
|
@@ -28,6 +30,12 @@ machine-mcp --verbose
|
|
|
28
30
|
|
|
29
31
|
Run these commands from the same environment used for startup so `HTTPS_PROXY`, `HTTP_PROXY`, and `NO_PROXY` are identical. After the foreground connection succeeds, run `machine-mcp service install` from that same PowerShell session. Installation stores only an allowlisted proxy/custom-CA environment snapshot in private local state so the logon task does not lose session-only `$env:` settings after reboot. A later start with no proxy variables does not erase the saved snapshot; set a variable explicitly to an empty value and reinstall to clear it. `machine-mcp service status` reports only the saved key names. Debug logs expose only the selected route and classified error; they never print a proxy endpoint or credentials.
|
|
30
32
|
|
|
33
|
+
### Worker quota controls
|
|
34
|
+
|
|
35
|
+
The standard public endpoint remains the automatically provisioned `workers.dev` URL printed by Machine Bridge; no personal domain is required. The outer Worker serves health, discovery metadata, CORS preflight, and unknown 404s without Durable Object state. Stateful routes pass a 120-per-minute Cloudflare Rate Limiting binding before DO dispatch. This guard is per Cloudflare location and executes after the Worker starts; it protects the Durable Object from ordinary bursts but is not an exact account-wide daily Workers quota. Configure Cloudflare account usage alerts when available. A binding outage fails open so quota protection cannot become an authentication outage.
|
|
36
|
+
|
|
37
|
+
`server_info.worker.observability.oauth_refresh` distinguishes normal rotation, bounded retry issuance, exhausted retry budget, post-grace family revocation, and rejected grants. `durable_budget` reports estimated stream-row writes plus alarm sets, deletes, and no-ops. These are process-lifetime logical counters for diagnosis, not Cloudflare billing records.
|
|
38
|
+
|
|
31
39
|
### Blocking-layer decision table
|
|
32
40
|
|
|
33
41
|
| Result | Interpretation |
|
|
@@ -37,7 +45,7 @@ Run these commands from the same environment used for startup so `HTTPS_PROXY`,
|
|
|
37
45
|
| `worker.sockets_live.authenticated` is nonzero but `worker.sockets_live.ready` is zero | Transport authentication exists, but the end-to-end result probe has not completed; no daemon tools are advertised and the candidate will be closed at the readiness deadline |
|
|
38
46
|
| `capability_routing.bootstrap_observed` is false | The current local runtime has not received `session_bootstrap`; reconnect or inspect host initialization handling |
|
|
39
47
|
| `task_resolution_observed` is false after a substantive task | The host/model did not call `resolve_task_capabilities`; server-side discovery cannot force that host decision |
|
|
40
|
-
| Task resolution ran but all match counts are zero |
|
|
48
|
+
| Task resolution ran but all match counts are zero | Check `application_discovery`: `available=false` or a nonzero warning count means application inventory was partial or unavailable; otherwise the resolver ran successfully but found no sufficiently relevant local skill, command, or application |
|
|
41
49
|
| No structured result because the host rejects the call | Host/connector approval or safety layer, or transport before daemon delivery |
|
|
42
50
|
| `mcp-host-to-daemon` passes but `local-filesystem` fails | Local state/runtime permissions, disk policy, sandbox, or endpoint security |
|
|
43
51
|
| Filesystem passes but `local-process-spawn` fails | Local executable policy, endpoint security, OS permissions, or damaged Node runtime |
|
|
@@ -53,7 +61,7 @@ Machine Bridge supports concurrent calls: the Worker admits up to 32 pending dae
|
|
|
53
61
|
|
|
54
62
|
`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
63
|
|
|
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.
|
|
64
|
+
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
65
|
|
|
58
66
|
`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
67
|
|
|
@@ -163,7 +171,7 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
|
|
|
163
171
|
|
|
164
172
|
### Lifecycle and pending-call diagnosis
|
|
165
173
|
|
|
166
|
-
`server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `server_info.observability.in_flight_calls` and `server_info.runtime.processes` distinguish a blocked call from a surviving process. `server_info.runtime.execution_guardrails` reports the enforced local concurrency/timeout/stdin/output/session limits and explicitly states that CPU quota, memory quota, and network isolation are not enforced in process. Worker `server_info.worker.pending_calls` reports the internal-call index, client request-key index, and detached-call count. All three must return to zero after a terminal result, explicit cancellation,
|
|
174
|
+
`server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `server_info.observability.in_flight_calls` and `server_info.runtime.processes` distinguish a blocked call from a surviving process. `server_info.runtime.execution_guardrails` reports the enforced local concurrency/timeout/stdin/output/session limits and explicitly states that CPU quota, memory quota, and network isolation are not enforced in process. Worker `server_info.worker.pending_calls` reports the internal-call index, client request-key index, and detached-call count. All three must return to zero after a terminal result, explicit cancellation, timeout, or reconnect-grace expiry. An HTTP/SSE client disconnect alone is transport disposal and may leave a recoverable call active until terminal delivery or the normal lifecycle boundary. During a brief daemon interruption, `active` and `request_keys` may remain nonzero while `detached` identifies the recoverable subset; after same-instance readiness, `detached` returns to zero without losing those requests. Nonzero request-key counts after active calls reach zero indicate a lifecycle defect rather than normal load. `worker.observability.calls.unmatched_results` is the bounded counter for late results that no longer have a receiver.
|
|
167
175
|
|
|
168
176
|
Stable errors include `policy_denied`, `invalid_request`, `timeout`, `cancelled`, `network_error`, `unavailable`, `limit_exceeded`, and `integrity_error`, with retryability metadata. Diagnose by code first; free-form messages are guidance, not an API contract.
|
|
169
177
|
|
package/docs/OVERVIEW.md
CHANGED
|
@@ -84,7 +84,7 @@ Low-level responsibilities remain in focused modules. Architecture tests enforce
|
|
|
84
84
|
|
|
85
85
|
Each canonical workspace has independent profile state, Worker identity, trusted clients, account/token state, legacy-lease cleanup state, audit-chain state, locks, service metadata, and managed jobs. State mutations use owner-only files where supported, bounded reads, atomic replacement, and process-identity-aware locks.
|
|
86
86
|
|
|
87
|
-
The relay distinguishes a root-certified ephemeral session, signed preflight, challenge authentication, readiness probing, and active service. A candidate daemon must prove possession of the certified session key and complete an end-to-end probe before replacing an incumbent.
|
|
87
|
+
The relay distinguishes a root-certified ephemeral session, signed preflight, challenge authentication, readiness probing, and active service. A candidate daemon must prove possession of the certified session key and complete an end-to-end probe before replacing an incumbent. A transient socket loss detaches ordinary calls for the bounded same-daemon reconnect window; only explicit cancellation, timeout, reconnect-grace expiry, runtime shutdown, or a non-recoverable daemon replacement terminates their local process trees.
|
|
88
88
|
|
|
89
89
|
Managed jobs use a separate durable lifecycle. Their plans are integrity-bound, runners are process-identity checked, transitions are lock-protected, terminal plans are scrubbed, and recovery is bounded.
|
|
90
90
|
|