machine-bridge-mcp 1.2.1 → 1.2.5

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.5 - 2026-07-17
4
+
5
+ ### End-to-end relay readiness and safe daemon handover
6
+
7
+ - Separate authenticated WebSocket transport from verified service readiness. A daemon now becomes externally usable only after the Worker sends a random readiness probe and receives its result through the same local message dispatch, relay-session binding, and `tool_result` delivery path used by real calls. `hello_ack` and heartbeats alone can no longer produce `daemon.connected=true` when result delivery is broken. The local runtime also rejects pre-ready tool calls and a premature `ready_ack` that was not preceded by a successfully delivered probe result.
8
+ - Keep the incumbent verified daemon active while a replacement is authenticated and probed. A failed, malformed, silent, or incompatible candidate is closed without displacing the working connection; only a successful `ready_ack` handover replaces the incumbent. Candidate hello, readiness, and steady-state liveness have independent bounded deadlines enforced by Durable Object alarms.
9
+ - Extract daemon attachment/state transitions into `DaemonSocketRegistry`, expose authenticated/probing/ready counts and readiness timestamps through `server_info`, and add fault-injection coverage for missing acknowledgements, invalid probe results, replacement races, reconnect backoff, session-generation propagation, and full OAuth/MCP/WebSocket routing.
10
+ - Correct capability ranking after the local research-skill rename: generic identity tokens such as `web`, `cli`, and `tool` no longer dominate selection, while concrete Chinese research/search intent maps to `research`, `search`, and `find`. This prevents a browser-form task from selecting a web-research skill without weakening explicit research requests.
11
+ - Remove a duplicate Worker route guard, require awaited alarm rescheduling after inbound activity, retain zero production dependency vulnerabilities, and refresh architecture, operations, logging, testing, upgrade, and audit documentation. State schema 6 and policy revision 5 remain unchanged.
12
+
13
+ ## 1.2.4 - 2026-07-17
14
+
15
+ ### Relay tool_result session context regression
16
+
17
+ - Pass the authenticated relay session generation into every inbound WebSocket `onMessage` callback. The 1.2.2 session-binding change discarded every `tool_result` with `session_ended` because handlers received `sessionId=0` even while heartbeats kept the socket live, so MCP tools timed out despite `daemon.connected=true`.
18
+ - Emit an explicit error when a tool result is discarded because the inbound call context lacked a session id, and include both expected and active session ids in the structured event.
19
+ - Add a regression that proves message dispatch attaches the current authenticated session generation.
20
+
21
+ ## 1.2.3 - 2026-07-17
22
+
23
+ ### Worker daemon false-online liveness
24
+
25
+ - Treat authenticated daemon sockets as live only when inbound traffic is recent. `role=daemon` plus `readyState=OPEN` is no longer enough after Durable Object hibernation or a half-closed transport, which previously left `daemon.connected=true` while every `tool_call` timed out.
26
+ - Persist `lastSeenAt` on daemon attachments, refresh it on `hello`, heartbeats, and `tool_result`, and reclaim silent sockets through the Durable Object alarm as well as on tool send failure or prolonged silence during a timed-out call.
27
+ - Report `daemon.last_seen_at`, `daemon.liveness_timeout_ms`, and `worker.sockets_live` from `server_info` so control-plane counters that reset on DO wake are not mistaken for live authenticated sockets.
28
+ - Add pure liveness helpers and regression coverage for fresh, silent, candidate, and legacy attachments without `lastSeenAt`.
29
+
30
+ ## 1.2.2 - 2026-07-17
31
+
32
+ ### Relay result lifecycle and human-readable diagnostics
33
+
34
+ - Bind every local `tool_result` to the authenticated relay session that delivered its `tool_call`. A result from a disconnected or replaced socket is discarded instead of being sent over a newer connection, and a caller cancellation marks the eventual local result as intentionally undeliverable. Routine late-result races are debug-only rather than repeated `relay.tool_result.delivery_failed` warnings; an actual synchronous WebSocket send failure still invalidates the ambiguous transport.
35
+ - Propagate MCP HTTP cancellation into the Worker pending-call registry. Incoming request cancellation removes both internal and session request-key indexes, sends a best-effort daemon cancellation, and records the call as cancelled. The Worker deployment now explicitly enables Cloudflare request-signal delivery and passthrough to the Durable Object.
36
+ - Add an `unmatched_results` Worker metric for results that arrive after their pending record was removed. Human log mode uses natural-language event messages and omits the redundant machine event key, while JSON mode retains stable event names and bounded structured fields.
37
+ - Add regression coverage for relay-session replacement, stale-result suppression, request-signal cleanup, human log rendering, compatibility flags, and unmatched-result observability. Local Wrangler integration continues to cover the complete OAuth/MCP/WebSocket flow and explicit MCP cancellation.
38
+
3
39
  ## 1.2.1 - 2026-07-16
4
40
 
5
41
  ### Fail-closed input contracts and bounded CLI adapters
@@ -309,7 +345,7 @@
309
345
 
310
346
  - Register bounded `package.*` commands from safe root `package.json` script names, while preserving explicit manifest override/deletion and never injecting script bodies. Windows uses a fixed `cmd.exe` wrapper for package-manager shims; Unix keeps direct executable argv. Extend default skill discovery to project `.codex/skills` and unrestricted `CODEX_HOME/skills` compatibility roots.
311
347
  - Match installed applications by their actual names for every canonical-full task instead of requiring generic “app/window” words, with a bounded discovery cache to avoid repeated filesystem scans.
312
- - Normalize a bounded set of common English inflections and Chinese workflow intents before skill/command ranking, and weight capability-name matches above incidental description overlap. This fixes Chinese selection of `skill-creator`, `smart-search-cli`, and `skill-installer` and prevents generic “create” wording from preferring unrelated design skills.
348
+ - Normalize a bounded set of common English inflections and Chinese workflow intents before skill/command ranking, and weight capability-name matches above incidental description overlap. This fixes Chinese selection of `skill-creator`, `web-research-cli`, and `skill-installer` and prevents generic “create” wording from preferring unrelated design skills.
313
349
  - Record privacy-preserving bootstrap and task-resolution telemetry in `server_info` and `project_overview`: counts, timestamps, source/load flags, selected capability metadata, and a runtime-keyed task fingerprint rather than raw task text. Suppress weak skill-overlap recommendations and clarify that the MCP host still controls whether the resolver and recommended tools are invoked.
314
350
 
315
351
  ### Process and network lifecycle
package/README.md CHANGED
@@ -49,6 +49,8 @@ Local clients such as Claude Desktop, Cursor, and Codex CLI
49
49
 
50
50
  The remote Worker authenticates and relays calls. It cannot directly read local files or start local processes. File, Git, image, patch, process, diagnostic, and managed-job operations execute in the local runtime.
51
51
 
52
+ Remote readiness is end-to-end: a daemon is advertised only after a Worker probe returns through the same local dispatch and session-bound result path used by real tools. A replacement is verified before it displaces the current healthy daemon.
53
+
52
54
  New users should follow the end-to-end [installation and first-use guide](docs/GETTING_STARTED.md). Before sharing a deployment or connecting several accounts, read [multi-client, multi-account, and tenancy architecture](docs/MULTI_ACCOUNT.md).
53
55
 
54
56
  ## Default behavior and policy profiles
package/SECURITY.md CHANGED
@@ -172,7 +172,7 @@ Public health and metadata do not expose live workspace or daemon status. The da
172
172
 
173
173
  ## Relay and denial of service
174
174
 
175
- Only one authenticated daemon is active. Candidates have a handshake deadline and cannot displace the current daemon before success. Pending calls are socket-bound, client-request-bound, concurrency-limited, size-limited, and timed out. Duplicate in-flight request IDs are rejected only within the same authenticated MCP session. Initialization returns a stateless HMAC-bound `MCP-Session-Id`; the signature binds the session nonce to the OAuth token identity, so one account can use multiple concurrent chat windows without sharing a cancellation or request-id namespace. Independent sessionless POST requests are not indexed by token and request id. A session id is an integrity/correlation token, not a second authorization credential.
175
+ Only one authenticated daemon is active. Candidates have a handshake deadline and cannot displace the current daemon before success. Authenticated sockets also carry a liveness deadline based on inbound traffic so half-open transports cannot remain the active daemon indefinitely. Pending calls are socket-bound, client-request-bound, concurrency-limited, size-limited, and timed out. Duplicate in-flight request IDs are rejected only within the same authenticated MCP session. Initialization returns a stateless HMAC-bound `MCP-Session-Id`; the signature binds the session nonce to the OAuth token identity, so one account can use multiple concurrent chat windows without sharing a cancellation or request-id namespace. Independent sessionless POST requests are not indexed by token and request id. A session id is an integrity/correlation token, not a second authorization credential.
176
176
 
177
177
  Request bodies, WebSocket messages, tool outputs, traversals, sessions, stdin writes, OAuth records, clients, codes, access tokens, refresh tokens, and failure identities are bounded. Disconnect/replacement terminates active child process trees.
178
178
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "1.2.1",
4
+ "version": "1.2.5",
5
5
  "description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -30,5 +30,5 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "1.2.1"
33
+ "version_name": "1.2.5"
34
34
  }
@@ -196,7 +196,7 @@ No persistent skill or project-context index is trusted as authoritative. `sessi
196
196
 
197
197
  `agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met. Under canonical `full`, it also compares the task with installed application names on every call; application discovery is cached briefly and refreshed after a bounded interval.
198
198
 
199
- Matching remains deterministic and local. Hyphens, underscores, dots, and whitespace are normalized; common English inflections are reduced to a small canonical form; and a bounded Chinese/English workflow vocabulary covers creation, improvement, installation, search, current/official documentation, verification, testing, frontend/design, browser/web, email, performance, and security intents. Capability-name token matches receive more weight than incidental words in a long description. This lets Chinese tasks select English-metadata skills such as `skill-creator`, `smart-search-cli`, and `skill-installer`, while avoiding the prior tie where generic “create” wording could select `frontend-design`. An explicitly named skill or registered command still receives the strongest deterministic boost.
199
+ Matching remains deterministic and local. Hyphens, underscores, dots, and whitespace are normalized; common English inflections are reduced to a small canonical form; and a bounded Chinese/English workflow vocabulary covers creation, improvement, installation, search, current/official documentation, verification, testing, frontend/design, browser/web, email, performance, and security intents. Capability-name token matches receive more weight than incidental words in a long description. This lets Chinese tasks select English-metadata skills such as `skill-creator`, `web-research-cli`, and `skill-installer`, while avoiding the prior tie where generic “create” wording could select `frontend-design`. An explicitly named skill or registered command still receives the strongest deterministic boost.
200
200
 
201
201
  This ranking is deterministic local assistance, not semantic certainty. Weak positive matches remain visible for diagnosis, but only a skill meeting the selection threshold is recommended for loading. The model must still evaluate whether the selected skill applies. Machine Bridge does not execute skill scripts implicitly and does not fabricate a dynamically named MCP tool per skill.
202
202
 
@@ -34,7 +34,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
34
34
  - `runtime-capabilities.mjs` composes agent, application, and browser capability results;
35
35
  - managed jobs, local resources, application automation, and browser automation remain separate managers.
36
36
 
37
- Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic, or capability-scoring logic to `LocalRuntime`. `RelayConnection` owns remote WebSocket transport, authenticated `hello_ack` readiness, heartbeat liveness, reconnect backoff, and outage logging. Stdio mode invokes `LocalRuntime` directly without that adapter.
37
+ Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic, or capability-scoring logic to `LocalRuntime`. `RelayConnection` owns remote WebSocket transport, `hello_ack` authentication, end-to-end `relay_probe`/`ready_ack` readiness, heartbeat liveness, reconnect backoff, outage logging, and a monotonically increasing in-memory session generation. Every relayed tool result is bound to the generation that delivered its call; a disconnected generation cannot send a late result over its replacement socket. Inbound WebSocket dispatch captures that generation at receive time and passes it to the runtime handler, so a successful local tool execution can publish only on the same generation. Stdio mode invokes `LocalRuntime` directly without that adapter.
38
38
 
39
39
  `daemon-process.mjs` owns workspace-daemon inspection and takeover. It distinguishes platform service state from the lock-owning Node process, validates PID and process-start identity, canonicalizes workspace/state paths before comparison, parses bounded process command lines without executing them, and sends `SIGTERM` only to a verified same-workspace `--daemon-only` process. POSIX daemons may ignore that signal and reach the bounded non-escalating timeout; Node's Windows signal mapping terminates the verified process directly. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
40
40
 
@@ -99,13 +99,13 @@ The stdio server implements newline-delimited JSON-RPC over stdin/stdout. It neg
99
99
  All requests for a deployed Worker route to one named Durable Object. It owns:
100
100
 
101
101
  - OAuth clients, authorization codes, hashed access-token records, an independently versioned hashed refresh-token store, and throttling metadata;
102
- - one active authenticated daemon WebSocket plus bounded candidate sockets;
102
+ - one active end-to-end-verified daemon WebSocket plus bounded candidate and probing sockets;
103
103
  - policy/tool metadata attached to the active socket;
104
104
  - a bounded in-memory map of pending daemon calls.
105
105
 
106
106
  `BridgeRoom` owns Durable Object routing, MCP dispatch, daemon WebSocket lifecycle, and pending relay calls. `OAuthController` owns OAuth-store pruning, registration throttling, authorization pages/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 for focused state-machine tests as well as bundled by Wrangler.
107
107
 
108
- The Worker verifies OAuth, validates MCP envelopes and optional protocol headers, converts `tools/call` into WebSocket messages, correlates cancellation by access-token hash and JSON-RPC ID, and formats text/structured/image results. It has no local filesystem or process API.
108
+ The Worker verifies OAuth, validates MCP envelopes and optional protocol headers, converts `tools/call` into WebSocket messages, correlates cancellation by access-token hash and JSON-RPC ID, and formats text/structured/image results. Pending calls also bind the incoming request `AbortSignal`: an HTTP client disconnect removes the pending indexes and sends a best-effort daemon cancellation. The deployment contract explicitly enables Cloudflare `enable_request_signal` and `request_signal_passthrough` so the signal reaches the named Durable Object. It has no local filesystem or process API.
109
109
 
110
110
  The primary OAuth store separates client registrations and named accounts from authorization codes and access-token records. A separate Durable Object key owns refresh-token records so refresh support can be added without rewriting the live primary-store schema. A `client_id` identifies an MCP application and redirect URIs; account records identify the authorized human or service identity. Codes, access tokens, and refresh tokens bind client ID, account ID, account version, role, scope, resource, token version, and expiration. Only hashes of bearer tokens are persisted, and every public-client refresh invalidates the presented refresh token while returning a replacement. The Worker intersects the role with the active daemon policy before advertising or relaying tools, and the local runtime validates the relayed role again. 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).
111
111
 
@@ -151,12 +151,13 @@ Remote OAuth binds each code, access token, and refresh token to a named Machine
151
151
  5. The Worker creates a five-minute code bound to client, redirect, resource, normalized scope, and PKCE challenge.
152
152
  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, and deployment token version, and atomically replaces the refresh token so replay returns `invalid_grant`.
153
153
  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.
154
- 8. `tools/list` is derived from the active daemon handshake; without a daemon, only `server_info` is advertised.
155
- 9. `tools/call` receives a random relay call ID and is bound to the current socket and authenticated client request key.
156
- 10. The runtime validates policy and arguments, executes the tool, and returns a bounded result.
157
- 11. The Durable Object accepts a result only from the socket that received that call.
158
- 12. A matching cancellation notification removes the pending call, tells the daemon to cancel, and terminates any ordinary child processes bound to it.
159
- 13. `start_job` is different: after durable acceptance, the detached runner is no longer bound to the relay call or socket. Later cancellation uses `cancel_job` or the local CLI.
154
+ 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.
155
+ 9. `tools/list` is derived only from the active end-to-end-verified daemon; without one, only `server_info` is advertised.
156
+ 10. `tools/call` receives a random relay call ID and is bound to the current daemon socket, authenticated client request key, incoming HTTP abort signal, and local relay-session generation.
157
+ 11. The runtime validates policy and arguments, executes the tool, and returns a bounded result.
158
+ 12. The Durable Object accepts a result only from the daemon socket that received that call, and the local runtime sends it only through the relay generation that delivered its call.
159
+ 13. A matching cancellation notification or incoming HTTP client disconnect removes the pending indexes, tells the daemon to cancel, and terminates ordinary child processes bound to the call. If local completion races with cancellation, the eventual result is discarded rather than treated as a user-visible delivery failure.
160
+ 14. `start_job` is different: after durable acceptance, the detached runner is no longer bound to the relay call or socket. Later cancellation uses `cancel_job` or the local CLI.
160
161
 
161
162
  Duplicate in-flight JSON-RPC IDs are rejected only within the same authenticated MCP session. The request key includes OAuth token identity, the HMAC-bound MCP session, JSON-RPC id type, and id value, so separate initialized clients may safely reuse the same numeric id.
162
163
 
@@ -220,13 +221,13 @@ Worker-name mutation is a separate identity transition. Existing state rejects a
220
221
 
221
222
  ## Daemon reconnect and replacement
222
223
 
223
- The local `RelayConnection` treats proxy selection, transport construction, transport open, authenticated readiness, and outage recovery as separate states. The shared proxy module maps WebSocket targets to standard HTTP(S) environment-proxy resolution, honors `NO_PROXY`, rejects non-HTTP(S) proxy schemes, and creates the proxy agent without exposing its URL or credentials. Invalid proxy configuration is a fatal configuration error rather than a retryable outage.
224
+ The local `RelayConnection` treats proxy selection, transport construction, WebSocket open, authentication, end-to-end readiness, and outage recovery as separate states. The shared proxy module maps WebSocket targets to standard HTTP(S) environment-proxy resolution, honors `NO_PROXY`, rejects non-HTTP(S) proxy schemes, and creates the proxy agent without exposing its URL or credentials. Invalid proxy configuration is a fatal configuration error rather than a retryable outage.
224
225
 
225
- The local `RelayConnection` otherwise treats transport construction, transport open, authenticated readiness, and outage recovery as separate states. A connection-attempt deadline terminates sockets stuck in `CONNECTING`; after WebSocket open it sends `hello` and reports readiness only after `hello_ack`. A missing acknowledgement reaches a distinct handshake deadline and the candidate is terminated. Once ready, application heartbeats require inbound activity; a silent half-open socket is terminated and reconnected. Outage reminders run on their own exponential-backoff timer rather than depending on another transport callback.
226
+ A connection-attempt deadline terminates sockets stuck in `CONNECTING`. After open, the daemon sends `hello`; `hello_ack` establishes an authenticated relay generation and starts heartbeats, but does not resolve startup or advertise readiness. The Worker then sends a random `relay_probe`. Its result must traverse the local runtime dispatcher and `sendForSession` on that generation before `ready_ack` marks the connection usable. The daemon rejects ordinary tool calls before that state and rejects a premature readiness acknowledgement without locally recorded probe delivery. Independent handshake and readiness deadlines terminate candidates that authenticate but cannot return results. Once ready, application heartbeats require inbound activity; a silent half-open socket is terminated and reconnected. Outage reminders run on their own exponential-backoff timer rather than depending on another transport callback.
226
227
 
227
228
  Reconnect uses bounded exponential backoff with jitter. Brief self-healing interruptions are debug-only. An unresolved outage is promoted to a rate-limited warning after a grace period, and recovery produces one summary. Raw close codes and reason strings remain debug-only.
228
229
 
229
- The Worker also treats a new connection as a bounded candidate until it authenticates and completes `hello`; a Durable Object alarm enforces the deadline across hibernation. Only a completed candidate replaces the old socket. Daemon messages must be valid JSON objects, duplicate `hello` and unknown message types are rejected with explicit protocol errors, and protocol violations close with standard WebSocket status codes. Pending calls retain their originating socket reference. A stale socket cannot complete or cancel replacement-socket calls. Closing a socket rejects only calls assigned to it.
230
+ 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, or incompatible 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. Pending calls retain their originating socket reference. A stale socket cannot complete or cancel replacement-socket calls. Closing a socket rejects only calls assigned to it. The local relay generation supplies the symmetric boundary: work accepted by an older authenticated connection cannot publish its result through a newer connection even when the call IDs are otherwise well formed.
230
231
 
231
232
  ## Persistence
232
233
 
@@ -246,7 +247,7 @@ Browser-origin handling separates CORS response sharing from protocol authentica
246
247
 
247
248
  ## Observability
248
249
 
249
- Public health exposes only server identity and version. Authenticated `server_info` exposes bounded runtime status, managed-job counts, resource alias names without paths or values, relay route state without endpoint details, and privacy-preserving capability-routing evidence. It separates the daemon capability ceiling from the authenticated account authority: `daemon.policy`/`daemon.tools` retain the pre-role ceiling, while `authorization.effective_policy`/`authorization.effective_tools` and the top-level `tools` report the role-intersected authority before any host-side filtering. It explicitly reports that the host-exposed subset is unknown to the server. `diagnose_runtime` runs fixed local probes and explicitly reports that its own request reached the daemon.
250
+ Public health exposes only server identity and version. Authenticated `server_info` exposes bounded runtime status, managed-job counts, resource alias names without paths or values, relay route state without endpoint details, authenticated/probing/ready socket counts, end-to-end readiness evidence, and privacy-preserving capability-routing evidence. It separates the daemon capability ceiling from the authenticated account authority: `daemon.policy`/`daemon.tools` retain the pre-role ceiling, while `authorization.effective_policy`/`authorization.effective_tools` and the top-level `tools` report the role-intersected authority before any host-side filtering. It explicitly reports that the host-exposed subset is unknown to the server. `diagnose_runtime` runs fixed local probes and explicitly reports that its own request reached the daemon.
250
251
 
251
252
  Foreground logging defaults to `info`; autostart uses `warn`. Authenticated readiness, persistent degradation, and recovery are user-visible state transitions. Brief relay interruptions, raw transport close details, retry timing, and all per-tool starts/successes/failures/cancellations/durations are debug-only. Unexpected local and Worker infrastructure errors are reduced to classes. Messages, strings, arrays, object depth/key counts, and serialized fields are bounded.
252
253
 
package/docs/AUDIT.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Engineering and security audit
2
2
 
3
+ ## 2026-07-17 version 1.2.5 end-to-end relay-readiness audit
4
+
5
+ The incident sequence disproved the previous meaning of “connected.” Version 1.2.3 correctly stopped treating every hibernation-restored `OPEN` WebSocket as live, and version 1.2.4 restored the missing local relay-session generation. Those changes repaired two concrete defects, but the startup contract still established readiness from `hello_ack` and heartbeat traffic. Both signals exercise transport authentication and one-way control messages; neither proves that an inbound request can traverse the local dispatcher and return on the same authenticated relay generation. A regression in that return path could therefore keep heartbeats healthy while every user tool timed out.
6
+
7
+ Version 1.2.5 replaces that proxy signal with direct evidence. After `hello`, the Worker stores the socket as `probing`, returns `hello_ack`, and sends a random `relay_probe`. The local runtime validates the probe and returns `relay_probe_result` through `deliverRelayToolResult` and `sendForSession`, using the exact session-generation boundary that protects real tool results. Only then does the Worker promote the socket to `daemon`, send `ready_ack`, advertise tools, and allow the local startup promise to resolve. The local side independently records successful probe-result delivery; it rejects `ready_ack` before that evidence and rejects any ordinary `tool_call` while the relay is authenticated but not ready. The probe identifier is random, bounded, short-lived attachment metadata; it contains no tool arguments, paths, commands, output, account data, or credentials.
8
+
9
+ Replacement is now fail-safe rather than eager. The current verified daemon remains authoritative while a candidate authenticates and completes the probe. Malformed or mismatched results close the candidate with a protocol error; missing results reach an independent readiness deadline; neither path rejects calls assigned to the incumbent or changes its advertised policy. Only a verified candidate closes the old socket. Durable Object alarms independently enforce candidate hello, probing readiness, and steady-state liveness deadlines across hibernation. `server_info` distinguishes current authenticated, probing, and ready socket counts and reports `readiness_verified`; control-plane consumers no longer need to infer service health from cumulative socket counters.
10
+
11
+ The review also found a lower-severity routing defect in the user's pending skill rename. The ranking algorithm gave short generic identity words such as `web` the same strong weight as a distinctive capability name, so unrelated browser work could cross the selection threshold. Removing only that weight exposed a hidden dependence on the same generic token for Chinese research requests. The correction suppresses strong identity credit for generic words and adds an explicit Chinese research/search semantic mapping. Positive and negative regressions now encode both sides of the decision boundary.
12
+
13
+ Architecture review extracted daemon attachment mutation from the Worker composition root into `DaemonSocketRegistry`, added an independent line cap and responsibility assertions, removed a duplicate HTTP method guard, and made inbound liveness updates await alarm rescheduling rather than leaving an unobserved promise. Fault injection covers no-ready-ack reconnect, invalid replacement probes, incumbent preservation, verified handover, session-bound probe return, and current observability. The complete package gate, Worker dry run, dependency/signature/privacy checks, and cross-platform CI remain the release boundary. This source change does not deploy the Worker, replace the running daemon, rotate credentials, publish npm, create a tag, or create a GitHub Release; a live installation remains on its old behavior until Worker, daemon, and extension converge on 1.2.5.
14
+
15
+ ## 2026-07-17 version 1.2.2 relay-lifecycle and logging audit
16
+
17
+ The reported warning was the visible symptom of a call-lifecycle mismatch rather than a failed local tool. The Worker could remove a pending call after MCP cancellation, timeout, daemon disconnect, or client abandonment, while the local runtime still completed its asynchronous operation and entered one unconditional result-delivery path. When the original WebSocket had closed, the daemon emitted one `relay.tool_result.delivery_failed` warning per late completion. More seriously, if a replacement relay had already authenticated, the old result could be written to the new socket; the Worker rejected it because its pending record was socket-bound, but the local transport did not enforce the symmetric session boundary.
18
+
19
+ The correction gives each authenticated local relay connection a monotonically increasing in-memory generation and captures that generation with every incoming call. A result may be sent only through the generation that delivered the call. Explicit cancellation and relay disconnect also mark the eventual local result for disposal. Expected cancellation/disconnect/reconnect races are therefore debug-only `relay.tool_result.discarded` events, while a synchronous send failure on the still-current socket continues to invalidate the ambiguous transport and enter the ordinary outage state machine.
20
+
21
+ A second defect existed one layer earlier. The Worker already handled MCP `notifications/cancelled`, but an HTTP client that disconnected without sending that notification could leave the pending internal ID and session request-key index until the tool timeout. The pending-call registry now owns AbortSignal registration and cleanup atomically with its timer and indexes. The Worker deployment explicitly enables Cloudflare `enable_request_signal` and `request_signal_passthrough`, which are both required because the public Worker forwards the request into a named Durable Object. A deterministic registry test covers active and already-aborted requests; the complete local workerd suite continues to cover OAuth, explicit MCP cancellation, WebSocket routing, timeout, and socket loss.
22
+
23
+ Logging was reviewed as a user interface rather than a serialization dump. Human mode now uses the supplied natural-language explanation and omits the redundant stable event identifier; newline-delimited JSON retains that identifier for ingestion. Tool starts, outcomes, cancellation, timing, and late-result disposal remain debug-only. Worker observability adds an `unmatched_results` counter for a result that arrives after its pending record has been removed, preserving anomaly evidence without storing tool arguments, command text, paths, or result content.
24
+
25
+ The broader pass rechecked architecture boundaries, cancellation and reconnect branches, release/configuration contracts, privacy redaction, package version convergence, TypeScript and checked-JavaScript contracts, lint, Worker integration, and documentation drift. An additional deployment defect was found during fault reproduction: request cancellation support was implemented in code but the required Cloudflare compatibility flags were absent, so the signal could not reach the Durable Object in production. Architecture tests now make both flags non-optional. Version 1.2.2 retains local state schema 6 and policy revision 5; normal startup must converge the Worker and daemon versions, and the unpacked browser extension must be reloaded. No live deployment, credential rotation, global installation, daemon replacement, npm publication, tag, or GitHub Release is performed by this source change.
26
+
3
27
  ## 2026-07-16 version 1.2.1 fail-closed input-contract audit
4
28
 
5
29
  The review began from clean `v1.2.0` with the complete repository gate, dependency audit, Worker dry run, and package checks passing. Hostile-input reproduction nevertheless found one shared JavaScript failure mechanism outside those gates: ordinary objects were being used as external string-key maps. Inherited names such as `constructor` and `__proto__` were therefore accepted as account roles, policy profiles, top-level commands, resource actions, and keyboard-table entries. The CLI variants could exit successfully without performing a command; a Worker account could persist an invalid role and later fail during authority calculation. Adjacent review found the same semantic hazard in form-field aggregation and valid local-resource names.
@@ -53,7 +53,7 @@ Rules:
53
53
  - Pure classification and normalization functions are exported and tested directly when practical.
54
54
  - Adapters may translate data but should not duplicate policy or schemas.
55
55
  - 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.
56
- - State transitions are explicit; readiness is not inferred from a lower-level event. For example, an open WebSocket is not an authenticated relay until `hello_ack` is received.
56
+ - 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.
57
57
  - Every externally controlled input is bounded before expensive allocation, traversal, parsing, storage, or execution.
58
58
  - 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.
59
59
  - Repository text must not contain invisible ASCII controls other than tab, CR, and LF; architecture tests enforce this even when JavaScript syntax remains valid.
package/docs/LOGGING.md CHANGED
@@ -26,6 +26,8 @@ The CLI accepts:
26
26
 
27
27
  Foreground mode defaults to `info` and human-readable text. Platform autostart services use `warn` with newline-delimited JSON. Foreground operators can select JSON explicitly with `--log-format json`.
28
28
 
29
+ Human mode treats the message as the primary interface: it uses a natural-language explanation and includes only bounded diagnostic fields that add meaning. It does not repeat the machine event key. JSON mode retains the stable `event` field for ingestion and correlation. Event identifiers such as `relay.tool_result.discarded` are implementation contracts for structured logs, not text that should be shown as the warning itself.
30
+
29
31
  | Level | Intended events |
30
32
  |---|---|
31
33
  | `error` | The operation or service cannot continue without intervention |
@@ -43,7 +45,7 @@ Autostart installation and daemon startup may report the names of allowlisted pr
43
45
 
44
46
  ## Relay connection event policy
45
47
 
46
- A TCP/WebSocket `open` event is only transport availability. The daemon is reported as connected only after the Worker returns `hello_ack` for the authenticated `hello` message.
48
+ A TCP/WebSocket `open` event is only transport availability, and `hello_ack` proves only authenticated bidirectional control traffic. The daemon is reported as ready only after a random Worker probe returns through the ordinary local dispatcher and session-bound result path and the Worker sends `ready_ack`.
47
49
 
48
50
  Brief network interruptions are expected on laptop network changes, Worker deployment, proxy rotation, and ordinary internet transport. They are handled as follows:
49
51
 
@@ -55,10 +57,10 @@ Brief network interruptions are expected on laptop network changes, Worker deplo
55
57
  - while the outage remains unresolved, reminders are scheduled independently of reconnect callbacks and use exponential backoff capped at 15 minutes;
56
58
  - a WebSocket that remains in `CONNECTING` beyond its deadline is terminated so one stalled network attempt cannot freeze the reconnect loop;
57
59
  - relay identity/version mismatch, authentication rejection, and unexpected protocol messages are non-transient: they produce an immediate actionable error and terminate that daemon instead of entering the reconnect loop;
58
- - a Worker `daemon_hello_timeout` error is transient and follows the normal reconnect path rather than being misclassified as authentication rejection;
60
+ - Worker `daemon_hello_timeout` and `daemon_ready_timeout` errors are transient and follow the normal reconnect path rather than being misclassified as authentication rejection;
59
61
  - recovery after a visible outage produces one information summary with a human-readable duration and attempt count; exact seconds and error classes remain debug-only;
60
- - an authenticated replacement is a distinct warning and permanently stops the older daemon;
61
- - failure to receive `hello_ack` within the handshake deadline terminates the candidate socket and retries;
62
+ - a verified replacement is a distinct warning and permanently stops the older daemon;
63
+ - failure to receive `hello_ack` within the handshake deadline, or `ready_ack` within the independent end-to-end readiness deadline, terminates the candidate socket and retries;
62
64
  - lack of inbound heartbeat activity terminates a half-open socket and reconnects.
63
65
 
64
66
  A WebSocket close code such as `1006` means the transport ended without a normal close handshake. It is useful for debug diagnosis but not useful as the default user message. Default logs therefore describe the effect and recovery behavior rather than printing `{"code":1006,"reason":""}`.
@@ -66,16 +68,18 @@ A WebSocket close code such as `1006` means the transport ended without a normal
66
68
  Examples:
67
69
 
68
70
  ```text
69
- [info] daemon: remote relay connected
71
+ [info] daemon: remote relay connected and end-to-end result delivery verified
70
72
  [warn] daemon: remote relay unavailable for 12 seconds; reconnecting automatically (3 reconnect attempts; connection interrupted).
71
73
  [info] daemon: remote relay connection restored after 18 seconds (4 reconnect attempts)
72
74
  ```
73
75
 
74
76
  With `--verbose`, the same incident additionally includes bounded structured fields such as exact seconds, coarse error class, retry delay, and transport close diagnostics.
75
77
 
76
- ## Tool events
78
+ ## Tool and result-delivery events
79
+
80
+ All per-tool starts, successes, failures, cancellations, timing, and expected late-result disposal are debug-only. The MCP response already reports the outcome to the caller; duplicating routine tool traffic at default levels creates noise and can reveal activity patterns.
77
81
 
78
- All per-tool starts, successes, failures, cancellations, and timing are debug-only. The MCP response already reports the outcome to the caller; duplicating routine tool traffic at default levels creates noise and can reveal activity patterns.
82
+ A completed local result is bound to the authenticated relay session that delivered its call. If the caller cancelled, the relay disconnected, or a replacement relay session became active, the result is discarded rather than sent to a different session. This is a normal terminal race and does not emit a warning. Debug output records only a shortened call ID and a coarse reason such as `caller_cancelled`, `relay_disconnected`, or `session_ended`. A synchronous send failure on the still-current socket is different: the socket is invalidated because delivery is ambiguous, but the per-call detail remains debug-only while the normal relay-outage state machine decides whether a user-visible warning is warranted.
79
83
 
80
84
  Debug per-tool fields may include tool name, duration, coarse outcome class, and a shortened random call identifier. The identifier is for correlating adjacent local events and is not a stable audit identifier.
81
85
 
@@ -113,7 +117,7 @@ This is defense in depth, not content classification. Unknown, split, transforme
113
117
 
114
118
  The local execution middleware emits bounded events such as `tool.call.started`, `tool.call.completed`, `tool.call.failed`, `tool.call.slow`, and `tool.call.cancel_requested`. Stable fields include a shortened call ID, tool name, origin, duration, error code, and retryability. The Worker emits JSON events for HTTP failures and daemon socket errors. Structured values still pass through field-name and value redaction; JSON format is not permission to log arguments or results.
115
119
 
116
- `server_info` is the operational metrics surface. Local metrics include lifecycle state, active and maximum calls, oldest-call age, active-process ownership, per-tool duration buckets, and error-code counts. Worker metrics include HTTP status classes, pending internal/request-key indexes, per-tool outcomes, daemon candidate/authenticated/disconnected counts, and protocol-error counts. Metrics contain counts and bounded identifiers, not request arguments or result contents.
120
+ `server_info` is the operational metrics surface. Local metrics include lifecycle state, active and maximum calls, oldest-call age, active-process ownership, per-tool duration buckets, and error-code counts. Worker metrics include HTTP status classes, pending internal/request-key indexes, per-tool outcomes, daemon candidate/authenticated/ready/disconnected event counts, current authenticated/probing/ready socket counts, and protocol-error counts. Metrics contain counts and bounded identifiers, not request arguments or result contents.
117
121
 
118
122
  ## Files
119
123
 
@@ -34,6 +34,7 @@ Run these commands from the same environment used for startup so `HTTPS_PROXY`,
34
34
  |---|---|
35
35
  | `authorization.effective_policy.profile` is `full` and the tool is in `authorization.effective_tools`, but the current session UI exposes fewer tools | Host/connector post-relay filtering; Machine Bridge cannot enumerate or override that subset |
36
36
  | `daemon.policy.profile` is `full` but `authorization.effective_policy.profile` is `review`, `edit`, or `agent` | Expected account-role narrowing; the daemon field is only a capability ceiling and must not be reported as the account permission |
37
+ | `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 |
37
38
  | `capability_routing.bootstrap_observed` is false | The current local runtime has not received `session_bootstrap`; reconnect or inspect host initialization handling |
38
39
  | `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 |
39
40
  | Task resolution ran but all match counts are zero | The resolver ran successfully but found no sufficiently relevant local skill, command, or application |
@@ -50,11 +51,13 @@ A successful diagnostic result applies only to that probe. An MCP host can still
50
51
 
51
52
  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.
52
53
 
53
- `server_info.worker.pending_calls` reports `active`, `request_keys`, `maximum`, `oldest_ms`, and `by_tool`. A nonzero `active` count means work is in flight, not that the bridge is locked. Calls for simple reads and probes should continue while another independent process call runs. A value that remains after the underlying local task completed indicates a transport-delivery defect; version 1 interrupts and reconnects the ambiguous daemon socket so Worker cleanup releases those records. Refreshing a chat page is not the recovery mechanism and should not be required.
54
+ `server_info.worker.pending_calls` reports `active`, `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. Calls for simple reads and probes should continue while another independent process call runs. Explicit MCP cancellation, an incoming HTTP client disconnect, timeout, and daemon-socket closure remove the pending record and its session request key. Local results are bound to the authenticated relay session that originated the call, so a result that finishes after cancellation or reconnection is discarded instead of being sent over a replacement socket. Refreshing a chat page is not the recovery mechanism and should not be required.
55
+
56
+ `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.
54
57
 
55
58
  ### Relay interruption messages
56
59
 
57
- A brief relay interruption is retried automatically and is visible only with `--verbose`. Default logs do not print raw WebSocket values such as `code=1006` with an empty reason. If a transient outage persists for 10 seconds, the daemon emits a readable duration/cause/reconnect summary; later reminders use autonomous exponential backoff capped at 15 minutes, and recovery produces one readable summary. Each transport connection attempt also has a deadline, so a socket stuck in `CONNECTING` cannot freeze retries. Identity/version mismatch, authentication rejection, and unexpected protocol messages are not retried as ordinary network faults: the daemon emits an immediate actionable error and exits, requiring upgrade/redeployment or credential repair. A Worker-side daemon handshake timeout remains retryable.
60
+ A brief relay interruption is retried automatically and is visible only with `--verbose`. Default logs do not print raw WebSocket values such as `code=1006` with an empty reason. If a transient outage persists for 10 seconds, the daemon emits a readable duration/cause/reconnect summary; later reminders use autonomous exponential backoff capped at 15 minutes, and recovery produces one readable summary. Each transport connection attempt also has a deadline, so a socket stuck in `CONNECTING` cannot freeze retries. Identity/version mismatch, authentication rejection, and unexpected protocol messages are not retried as ordinary network faults: the daemon emits an immediate actionable error and exits, requiring upgrade/redeployment or credential repair. Worker-side hello and end-to-end readiness timeouts remain retryable. Authentication is not reported as usable service readiness until a session-bound probe result returns.
58
61
 
59
62
  Use `--verbose` only when close codes, close reasons, heartbeat timeouts, and retry delays are needed for diagnosis. A close code of 1006 means the transport ended without a normal close handshake; it does not by itself identify the cause.
60
63
 
@@ -144,7 +147,7 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
144
147
 
145
148
  ### Lifecycle and pending-call diagnosis
146
149
 
147
- `server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `runtime.observability.in_flight_calls` and `runtime.observability.processes` distinguish a blocked call from a surviving process. Worker `server_info.worker.pending_calls` reports both the internal-call index and client request-key index; they must return to zero after a terminal result, cancellation, timeout, send failure, or daemon disconnect. Nonzero request-key counts after active calls reach zero indicate a lifecycle defect rather than normal load.
150
+ `server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `runtime.observability.in_flight_calls` and `runtime.observability.processes` distinguish a blocked call from a surviving process. Worker `server_info.worker.pending_calls` reports both the internal-call index and client request-key index; they must return to zero after a terminal result, explicit cancellation, client disconnect, timeout, send failure, or daemon disconnect. 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.
148
151
 
149
152
  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.
150
153
 
@@ -161,7 +164,7 @@ Logging is level-based:
161
164
  error unrecoverable local/transport/service failures
162
165
  warn persistent retryable relay degradation, supersession, and service problems
163
166
  info startup/deploy/connect transitions
164
- debug all per-tool starts/successes/failures/cancellations/timing, correlation and reconnect details
167
+ debug all per-tool starts/successes/failures/cancellations/timing, late-result disposal, correlation and reconnect details
165
168
  ```
166
169
 
167
170
  Foreground mode defaults to `info`; autostart uses `warn`. Use `--verbose` or `--log-level debug` only for diagnosis. `--quiet` is an alias for `--log-level error`.
@@ -228,7 +231,9 @@ Claude and Copilot Studio call the public Worker from their cloud connectivity l
228
231
 
229
232
  ## Reconnect and replacement
230
233
 
231
- The daemon sends heartbeats and reconnects with bounded exponential backoff and jitter. A new socket remains a candidate until it authenticates and sends a valid `hello`; only then does it replace the previous daemon.
234
+ The daemon sends heartbeats and reconnects with bounded exponential backoff and jitter. A new socket progresses through candidate, authenticated/probing, and ready states. After `hello_ack`, the Worker sends a random probe that must return through the exact local dispatcher and relay-session result path used by real calls. Only the matching result produces `ready_ack`, `daemon.connected=true`, and tool advertisement. A healthy incumbent remains active until that proof succeeds, so a bad replacement cannot create an avoidable outage.
235
+
236
+ The Worker tracks inbound `lastSeenAt` and reclaims ready or probing sockets that go silent past their applicable liveness/readiness windows. Diagnose with `daemon.readiness_verified`, `worker.sockets_live.ready`, `worker.sockets_live.probing`, and `daemon.last_seen_at`; cumulative `worker.observability.sockets.authenticated` is an event counter, not current liveness. A state with no ready daemon is unavailable by construction rather than falsely online. Restart the daemon only after inspecting the classified timeout/protocol error; refreshing an MCP client does not repair the local transport.
232
237
 
233
238
  Pending calls are bound to the socket that received them. Results from another socket are ignored. A lost or replaced socket rejects only its own pending calls and terminates locally tracked child process trees. Process sessions are in-memory and do not survive daemon restart or replacement.
234
239
 
package/docs/TESTING.md CHANGED
@@ -49,7 +49,7 @@ The suite includes:
49
49
  - guarded state-root removal, unsafe state-root/workspace overlap rejection before creation, all-profile lock/daemon scanning, strict current-schema validation, corrupt-JSON isolation, and policy-origin persistence;
50
50
  - no filename-based sensitive-file denial under unrestricted policy;
51
51
  - log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, service warning-level configuration, current-schema reset, and bounded tail trimming;
52
- - deterministic relay connection lifecycle coverage for transport construction/error/deadline, pre-handshake `welcome` validation, authenticated `hello_ack` readiness, identity/version mismatch, retryable Worker handshake errors, fatal protocol errors, autonomous outage-reminder backoff, handshake and heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
52
+ - deterministic relay connection lifecycle coverage for transport construction/error/deadline, pre-handshake `welcome` validation, separate `hello_ack` authentication and `ready_ack` end-to-end readiness, session-bound probe return, pre-ready tool rejection, premature-ready rejection, identity/version mismatch, retryable Worker hello/readiness errors, fatal protocol errors, autonomous outage-reminder backoff, handshake/readiness/heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
53
53
  - shared no-follow bounded-file reads for normal files, over-limit data, directories, and symbolic links;
54
54
  - owner-only directory enforcement rejecting final symlinks, failing closed on POSIX chmod errors, verifying `0700`, and retaining Windows portability; Worker temporary-secret lifecycle coverage for process-start-bound names, valid stale-owner reclamation, ambiguous-owner retention, `0600` mode, deletion failures, and simultaneous deployment/cleanup failures;
55
55
  - SARIF security-gate behavior for unknown findings, exact accepted rule/path matches, path mismatch rejection, rationale quality, and exception expiry;
@@ -60,7 +60,7 @@ The suite includes:
60
60
  - independently injected service CLI status/install/start/stop/uninstall/remove paths, including provider failure, no selected workspace, no deployed Worker, aliases, and default output/exit adapters;
61
61
  - CLI parsing, policy profiles, and client configuration boundaries;
62
62
  - live stdio MCP initialization with session instructions, capability resolution, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
63
- - live local Worker OAuth registration, the unauthenticated `resource_metadata` challenge, protected-resource and authorization-server discovery, Streamable transport metadata, consent, URL-constructed `303` callbacks including the ChatGPT and hosted Claude redirect URIs with encoded state, PKCE, `offline_access`, form-encoded authorization-code and refresh-token exchanges, access/refresh rotation, stale refresh replay rejection, account-version refresh revocation, authorization-code replay rejection, pending-registration throttling that excludes already authorized DCR clients, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, exact per-request redirect-origin CSP with narrowly scoped Microsoft regional-consent and final Copilot Studio handoff exceptions, accessible credential-error rendering, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure. The metadata/refresh contract is the path used by Claude DCR and Copilot Studio Dynamic discovery. The same integration runs an `editor` account against a canonical `full` daemon and proves that `server_info` and remote `project_overview` report effective `edit` authority while retaining the full daemon ceiling only in explicitly scoped fields.
63
+ - live local Worker OAuth registration, the unauthenticated `resource_metadata` challenge, protected-resource and authorization-server discovery, Streamable transport metadata, consent, URL-constructed `303` callbacks including the ChatGPT and hosted Claude redirect URIs with encoded state, PKCE, `offline_access`, form-encoded authorization-code and refresh-token exchanges, access/refresh rotation, stale refresh replay rejection, account-version refresh revocation, authorization-code replay rejection, pending-registration throttling that excludes already authorized DCR clients, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, exact per-request redirect-origin CSP with narrowly scoped Microsoft regional-consent and final Copilot Studio handoff exceptions, accessible credential-error rendering, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, candidate/probing/ready transitions, invalid readiness-result rejection, incumbent preservation until verified handover, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure. The metadata/refresh contract is the path used by Claude DCR and Copilot Studio Dynamic discovery. The same integration runs an `editor` account against a canonical `full` daemon and proves that `server_info` and remote `project_overview` report effective `edit` authority while retaining the full daemon ceiling only in explicitly scoped fields.
64
64
  - local runtime proof that one blocked tool handler does not serialize an independent handler, plus relay fault injection proving an undeliverable terminal result interrupts the ambiguous socket and enters reconnect backoff.
65
65
  - a real headless-Chrome OAuth navigation regression with four cases: `form-action 'self'` blocks the first cross-origin callback, allowing only the registered callback blocks the regional redirect, allowing the registered and regional callbacks blocks the final Copilot Studio redirect, and the complete policy preserves `code` and `state` through all three cross-origin hops. Linux CI fails if Chrome is unavailable; other environments skip only this browser executable check while retaining the Worker CSP assertions.
66
66
 
@@ -108,7 +108,7 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows using the pi
108
108
  - Every permission-expanding feature needs a denial test. Browser/application features also require a token/value/content non-disclosure assertion.
109
109
  - Every bounded resource needs an over-limit test.
110
110
  - Every multi-stage mutation needs a no-partial-commit test.
111
- - Every remote call correlation change needs daemon replacement and cancellation coverage.
111
+ - Every remote call correlation or readiness change needs daemon replacement, incumbent-preservation, timeout, malformed-result, and cancellation coverage.
112
112
  - Every durable workflow needs disconnect, cancellation, cleanup-failure, dead-runner, and plan-scrubbing coverage.
113
113
  - Secret-bearing resource tests must assert absence of raw, path, base64, and hex forms from MCP-visible results.
114
114
  - Logs and public metadata should be tested for absence of sensitive fields, arguments, outputs, and routine success noise—not only presence of expected fields.
package/docs/UPGRADING.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  Machine Bridge does not retain parallel implementations for obsolete MCP protocol dates, policy revisions, state schemas, lock formats, or browser-extension protocols. The supported path is a direct upgrade from the immediately preceding published package while its state already uses the current schema.
6
6
 
7
- Version 1.2.1 keeps local state schema version 6 and policy revision 5 unchanged. Existing 1.2.0 workspaces, named accounts, resource registrations, managed-job history, Worker identity, and browser pairing state are reused without a schema conversion. The package change affects validation and adapter ownership, not the stored authority model.
7
+ Version 1.2.5 keeps local state schema version 6 and policy revision 5 unchanged. Existing 1.2.4 workspaces, named accounts, resource registrations, managed-job history, Worker identity, and browser pairing state are reused without a schema conversion. The package change replaces relay startup/replacement semantics and capability ranking; it does not alter stored authority or credential records.
8
8
 
9
- Version 1.2.0 could accept prototype-shaped account roles through malformed administration input. On the first 1.2.1 Worker access, such an account is preserved for recovery but repaired fail-closed: its role becomes `reviewer`, it is disabled, its account version advances, and its authorization codes and tokens are removed. An operator can then assign a valid role, enable the account, and rotate its password through the normal account administration flow. A local policy record with an unknown profile label is normalized to `custom` while retaining its explicit capability fields; an invalid explicit `--profile` is rejected.
9
+ Version 1.2.5 requires the Worker, local daemon, and browser extension to converge on the same package version. The new Worker expects candidate probing ready negotiation and the new daemon waits for `ready_ack`; mixed 1.2.4/1.2.5 components therefore fail closed rather than falling back to the old false-ready handshake. Normal startup redeploys the versioned Worker when required and restarts only a verified same-workspace daemon. Reload the unpacked extension after convergence even though this release does not change its browser protocol.
10
+
11
+ Version 1.2.0 could accept prototype-shaped account roles through malformed administration input. On the first 1.2.1 or later Worker access, such an account is preserved for recovery but repaired fail-closed: its role becomes `reviewer`, it is disabled, its account version advances, and its authorization codes and tokens are removed. An operator can then assign a valid role, enable the account, and rotate its password through the normal account administration flow. A local policy record with an unknown profile label is normalized to `custom` while retaining its explicit capability fields; an invalid explicit `--profile` is rejected.
10
12
 
11
13
  A state file from an older unsupported schema is rejected rather than guessed or silently rewritten. Upgrade an old installation through the last release that understands its schema, or initialize a new workspace and re-register resources. Do not edit schema numbers by hand.
12
14
 
@@ -27,6 +29,6 @@ Machine Bridge never treats an unreadable or foreign-schema state file as empty
27
29
 
28
30
  ## Rollback
29
31
 
30
- Rollback is supported only when the older package understands every persisted schema and protocol already written by the newer package. Version 1.2.1 does not advance local state or policy schemas, so rollback to 1.2.0 remains structurally possible. A repaired malformed account remains a valid disabled reviewer record that 1.2.0 can read. The preferred recovery is still to fix forward because the browser extension and deployed Worker must match the running package exactly, and rollback would restore the defective input validation.
32
+ Rollback is supported only when the older package understands every persisted schema and protocol already written by the newer package. Version 1.2.5 does not advance local state or policy schemas, so a complete rollback to 1.2.4 is structurally possible. The Worker, daemon, and extension must be rolled back together; a 1.2.5 daemon cannot negotiate the old handshake safely, and restoring only one component recreates an unavailable mixed-version system. The preferred recovery is still to fix forward because 1.2.4 can report authenticated transport before proving the result path.
31
33
 
32
34
  Never roll back by copying only selected state files or changing version fields. Restore one complete verified state backup, package version, Worker build, and browser extension as a single operational unit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "1.2.1",
3
+ "version": "1.2.5",
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",
@@ -100,12 +100,18 @@ export class CallRegistry {
100
100
  return true;
101
101
  }
102
102
 
103
+ /** @param {unknown} origin */
104
+ idsByOrigin(origin) {
105
+ const expected = String(origin || "");
106
+ return [...this.calls.values()]
107
+ .filter((record) => record.origin === expected)
108
+ .map((record) => record.id);
109
+ }
110
+
103
111
  /** @param {unknown} origin @param {unknown} [reason] */
104
112
  cancelOrigin(origin, reason = "transport disconnected") {
105
- const expected = String(origin || "");
106
113
  let cancelled = 0;
107
- for (const [id, record] of this.calls) {
108
- if (record.origin !== expected) continue;
114
+ for (const id of this.idsByOrigin(origin)) {
109
115
  if (this.cancel(id, reason)) cancelled += 1;
110
116
  }
111
117
  return cancelled;
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
  const TOKEN_STOP_WORDS = new Set(["the", "and", "for", "with", "from", "this", "that", "use", "using", "into", "of", "to", "a", "an", "in", "on", "is", "are", "or", "及", "和", "的", "了", "在", "用", "使用", "进行", "根据"]);
3
+ const GENERIC_IDENTITY_TOKENS = new Set(["app", "application", "cli", "local", "tool", "web"]);
3
4
  const ENGLISH_TOKEN_CANONICAL = new Map([
4
5
  ["created", "create"], ["creating", "create"], ["creation", "create"], ["creator", "create"],
5
6
  ["improved", "improve"], ["improving", "improve"], ["improvement", "improve"],
@@ -17,7 +18,7 @@ const HAN_TOKEN_ALIASES = Object.freeze([
17
18
  [/技能/u, ["skill"]],
18
19
  [/安装/u, ["install", "installer"]],
19
20
  [/部署/u, ["deploy", "deployment"]],
20
- [/查找|搜索|检索/u, ["search", "find"]],
21
+ [/查找|搜索|检索|调研|研究/u, ["search", "find", "research"]],
21
22
  [/最新|当前/u, ["latest", "current"]],
22
23
  [/官方/u, ["official"]],
23
24
  [/文档|资料/u, ["docs", "documentation"]],
@@ -47,7 +48,7 @@ export function relevanceScore(task, candidate, identity = "") {
47
48
  let score = 0;
48
49
  for (const token of taskTokens) {
49
50
  if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
50
- if (identityTokens.has(token)) score += token.length >= 6 ? 4 : 3;
51
+ if (identityTokens.has(token) && !GENERIC_IDENTITY_TOKENS.has(token)) score += token.length >= 6 ? 4 : 3;
51
52
  }
52
53
  const taskComparable = comparableText(task);
53
54
  const candidateComparable = comparableText(candidate);
package/src/local/log.mjs CHANGED
@@ -55,7 +55,7 @@ export function createLogger(options = {}) {
55
55
  const normalizedLevel = normalizeEventLevel(level);
56
56
  const eventName = sanitizeEventName(name);
57
57
  const payload = { event: eventName, ...fields };
58
- const humanMessage = message || eventName;
58
+ const humanMessage = message || humanizeEventName(eventName);
59
59
  if (options.format === "json") {
60
60
  if (LEVEL_RANK[normalizedLevel] < LEVEL_RANK[minimumLevel]) return;
61
61
  const entry = sanitizeLogValue({
@@ -77,7 +77,7 @@ export function createLogger(options = {}) {
77
77
  error: [stderr, "[error]", COLORS.red],
78
78
  };
79
79
  const [stream, label, color] = methods[normalizedLevel];
80
- write(stream, normalizedLevel, label, color, humanMessage, payload);
80
+ write(stream, normalizedLevel, label, color, humanMessage, fields);
81
81
  };
82
82
 
83
83
  return {
@@ -189,6 +189,14 @@ function normalizeEventLevel(value) {
189
189
  return Object.prototype.hasOwnProperty.call(LEVEL_RANK, level) ? level : "info";
190
190
  }
191
191
 
192
+ function humanizeEventName(value) {
193
+ const words = String(value || "event")
194
+ .replace(/[._-]+/g, " ")
195
+ .replace(/\s+/g, " ")
196
+ .trim();
197
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Event";
198
+ }
199
+
192
200
  function sanitizeEventName(value) {
193
201
  const name = String(value || "event").toLowerCase().replace(/[^a-z0-9._-]/g, "_").slice(0, 128);
194
202
  return name || "event";