machine-bridge-mcp 3.0.0-beta.190 → 3.0.0-beta.192

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,10 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 3.0.0-beta.190 - 2026-09-13
3
+ ## 3.0.0-beta.192 - 2026-09-16
4
4
 
5
- - Reuse generation-bound V8 coverage collected during the authoritative full verification plan so the coverage gate only executes fixtures whose evidence is still missing; standalone coverage verification remains self-contained.
6
- - Make GitHub release publication consume exact-main provider CI instead of repeating a local dependency install and full verification after merge, while retaining synchronized-version checks, accepted-candidate byte and promotion-digest revalidation, exact-main revalidation before remote mutation, and uploaded-asset digest verification.
7
- - Increase only the Windows runtime self-test success-fixture budget for loaded hosted CI; production process and resource-admission deadlines remain unchanged.
5
+ - Replace the shipped current-audit document's stale release chronology with version-agnostic security, continuity, privacy, and residual-limit invariants; repository hygiene now rejects numbered prerelease chronology and mutable acceptance/candidate authority claims.
6
+ - Keep WebSocket daemon handshake diagnostics aligned with the resilient relay wrapper so Worker `server_info` preserves the same bounded per-outage HTTPS fallback takeover attribution as HTTPS descriptors and local diagnostics; private relay values remain excluded, and execution/replay semantics are unchanged.
7
+ - Keep durable managed-job execution available across in-place Node package-manager upgrades by falling back from a removed daemon `process.execPath` only to the still-executable absolute Node launcher that originally started the daemon; diagnostics expose provenance/availability without local paths.
8
+ - Make macOS sleep diagnosis retain actual timestamped `Sleep` records with a bounded 15-second power-log probe, avoiding the previous five-second timeout and broad filter that could hide evidence explaining relay suspension.
9
+ - Add regressions for stale runtime launcher recovery and sleep-probe boundaries; relay authentication, replay, duplicate-side-effect prevention, and reconnect policy are unchanged.
8
10
 
9
11
  ## Historical releases
10
12
 
@@ -30,6 +30,6 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "3.0.0-beta.190",
33
+ "version_name": "3.0.0-beta.192",
34
34
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
35
35
  }
package/docs/AUDIT.md CHANGED
@@ -1,35 +1,31 @@
1
1
  # Current audit status
2
2
 
3
- This file is the current audit summary. Historical findings, closed incidents, and release-by-release evidence are retained by Git tags and repository history rather than repeated in the default maintenance path. For a historical snapshot, inspect this file at the corresponding version tag.
3
+ This file is a compact statement of current cross-cutting audit conclusions and residual limits. It is intentionally version-agnostic: release identity, candidate acceptance, publication state, and soak state are authoritative only in package metadata, release records, Git history/tags, and the external release systems they describe. This file is not a release-state authority.
4
4
 
5
5
  ## Current conclusions
6
6
 
7
- - MCP execution is current-protocol-only. Remote HTTP and stdio support MCP `2026-07-28`; removed initialization/session/replay requests fail closed and cannot dispatch old behavior.
8
- - Browser computer observation requires the current atomic observation capability, and desktop visual-point decisions require the formal capability interface. Runtime method-presence fallbacks are not accepted as compatibility.
9
- - Durable managed-job recovery, idempotent submission, ambiguous-side-effect settlement, process-tree termination confirmation, WebSocket/HTTPS relay recovery, authorization intersections, atomic state writes, resource admission, and rollback evidence remain current product guarantees rather than historical compatibility.
10
- - Persisted-state migration is treated separately from protocol compatibility. Browser pairing and OAuth refresh-family readers that protect credential continuity or replay prevention remain until their supported one-way migration can be moved to an explicit upgrade phase and proven complete; they are not deleted merely because the executable protocol is current-only.
11
- - A hosted transient process result promised for recovery remains non-evictable during its fixed 24-hour undelivered-result grace. Delegated pending recovery retains the account-scoped capacity boundary that prevents one principal from exhausting the shared retained-state store.
12
- - Hosted result pressure is now bounded independently from execution correctness: relay `read_file` returns at most a 64 KiB complete serialized result with whole-line continuation, relay `read_process` pages at 32 KiB, and account-owned transient one-step process carriers capture at most 32 KiB aggregate output while retaining explicit truncation counts. Local/stdio capacities are unchanged.
13
- - Owner diagnostics expose only content-free 15-minute result-byte aggregates alongside call density and keep `host_side_events_observable=false`; they cannot prove ChatGPT host-turn termination or final-response receipt.
14
- - Resource-state reads retry only `MBM_IDENTITY_CHANGED` for at most four observations; permission, link, malformed-content, and persistent identity failures remain fail closed.
15
- - Relay application-layer fault domains are now independently configurable: WSS may remain on `MBM_RELAY_PROXY` while signed HTTP fallback uses a distinct `MBM_RELAY_FALLBACK_PROXY` or explicitly returns to standard environment-proxy resolution. This reduces common-mode application-proxy coupling but is not evidence that an operating-system VPN/TUN or shared upstream cannot affect both routes.
16
- - macOS idle-sleep continuity now has an explicit persistent policy rather than an implicit one-size-fits-all lease. `activity` remains the default; `ac-continuous` and `continuous` add daemon-lifetime assertions with different battery semantics, and every assertion uses bounded desired-state self-healing after unexpected child failure. Diagnostics expose only coarse mode/recovery ownership and bounded unprotected duration.
17
- - Portable JWK device roots now renew the daemon's 24-hour root-certified ephemeral session in-process before expiry. WSS and signed HTTPS share one current-session provider; suspension-delayed authentication renews synchronously at the authentication boundary, while renewal failures retain the old session and preserve the existing fail-closed expiry path. Secure Enclave roots remain non-renewable without user presence.
18
- - The reviewed Sharp security floor is 0.35.4 in both the main development graph and the private Wrangler toolchain. The beta.171 activation preflight was blocked before Worker/service mutation when a newly published libheif advisory caused npm audit to report three high-severity aggregate findings through Sharp -> Miniflare -> Wrangler; an isolated 0.35.4 override retained Wrangler 4.127.1 and workerd 1.20260828.1 while reducing both current production-only audits to zero vulnerabilities.
19
- - Process-lock readers now treat only `MBM_IDENTITY_CHANGED` as a bounded atomic-publication race and retry at most four observations. This is narrower than generic activation recovery: persistent identity churn and every other storage/link/permission failure still fail closed. The change is driven by beta.172 live evidence where the exact candidate Worker and service were already verified ready, but the activation subprocess observed the daemon lock while startup-readiness publication atomically replaced that file and therefore returned failure instead of writing the activation record.
20
- - Same-daemon relay recovery now separates execution authority from terminal-result delivery. A transient transport loss may retain only the original in-memory settlement owner for one fixed fifteen-second window beyond its original settlement deadline, including when that original settlement deadline was already at the ordinary tool ceiling. Safe missing-call redelivery remains bound to the original execution deadline, repeated handovers cannot accumulate grace, cancellation still removes ownership, and no client-visible replay or persistent result store is introduced.
21
- - Compound-task continuity is now explicit planning guidance rather than an implicit host behavior. Multi-command, multi-project, long-running, or interruption-sensitive non-interactive work is routed toward one durable managed job, recovery guidance requires resuming the same accepted job after reconnect, and built-in working agreements reject directory-name freshness as checkout identity evidence. Direct shell remains an available fallback; durability is ownership semantics, not a claim that the relay transport cannot fail.
22
- - Workspace relocation now has an explicit offline state-migration invariant. The beta.175 candidate exposed stale global selection after the beta165 worktree was archived; beta.176 then proved that changing selection alone still leaves the deployed profile and machine-service owner keyed to the historical canonical path; beta.177 exposed targetless `service stop` still loading selected-workspace state. The unaccepted beta.178 candidate added the migration path, but owner-machine recovery then exposed two post-verification gaps: read-only inspection can leave an unpopulated destination shell, and a recurring external launchd helper that stopped the daemon before later preflight checks converted a populated-destination rejection into repeated service removal. Beta.179 makes targetless stop provider-global/state-free, accepts only absent or provably unpopulated destination profiles, proves provider/job/lock quiescence before pruning any empty shell, never recursively clears destination state, tolerates only an absent historical owner entry script while validating one that still exists, canonicalizes path aliases through `realpath`, and injects `HOME` into macOS launchd definitions. Recurring external migration helpers and compatibility symlinks are not accepted repairs.
23
-
24
- - Beta.180 is a controlled, unaccepted continuity candidate. Its target is two-layer fault containment rather than an impossible no-disconnect promise: keep signed HTTPS standby and the fallback proxy transport warm so a transient WSS 1006 can take the bounded exact-generation takeover path without a cold start, and make long/compound/keep-working execution explicitly owned by one durable task supervisor instead of host-fragmented one-step calls. The candidate must preserve no-replay and fail-closed transport ownership, keep short/interactive routing unchanged, and may not be called accepted until frozen-tree verification and live owner-machine activation evidence exist.
25
-
26
- - Beta.181 supersedes the activated-but-unaccepted beta.180 candidate after live owner-machine routing exposed two semantic classification defects rather than a relay failure: `非交互工作` and conditional `外部输入或授权` wording could exclude task supervision, while a project token could weak-match an unrelated installed application sharing one lexical fragment. The repair narrows interactive intent to explicit process/input contexts, requires lexical-token evidence for partial application matches, and makes an already-positive task-supervisor continuation contract authoritative for primary route selection. beta.180 relay standby/takeover behavior is unchanged.
27
- - Beta.182 is the reviewed predecessor candidate that added repository-specific durable routing for known long prerelease commands, extracted reconnect result settlement into a focused policy module while retaining the then-existing per-tool settlement ceiling, and recorded the controlled relay A/B. Its packaged bytes remain associated with beta.182 and are not reused after the independent re-review changed shipped source.
28
- - Beta.183 supersedes beta.182 after that re-review found package-affecting continuity, routing, privacy, and auditability defects. Repository-specific long workflows now request durable task supervision through project-provided registered-command metadata rather than hard-coded script names in generic routing; explicit read-only/negated/hypothetical/interactive/existing-job wording is kept out of new-job creation, and weak application-name matches require UI-operation intent. Worker reconnect settlement keeps execution/redelivery on the original deadline but grants the verified same-daemon terminal-result owner one fixed non-cumulative 15-second delivery-only extension beyond the original settlement deadline. Managed-job output redaction is byte-first and truncation-boundary safe; saturated retention tolerates a stale active-to-terminal dependency-plan deletion race without weakening genuine active-state fail-closed behavior; multiline static module edges are included in the architecture graph; and public worktree resolution no longer depends on maintainer-home tooling. The beta.182 controlled application-proxy A/B remains evidence only for the induced fault branch and does not identify the cause of spontaneous historical 1006 resets.
29
- - Beta.186 is the source candidate produced by an independent beta.185 review. The first review confirmed two issues: a zero-wait `read_job` starts with exactly the 10-second managed-read headroom, so any positive reconnect delay made daemon-proven non-delivery ineligible for redelivery; and the output-redaction sink itself accepted an empty literal even though current resource materialization already filters empty patterns. Subsequent hardening reproduced four adjacent boundary defects before acceptance: truncation-tail protection could receive truthy non-string redaction entries; repeated missing acknowledgements could re-enter redelivery policy; `resume_calls_ack.missing_ids` was not bound to the exact resume set emitted on that connection; and shorter protected bytes/paths/literals could partially replace a longer overlapping value and leave its suffix visible. The candidate now uses one literal-pattern filter, processes overlapping protected values longest-first, binds resume acknowledgement to the exact connection-scoped resume set, consumes one semantic acknowledgement while treating only its exact duplicate as idempotent, and records the channel of the one successful same-ID transport redelivery. A different later acknowledgement or out-of-set ID is a protocol error, while a later reconnect that again proves non-delivery returns the existing retryable no-side-effect failure. Low-headroom `read_job` redelivery still becomes `wait_ms: 0`, preserves the original execution deadline, and refuses less than one second of execution budget; fresh dispatch still requires the ordinary 10-second reconciliation headroom. The existing socket protocol-error counter is now driven by real WebSocket and signed-HTTPS ready-channel rejection paths without recording daemon payload values. Here “production protocol rejection paths” means the shipped Worker handler paths that can serve production traffic; it is a source-path reachability claim, not evidence that beta.186 has been deployed to or observed in a production environment. Other report proposals are not treated as defects without stronger evidence: request-stream closure is deliberately cancellation for request-scoped work; the 2-second durable-process initial settlement window is a response-coalescing bound rather than task lifetime and same-response `read_job` continuation remains the orchestration contract; historical macOS relocation already canonicalizes existing realpath ancestors, so platform-wide lowercasing would be unsound on case-sensitive volumes; beta.179 and beta.181 have passed acceptance records and are retained; and disabling fallback-proxy keep-alive is not justified by the current fault-injection evidence alone.
30
-
31
- ## Residual review requirements
32
-
33
- A green fast or full suite is necessary but not sufficient security evidence for publication. Release acceptance still requires the package/install/security gates and any hosted or live boundary evidence required by the changed surface. This summary does not authorize deployment or npm publication.
34
-
35
- Beta.185 is the latest repository acceptance record and remains the accepted prior-byte baseline. Beta.186 is a new local source candidate and must not inherit beta.185 acceptance: frozen-tree verification, packaging/install/security checks, and any exact-candidate live activation evidence required by the changed surface must be produced again before acceptance. The spontaneous historical upstream reset source remains unassigned within the current privacy-bounded evidence; a future relay reset may still occur. npm publication remains separately gated.
7
+ - **Authority remains an intersection, never an accumulation.** Effective MCP authority is bounded by authenticated account role, daemon capability ceiling, and tool-specific policy. Reconnects, retries, stale host schemas, browser focus changes, or recovery metadata do not expand authority.
8
+ - **Removed protocol behavior stays removed.** Remote HTTP and stdio use the current MCP protocol only. Removed initialization, session, and replay behavior fails closed instead of silently falling back to historical compatibility.
9
+ - **State publication is atomic and identity-sensitive.** Security-sensitive state readers reject symlinks, unexpected hard links, malformed content, permission failures, and persistent identity changes. Bounded retries are reserved for explicitly classified atomic-publication races; durable state is complete before it becomes visible.
10
+ - **Durable execution owns continuity explicitly.** Managed jobs persist recovery state independently of the hosted MCP response. Idempotency keys bind uncertain acceptance retries only while the retained record exists; cancellation, dependency state, cleanup, output budgets, and terminal persistence have explicit fail-closed boundaries. Callers recover the same accepted job instead of replaying its side effect.
11
+ - **Daemon-owned Node children use a verified runtime launcher.** A live concrete runtime executable is preferred; if it disappears after a package-manager upgrade, only the original absolute Node launcher is eligible as fallback. Diagnostics expose provenance booleans rather than executable paths, and an unavailable launcher requires service restart instead of PATH guessing.
12
+ - **Relay recovery separates transport from execution semantics.** WebSocket and signed HTTPS are transport paths, not permission or replay authorities. Ambiguous side effects are not transparently replayed. Same-daemon terminal-result retention is bounded, cancellation removes ownership, and repeated reconnects cannot accumulate execution time or delivery grace. Relay diagnostics project the same privacy-bounded fallback takeover history across WebSocket and HTTPS control paths.
13
+ - **Operating-system sleep is an observable suspension boundary, not a network guarantee.** On supported macOS hosts, diagnostics correlate bounded sleep history with runtime stalls and recovered relay outages. Machine Bridge cannot keep JavaScript or a socket executing while the machine is suspended, and sleep correlation does not identify unrelated upstream network causes.
14
+ - **Browser automation remains existing-profile and snapshot-bound.** The extension controls an already installed Chromium profile; hosted calls bind explicit tab identity, and later Computer Use actions bind the exact observation snapshot. Mutation response loss, timeout, extension replacement, or ambiguous trusted-input settlement yields an unknown/non-retryable boundary that must be inspected before any retry.
15
+ - **Desktop automation is permission- and evidence-bound.** Application actions use structured Accessibility operations or an explicitly enabled snapshot-bound visual backend. The runtime does not silently substitute arbitrary scripting or unverified pixel guessing when required authority/evidence is unavailable.
16
+ - **Logging and diagnostics are privacy-minimized by contract.** Runtime logs are structured and must not contain tool arguments, command output, credentials, account identity, arbitrary URLs, private paths, or other user content. Explicit local CLI/tool results are a separate user-requested output surface. Security-audit telemetry is content-free, bounded, and hash-chained; diagnostics expose coarse provenance/health rather than secrets or private values.
17
+ - **Resource admission is cooperative scheduling, not a hostile-code sandbox.** CPU, memory, I/O, disk-reserve, queue, and result budgets protect service continuity, but ordinary execution still has the daemon OS user's ambient authority unless a dedicated low-privilege account or VM/container supplies the stronger boundary.
18
+ - **Release and package evidence remain separate from audit prose.** Shipped-source or packed-document changes require synchronized package identity and fresh verification. Candidate acceptance, publication, activation, and stable promotion must be proven by their dedicated records and external systems; no sentence in this audit summary inherits or confers release authority.
19
+
20
+ ## Residual limits
21
+
22
+ - The default full profile is intentionally powerful. It is appropriate only where the daemon OS user is itself an acceptable trust boundary; stronger hostile-code isolation requires a separate OS account or VM/container.
23
+ - The relay can recover from many transport failures, but no software can promise uninterrupted connectivity across machine sleep, host suspension, upstream outages, VPN/TUN behavior, or process replacement. Recovery evidence must not be restated as a no-disconnect guarantee.
24
+ - Machine Bridge can observe that a hosted tool call reached the daemon and can retain durable job results, but it cannot prove that a ChatGPT host turn ended normally or that a final UI frame rendered to the user.
25
+ - Browser status proves the connected extension identity/capabilities, not that the paired profile is the user's intended daily versus isolated profile. Unpacked extensions may require an explicit reload after upgrades before browser automation counts as exercised.
26
+ - Privacy-preserving diagnostics deliberately leave some root causes unassigned. In particular, spontaneous upstream transport resets may remain indistinguishable once exposing packet contents, private URLs, or user traffic would be required to disambiguate them.
27
+ - Passing local tests is necessary but not sufficient release evidence. External CI, package integrity, installation, deployment/activation, live boundary tests, and soak requirements remain separate when the changed surface requires them.
28
+
29
+ ## Historical findings
30
+
31
+ Historical incidents, closed hypotheses, release-by-release evidence, and superseded candidate details belong in Git history/tags and the [changelog](../CHANGELOG.md), not in this current summary. Inspect the file at the corresponding historical revision when reconstructing an old incident or release decision.
@@ -40,7 +40,9 @@ Interpretation:
40
40
  | Observation | Likely boundary |
41
41
  |---|---|
42
42
  | The tool call is rejected before any structured response | MCP host, connector gateway, approval system, or transport |
43
- | `diagnose_runtime` responds, but `local-process-spawn` fails | Local OS permissions, endpoint security, executable policy, or broken runtime |
43
+ | `runtime-node-executable` reports `source=original_launcher` and `fallback_active=true` | The daemon's concrete Node executable was replaced or removed after startup, but the original absolute Node launcher is still executable. Durable runners continue through that launcher; restart the service to refresh the daemon's concrete runtime identity. |
44
+ | `runtime-node-executable` is available but `local-process-spawn` fails | Local OS permissions, endpoint security, executable policy, or a lower-level process-creation failure |
45
+ | `runtime-node-executable` is unavailable | Neither the daemon's concrete Node executable nor its trusted original absolute Node launcher can be executed; restart Machine Bridge from a valid Node installation |
44
46
  | Process spawn passes but `local-shell` fails | Shell configuration or shell-specific local policy |
45
47
  | Managed-job storage fails | State-root permissions, disk, filesystem policy, or endpoint security |
46
48
  | A job was accepted and later MCP calls are rejected | The detached job continues; inspect it through local CLI |
@@ -10,7 +10,7 @@ machine-mcp service status
10
10
 
11
11
  Routine remote checks should use authenticated `server_info` with `detail: "summary"`; request the default/full projection only when the caller's authority permits and exact effective-tool, OAuth/account, or detailed owner observability is actually needed. Non-owner full responses intentionally retain hidden markers/counts instead of cross-principal activity, resource aliases, stable device-key identity, or daemon-only tool names. Remote `diagnose_runtime` is owner-only because its fixed probes expose machine-wide control-plane activity; narrower roles use `server_info`/`project_overview` for authority-scoped readiness and workspace state. `status` prints redacted profile state and verifies the deployed Worker version. Resource source paths remain redacted. `doctor` checks Node.js, the package-installed Wrangler binary, Cloudflare login, Worker health, the configured policy, the automatic-without-per-operation-prompts authorization model, and the same fixed local filesystem/process/shell/job-storage/resource probes exposed to the remote owner by `diagnose_runtime`. It constructs an isolated local runtime: `diagnosticScope.running_service_process_inspected=false` and `remote_relay_inspected=false` are deliberate, so a green doctor result is not evidence that the launchd/systemd/Scheduled Task daemon retained its Worker WebSocket. Inspect authenticated `server_info.daemon.relay_transport` for the running service relay. Authenticated `server_info.authorization.execution_model` reports the authority contract and identifies whether the account has daemon-OS-user ambient authority. Public `/healthz` output contains only server identity and version; daemon details require an authenticated `server_info` call.
12
12
 
13
- For interruption analysis, prefer one owner `diagnose_runtime` call over a chain of inventory probes. It now includes `runtime.managed_jobs.recent_activity`, `runtime.security_audit.recent_activity`, bounded `runtime.resource_admission.waiters.diagnostics`, `runtime.system_sleep`, `runtime.event_loop_pause_analysis`, and `runtime.relay_outage_analysis`. The audit aggregate contains only counts, bounded tool names, failure totals, calls-per-minute density, and numeric result-pressure fields (`output_bytes_last_15m`, `maximum_output_bytes_last_15m`, `large_result_calls_last_15m`, and `peak_output_bytes_per_minute_last_15m`) derived from the existing content-free hash-chained audit log; it contains no tool arguments or result content. Its `coverage=daemon_reached_relay_tool_calls_only` and `host_side_events_observable=false` fields make the evidence boundary explicit: host-only discovery/control-plane/final-delivery events are not counted. The waiter projection reports only resource-request shape and the current admission reason. On macOS with shell-capable owner diagnostics, the fixed power probe reduces `pmset` history to a small list of sleep start/end/duration/reason classes; it never returns raw power-log lines. If that bounded power-history probe is unavailable or times out, `runtime.system_sleep` remains `supported=true` with `available=false` and an `error_class`, while the `system-sleep-history` check is retained as skipped auxiliary causality evidence. That auxiliary evidence gap does not by itself make `diagnose_runtime.ok=false`; relay readiness, local filesystem/process/shell execution, managed-job storage, resource admission, and registered-resource availability remain health gates. `event_loop_pause_analysis.classification=matched_system_sleep` requires both the recorded runtime-stall end time and duration to match one of those bounded operating-system sleep intervals within a fixed tolerance. `relay_outage_analysis` separately compares the most recent completed `recent_outages[0].disconnected_at` -> `recent_outages[0].ready_at` interval with the same bounded sleep history and reports exact overlap duration/ratio; an active outage uses `outage_started_at` instead of the later `last_disconnected_at`. `majority_system_sleep_overlap` means at least half of that observed relay outage occurred while macOS was suspended. A sleeping JavaScript process may be unable to observe the stale socket until wake, so a zero-overlap close-to-ready interval is not automatically awake-network evidence: `wake_boundary_system_sleep_aftermath` is emitted only when the disconnect occurs within the fixed wake tolerance and that same sleep independently matches the event-loop stall in both end time and duration. Either sleep classification makes a retained `connection_reset`/timeout transport aftermath rather than sufficient evidence of a separate network root cause. `no_matching_recent_system_sleep` remains the classification for an awake reset or a merely coincidental near-wake reset without same-sleep stall evidence; it leaves the cause unassigned rather than guessing a VPN, edge, Worker, or host cause. Full relay diagnostics additionally expose `recent_outages`, a newest-first in-memory history capped at eight completed WebSocket reconnect episodes. Each entry also carries `https_fallback_taken_over` and bounded `https_fallback_takeover_ms`, attributed by the stable WebSocket outage number; a later fallback takeover cannot relabel an earlier or no-takeover episode. The top-level `https_fallback_last_takeover_outage_number` is only a bounded correlation aid for the latest takeover and must not be applied to another outage number. Each entry contains only bounded outage numbering, first/final disconnect and ready timestamps, duration, close/error classes, previous-ready duration/silence, first-disconnect liveness phase/timing, coarse application-route class, and connection-stage timings. `disconnected_at` is the first outage transition and `last_disconnect_at` is the final failed reconnect transition. A protocol/application Pong that clears a liveness suspicion without rebuilding the WebSocket remains heartbeat evidence and is deliberately absent from `recent_outages`; the array is therefore reconnect history, not a list of every transient transport suspicion. ChatGPT host-turn termination/final-message receipt remains explicitly unobservable.
13
+ For interruption analysis, prefer one owner `diagnose_runtime` call over a chain of inventory probes. It now includes `runtime.managed_jobs.recent_activity`, `runtime.security_audit.recent_activity`, bounded `runtime.resource_admission.waiters.diagnostics`, `runtime.system_sleep`, `runtime.event_loop_pause_analysis`, and `runtime.relay_outage_analysis`. The `runtime-node-executable` check exposes only the executable provenance class and availability booleans, never a local path. `source=original_launcher` means the daemon's concrete Node binary disappeared after startup while the same absolute launcher that started the daemon remains executable; durable child Node processes use that launcher, and a service restart refreshes the concrete runtime identity. The audit aggregate contains only counts, bounded tool names, failure totals, calls-per-minute density, and numeric result-pressure fields (`output_bytes_last_15m`, `maximum_output_bytes_last_15m`, `large_result_calls_last_15m`, and `peak_output_bytes_per_minute_last_15m`) derived from the existing content-free hash-chained audit log; it contains no tool arguments or result content. Its `coverage=daemon_reached_relay_tool_calls_only` and `host_side_events_observable=false` fields make the evidence boundary explicit: host-only discovery/control-plane/final-delivery events are not counted. The waiter projection reports only resource-request shape and the current admission reason. On macOS with shell-capable owner diagnostics, the fixed power probe selects actual timestamped `Sleep` records before applying the bounded tail, then reduces them to a small list of sleep start/end/duration/reason classes; it never returns raw power-log lines. The probe has a bounded 15-second execution budget so normal multi-second `pmset -g log` latency does not erase sleep evidence. If that bounded power-history probe is unavailable or times out, `runtime.system_sleep` remains `supported=true` with `available=false` and an `error_class`, while the `system-sleep-history` check is retained as skipped auxiliary causality evidence. That auxiliary evidence gap does not by itself make `diagnose_runtime.ok=false`; relay readiness, local filesystem/process/shell execution, managed-job storage, resource admission, and registered-resource availability remain health gates. `event_loop_pause_analysis.classification=matched_system_sleep` requires both the recorded runtime-stall end time and duration to match one of those bounded operating-system sleep intervals within a fixed tolerance. `relay_outage_analysis` separately compares the most recent completed `recent_outages[0].disconnected_at` -> `recent_outages[0].ready_at` interval with the same bounded sleep history and reports exact overlap duration/ratio; an active outage uses `outage_started_at` instead of the later `last_disconnected_at`. `majority_system_sleep_overlap` means at least half of that observed relay outage occurred while macOS was suspended. A sleeping JavaScript process may be unable to observe the stale socket until wake, so a zero-overlap close-to-ready interval is not automatically awake-network evidence: `wake_boundary_system_sleep_aftermath` is emitted only when the disconnect occurs within the fixed wake tolerance and that same sleep independently matches the event-loop stall in both end time and duration. Either sleep classification makes a retained `connection_reset`/timeout transport aftermath rather than sufficient evidence of a separate network root cause. `no_matching_recent_system_sleep` remains the classification for an awake reset or a merely coincidental near-wake reset without same-sleep stall evidence; it leaves the cause unassigned rather than guessing a VPN, edge, Worker, or host cause. Full relay diagnostics additionally expose `recent_outages`, a newest-first in-memory history capped at eight completed WebSocket reconnect episodes. Each entry also carries `https_fallback_taken_over` and bounded `https_fallback_takeover_ms`, attributed by the stable WebSocket outage number; a later fallback takeover cannot relabel an earlier or no-takeover episode. The top-level `https_fallback_last_takeover_outage_number` is only a bounded correlation aid for the latest takeover and must not be applied to another outage number. Each entry contains only bounded outage numbering, first/final disconnect and ready timestamps, duration, close/error classes, previous-ready duration/silence, first-disconnect liveness phase/timing, coarse application-route class, and connection-stage timings. `disconnected_at` is the first outage transition and `last_disconnect_at` is the final failed reconnect transition. A protocol/application Pong that clears a liveness suspicion without rebuilding the WebSocket remains heartbeat evidence and is deliberately absent from `recent_outages`; the array is therefore reconnect history, not a list of every transient transport suspicion. ChatGPT host-turn termination/final-message receipt remains explicitly unobservable.
14
14
 
15
15
  `runtime.idle_sleep_guard` carries coarse activity/grace/release timestamps plus `mode`, `requests_idle_sleep_prevention`, `requests_system_sleep_prevention_on_ac`, `assertion_generation`, `restart_count`, `recovery_pending`, and bounded current/last unprotected duration. These fields are diagnostic ownership evidence only. The default `activity` mode is backward compatible: authorized relay activity holds `/usr/bin/caffeinate -i -s -w <owner-pid>` through execution and the fixed thirty-minute inactivity grace. In `activity` and `ac-continuous`, that grace begins only after the last owned daemon-side activity settles; a new authorized activity cancels a pending release and receives the full grace after it later settles. `machine-mcp idle-sleep set ac-continuous` adds a daemon-lifetime `/usr/bin/caffeinate -s -w <daemon-pid>` assertion while keeping the normal activity assertion, so idle-sleep prevention on battery remains activity-scoped. `machine-mcp idle-sleep set continuous` instead holds `/usr/bin/caffeinate -i -s -w <daemon-pid>` for the daemon lifetime and does not arm inactivity grace; restart the daemon/service after changing the persisted mode. Every assertion uses fixed 1/5/30-second recovery after unexpected child exit or setup failure, while explicit release/shutdown cancels recovery. The `-s` request is effective only on AC power; `continuous` adds `-i` specifically to request Idle Sleep prevention on battery as well. None of these modes claims to defeat explicit sleep, lid-close policy, power loss, or operating-system behavior outside the documented assertion contracts. Remote account managed-job runners retain their separate runner-owned assertion and now inherit the same bounded child self-healing.
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "3.0.0-beta.190",
3
+ "version": "3.0.0-beta.192",
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",
@@ -4,6 +4,8 @@ import { closeSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { classifyOperationalError } from "./log.mjs";
7
+ import { BridgeError } from "./errors.mjs";
8
+ import { resolveRuntimeNodeExecutable } from "./runtime-node-executable.mjs";
7
9
  import { ownerOnlyFile } from "./secure-file.mjs";
8
10
  import { openPrivateAppendFile, trimDiagnosticFile } from "./managed-job-storage.mjs";
9
11
  import { publishProvisionalRunnerClaim } from "./managed-job-runner-claim.mjs";
@@ -16,6 +18,22 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
16
18
  const launchToken = randomBytes(16).toString("hex");
17
19
  const args = [RUNNER_PATH, "--job-dir", dir];
18
20
  if (recover) args.push("--recover");
21
+ const logger = options.logger || console;
22
+ const runtimeNode = resolveRuntimeNodeExecutable({
23
+ execPath: options.runtimeExecutable ?? process.execPath,
24
+ argv0: options.runtimeLauncher ?? process.argv0,
25
+ platform: options.platform ?? process.platform,
26
+ isExecutable: options.isExecutable,
27
+ });
28
+ if (!runtimeNode.available) throw new BridgeError(
29
+ "unavailable",
30
+ "managed job runtime executable is unavailable; restart Machine Bridge to refresh its Node launcher",
31
+ { retryable: true },
32
+ );
33
+ if (runtimeNode.fallback_active) logger.warn?.(
34
+ "managed job runner is using the daemon original Node launcher because its concrete runtime executable is no longer available",
35
+ { runtime_executable_source: runtimeNode.source, stale_exec_path: true, recovery: recover },
36
+ );
19
37
  const stdoutFile = join(dir, "runner.out.log");
20
38
  const stderrFile = join(dir, "runner.err.log");
21
39
  trimDiagnosticFile(stdoutFile);
@@ -27,7 +45,7 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
27
45
  stdoutFd = openPrivateAppendFile(stdoutFile);
28
46
  stderrFd = openPrivateAppendFile(stderrFile);
29
47
  const spawnProcess = typeof options.spawnProcess === "function" ? options.spawnProcess : spawn;
30
- child = spawnProcess(process.execPath, args, {
48
+ child = spawnProcess(runtimeNode.command, args, {
31
49
  detached: true,
32
50
  stdio: ["ignore", stdoutFd, stderrFd],
33
51
  windowsHide: true,
@@ -40,7 +58,6 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
40
58
  }
41
59
  ownerOnlyFile(stdoutFile);
42
60
  ownerOnlyFile(stderrFile);
43
- const logger = options.logger || console;
44
61
  child.once?.("error", (error) => {
45
62
  logger.error?.("managed job runner process reported an asynchronous failure", {
46
63
  recovery: recover,
@@ -4,7 +4,8 @@ export function diagnosticInterpretation() {
4
4
  return {
5
5
  current_request_delivery: "confirmed: this diagnose_runtime request reached the local runtime; this evidence does not support a blanket current platform disable of Machine Bridge",
6
6
  tool_call_blocked_before_response: "not observable by Machine Bridge; possible causes include conversation/surface app routing state, a stale host action/tool snapshot, host tool filtering, connector gateway, client routing, or platform policy; do not attribute one without host-side evidence",
7
- diagnostic_reached_daemon_but_spawn_failed: "the fixed local spawn/shell probes bypass cooperative resource admission, so a true probe failure points to the local OS, endpoint security, shell configuration, or Machine Bridge policy; a child exit code or bounded stdout/stderr instead proves spawn succeeded and the nested command or remote target decided the failure",
7
+ runtime_node_executable: "runtime-node-executable reports only provenance class and availability booleans, never an executable path. source=original_launcher means the daemon's concrete Node executable disappeared after startup but its original absolute Node launcher remains executable; durable child Node processes use that launcher and a service restart refreshes the concrete runtime identity",
8
+ diagnostic_reached_daemon_but_spawn_failed: "inspect runtime-node-executable first. If it is unavailable, the daemon can no longer launch child Node processes and should be restarted to refresh its runtime launcher. If that layer is available, the fixed local spawn/shell probes bypass cooperative resource admission, so a true probe failure then points to the local OS, endpoint security, shell configuration, or Machine Bridge policy; a child exit code or bounded stdout/stderr instead proves spawn succeeded and the nested command or remote target decided the failure",
8
9
  system_network_stack_scope: "application proxy selection only; an operating-system VPN or TUN may still intercept the relay connection",
9
10
  tunnel_default_route_detected: "the operating-system route is carried by a VPN/TUN; node selection and repair remain outside Machine Bridge",
10
11
  managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
@@ -8,6 +8,7 @@ import { diagnosticControlPlaneState } from "./runtime-diagnostic-state.mjs";
8
8
  import { resourceAdmissionDiagnostic } from "./resource-admission-diagnostics.mjs";
9
9
  import { diagnosticActivityProjection, diagnosticInterpretation } from "./runtime-diagnostic-projection.mjs";
10
10
  import { correlateEventLoopStallWithSystemSleep, correlateRelayOutageWithSystemSleep, systemSleepDiagnostic } from "./system-sleep-diagnostics.mjs";
11
+ import { resolveRuntimeNodeExecutable, runtimeNodeExecutableCheck, runtimeNodeProcessCheck } from "./runtime-node-executable.mjs";
11
12
  export const RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS = 30_000;
12
13
  export async function diagnoseRuntime({
13
14
  policy,
@@ -16,6 +17,7 @@ export async function diagnoseRuntime({
16
17
  runFixedInternal,
17
18
  probeShell,
18
19
  managedJobManager,
20
+ runtimeNodeOptions = {},
19
21
  resourceCoordinatorSnapshot = null,
20
22
  relayStatus = () => null,
21
23
  controlPlaneState = {},
@@ -31,6 +33,8 @@ export async function diagnoseRuntime({
31
33
  ok: policy.execMode === "direct" || policy.execMode === "shell",
32
34
  detail: `profile=${policy.profile}; exec_mode=${policy.execMode}; unrestricted_paths=${policy.unrestrictedPaths}`,
33
35
  }];
36
+ const runtimeNode = resolveRuntimeNodeExecutable(runtimeNodeOptions);
37
+ checks.push(runtimeNodeExecutableCheck(runtimeNode));
34
38
  const relay = typeof relayStatus === "function" ? relayStatus() : null;
35
39
  checks.push(relay ? {
36
40
  layer: "remote-relay",
@@ -61,20 +65,10 @@ export async function diagnoseRuntime({
61
65
  await rm(probe, { force: true }).catch(() => { /* Diagnostic scratch cleanup cannot change the already-observed probe result. */ });
62
66
  }
63
67
  if (policy.execMode === "direct" || policy.execMode === "shell") {
64
- const direct = await runFixedInternal(
65
- process.execPath,
66
- ["-e", "process.stdout.write('ok')"],
67
- RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS,
68
- true,
69
- 1024,
70
- context,
71
- workspace,
72
- ).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
73
- checks.push({
74
- layer: "local-process-spawn",
75
- ok: direct.code === 0 && direct.stdout === "ok",
76
- error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
77
- });
68
+ checks.push(await runtimeNodeProcessCheck({
69
+ runtimeNode, runFixedInternal, timeoutMs: RUNTIME_DIAGNOSTIC_PROCESS_TIMEOUT_MS,
70
+ context, workspace, classifyError: classifyOperationalError,
71
+ }));
78
72
  } else {
79
73
  checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
80
74
  }
@@ -0,0 +1,84 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+
4
+ export function resolveRuntimeNodeExecutable({
5
+ execPath = process.execPath,
6
+ argv0 = process.argv0,
7
+ platform = process.platform,
8
+ isExecutable = null,
9
+ } = {}) {
10
+ const executable = typeof isExecutable === "function"
11
+ ? (candidate) => safeExecutableCheck(isExecutable, candidate)
12
+ : (candidate) => executableFile(candidate, platform);
13
+ const execPathAvailable = absoluteCandidate(execPath, platform) && executable(execPath);
14
+ const launcherAvailable = trustedOriginalNodeLauncher(argv0, platform) && executable(argv0);
15
+ if (execPathAvailable) return {
16
+ available: true, command: execPath, source: "exec_path", exec_path_available: true,
17
+ original_launcher_available: launcherAvailable, fallback_active: false,
18
+ };
19
+ if (launcherAvailable) return {
20
+ available: true, command: argv0, source: "original_launcher", exec_path_available: false,
21
+ original_launcher_available: true, fallback_active: true,
22
+ };
23
+ return {
24
+ available: false, command: null, source: "unavailable", exec_path_available: false,
25
+ original_launcher_available: false, fallback_active: false,
26
+ };
27
+ }
28
+
29
+ export function runtimeNodeExecutableCheck(runtimeNode) {
30
+ return {
31
+ layer: "runtime-node-executable",
32
+ ok: runtimeNode.available,
33
+ source: runtimeNode.source,
34
+ exec_path_available: runtimeNode.exec_path_available,
35
+ original_launcher_available: runtimeNode.original_launcher_available,
36
+ fallback_active: runtimeNode.fallback_active,
37
+ error_class: runtimeNode.available ? null : "not_found",
38
+ };
39
+ }
40
+
41
+ export async function runtimeNodeProcessCheck({
42
+ runtimeNode, runFixedInternal, timeoutMs, context, workspace, classifyError,
43
+ }) {
44
+ if (!runtimeNode.available) return {
45
+ layer: "local-process-spawn", ok: false, error_class: "not_found",
46
+ runtime_executable_source: runtimeNode.source,
47
+ };
48
+ const direct = await runFixedInternal(
49
+ runtimeNode.command, ["-e", "process.stdout.write('ok')"], timeoutMs, true, 1024, context, workspace,
50
+ ).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyError(error) }));
51
+ return {
52
+ layer: "local-process-spawn",
53
+ ok: direct.code === 0 && direct.stdout === "ok",
54
+ runtime_executable_source: runtimeNode.source,
55
+ error_class: direct.error_class
56
+ || (direct.code === 0 ? null : classifyError(direct.stderr || direct.stdout || "execution failed")),
57
+ };
58
+ }
59
+
60
+ function absoluteCandidate(value, platform) {
61
+ return typeof value === "string" && value.length > 0 && pathApi(platform).isAbsolute(value);
62
+ }
63
+
64
+ function trustedOriginalNodeLauncher(value, platform) {
65
+ if (!absoluteCandidate(value, platform)) return false;
66
+ const name = pathApi(platform).basename(value).toLowerCase();
67
+ return name === "node" || name === "node.exe";
68
+ }
69
+
70
+ function executableFile(candidate, platform) {
71
+ try {
72
+ if (!statSync(candidate).isFile()) return false;
73
+ accessSync(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
74
+ return true;
75
+ } catch { return false; }
76
+ }
77
+
78
+ function safeExecutableCheck(check, candidate) {
79
+ try { return check(candidate) === true; } catch { return false; }
80
+ }
81
+
82
+ function pathApi(platform) {
83
+ return platform === "win32" ? win32 : posix;
84
+ }
@@ -18,7 +18,7 @@ export function runtimeRelayConnectionOptions(runtime, input) {
18
18
  helloMessage: async (welcome, relayStatus) => ({
19
19
  type: "hello", instance_id: runtime.relayInstanceId, tools: runtime.tools(), policy: runtime.policy,
20
20
  protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
21
- relay_diagnostics: relayHandshakeDiagnostics(relayStatus),
21
+ relay_diagnostics: relayHandshakeDiagnostics(runtime.relay?.status?.() || relayStatus),
22
22
  authentication: await createDaemonAuthentication(currentSessionIdentity(), welcome, runtime.relayInstanceId),
23
23
  }),
24
24
  onMessage,
@@ -1,7 +1,7 @@
1
1
  import { classifyOperationalError } from "./log.mjs";
2
2
 
3
- const POWER_LOG_COMMAND = "/usr/bin/pmset -g log | /usr/bin/grep -E ' (Sleep|DarkWake|Wake)[[:space:]]' | /usr/bin/tail -n 120";
4
- const POWER_LOG_TIMEOUT_MS = 5_000;
3
+ const POWER_LOG_COMMAND = "/usr/bin/pmset -g log | /usr/bin/grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [+-][0-9]{4}[[:space:]]+Sleep[[:space:]]{2,}' | /usr/bin/tail -n 120";
4
+ const POWER_LOG_TIMEOUT_MS = 15_000;
5
5
  const POWER_LOG_MAX_BYTES = 128 * 1024;
6
6
  const CORRELATION_TOLERANCE_MS = 30_000;
7
7
 
@@ -57,7 +57,7 @@ import { hostedManagedJobDaemonArguments, projectHostedManagedJobResult } from "
57
57
  import { cancelManagedJobMonitorClaimsIfAvailable, claimManagedJobMonitor, hasManagedJobMonitorClaimIfAvailable, ManagedJobMonitorClaimStore } from "./mcp-job-monitor-claims.ts";
58
58
  import { JOB_MONITOR_CLAIM_TOOL, JOB_MONITOR_READ_TOOL, JOB_MONITOR_RENDER_TOOL, managedJobMonitorReadDaemonArguments, projectManagedJobMonitorStatus, renderManagedJobMonitor } from "./mcp-job-monitor-tools.ts";
59
59
  import { closeWebSocketQuietly, daemonErrorCloseCode, isObjectRecord, rejectDaemonMessage, sendWebSocketQuietly, trySendWebSocket } from "./websocket-protocol.ts";
60
- const SERVER_VERSION = "3.0.0-beta.190";
60
+ const SERVER_VERSION = "3.0.0-beta.192";
61
61
  const MCP_SERVER_INFO = mcpServerInfo(SERVER_VERSION);
62
62
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
63
63
  const DAEMON_RECONNECT_GRACE_MS = relayContract.reconnectGraceMs; const NEW_CALL_RECONNECT_GRACE_MS = relayContract.newCallReconnectGraceMs;