machine-bridge-mcp 1.2.5 → 1.2.7

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.7 - 2026-07-17
4
+
5
+ ### Process supervision, lifecycle, and isolation audit
6
+
7
+ - Separate argv validation, execution limits, process-tree supervision, one-shot execution, and interactive process sessions into explicit modules. Shell helpers, managed jobs, call cancellation, runtime shutdown, and process sessions now share one graceful `SIGTERM` followed by forced tree-termination contract instead of importing session internals or maintaining duplicate platform branches.
8
+ - Reclaim an unresponsive detached service daemon only after revalidating PID, process start time, entrypoint, command line, daemon mode, workspace, and state root immediately before `SIGKILL`. PID reuse, identity drift, foreground ownership, and ambiguous records remain fail closed. Process-session termination now also escalates after a bounded grace period.
9
+ - Add a machine-readable `server_info.runtime.execution_guardrails` contract for tool-call concurrency, process timeout/stdin/output limits, process-session limits, and cleanup semantics. CPU quota, memory quota, and network isolation are reported explicitly as `not-enforced`; hard isolation still requires a dedicated account, container, or VM.
10
+ - Make browser-broker startup generation-aware so `stop()` cannot race an asynchronous listen/proxy connection and leave a listener alive. Pending proxy routes now receive a terminal error during shutdown, broker recovery failures emit structured debug events, and the local browser-health probe uses bounded direct `127.0.0.1` HTTP instead of environment-routed `fetch`.
11
+ - Reject detached managed-job launch when no process ID was obtained, attach an asynchronous child-error observer, make `shell: false` explicit, and remove duplicate plan-scrubbing logic. Correct stale relay-readiness and runtime-observability documentation, and add fault-path tests for resistant descendants, forced daemon reclamation, startup cancellation, proxy-bypassed loopback health, runner spawn failure, and honest OS-enforcement reporting.
12
+
13
+ ## 1.2.6 - 2026-07-17
14
+
15
+ ### Relay ready-context and implicit service-daemon takeover
16
+
17
+ - Keep end-to-end readiness fail-closed, but stop treating an incomplete inbound relay context that only carries `sessionId` as permanently unready. After a verified ready connection, the runtime consults live relay status when the per-message snapshot omits `ready`; an explicit `ready: false` snapshot still rejects tool calls. `RelayConnection` now always forwards boolean `authenticated` and `ready` with the session generation.
18
+ - Recognize managed service daemons started with only `--daemon-only` (no explicit `--workspace` / `--state-dir` on the process argv) when the lock owner already matches the active workspace state. Partial identity (one of the two path flags) remains rejected so foreign processes cannot be taken over. This allows recovery of source-tree recovery daemons that previously stayed orphaned across CLI upgrades.
19
+ - Add regression coverage for pre-ready vs ready inbound message contexts, sessionId-only dispatch after readiness, explicit `ready: false` fail-closed behavior, and implicit daemon-only stop/takeover.
20
+
3
21
  ## 1.2.5 - 2026-07-17
4
22
 
5
23
  ### End-to-end relay readiness and safe daemon handover
package/README.md CHANGED
@@ -264,7 +264,7 @@ Machine Bridge can discover, refresh, rank, and load capabilities automatically.
264
264
 
265
265
  ## Runtime lifecycle and observability
266
266
 
267
- Every tool call passes through one execution pipeline: bounded call registration and deadline/cancellation ownership, structured observability, shared policy authorization, then the typed handler. Errors use stable codes and retryability metadata instead of transport-specific message parsing. `server_info` exposes the local lifecycle state, in-flight calls, active process ownership, per-tool outcomes, durations, and error-code counts. The Worker adds pending internal/request-key indexes, daemon candidate/socket counters, and Worker-side per-tool outcomes.
267
+ Every tool call passes through one execution pipeline: bounded call registration and deadline/cancellation ownership, structured observability, shared policy authorization, then the typed handler. Errors use stable codes and retryability metadata instead of transport-specific message parsing. `server_info` exposes the local lifecycle state, in-flight calls, active process ownership, per-tool outcomes, durations, error-code counts, enforced execution limits, and explicit `not-enforced` CPU/memory/network-isolation fields. The Worker adds pending internal/request-key indexes, daemon candidate/socket counters, and Worker-side per-tool outcomes.
268
268
 
269
269
  Foreground logging defaults to human-readable text. Installed background services use warning-level JSON events by default. Use `--log-format json` for machine-readable foreground logs; arguments, outputs, credentials, resource contents, and local paths remain excluded or redacted.
270
270
 
@@ -526,7 +526,7 @@ Default state roots:
526
526
 
527
527
  State/config writes use owner-only temporary files, `fsync`, and atomic replacement. Exclusive locks are fully written before a same-directory hard-link claim becomes visible; lock ownership includes a token and process start time so PID reuse and lock replacement cannot silently transfer ownership. Only successfully read but invalid JSON is retained as a bounded corrupt backup; permission, symbolic-link, size, encoding, and I/O failures remain explicit. A custom state root must not equal, contain, or be contained by the selected workspace. Resource source paths are redacted from `status` output. Active managed-job plans are owner-only and are deleted after a terminal result; bounded redacted results are retained temporarily. Uninstall first acquires a state-root maintenance lock that blocks new profile/state operations and state-backed operations from already constructed managed-job/browser managers, stops the platform service and all verified workspace daemons before removing definitions, rechecks jobs and locks, then validates markers, canonical paths, workspace/source exclusions, and known contents before recursive deletion.
528
528
 
529
- Worker health probes and the relay honor standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment routing. Invalid or unsupported proxy configuration fails fast. Health and relay diagnostics report only coarse `direct`, `proxy`, or invalid-route state and never proxy URLs or credentials.
529
+ Remote Worker health probes and the relay honor standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment routing. Invalid or unsupported proxy configuration fails fast. Browser-broker health is strictly loopback-direct and does not rely on `NO_PROXY`. Health and relay diagnostics report only coarse `direct`, `proxy`, or invalid-route state and never proxy URLs or credentials.
530
530
 
531
531
  Default foreground logs report authenticated relay readiness, readable persistent-degradation summaries, and recovery rather than raw WebSocket callbacks or JSON field dumps. Brief self-healing disconnects and close codes/reasons are debug-only. Stalled connection attempts have a deadline, and sustained-outage reminders use autonomous exponential backoff. Every per-tool event—including success, failure, cancellation, and slow-call timing—also appears only at `--log-level debug` or `--verbose`. Background services use `warn`, so ordinary tool outcomes and brief network changes do not fill daemon logs. Log messages and structured fields are bounded, secret-like keys and known token formats are redacted, and tool arguments/results are not written. See [docs/LOGGING.md](docs/LOGGING.md) and [docs/OPERATIONS.md](docs/OPERATIONS.md).
532
532
 
package/SECURITY.md CHANGED
@@ -59,7 +59,7 @@ The default for newly selected workspaces is `full`, which prioritizes ease of u
59
59
 
60
60
  `exec_command` has both executable authority and shell expansion. Use `--no-exec` or `review`/`edit` when process execution is unnecessary.
61
61
 
62
- For untrusted repositories or instructions, run the bridge inside a disposable VM/container or under a dedicated low-privilege OS account. On macOS and Windows, this external isolation is especially important. The project does not claim an in-process OS sandbox.
62
+ For untrusted repositories or instructions, run the bridge inside a disposable VM/container or under a dedicated low-privilege OS account. On macOS and Windows, this external isolation is especially important. The project does not claim an in-process OS sandbox, CPU quota, resident-memory quota, or network egress policy. `server_info.runtime.execution_guardrails.operating_system_enforcement` reports these gaps explicitly.
63
63
 
64
64
  ## Agent instructions, skills, and command manifests
65
65
 
@@ -69,7 +69,7 @@ Repository and user instruction files remain untrusted content from the model's
69
69
 
70
70
  Automatic `package.*` commands expose only validated script names and fixed package-manager argv; they do not expose script bodies. Executing one still runs repository-controlled package-script code with local-user authority and is not a sandbox or trust upgrade.
71
71
 
72
- Relay proxy selection honors `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. Only HTTP(S) proxy URLs are accepted. Autostart installation persists an allowlist of proxy and custom-CA environment values in `service-environment.json` so a background daemon can reproduce the foreground network route; a proxy URL may itself contain credentials, so this file is sensitive local state and must remain protected with the rest of the state root. Proxy endpoints, credentials, certificate paths, and authorization headers are not returned through MCP or written to operational logs; service status exposes only configured environment key names and relay status exposes only coarse route state.
72
+ Remote Worker-health and relay proxy selection honors `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. Only HTTP(S) proxy URLs are accepted. The local browser-broker health probe never uses those proxy variables: it accepts only canonical `127.0.0.1`, uses a direct bounded HTTP request, and rejects alternate hosts, credentials, query strings, and fragments. Autostart installation persists an allowlist of proxy and custom-CA environment values in `service-environment.json` so a background daemon can reproduce the foreground network route; a proxy URL may itself contain credentials, so this file is sensitive local state and must remain protected with the rest of the state root. Proxy endpoints, credentials, certificate paths, and authorization headers are not returned through MCP or written to operational logs; service status exposes only configured environment key names and relay status exposes only coarse route state.
73
73
 
74
74
  Skill loading is non-executing. `load_local_skill` returns an entrypoint and bounded file inventory; scripts remain inert until a separate process or command tool is called. Symlinked skill directories are followed only after canonical path-policy validation, while symbolic-link `SKILL.md` entrypoints are rejected. Traversal, cycles, content, summaries, and inventory are bounded.
75
75
 
@@ -154,7 +154,7 @@ Active plans are owner-only and may temporarily contain argv, non-secret stdin,
154
154
 
155
155
  Exact canonical and registration-time resource path aliases, exact resource bytes interpreted as text, and bounded exact base64/hex forms are redacted from retained output. This cannot detect partial, transformed, encrypted, compressed, or application-specific encodings. It also cannot redact unrelated secrets inherited through the full parent environment. Use `capture_output: "discard"` whenever a process may echo credentials, and never place a secret directly in argv, ordinary env, stdin, temporary-file content, or a JSON plan.
156
156
 
157
- `finally_steps` run after ordinary success, failure, timeout, and cancellation. Cancellation uses an owner-only marker rather than signaling the runner process itself, so the coordinator remains alive to execute cleanup consistently across platforms. Timeout/cancellation target the process group/tree and keep a forced-termination escalation alive even if the direct child exits before a resistant descendant. Runner identity includes process start time; recovery does not trust a reused PID. A dead runner is detected on the next daemon or local job-CLI start; stale private resource copies are removed and cleanup is retried. This is best effort. Power loss, disk failure, permanent loss of credentials/network access, SIGKILL without later recovery, or security software denying the cleanup executable can prevent cleanup. Finally steps must be idempotent and safe to repeat. Automatic recovery is capped at three attempts so persistent endpoint-security or executable-policy denial cannot create an endless launch loop. Uninstall refuses to remove local state while managed jobs are active; operators must inspect or cancel them first.
157
+ `finally_steps` run after ordinary success, failure, timeout, and cancellation. Cancellation uses an owner-only marker rather than signaling the runner process itself, so the coordinator remains alive to execute cleanup consistently across platforms. Timeout/cancellation target the process group/tree through one shared supervisor and keep a forced-termination escalation alive even if the direct child exits before a resistant descendant. Verified service-daemon stop uses the same graceful-then-force principle but revalidates the complete process identity immediately before escalation. Runner identity includes process start time; recovery does not trust a reused PID. A dead runner is detected on the next daemon or local job-CLI start; stale private resource copies are removed and cleanup is retried. This is best effort. Power loss, disk failure, permanent loss of credentials/network access, SIGKILL without later recovery, or security software denying the cleanup executable can prevent cleanup. Finally steps must be idempotent and safe to repeat. Automatic recovery is capped at three attempts so persistent endpoint-security or executable-policy denial cannot create an endless launch loop. Uninstall refuses to remove local state while managed jobs are active; operators must inspect or cancel them first.
158
158
 
159
159
  Job-scoped `temporary_files` should be used instead of loose helper scripts. They are materialized only below the private job runtime. Remote scripts should preferably be sent through a process stdin instead of written to the remote filesystem.
160
160
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "1.2.5",
4
+ "version": "1.2.7",
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.5"
33
+ "version_name": "1.2.7"
34
34
  }
@@ -28,7 +28,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
28
28
  `LocalRuntime` is the transport-independent local tool orchestrator. It owns the shared authorization/execution pipeline, manager construction, mutation serialization, cancellation, and the narrow delegation surface used by stdio and relay transports. Domain behavior remains in focused services:
29
29
 
30
30
  - `workspace-file-service.mjs` and `git-service.mjs` own canonical filesystem/Git operations;
31
- - `process-execution.mjs`, `process-sessions.mjs`, and `process-tracker.mjs` own child-process authority and lifecycle;
31
+ - `process-contract.mjs` owns argv shape/size validation, `process-tree.mjs` owns cross-platform tree termination, `process-execution.mjs` and `process-sessions.mjs` own one-shot and interactive execution, and `process-tracker.mjs` owns runtime process accounting;
32
32
  - `runtime-reporting.mjs` builds privacy-aware runtime and project snapshots;
33
33
  - `runtime-diagnostics.mjs` owns fixed local probes and their stable interpretation;
34
34
  - `runtime-capabilities.mjs` composes agent, application, and browser capability results;
@@ -36,7 +36,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
36
36
 
37
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
- `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.
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 accepts lock-backed `--daemon-only` recovery processes that omit repeated path flags. Stop/takeover sends `SIGTERM` only to a verified same-workspace service daemon. If it remains alive after the grace period, the code revalidates PID, process-start identity, command line, entrypoint, daemon mode, workspace, and state root before sending `SIGKILL`; a foreground, replaced-PID, or otherwise unverifiable process remains untouched. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
40
40
 
41
41
  ### Agent context and capability resolver
42
42
 
@@ -54,7 +54,7 @@ See [Session instructions, skills, commands, and capability discovery](AGENT_CON
54
54
 
55
55
  ### Browser extension and machine broker
56
56
 
57
- `BrowserBridgeManager` owns only connection orchestration for the loopback HTTP/WebSocket broker, owner/client failover, routed requests, cancellation, and extension replacement. `browser-operation-service.mjs` owns MCP-facing browser argument normalization, resource-backed values/uploads, form semantics, screenshot conversion, and status presentation. Extension version/capability parsing lives in the strict checked `browser-extension-protocol.mjs`; pairing files and local HTML/Host/Origin helpers live in `browser-pairing-store.mjs`. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
57
+ `BrowserBridgeManager` owns only connection orchestration for the loopback HTTP/WebSocket broker, owner/client failover, routed requests, cancellation, extension replacement, and start/stop generation control. Every asynchronous startup boundary rechecks the generation so a listener or upstream socket cannot appear after `stop()` has invalidated that start. `browser-operation-service.mjs` owns MCP-facing browser argument normalization, resource-backed values/uploads, form semantics, screenshot conversion, and status presentation. Extension version/capability parsing lives in the strict checked `browser-extension-protocol.mjs`; pairing files and local HTML/Host/Origin helpers live in `browser-pairing-store.mjs`. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
58
58
 
59
59
  The packaged Manifest V3 extension runs in the user's existing Chromium profile. Its service worker is limited to pairing, transport, acknowledged protocol readiness, cancellation, and response routing. Fixed `browser-operations.js` owns tab lifecycle, aggregate frame/source budgets, waits, screenshots, and input-backend selection; fixed `page-automation.js` is injected into selected frames for snapshot-version-2 semantics, stable refs, bounded DOM/text traversal, actionability checks, open-Shadow-DOM traversal, structured DOM operations, multi-field forms, and resource-backed file inputs. Fixed `devtools-input.js` exposes only bounded mouse, keyboard, and text sequences through the Chromium debugger API; callers cannot select CDP methods. Trusted sessions attach for one action and detach in `finally`; DOM fallback is allowed only before any DevTools Input dispatch, preventing duplicate side effects after an ambiguous command failure. Protocol 3 requires bidirectional `hello`/`hello_ack` plus exact packaged-version and capability equality; pairing state and replacement are committed only after validation, so an invalid candidate cannot displace or overwrite the working configuration. The broker validates loopback hostnames, canonical extension IDs, matching pairing/broker ports, bearer subprotocols, message sizes, concurrency, and deadlines. Pairing material is owner-only and omitted from MCP/log output.
60
60
 
@@ -205,7 +205,7 @@ This is a process-level transaction, not a filesystem-wide atomic transaction ac
205
205
 
206
206
  The default `full` profile passes the complete parent environment. Isolated environment mode, used by the narrower named profiles unless overridden, creates private runtime HOME, temporary, and cache directories and passes only a small set of path/locale/platform variables. It reduces accidental credential inheritance but cannot prevent explicit access to known filesystem paths, credential stores, network services, or other user resources.
207
207
 
208
- One-shot calls have bounded output and timeouts. Startup-lock waits, daemon takeover, process-session reads, managed-job recovery handoff, browser/page waits, application-cache freshness, and in-memory duration metrics use monotonic elapsed time, so wall-clock correction cannot extend or prematurely terminate their configured duration. Persisted timestamps and retention/credential expiry continue to use wall time. Process sessions retain bounded byte buffers with monotonic offsets, accept bounded stdin, support short output/exit waits, and are capped per runtime. Valid UTF-8 is returned as text; byte slices that are not valid UTF-8 also include lossless base64 data. Session IDs are random. Sessions are memory-only and are killed on runtime stop, remote disconnect, or daemon replacement.
208
+ `execution-limits.mjs` is the shared source for local tool-call concurrency, one-shot process timeout/stdin/output limits, and process-session count/stdin/output/retention limits. `server_info.runtime.execution_guardrails` reports those enforced limits together with explicit `not-enforced` values for CPU quota, memory quota, and network isolation. One-shot calls have bounded output and timeouts. Startup-lock waits, daemon takeover, process-session reads, managed-job recovery handoff, browser/page waits, application-cache freshness, and in-memory duration metrics use monotonic elapsed time, so wall-clock correction cannot extend or prematurely terminate their configured duration. Persisted timestamps and retention/credential expiry continue to use wall time. Process sessions retain bounded byte buffers with monotonic offsets, accept bounded stdin, support short output/exit waits, and are capped per runtime. Valid UTF-8 is returned as text; byte slices that are not valid UTF-8 also include lossless base64 data. Session IDs are random. Sessions are memory-only and are killed on runtime stop, remote disconnect, or daemon replacement.
209
209
 
210
210
  Child processes run in a separate process group where supported. Timeout, cancellation, disconnect, and replacement send termination to process trees, with a referenced forced-escalation timer that remains alive even when the direct child exits before a resistant descendant. Windows uses tree-aware task termination.
211
211
 
@@ -215,7 +215,7 @@ Managed jobs use the same argv/environment primitives but a different lifecycle.
215
215
 
216
216
  Worker deployment is an explicit two-evidence state machine owned by `worker-deployment.mjs`. Wrangler upload is the authoritative remote write. Public `/healthz` is a subsequent read used to verify identity and package version; it is not a transaction commit signal for the upload. After a successful Wrangler result, local state atomically records the detected `workers.dev` URL, MCP URL, content/secret fingerprint, deployed package version, and timestamp before health verification begins. If verification is ambiguous, the next start compares the same fingerprint and performs a read-only verification rather than repeating the remote write.
217
217
 
218
- `worker-health.mjs` owns bounded health I/O: exact HTTPS `workers.dev` origin and Worker-name validation, environment-proxy selection, request timeout, redirect rejection, response-size limit, JSON/identity/version validation, and coarse error classification. `network-proxy.mjs` is shared by HTTP health probes and WebSocket relay construction so both paths honor `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` without exposing proxy details. Definitive stale evidence is retried for propagation and then permits a same-name redeploy; timeout, TLS, network, proxy, and temporary server failure remain ambiguous and fail without upload.
218
+ `worker-health.mjs` owns bounded health I/O: exact HTTPS `workers.dev` origin and Worker-name validation, environment-proxy selection, request timeout, redirect rejection, response-size limit, JSON/identity/version validation, and coarse error classification. `network-proxy.mjs` is shared by remote HTTP health probes and WebSocket relay construction so both paths honor `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` without exposing proxy details. Local browser-broker health uses `loopback-health.mjs`, which accepts only canonical `http://127.0.0.1:<port>/healthz`, disables agent reuse, bounds the response, and deliberately bypasses environment proxies. Definitive stale evidence is retried for propagation and then permits a same-name redeploy; timeout, TLS, network, proxy, and temporary server failure remain ambiguous and fail without upload.
219
219
 
220
220
  Worker-name mutation is a separate identity transition. Existing state rejects a different name unless the caller also supplies the explicit force option. An authorized transition clears the current URL/fingerprint and appends the prior validated name to bounded uninstall inventory. This prevents a health-retry workaround from silently becoming a new remote resource while preserving cleanup of intentional replacements.
221
221
 
@@ -247,7 +247,7 @@ Browser-origin handling separates CORS response sharing from protocol authentica
247
247
 
248
248
  ## Observability
249
249
 
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
+ 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, local execution guardrails, explicit OS-enforcement gaps, 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.
251
251
 
252
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.
253
253
 
package/docs/AUDIT.md CHANGED
@@ -1,3 +1,27 @@
1
+ # Security and privacy audit notes
2
+
3
+ ## 2026-07-17 version 1.2.7 process-supervision and lifecycle audit
4
+
5
+ The core question in this review was not whether the existing green suite covered many paths; it was whether failure and ownership boundaries remained truthful when processes resist termination, startup is interrupted, proxy state is hostile, or the operating system refuses a spawn. The audit found that generic argv validation and process-tree termination had drifted into the interactive-session module, while shell and managed-job paths maintained adjacent termination branches. This created a dependency inversion: one-shot execution and process accounting depended on an adapter whose responsibility should have been only interactive session state.
6
+
7
+ Version 1.2.7 extracts `process-contract.mjs`, `process-tree.mjs`, and `execution-limits.mjs`. All process-owning paths now share the same cross-platform tree supervisor and bounded graceful-to-forced escalation. A verified detached service daemon that ignores `SIGTERM` is no longer left indefinitely alive: immediately before `SIGKILL`, Machine Bridge repeats process-start identity and complete daemon command/workspace/state validation. A reused PID, changed command, foreground process, partial identity, or other ambiguity blocks escalation. Process sessions use the same escalation, and tests use resistant descendants rather than source-shape assertions alone.
8
+
9
+ A separate lifecycle race existed in the browser broker. `stop()` could run while pairing/listen/proxy startup was awaiting I/O; the old start could then finish and repopulate a listener after shutdown. Startup now carries a monotonically increasing generation through every asynchronous boundary. Stop invalidates that generation, and stale completion closes all HTTP/WebSocket transports, pending calls, and routed proxy requests. Browser status also no longer relies on global `fetch` for loopback health: a strict bounded direct-HTTP adapter accepts only canonical `127.0.0.1` and cannot be diverted by environment proxy configuration. Remote Worker health and relay traffic continue to use the documented proxy resolver.
10
+
11
+ The resource review distinguished application bounds from kernel controls. The implementation has real concurrency, timeout, stdin, output, retention, and job-size limits, but it does not create CPU quotas, resident-memory ceilings, syscall sandboxes, or egress filters. `server_info.runtime.execution_guardrails` now states both categories explicitly so a timeout cannot be misread as a CPU quota and a proxy route cannot be misread as network isolation. Hard isolation remains an external deployment responsibility: dedicated low-privilege account, container, or VM.
12
+
13
+ Managed-job launch previously assumed `spawn()` returned a usable child. A system-level launch failure could emit an unhandled child `error` after the job had been reported accepted. Launch now attaches the error observer before returning, refuses acceptance without a positive PID, records a coarse structured failure class, and keeps shell parsing disabled explicitly. The audit also corrected stale `hello_ack`-only readiness wording, incorrect `server_info` field paths, a duplicated plan-retention guard, and documentation that still described daemon takeover as non-escalating.
14
+
15
+ Policy revision 5, state schema 6, OAuth/account records, browser-extension protocol 3, and the account role mapping are unchanged. Profile ACL remains a two-sided intersection: the Worker filters the account role against the daemon ceiling, and the local runtime repeats the role check before handler dispatch. This remains application authorization, not per-account OS tenancy. No Worker deployment, daemon/service replacement, credential rotation, global installation, npm publication, tag, or GitHub Release is performed by the source changes themselves.
16
+
17
+ ## 2026-07-17 version 1.2.6 relay ready-context and daemon-takeover audit
18
+
19
+ Version 1.2.5 introduced fail-closed rejection of ordinary tool calls before end-to-end readiness. That boundary is correct, but an incomplete inbound dispatch object that carried only `sessionId` (the 1.2.4 session-binding shape) evaluated as not ready and converted the first real tool call into a permanent local protocol error. Version 1.2.6 keeps the explicit `ready: false` fail-closed path, requires RelayConnection to forward boolean `authenticated`/`ready`, and allows a live ready relay status to satisfy readiness only when the snapshot omitted the field.
20
+
21
+ Service-daemon takeover previously required both `--workspace` and `--state-dir` on the live process command line. Recovery daemons started with only `--daemon-only` from a source tree therefore remained unverified and blocked upgrade takeover. Identity verification now accepts that implicit form when the lock owner already matches the active workspace and state root, while partial path identity remains rejected.
22
+
23
+ No schema, policy revision, credential, or browser-extension protocol change is included. This source change does not deploy the Worker, replace a running daemon, rotate secrets, publish npm, or mutate live operator state.
24
+
1
25
  # Engineering and security audit
2
26
 
3
27
  ## 2026-07-17 version 1.2.5 end-to-end relay-readiness audit
@@ -61,7 +61,7 @@ A brief relay interruption is retried automatically and is visible only with `--
61
61
 
62
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.
63
63
 
64
- The daemon honors `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` through standard environment-proxy resolution. `wss:` targets use HTTPS proxy selection and `ws:` targets use HTTP proxy selection. Only HTTP and HTTPS proxy URLs are accepted. Invalid URLs or unsupported protocols fail startup with corrective guidance instead of entering the reconnect loop. `server_info.runtime.relay.network_route` reports only `direct`, `proxy`, or `invalid-proxy-configuration`; proxy endpoints and credentials are never returned or logged.
64
+ The daemon honors `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` through standard environment-proxy resolution for remote Worker health and relay traffic. `wss:` targets use HTTPS proxy selection and `ws:` targets use HTTP proxy selection. Only HTTP and HTTPS proxy URLs are accepted. Invalid URLs or unsupported protocols fail startup with corrective guidance instead of entering the reconnect loop. `server_info.runtime.relay.network_route` reports only `direct`, `proxy`, or `invalid-proxy-configuration`; proxy endpoints and credentials are never returned or logged. The browser-broker CLI health probe is a separate loopback-only path: it accepts only canonical `127.0.0.1`, uses direct Node HTTP with no proxy agent, and does not depend on `NO_PROXY`.
65
65
 
66
66
  ## Browser extension setup and diagnosis
67
67
 
@@ -95,7 +95,7 @@ Application UI inspection/actions require Accessibility permission for the Node/
95
95
 
96
96
  `machine-mcp` is a foreground command. It remains attached to the terminal, defaults to `info` logging, and stops on `Ctrl+C`. `machine-mcp service start` launches the installed platform service in the background and returns to the shell; that service uses `warn` logging.
97
97
 
98
- A global npm install changes the CLI files on disk but does not replace an already running Node process. Startup and other state-changing CLI operations use a token/process-identity lock and wait up to 30 seconds for a normal concurrent startup to finish; duration limits use monotonic elapsed time, so NTP or manual wall-clock correction does not lengthen or shorten the wait; a short launchd/systemd overlap is therefore serialized rather than reported immediately as an error. On a normal foreground start, Machine Bridge unloads the platform service and then independently examines the workspace daemon lock. This second path handles a detached/orphan `--daemon-only` process that launchd, systemd, or Task Scheduler no longer tracks. Only current lock records containing service mode, version, PID, process start time, entrypoint, workspace, and state root are eligible for takeover. Before sending `SIGTERM`, Machine Bridge verifies PID and process start time plus the live command line, entrypoint, canonical workspace, canonical state root, and daemon-only flag. It waits up to 15 seconds for both the PID and lock to disappear and does not add a later forced-kill escalation. On POSIX, a daemon that handles and ignores `SIGTERM` therefore reaches the bounded timeout and remains alive. Node maps the supported termination signals to immediate process termination on Windows, so the same verified-daemon stop normally completes rather than exercising the POSIX timeout branch. A foreground or unverifiable process is left untouched; stop a foreground instance with `Ctrl+C`.
98
+ A global npm install changes the CLI files on disk but does not replace an already running Node process. Startup and other state-changing CLI operations use a token/process-identity lock and wait up to 30 seconds for a normal concurrent startup to finish; duration limits use monotonic elapsed time, so NTP or manual wall-clock correction does not lengthen or shorten the wait; a short launchd/systemd overlap is therefore serialized rather than reported immediately as an error. On a normal foreground start, Machine Bridge unloads the platform service and then independently examines the workspace daemon lock. This second path handles a detached/orphan `--daemon-only` process that launchd, systemd, or Task Scheduler no longer tracks. Only current lock records containing service mode, version, PID, process start time, entrypoint, workspace, and state root are eligible for takeover. Before sending `SIGTERM`, Machine Bridge verifies PID and process start time plus the live command line, entrypoint, and daemon-only flag. Explicit `--workspace` and `--state-dir` must both match the active state when present; a recovery daemon started with only `--daemon-only` is accepted when the lock owner already records that workspace and state root. Partial path identity (only one of the two flags) is rejected. If the verified daemon ignores graceful termination, Machine Bridge waits for the grace period, then repeats process-instance and full daemon-identity verification before sending `SIGKILL`. PID reuse, identity drift, foreground mode, or any ambiguity blocks escalation. The total stop remains bounded at 15 seconds and stale lock reclamation still uses token-aware ownership. A foreground or unverifiable process is left untouched; stop a foreground instance with `Ctrl+C`.
99
99
 
100
100
  `machine-mcp service status [WORKSPACE]` reports two independent layers: the platform service (`active`) and `workspace_daemon`, plus `effective_active` and `orphaned_workspace_daemon` summary flags. On macOS it is possible for launchd to report inactive while a prior Node process remains alive with parent PID 1; that is an orphan-daemon condition, not proof that the daemon stopped. `service stop` unloads the provider when present and then terminates only a verified service-style workspace daemon. `service uninstall` and full uninstall are ordered fail-closed operations: provider stop → verified daemon stop(s) → definition removal. A failed or ambiguous stop leaves definitions and state intact. If takeover reaches its deadline, run:
101
101
 
@@ -147,7 +147,7 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
147
147
 
148
148
  ### Lifecycle and pending-call diagnosis
149
149
 
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.
150
+ `server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `server_info.observability.in_flight_calls` and `server_info.runtime.processes` distinguish a blocked call from a surviving process. `server_info.runtime.execution_guardrails` reports the enforced local concurrency/timeout/stdin/output/session limits and explicitly states that CPU quota, memory quota, and network isolation are not enforced in process. Worker `server_info.worker.pending_calls` reports 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.
151
151
 
152
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.
153
153
 
@@ -268,6 +268,8 @@ Defense-in-depth limits include:
268
268
  - application Accessibility inspection: 500 elements and depth 12; action text: 4,000 characters;
269
269
  - job-scoped temporary files: 16 files, 512 KiB total content.
270
270
 
271
+ The list above describes bounded application resources, not OS quotas. CPU time shares, resident-memory ceilings, syscall sandboxes, and egress policy must be imposed by the account/container/VM that runs Machine Bridge. Check `server_info.runtime.execution_guardrails.operating_system_enforcement`; current in-process values are intentionally `not-enforced` rather than inferred from timeouts or output limits.
272
+
271
273
  ## Upgrade behavior
272
274
 
273
275
  Policy revision 5 makes named profiles canonical and evaluates compound tool requirements from the shared contract. A state entry labelled `full` means writes, direct processes, process sessions, shell execution, unrestricted direct filesystem paths, absolute path output, the complete parent environment, and the complete tool catalog. CLI capability overrides are stored as `custom`. Persisted policies from another revision are rejected rather than interpreted.
package/docs/TESTING.md CHANGED
@@ -24,7 +24,7 @@ The suite includes:
24
24
  - foreground takeover of active and orphaned background daemons with current service-lock metadata, foreground-process protection, bounded final lock-handoff retry, actual-PID exit waiting, POSIX non-escalating timeout behavior, Windows verified-daemon stop semantics, daemon lock mode/version/process-start metadata, launchd service-target semantics, and silent idempotent duplicate service starts; daemon fixture subprocesses intentionally do not inherit V8 coverage because their purpose is ownership timing rather than code measurement;
25
25
  - fail-closed service lifecycle ordering for provider-stop, all-workspace daemon-stop, and definition removal, including platform/daemon/removal failure injection and normalized macOS/systemd/Windows results; Windows coverage reproduces an inline `/TR` command above 262 characters, proves the short launcher action remains bounded, verifies least-privilege logon registration, restart/log routing, language-independent `Ready`/`Running` observation, and state-observed stop/removal despite localized nonzero command output;
26
26
  - private service-environment capture/load coverage for exact allowlisting, value bounds and control-character rejection, non-proxy secret exclusion, runtime-value precedence, Windows case-insensitive replacement, explicit empty-value clearing, and preservation across a later environment-free startup;
27
- - machine-level browser-broker ownership/client proxying, authenticated extension origin/subprotocol, non-cacheable local pairing, pairing-token non-disclosure, resource-backed upload routing, broker result redaction, pre-registered runtime-handshake listeners, bounded socket/HTTP waits, frozen-wall-clock browser deadline coverage, installed-application discovery caching, and name-based task matching;
27
+ - machine-level browser-broker ownership/client proxying, authenticated extension origin/subprotocol, non-cacheable local pairing, pairing-token non-disclosure, resource-backed upload routing, broker result redaction, pre-registered runtime-handshake listeners, bounded socket/HTTP waits, direct loopback health despite hostile environment-proxy settings, stop-during-start generation invalidation, frozen-wall-clock browser deadline coverage, installed-application discovery caching, and name-based task matching;
28
28
  - Worker health direct/proxy routing through a real local HTTP CONNECT proxy, `NO_PROXY` bypass, exact `workers.dev` origin/name validation, redirect rejection, body/deadline bounds, error classification, bounded propagation retry, and invalid proxy fail-fast behavior without endpoint or credential disclosure;
29
29
  - Worker deployment ambiguity/idempotency: a successful Wrangler result followed by health timeout persists the fingerprint, a second start performs no upload, an actual process restart and disk-state reload still performs no upload, definitive stale-version evidence redeploys under the same name, accidental name changes are rejected, forced replacements clear current endpoint state, and prior names remain in uninstall inventory;
30
30
  - relay environment-proxy direct/bypass/agent selection, unsupported proxy rejection, fail-fast invalid configuration, and route observability without endpoint or credential disclosure;
@@ -38,13 +38,13 @@ The suite includes:
38
38
  - author-email privacy in `git_log`;
39
39
  - isolated command HOME/temp/cache behavior;
40
40
  - one-shot timeout, descendant process-group/tree termination including descendants that ignore graceful shutdown after the direct child has already exited, cancellation, and process-session interaction;
41
- - layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability;
41
+ - layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability; machine-readable execution guardrails that must keep CPU, memory, and network isolation marked unenforced unless an actual OS boundary is added;
42
42
  - local resource CLI registration, permission checks, dynamic reload, state-path redaction, and content non-disclosure;
43
43
  - real Ed25519 and RSA generation, idempotent reuse, public/private correspondence, mode enforcement, incomplete/mismatched/symlink rejection, and private-content non-disclosure;
44
44
  - real-machine canonical-full sandbox acceptance for outside-workspace I/O, direct/shell execution, full environment inheritance, SSH prerequisites, temporary authorized-key writing, and detached cleanup without external state changes;
45
45
  - deterministic injected atomic-replace failures, sustained transient Windows sharing contention beyond the old retry budget, bounded exponential delay selection, and repeated Windows full-sandbox runs;
46
46
  - canonical named-profile repair and full-only tool exposure parity between local and Worker policy filters;
47
- - managed-job staging/local approval/cancel-before-start, detachment, job-scoped temporary files, resource hash verification/redaction, discard capture, finally execution, descendant-tree escalation, token/snapshot-safe transition locks, runner process identity, plan scrubbing, PID-reuse-safe dead-runner recovery, and rejection of numeric-only runner records;
47
+ - managed-job staging/local approval/cancel-before-start, detachment, job-scoped temporary files, resource hash verification/redaction, discard capture, finally execution, descendant-tree escalation, token/snapshot-safe transition locks, runner process identity, spawn-without-PID rejection, asynchronous runner-error observability, plan scrubbing, PID-reuse-safe dead-runner recovery, and rejection of numeric-only runner records;
48
48
  - daemon/startup locking, successfully-read corrupt-JSON recovery, and explicit propagation/preservation of oversized, symbolic-link, permission, and I/O failures;
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;
package/docs/UPGRADING.md CHANGED
@@ -4,9 +4,9 @@
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.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.
7
+ Version 1.2.6 keeps local state schema version 6 and policy revision 5 unchanged. Existing 1.2.5 workspaces, named accounts, resource registrations, managed-job history, Worker identity, and browser pairing state are reused without a schema conversion. The package change hardens relay ready-context propagation and verified service-daemon takeover for recovery daemons; it does not alter stored authority or credential records.
8
8
 
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.
9
+ Version 1.2.6 still requires the Worker, local daemon, and browser extension to converge on the same package version. The 1.2.5 candidate → probing → ready negotiation remains mandatory; mixed components fail closed. Normal startup redeploys the versioned Worker when required and can now stop a verified same-workspace service daemon even when its process argv used only `--daemon-only` without repeating `--workspace` and `--state-dir`. Reload the unpacked extension after convergence even though this release does not change its browser protocol.
10
10
 
11
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.
12
12
 
@@ -29,6 +29,6 @@ Machine Bridge never treats an unreadable or foreign-schema state file as empty
29
29
 
30
30
  ## Rollback
31
31
 
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.
32
+ Rollback is supported only when the older package understands every persisted schema and protocol already written by the newer package. Version 1.2.6 does not advance local state or policy schemas, so a complete rollback to 1.2.5 is structurally possible. The Worker, daemon, and extension must be rolled back together; restoring only one component recreates an unavailable mixed-version system. The preferred recovery is still to fix forward so incomplete ready-context dispatch and orphan recovery-daemon takeover remain corrected.
33
33
 
34
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.5",
3
+ "version": "1.2.7",
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",
@@ -14,13 +14,14 @@ import {
14
14
  } from "./browser-pairing-store.mjs";
15
15
  import { clampInt } from "./browser-command.mjs";
16
16
  import { BrowserOperationService } from "./browser-operation-service.mjs";
17
+ import { classifyOperationalError } from "./log.mjs";
17
18
 
18
19
  const MAX_PORT_ATTEMPTS = 10;
19
20
  const MAX_PENDING = 32;
20
21
  const EXTENSION_HANDSHAKE_MS = 3_000;
21
22
 
22
23
  export class BrowserBridgeManager {
23
- constructor({ policy, authorizeTool = null, stateRoot = "", runProcess, readResourceText, readResourceBinary, throwIfCancelled = () => {} }) {
24
+ constructor({ policy, authorizeTool = null, stateRoot = "", runProcess, readResourceText, readResourceBinary, throwIfCancelled = () => {}, logger = null }) {
24
25
  this.policy = policy || {};
25
26
  this.authorizeTool = createToolAuthorizer(this.policy, authorizeTool);
26
27
  this.stateRoot = stateRoot ? resolve(stateRoot) : "";
@@ -28,6 +29,7 @@ export class BrowserBridgeManager {
28
29
  this.readResourceText = readResourceText;
29
30
  this.readResourceBinary = readResourceBinary;
30
31
  this.throwIfCancelled = throwIfCancelled;
32
+ this.logger = logger || { event() {} };
31
33
  this.server = null;
32
34
  this.wss = null;
33
35
  this.socket = null;
@@ -44,6 +46,7 @@ export class BrowserBridgeManager {
44
46
  this.stopping = false;
45
47
  this.pending = new Map();
46
48
  this.startPromise = null;
49
+ this.startGeneration = 0;
47
50
  this.port = 0;
48
51
  this.token = "";
49
52
  this.extensionPath = resolve(packageRoot, "browser-extension");
@@ -124,30 +127,59 @@ export class BrowserBridgeManager {
124
127
  if (this.stateRoot) assertStateMaintenanceAvailable(this.stateRoot);
125
128
  this.stopping = false;
126
129
  if (this.server || this.upstream?.readyState === 1) return;
127
- if (!this.startPromise) this.startPromise = this.start();
128
- try { await this.startPromise; } finally { this.startPromise = null; }
130
+ if (!this.startPromise) {
131
+ const generation = ++this.startGeneration;
132
+ this.startPromise = this.start(generation);
133
+ }
134
+ const pending = this.startPromise;
135
+ try { await pending; } finally {
136
+ if (this.startPromise === pending) this.startPromise = null;
137
+ }
129
138
  }
130
139
 
131
- async start() {
132
- const pairing = await loadOrCreatePairing(this.stateRoot);
133
- this.token = pairing.token;
134
- for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
135
- const port = pairing.port + offset;
136
- try {
137
- await this.listen(port);
138
- if (port !== pairing.port && this.stateRoot) await savePairing(this.stateRoot, { token: this.token, port });
139
- return;
140
- } catch (error) {
141
- if (error?.code !== "EADDRINUSE") throw error;
142
- if (await this.connectProxy(port)) {
143
- this.port = port;
140
+ async start(generation = this.startGeneration) {
141
+ try {
142
+ const pairing = await loadOrCreatePairing(this.stateRoot);
143
+ this.assertStartCurrent(generation);
144
+ this.token = pairing.token;
145
+ for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
146
+ const port = pairing.port + offset;
147
+ try {
148
+ await this.listen(port);
149
+ this.assertStartCurrent(generation);
150
+ if (port !== pairing.port && this.stateRoot) {
151
+ await savePairing(this.stateRoot, { token: this.token, port });
152
+ this.assertStartCurrent(generation);
153
+ }
144
154
  return;
155
+ } catch (error) {
156
+ this.assertStartCurrent(generation);
157
+ if (error?.code !== "EADDRINUSE") throw error;
158
+ if (await this.connectProxy(port, generation)) {
159
+ this.assertStartCurrent(generation);
160
+ this.port = port;
161
+ return;
162
+ }
163
+ this.assertStartCurrent(generation);
164
+ if (offset === MAX_PORT_ATTEMPTS - 1) throw error;
145
165
  }
146
- if (offset === MAX_PORT_ATTEMPTS - 1) throw error;
147
166
  }
167
+ } catch (error) {
168
+ const cancelled = !this.isStartCurrent(generation);
169
+ this.closeBrokerTransports(cancelled ? "browser bridge start cancelled" : "browser bridge start failed");
170
+ if (cancelled) throw new Error("browser bridge start cancelled");
171
+ throw error;
148
172
  }
149
173
  }
150
174
 
175
+ isStartCurrent(generation) {
176
+ return !this.stopping && generation === this.startGeneration;
177
+ }
178
+
179
+ assertStartCurrent(generation) {
180
+ if (!this.isStartCurrent(generation)) throw new Error("browser bridge start cancelled");
181
+ }
182
+
151
183
  async listen(port) {
152
184
  this.port = port;
153
185
  const server = createServer((request, response) => this.handleHttp(request, response));
@@ -192,7 +224,7 @@ export class BrowserBridgeManager {
192
224
  this.wss = wss;
193
225
  }
194
226
 
195
- async connectProxy(port) {
227
+ async connectProxy(port, generation = this.startGeneration) {
196
228
  const url = `ws://127.0.0.1:${port}/runtime`;
197
229
  return new Promise((resolvePromise) => {
198
230
  let settled = false;
@@ -220,6 +252,11 @@ export class BrowserBridgeManager {
220
252
  closeProtocolSocket(ws, 1002, "runtime hello required");
221
253
  return;
222
254
  }
255
+ if (!this.isStartCurrent(generation)) {
256
+ try { ws.close(1001, "runtime stopped"); } catch {}
257
+ finish(false);
258
+ return;
259
+ }
223
260
  this.upstream = ws;
224
261
  const claimedExtension = message.extension_connected === true;
225
262
  this.proxyExtensionInfo = claimedExtension ? normalizeCompatibleExtensionInfo(message.extension_info) : null;
@@ -450,7 +487,11 @@ export class BrowserBridgeManager {
450
487
  if (this.stopping || this.recoveryTimer) return;
451
488
  this.recoveryTimer = setTimeout(() => {
452
489
  this.recoveryTimer = null;
453
- void this.ensureStarted().catch(() => this.scheduleBrokerRecovery());
490
+ void this.ensureStarted().catch((error) => {
491
+ this.logger.event?.("debug", "browser.broker.recovery_failed", { error_class: classifyOperationalError(error) },
492
+ "browser broker recovery failed; retrying");
493
+ this.scheduleBrokerRecovery();
494
+ });
454
495
  }, 250);
455
496
  this.recoveryTimer.unref?.();
456
497
  }
@@ -525,9 +566,15 @@ export class BrowserBridgeManager {
525
566
 
526
567
  stop() {
527
568
  this.stopping = true;
569
+ this.startGeneration += 1;
528
570
  clearTimeout(this.recoveryTimer);
529
571
  this.recoveryTimer = null;
530
- this.rejectPending("browser bridge stopped");
572
+ this.closeBrokerTransports("browser bridge stopped");
573
+ }
574
+
575
+ closeBrokerTransports(message) {
576
+ this.rejectPending(message);
577
+ this.rejectProxyRoutes(message);
531
578
  try { this.upstream?.close(1001, "runtime stopped"); } catch {}
532
579
  try { this.socket?.close(1001, "runtime stopped"); } catch {}
533
580
  try { this.pendingExtensionSocket?.close(1001, "runtime stopped"); } catch {}
@@ -541,8 +588,6 @@ export class BrowserBridgeManager {
541
588
  this.proxyExtensionReloadRequired = false;
542
589
  this.extensionReloadRequiredFlag = false;
543
590
  this.runtimeClients.clear();
544
- for (const route of this.proxyRoutes.values()) clearTimeout(route.timeout);
545
- this.proxyRoutes.clear();
546
591
  try { this.wss?.close(); } catch {}
547
592
  try { this.server?.close(); } catch {}
548
593
  this.wss = null;
@@ -10,6 +10,7 @@ import {
10
10
  acquireStartupLockWithWait, expandHome, loadState, ownerOnlyFile, packageRoot, saveState,
11
11
  } from "./state.mjs";
12
12
  import { resolvePolicy } from "./cli-policy.mjs";
13
+ import { readLoopbackJson } from "./loopback-health.mjs";
13
14
 
14
15
  export function createLocalAdminCommands(dependencies) {
15
16
  const chooseWorkspace = dependencies.chooseWorkspace;
@@ -249,9 +250,7 @@ function readBrowserPairingState(pairingFile) {
249
250
  }
250
251
 
251
252
  function readBrowserHealth(healthUrl) {
252
- return fetch(healthUrl, { signal: AbortSignal.timeout(2000), cache: "no-store" })
253
- .then(async (response) => response.ok ? await response.json() : null)
254
- .catch(() => null);
253
+ return readLoopbackJson(healthUrl, { pathname: "/healthz" });
255
254
  }
256
255
 
257
256
  function browserStatusResult(health, extensionPath, pairingUrl) {