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 +6 -4
- package/browser-extension/manifest.json +1 -1
- package/docs/AUDIT.md +26 -30
- package/docs/MANAGED_JOBS.md +3 -1
- package/docs/OPERATIONS.md +1 -1
- package/package.json +1 -1
- package/src/local/managed-job-runner.mjs +19 -2
- package/src/local/runtime-diagnostic-projection.mjs +2 -1
- package/src/local/runtime-diagnostics.mjs +8 -14
- package/src/local/runtime-node-executable.mjs +84 -0
- package/src/local/runtime-relay-connection-options.mjs +1 -1
- package/src/local/system-sleep-diagnostics.mjs +2 -2
- package/src/worker/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.0.0-beta.
|
|
3
|
+
## 3.0.0-beta.192 - 2026-09-16
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- A
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
package/docs/MANAGED_JOBS.md
CHANGED
|
@@ -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
|
-
| `
|
|
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 |
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 '
|
|
4
|
-
const POWER_LOG_TIMEOUT_MS =
|
|
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
|
|
package/src/worker/index.ts
CHANGED
|
@@ -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.
|
|
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;
|