machine-bridge-mcp 3.0.0-beta.185 → 3.0.0-beta.191

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,11 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 3.0.0-beta.185 - 2026-09-11
3
+ ## 3.0.0-beta.191 - 2026-09-15
4
4
 
5
- - Preserve interrupted workspace-profile migration after a source-profile rename when Windows exposes the same state root through a different long/short or junction-equivalent path: relocated historical proof canonicalizes only the nearest existing ancestor, still requires the exact historical profile hash plus `state.json` suffix, and rejects a different canonical ancestor.
6
- - Normalize Windows native-path identity during workspace migration so active locks and relocated historical profile state remain comparable across ordinary drive paths and native namespace-prefixed forms without weakening containment checks.
7
- - Keep hosted validation compatible with CodeQL by replacing dynamic resolver regular-expression construction with fixed token parsing while retaining the same release-carrier and worktree-selection behavior.
8
- - Invalidate the stale beta.184 acceptance after the packaged migration repair and advance package, Worker, and browser-extension identity to `3.0.0-beta.185`; fresh activation, deployed OAuth canary evidence, and observed live verification are required before recording acceptance.
5
+ - 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.
6
+ - 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.
7
+ - Add regressions for stale runtime launcher recovery and sleep-probe boundaries; relay authentication, replay, duplicate-side-effect prevention, and reconnect policy are unchanged.
9
8
 
10
9
  ## Historical releases
11
10
 
@@ -30,6 +30,6 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "3.0.0-beta.185",
33
+ "version_name": "3.0.0-beta.191",
34
34
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
35
35
  }
package/docs/AUDIT.md CHANGED
@@ -26,9 +26,10 @@ This file is the current audit summary. Historical findings, closed incidents, a
26
26
  - Beta.181 supersedes the activated-but-unaccepted beta.180 candidate after live owner-machine routing exposed two semantic classification defects rather than a relay failure: `非交互工作` and conditional `外部输入或授权` wording could exclude task supervision, while a project token could weak-match an unrelated installed application sharing one lexical fragment. The repair narrows interactive intent to explicit process/input contexts, requires lexical-token evidence for partial application matches, and makes an already-positive task-supervisor continuation contract authoritative for primary route selection. beta.180 relay standby/takeover behavior is unchanged.
27
27
  - Beta.182 is the reviewed predecessor candidate that added repository-specific durable routing for known long prerelease commands, extracted reconnect result settlement into a focused policy module while retaining the then-existing per-tool settlement ceiling, and recorded the controlled relay A/B. Its packaged bytes remain associated with beta.182 and are not reused after the independent re-review changed shipped source.
28
28
  - Beta.183 supersedes beta.182 after that re-review found package-affecting continuity, routing, privacy, and auditability defects. Repository-specific long workflows now request durable task supervision through project-provided registered-command metadata rather than hard-coded script names in generic routing; explicit read-only/negated/hypothetical/interactive/existing-job wording is kept out of new-job creation, and weak application-name matches require UI-operation intent. Worker reconnect settlement keeps execution/redelivery on the original deadline but grants the verified same-daemon terminal-result owner one fixed non-cumulative 15-second delivery-only extension beyond the original settlement deadline. Managed-job output redaction is byte-first and truncation-boundary safe; saturated retention tolerates a stale active-to-terminal dependency-plan deletion race without weakening genuine active-state fail-closed behavior; multiline static module edges are included in the architecture graph; and public worktree resolution no longer depends on maintainer-home tooling. The beta.182 controlled application-proxy A/B remains evidence only for the induced fault branch and does not identify the cause of spontaneous historical 1006 resets.
29
+ - Beta.186 is the source candidate produced by an independent beta.185 review. The first review confirmed two issues: a zero-wait `read_job` starts with exactly the 10-second managed-read headroom, so any positive reconnect delay made daemon-proven non-delivery ineligible for redelivery; and the output-redaction sink itself accepted an empty literal even though current resource materialization already filters empty patterns. Subsequent hardening reproduced four adjacent boundary defects before acceptance: truncation-tail protection could receive truthy non-string redaction entries; repeated missing acknowledgements could re-enter redelivery policy; `resume_calls_ack.missing_ids` was not bound to the exact resume set emitted on that connection; and shorter protected bytes/paths/literals could partially replace a longer overlapping value and leave its suffix visible. The candidate now uses one literal-pattern filter, processes overlapping protected values longest-first, binds resume acknowledgement to the exact connection-scoped resume set, consumes one semantic acknowledgement while treating only its exact duplicate as idempotent, and records the channel of the one successful same-ID transport redelivery. A different later acknowledgement or out-of-set ID is a protocol error, while a later reconnect that again proves non-delivery returns the existing retryable no-side-effect failure. Low-headroom `read_job` redelivery still becomes `wait_ms: 0`, preserves the original execution deadline, and refuses less than one second of execution budget; fresh dispatch still requires the ordinary 10-second reconciliation headroom. The existing socket protocol-error counter is now driven by real WebSocket and signed-HTTPS ready-channel rejection paths without recording daemon payload values. Here “production protocol rejection paths” means the shipped Worker handler paths that can serve production traffic; it is a source-path reachability claim, not evidence that beta.186 has been deployed to or observed in a production environment. Other report proposals are not treated as defects without stronger evidence: request-stream closure is deliberately cancellation for request-scoped work; the 2-second durable-process initial settlement window is a response-coalescing bound rather than task lifetime and same-response `read_job` continuation remains the orchestration contract; historical macOS relocation already canonicalizes existing realpath ancestors, so platform-wide lowercasing would be unsound on case-sensitive volumes; beta.179 and beta.181 have passed acceptance records and are retained; and disabling fallback-proxy keep-alive is not justified by the current fault-injection evidence alone.
29
30
 
30
31
  ## Residual review requirements
31
32
 
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.
33
34
 
34
- Beta.181 is the current accepted/live baseline. Beta.182 remains the reviewed but superseded predecessor candidate. Beta.183 is the unaccepted replacement candidate and must pass frozen-tree verification plus exact-candidate owner-machine activation before acceptance. The controlled application-proxy A/B establishes only the induced fault branch; the spontaneous historical upstream reset source remains unassigned within the current privacy-bounded evidence. A future relay reset may still occur; continuity success means bounded warm HTTPS takeover plus durable task ownership, not an impossible no-disconnect guarantee. npm publication remains separately gated.
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.
package/docs/LOGGING.md CHANGED
@@ -156,7 +156,7 @@ Each managed job has owner-only runner diagnostic logs. Child-step output is ret
156
156
 
157
157
  `network_route` describes only Machine Bridge's application-level proxy decision. `system-network-stack` does **not** mean a direct physical path: an operating-system VPN, TUN, packet tunnel, DNS interceptor, or endpoint-security product may still carry the connection. `network_route_scope` therefore remains `application-proxy-selection-only`.
158
158
 
159
- During an outage, remote-owner `diagnose_runtime.runtime.relay` and local stdio `server_info.runtime.relay` expose bounded live fields: outage count/start/duration, attempts, last close category/code, coarse transport error class plus strict allowlisted `last_transport_error_reason`, last disconnect/ready time, prior ready duration, prior ready inbound-silence duration, the thirty-second WSS connect budget, bounded `last_connect_milestones_ms`, transport-probe queue/dispatch/Pong state, second-stage transport-confirmation timing/recovery state, bounded sender backlog bytes, HTTPS fallback active/warming state, `https_fallback_last_takeover_ms`, and next retry timing. `https_fallback_last_takeover_ms` retains the bounded WSS-close-to-verified-HTTPS-ready interval even after WSS later reclaims primary ownership, so a longer WebSocket outage is not misreported as the same duration of whole-bridge unavailability when HTTPS recovered earlier. The fallback status separates `last_success_at` (any successful signed HTTP exchange, including standby) from `last_ready_at` (only verified ready ownership), so a successful standby poll can no longer be misread as a completed failover. Connect milestones are relative durations only and never include hostnames, addresses, DNS answers, certificates, proxy endpoints, or credentials. The current-attempt milestones and separate `last_failed_connect_*` fields are retained independently so a successful retry cannot erase the immediately preceding failed DNS/TCP/TLS/upgrade evidence; `last_transport_error_ready` and `last_transport_error_authenticated` state whether the retained transport error occurred after channel authentication/readiness. Worker-side `daemon.websocket.closed` records only a bounded close code plus `was_clean`; raw peer close reasons are deliberately omitted. An already-dispatched Worker call detached from a failed channel is retained only until the smaller of reconnect grace and that call's original remaining absolute deadline. If it settles without rebinding, the public bounded error distinguishes `original call deadline expired during reconnect` from a true full `reconnect grace expired`; neither diagnostic includes tool arguments, paths, account identity, endpoint data, or result content, and the distinction does not extend the hosted deadline. Resource-coordinator snapshot contention is likewise classified separately from execution failure: when the bounded diagnostic cannot acquire a transaction/staging lock, it reports retryable `unavailable`, `reason=coordinator_busy`, and `snapshot_available=false`. This says the diagnostic snapshot was unavailable under contention; it does not relax admission policy or claim that the underlying host pressure is Green. `outage_duration_ms` is the close-to-ready recovery interval; `last_ready_inbound_silence_ms` is the pre-close interval since the preceding ready transport last proved inbound activity. After recovery, authenticated remote `server_info.daemon.relay_transport` retains the bounded preceding episode supplied during the current connection handshake, including `previous_ready_inbound_silence_ms` and brief interruptions below the default warning threshold. Promotion to a ready socket sets `outage_active=false`, extends the outage duration through actual readiness, preserves the preceding healthy-ready duration and inbound-silence evidence across failed candidates, canonicalizes timestamps, and accepts only enumerated coarse operational error classes; it does not claim that the recovered connection remains in outage. The `local_authority_revocation_retry` category is deliberately not diagnosed as a network failure: a sustained warning directs the operator to local authority, process-session, and managed-job state while the retained Worker revocation retries on reconnection; ordinary transport categories retain network/Worker troubleshooting guidance. On macOS, `diagnose_runtime` may also return a coarse default-route class, `operating_system_interception` boolean, privacy-bounded `runtime.idle_sleep_guard` state (`supported`, `enabled`, `active`, `grace_ms`, `requests_system_sleep_prevention_on_ac`, `last_error_class`), bounded `runtime.system_sleep`, and `runtime.relay_outage_analysis`. The sleep correlation distinguishes direct close-to-ready overlap from `wake_boundary_system_sleep_aftermath`, which requires the same sleep interval to match both the observed wake-adjacent disconnect and the daemon event-loop stall end/duration; a socket close cannot necessarily be observed while JavaScript is suspended. A near-wake reset without that independent stall match remains unassigned. That diagnostic is returned on demand and is not promoted to default logs; interface names, IP addresses, DNS answers, proxy endpoints/credentials, Worker endpoints, tool arguments, and results remain absent. `relay.outage.active` and `relay.outage.recovered` carry the existing safe relay fields.
159
+ During an outage, remote-owner `diagnose_runtime.runtime.relay` and local stdio `server_info.runtime.relay` expose bounded live fields: outage count/start/duration, attempts, last close category/code, coarse transport error class plus strict allowlisted `last_transport_error_reason`, last disconnect/ready time, prior ready duration, prior ready inbound-silence duration, the thirty-second WSS connect budget, bounded `last_connect_milestones_ms`, transport-probe queue/dispatch/Pong state, second-stage transport-confirmation timing/recovery state, bounded sender backlog bytes, HTTPS fallback active/warming state, `https_fallback_last_takeover_ms`, `https_fallback_last_takeover_outage_number`, and next retry timing. `https_fallback_last_takeover_ms` retains the bounded WSS-close-to-verified-HTTPS-ready interval even after WSS later reclaims primary ownership, so a longer WebSocket outage is not misreported as the same duration of whole-bridge unavailability when HTTPS recovered earlier; `https_fallback_last_takeover_outage_number` binds that takeover to the exact WebSocket outage episode. The fallback status separates `last_success_at` (any successful signed HTTP exchange, including standby) from `last_ready_at` (only verified ready ownership), so a successful standby poll can no longer be misread as a completed failover. In addition, each completed episode in `recent_outages` independently records `https_fallback_taken_over` and bounded `https_fallback_takeover_ms` keyed to that outage number, ensuring prior takeovers or intervening no-takeover episodes are not conflated. Connect milestones are relative durations only and never include hostnames, addresses, DNS answers, certificates, proxy endpoints, or credentials. The current-attempt milestones and separate `last_failed_connect_*` fields are retained independently so a successful retry cannot erase the immediately preceding failed DNS/TCP/TLS/upgrade evidence; `last_transport_error_ready` and `last_transport_error_authenticated` state whether the retained transport error occurred after channel authentication/readiness. Worker-side `daemon.websocket.closed` records only a bounded close code plus `was_clean`; raw peer close reasons are deliberately omitted. An already-dispatched Worker call detached from a failed channel is retained only until the smaller of reconnect grace and that call's original remaining absolute deadline. If it settles without rebinding, the public bounded error distinguishes `original call deadline expired during reconnect` from a true full `reconnect grace expired`; neither diagnostic includes tool arguments, paths, account identity, endpoint data, or result content, and the distinction does not extend the hosted deadline. Resource-coordinator snapshot contention is likewise classified separately from execution failure: when the bounded diagnostic cannot acquire a transaction/staging lock, it reports retryable `unavailable`, `reason=coordinator_busy`, and `snapshot_available=false`. This says the diagnostic snapshot was unavailable under contention; it does not relax admission policy or claim that the underlying host pressure is Green. `outage_duration_ms` is the close-to-ready recovery interval; `last_ready_inbound_silence_ms` is the pre-close interval since the preceding ready transport last proved inbound activity. After recovery, authenticated remote `server_info.daemon.relay_transport` retains the bounded preceding episode supplied during the current connection handshake, including `previous_ready_inbound_silence_ms` and brief interruptions below the default warning threshold. Promotion to a ready socket sets `outage_active=false`, extends the outage duration through actual readiness, preserves the preceding healthy-ready duration and inbound-silence evidence across failed candidates, canonicalizes timestamps, and accepts only enumerated coarse operational error classes; it does not claim that the recovered connection remains in outage. The `local_authority_revocation_retry` category is deliberately not diagnosed as a network failure: a sustained warning directs the operator to local authority, process-session, and managed-job state while the retained Worker revocation retries on reconnection; ordinary transport categories retain network/Worker troubleshooting guidance. On macOS, `diagnose_runtime` may also return a coarse default-route class, `operating_system_interception` boolean, privacy-bounded `runtime.idle_sleep_guard` state (`supported`, `enabled`, `active`, `grace_ms`, `requests_system_sleep_prevention_on_ac`, `last_error_class`), bounded `runtime.system_sleep`, and `runtime.relay_outage_analysis`. The sleep correlation distinguishes direct close-to-ready overlap from `wake_boundary_system_sleep_aftermath`, which requires the same sleep interval to match both the observed wake-adjacent disconnect and the daemon event-loop stall end/duration; a socket close cannot necessarily be observed while JavaScript is suspended. A near-wake reset without that independent stall match remains unassigned. That diagnostic is returned on demand and is not promoted to default logs; interface names, IP addresses, DNS answers, proxy endpoints/credentials, Worker endpoints, tool arguments, and results remain absent. `relay.outage.active` and `relay.outage.recovered` carry the existing safe relay fields.
160
160
 
161
161
  Schema 4 is strict NDJSON. Before daemon startup, both active log files are opened as owner-only regular single-link files. A schema change clears both only after validation and commits the marker only after the transition succeeds. A symlink, multiple-hard-link inode, permission error, or marker-write failure blocks startup rather than mixing formats or repeatedly erasing evidence.
162
162
 
@@ -40,7 +40,9 @@ Interpretation:
40
40
  | Observation | Likely boundary |
41
41
  |---|---|
42
42
  | The tool call is rejected before any structured response | MCP host, connector gateway, approval system, or transport |
43
- | `diagnose_runtime` responds, but `local-process-spawn` fails | Local OS permissions, endpoint security, executable policy, or broken runtime |
43
+ | `runtime-node-executable` reports `source=original_launcher` and `fallback_active=true` | The daemon's concrete Node executable was replaced or removed after startup, but the original absolute Node launcher is still executable. Durable runners continue through that launcher; restart the service to refresh the daemon's concrete runtime identity. |
44
+ | `runtime-node-executable` is available but `local-process-spawn` fails | Local OS permissions, endpoint security, executable policy, or a lower-level process-creation failure |
45
+ | `runtime-node-executable` is unavailable | Neither the daemon's concrete Node executable nor its trusted original absolute Node launcher can be executed; restart Machine Bridge from a valid Node installation |
44
46
  | Process spawn passes but `local-shell` fails | Shell configuration or shell-specific local policy |
45
47
  | Managed-job storage fails | State-root permissions, disk, filesystem policy, or endpoint security |
46
48
  | A job was accepted and later MCP calls are rejected | The detached job continues; inspect it through local CLI |
@@ -10,7 +10,7 @@ machine-mcp service status
10
10
 
11
11
  Routine remote checks should use authenticated `server_info` with `detail: "summary"`; request the default/full projection only when the caller's authority permits and exact effective-tool, OAuth/account, or detailed owner observability is actually needed. Non-owner full responses intentionally retain hidden markers/counts instead of cross-principal activity, resource aliases, stable device-key identity, or daemon-only tool names. Remote `diagnose_runtime` is owner-only because its fixed probes expose machine-wide control-plane activity; narrower roles use `server_info`/`project_overview` for authority-scoped readiness and workspace state. `status` prints redacted profile state and verifies the deployed Worker version. Resource source paths remain redacted. `doctor` checks Node.js, the package-installed Wrangler binary, Cloudflare login, Worker health, the configured policy, the automatic-without-per-operation-prompts authorization model, and the same fixed local filesystem/process/shell/job-storage/resource probes exposed to the remote owner by `diagnose_runtime`. It constructs an isolated local runtime: `diagnosticScope.running_service_process_inspected=false` and `remote_relay_inspected=false` are deliberate, so a green doctor result is not evidence that the launchd/systemd/Scheduled Task daemon retained its Worker WebSocket. Inspect authenticated `server_info.daemon.relay_transport` for the running service relay. Authenticated `server_info.authorization.execution_model` reports the authority contract and identifies whether the account has daemon-OS-user ambient authority. Public `/healthz` output contains only server identity and version; daemon details require an authenticated `server_info` call.
12
12
 
13
- For interruption analysis, prefer one owner `diagnose_runtime` call over a chain of inventory probes. It now includes `runtime.managed_jobs.recent_activity`, `runtime.security_audit.recent_activity`, bounded `runtime.resource_admission.waiters.diagnostics`, `runtime.system_sleep`, `runtime.event_loop_pause_analysis`, and `runtime.relay_outage_analysis`. The audit aggregate contains only counts, bounded tool names, failure totals, calls-per-minute density, and numeric result-pressure fields (`output_bytes_last_15m`, `maximum_output_bytes_last_15m`, `large_result_calls_last_15m`, and `peak_output_bytes_per_minute_last_15m`) derived from the existing content-free hash-chained audit log; it contains no tool arguments or result content. Its `coverage=daemon_reached_relay_tool_calls_only` and `host_side_events_observable=false` fields make the evidence boundary explicit: host-only discovery/control-plane/final-delivery events are not counted. The waiter projection reports only resource-request shape and the current admission reason. On macOS with shell-capable owner diagnostics, the fixed power probe reduces `pmset` history to a small list of sleep start/end/duration/reason classes; it never returns raw power-log lines. `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 contains only bounded outage numbering, first/final disconnect and ready timestamps, duration, close/error classes, previous-ready duration/silence, first-disconnect liveness phase/timing, coarse application-route class, and connection-stage timings. `disconnected_at` is the first outage transition and `last_disconnect_at` is the final failed reconnect transition. A protocol/application Pong that clears a liveness suspicion without rebuilding the WebSocket remains heartbeat evidence and is deliberately absent from `recent_outages`; the array is therefore reconnect history, not a list of every transient transport suspicion. ChatGPT host-turn termination/final-message receipt remains explicitly unobservable.
13
+ For interruption analysis, prefer one owner `diagnose_runtime` call over a chain of inventory probes. It now includes `runtime.managed_jobs.recent_activity`, `runtime.security_audit.recent_activity`, bounded `runtime.resource_admission.waiters.diagnostics`, `runtime.system_sleep`, `runtime.event_loop_pause_analysis`, and `runtime.relay_outage_analysis`. The `runtime-node-executable` check exposes only the executable provenance class and availability booleans, never a local path. `source=original_launcher` means the daemon's concrete Node binary disappeared after startup while the same absolute launcher that started the daemon remains executable; durable child Node processes use that launcher, and a service restart refreshes the concrete runtime identity. The audit aggregate contains only counts, bounded tool names, failure totals, calls-per-minute density, and numeric result-pressure fields (`output_bytes_last_15m`, `maximum_output_bytes_last_15m`, `large_result_calls_last_15m`, and `peak_output_bytes_per_minute_last_15m`) derived from the existing content-free hash-chained audit log; it contains no tool arguments or result content. Its `coverage=daemon_reached_relay_tool_calls_only` and `host_side_events_observable=false` fields make the evidence boundary explicit: host-only discovery/control-plane/final-delivery events are not counted. The waiter projection reports only resource-request shape and the current admission reason. On macOS with shell-capable owner diagnostics, the fixed power probe selects actual timestamped `Sleep` records before applying the bounded tail, then reduces them to a small list of sleep start/end/duration/reason classes; it never returns raw power-log lines. The probe has a bounded 15-second execution budget so normal multi-second `pmset -g log` latency does not erase sleep evidence. If that bounded power-history probe is unavailable or times out, `runtime.system_sleep` remains `supported=true` with `available=false` and an `error_class`, while the `system-sleep-history` check is retained as skipped auxiliary causality evidence. That auxiliary evidence gap does not by itself make `diagnose_runtime.ok=false`; relay readiness, local filesystem/process/shell execution, managed-job storage, resource admission, and registered-resource availability remain health gates. `event_loop_pause_analysis.classification=matched_system_sleep` requires both the recorded runtime-stall end time and duration to match one of those bounded operating-system sleep intervals within a fixed tolerance. `relay_outage_analysis` separately compares the most recent completed `recent_outages[0].disconnected_at` -> `recent_outages[0].ready_at` interval with the same bounded sleep history and reports exact overlap duration/ratio; an active outage uses `outage_started_at` instead of the later `last_disconnected_at`. `majority_system_sleep_overlap` means at least half of that observed relay outage occurred while macOS was suspended. A sleeping JavaScript process may be unable to observe the stale socket until wake, so a zero-overlap close-to-ready interval is not automatically awake-network evidence: `wake_boundary_system_sleep_aftermath` is emitted only when the disconnect occurs within the fixed wake tolerance and that same sleep independently matches the event-loop stall in both end time and duration. Either sleep classification makes a retained `connection_reset`/timeout transport aftermath rather than sufficient evidence of a separate network root cause. `no_matching_recent_system_sleep` remains the classification for an awake reset or a merely coincidental near-wake reset without same-sleep stall evidence; it leaves the cause unassigned rather than guessing a VPN, edge, Worker, or host cause. Full relay diagnostics additionally expose `recent_outages`, a newest-first in-memory history capped at eight completed WebSocket reconnect episodes. Each entry also carries `https_fallback_taken_over` and bounded `https_fallback_takeover_ms`, attributed by the stable WebSocket outage number; a later fallback takeover cannot relabel an earlier or no-takeover episode. The top-level `https_fallback_last_takeover_outage_number` is only a bounded correlation aid for the latest takeover and must not be applied to another outage number. Each entry contains only bounded outage numbering, first/final disconnect and ready timestamps, duration, close/error classes, previous-ready duration/silence, first-disconnect liveness phase/timing, coarse application-route class, and connection-stage timings. `disconnected_at` is the first outage transition and `last_disconnect_at` is the final failed reconnect transition. A protocol/application Pong that clears a liveness suspicion without rebuilding the WebSocket remains heartbeat evidence and is deliberately absent from `recent_outages`; the array is therefore reconnect history, not a list of every transient transport suspicion. ChatGPT host-turn termination/final-message receipt remains explicitly unobservable.
14
14
 
15
15
  `runtime.idle_sleep_guard` carries coarse activity/grace/release timestamps plus `mode`, `requests_idle_sleep_prevention`, `requests_system_sleep_prevention_on_ac`, `assertion_generation`, `restart_count`, `recovery_pending`, and bounded current/last unprotected duration. These fields are diagnostic ownership evidence only. The default `activity` mode is backward compatible: authorized relay activity holds `/usr/bin/caffeinate -i -s -w <owner-pid>` through execution and the fixed thirty-minute inactivity grace. In `activity` and `ac-continuous`, that grace begins only after the last owned daemon-side activity settles; a new authorized activity cancels a pending release and receives the full grace after it later settles. `machine-mcp idle-sleep set ac-continuous` adds a daemon-lifetime `/usr/bin/caffeinate -s -w <daemon-pid>` assertion while keeping the normal activity assertion, so idle-sleep prevention on battery remains activity-scoped. `machine-mcp idle-sleep set continuous` instead holds `/usr/bin/caffeinate -i -s -w <daemon-pid>` for the daemon lifetime and does not arm inactivity grace; restart the daemon/service after changing the persisted mode. Every assertion uses fixed 1/5/30-second recovery after unexpected child exit or setup failure, while explicit release/shutdown cancels recovery. The `-s` request is effective only on AC power; `continuous` adds `-i` specifically to request Idle Sleep prevention on battery as well. None of these modes claims to defeat explicit sleep, lid-close policy, power loss, or operating-system behavior outside the documented assertion contracts. Remote account managed-job runners retain their separate runner-owned assertion and now inherit the same bounded child self-healing.
16
16
 
@@ -171,7 +171,7 @@ npm --version
171
171
  machine-mcp --verbose
172
172
  ```
173
173
 
174
- The published runtime package does not install Wrangler or Miniflare into its global dependency tree. On the first start or deployment, Machine Bridge first constructs a private hardened npm 12.0.2 from exact integrity-pinned npm, undici 6.28.0, and brace-expansion 5.0.9 registry tarballs, then uses that CLI to install the exact private Wrangler toolchain under its owner-only state root. It audits the resulting production tree and verifies registry signatures before contacting Cloudflare. This requires npm registry access and may add one bounded setup step; do not manually edit either private toolchain directory. Candidate installation, published global installation, GitHub Release publication checks, and npm publication also create temporary hardened/private staging sessions. Published installation preserves the global prefix selected by the owner's npm. GitHub and npm artifacts must match the accepted candidate digest/hash evidence before service mutation. Permission, I/O, quota, memory, retry, stale-handle, storage, read-only-filesystem, or timeout errors are not treated as an empty/corrupt tree and must be corrected without deleting the state root.
174
+ The published runtime package does not install Wrangler or Miniflare into its global dependency tree. On the first start or deployment, Machine Bridge first constructs a private hardened npm 12.0.2 from exact integrity-pinned npm, undici 6.28.0, and brace-expansion 5.0.9 registry tarballs, then uses that CLI to install the exact private Wrangler toolchain under its owner-only state root. It audits the resulting production tree and verifies registry signatures before contacting Cloudflare. This requires npm registry access and may add one bounded setup step; do not manually edit either private toolchain directory. Candidate installation, published global installation, GitHub Release publication checks, and npm publication also create temporary hardened/private staging sessions. Published installation preserves the global prefix selected by the owner's npm. GitHub and npm artifacts must match the accepted candidate digest/hash evidence before service mutation. GitHub release publication after merge does not reinstall the source dependency tree or repeat local `check:full`: it first fetches `origin/main`, requires local `HEAD` to equal that exact commit, and requires every configured push-triggered provider workflow for that commit to have succeeded. It then uses the hardened npm session to recheck synchronized versions and rematerialize the accepted candidate bytes/promotion digest. Immediately before any tag or GitHub Release mutation it fetches `origin/main` again and requires the same CI-verified commit; publication aborts if main moved. Remote asset SHA-256 reconciliation remains mandatory after upload. Permission, I/O, quota, memory, retry, stale-handle, storage, read-only-filesystem, or timeout errors are not treated as an empty/corrupt tree and must be corrected without deleting the state root.
175
175
 
176
176
  ### Ambiguous publication outcome
177
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "3.0.0-beta.185",
3
+ "version": "3.0.0-beta.191",
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",
@@ -108,6 +108,7 @@
108
108
  "stdio:integration-test": "node tests/stdio-integration-test.mjs",
109
109
  "catalog:test": "node tests/catalog-test.mjs",
110
110
  "tool-arguments:test": "node tests/tool-argument-validation-test.mjs",
111
+ "managed-job-output-redaction:test": "node tests/managed-job-output-redaction-test.mjs",
111
112
  "managed-jobs:test": "node tests/managed-jobs-test.mjs",
112
113
  "ssh-key:test": "node tests/ssh-key-test.mjs",
113
114
  "full-access:test": "node tests/full-access-test.mjs",
@@ -10,13 +10,12 @@ import { tmpdir } from "node:os";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { runNetworkCommand } from "./network-retry.mjs";
13
- import { requireSuccessfulWorkflowRun } from "./release-ci.mjs";
13
+ import { waitForSuccessfulWorkflowRun } from "./release-ci.mjs";
14
14
  import { tagSyncError } from "./release-state.mjs";
15
15
  import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
16
16
  import { stageAcceptedCandidateTarball } from "./accepted-candidate-tarball.mjs";
17
17
  import { createHardenedNpmSession } from "./hardened-npm-session.mjs";
18
18
  import { nestedNpmEnvironment } from "../src/local/npm-environment.mjs";
19
- import { sourceDependencyTreeInstallArguments, sourceDependencyTreeInstallTimeoutMs } from "./source-dependency-tree.mjs";
20
19
  import { runExecutable } from "../src/local/shell.mjs";
21
20
  import { resolveTrustedGitExecutable } from "../src/local/trusted-git-executable.mjs";
22
21
  import { resolveTrustedGithubCli } from "../src/local/trusted-github-cli.mjs";
@@ -30,6 +29,8 @@ import { fileURLToPath } from "node:url";
30
29
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
30
  const git = resolveTrustedGitExecutable({ workspace: root });
32
31
  const gh = resolveTrustedGithubCli({ workspace: root });
32
+ const RELEASE_CI_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
33
+ const RELEASE_CI_POLL_INTERVAL_MS = 15_000;
33
34
  process.chdir(root);
34
35
 
35
36
  function fail(message) {
@@ -92,17 +93,6 @@ async function runNpmScript(npmCli, task) {
92
93
  });
93
94
  }
94
95
 
95
- async function installSourceDependencyTree(npmCli) {
96
- await runExecutable(process.execPath, [npmCli, ...sourceDependencyTreeInstallArguments(root)], {
97
- cwd: root,
98
- capture: true,
99
- env: nestedNpmEnvironment(process.env),
100
- timeoutMs: sourceDependencyTreeInstallTimeoutMs,
101
- hardTimeout: true,
102
- maxOutputBytes: 8 * 1024 * 1024,
103
- });
104
- ensureClean();
105
- }
106
96
 
107
97
  function packageMetadata() {
108
98
  const data = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
@@ -158,7 +148,7 @@ function remoteTagCommit(tag) {
158
148
  return (peeled ?? direct)?.[0] ?? null;
159
149
  }
160
150
 
161
- function assertSuccessfulCi(head) {
151
+ async function waitForSuccessfulCi(head) {
162
152
  const required = [
163
153
  [".github/workflows/ci.yml", "CI"],
164
154
  [".github/workflows/codeql.yml", "CodeQL"],
@@ -166,34 +156,32 @@ function assertSuccessfulCi(head) {
166
156
  [".github/workflows/scorecard.yml", "OpenSSF Scorecard"],
167
157
  [".github/workflows/workflow-policy.yml", "Workflow Policy Gate"],
168
158
  ];
159
+ const deadlineMs = performance.now() + RELEASE_CI_WAIT_TIMEOUT_MS;
169
160
  const verified = [];
170
161
  for (const [workflow, name] of required) {
171
- const text = outputNetwork(gh, [
172
- "run",
173
- "list",
174
- "--workflow",
175
- workflow,
176
- "--commit",
177
- head,
178
- "--event",
179
- "push",
180
- "--limit",
181
- "20",
182
- "--json",
183
- "databaseId,status,conclusion,headSha,event,createdAt,url",
184
- ]);
185
- let runs;
186
- try { runs = JSON.parse(text); }
187
- catch { fail(`GitHub Actions did not return valid JSON for ${name}`); }
162
+ const loadRuns = () => {
163
+ const text = outputNetwork(gh, [
164
+ "run", "list", "--workflow", workflow, "--commit", head, "--event", "push",
165
+ "--limit", "20", "--json", "databaseId,status,conclusion,headSha,event,createdAt,url",
166
+ ]);
167
+ try { return JSON.parse(text); }
168
+ catch { fail(`GitHub Actions did not return valid JSON for ${name}`); }
169
+ };
188
170
  let run;
189
- try { run = requireSuccessfulWorkflowRun(runs, head, name); }
190
- catch (error) { fail(String(error?.message || error)); }
171
+ try {
172
+ run = await waitForSuccessfulWorkflowRun(loadRuns, head, name, {
173
+ deadlineMs,
174
+ pollIntervalMs: RELEASE_CI_POLL_INTERVAL_MS,
175
+ now: () => performance.now(),
176
+ });
177
+ } catch (error) {
178
+ fail(String(error?.message || error));
179
+ }
191
180
  console.log(`GitHub Actions ${name} succeeded for ${head} (run ${run.databaseId}).`);
192
181
  verified.push(run);
193
182
  }
194
183
  return verified;
195
184
  }
196
-
197
185
  function releaseInfo(tag) {
198
186
  const args = ["api", githubReleaseByTagEndpoint(tag)];
199
187
  const result = runNetwork(gh, args, { capture: true, allowFailure: true });
@@ -230,7 +218,7 @@ async function assertCoreSync({ requireReleaseAsset }) {
230
218
  if (head !== originMain) {
231
219
  fail(`HEAD ${head} does not match origin/main ${originMain}`);
232
220
  }
233
- assertSuccessfulCi(head);
221
+ await waitForSuccessfulCi(head);
234
222
 
235
223
  const localCommit = localTagCommit(tag);
236
224
  const localTagError = tagSyncError({ scope: "local", tag, head, commit: localCommit });
@@ -349,13 +337,14 @@ async function publishCurrent({ prereleaseMode = false } = {}) {
349
337
  fail(`CHANGELOG.md has no section for ${pkg.version}`);
350
338
  }
351
339
 
340
+ const releaseHead = exactReleaseHead();
341
+ await waitForSuccessfulCi(releaseHead);
342
+
352
343
  const npmSession = await createHardenedNpmSession();
353
344
  let acceptance;
354
345
  let candidate = null;
355
346
  let verificationError = null;
356
347
  try {
357
- await installSourceDependencyTree(npmSession.cli);
358
- await runNpmScript(npmSession.cli, "check");
359
348
  await runNpmScript(npmSession.cli, "version:check");
360
349
  ensureClean();
361
350
  acceptance = assertLocalAcceptance(npmSession.cli);
@@ -382,24 +371,19 @@ async function publishCurrent({ prereleaseMode = false } = {}) {
382
371
  let primaryError = null;
383
372
  let releaseVerified = false;
384
373
  try {
385
- const head = output(git, ["rev-parse", "HEAD"]);
386
- const originMain = output(git, ["rev-parse", "origin/main"]);
387
- if (head !== originMain) {
388
- fail("HEAD does not match origin/main; local acceptance must be committed, pushed through npm run github:push, reviewed, and merged before release publication");
389
- }
390
- assertSuccessfulCi(head);
374
+ revalidateReleaseHead(releaseHead);
391
375
 
392
376
  const existingLocal = localTagCommit(tag);
393
- if (existingLocal && existingLocal !== head) {
394
- fail(`local ${tag} points to ${existingLocal}, not ${head}`);
377
+ if (existingLocal && existingLocal !== releaseHead) {
378
+ fail(`local ${tag} points to ${existingLocal}, not ${releaseHead}`);
395
379
  }
396
380
  if (!existingLocal) {
397
381
  run(git, ["tag", "-a", tag, "-m", `Release ${pkg.version}`]);
398
382
  }
399
383
 
400
384
  const existingRemote = remoteTagCommit(tag);
401
- if (existingRemote && existingRemote !== head) {
402
- fail(`remote ${tag} points to ${existingRemote}, not ${head}`);
385
+ if (existingRemote && existingRemote !== releaseHead) {
386
+ fail(`remote ${tag} points to ${existingRemote}, not ${releaseHead}`);
403
387
  }
404
388
  if (!existingRemote) {
405
389
  runNetwork(git, ["push", "origin", tag]);
@@ -442,6 +426,24 @@ async function publishCurrent({ prereleaseMode = false } = {}) {
442
426
  await assertCoreSync({ requireReleaseAsset: true });
443
427
  }
444
428
 
429
+ function exactReleaseHead() {
430
+ const head = output(git, ["rev-parse", "HEAD"]);
431
+ const originMain = output(git, ["rev-parse", "origin/main"]);
432
+ if (head !== originMain) {
433
+ fail(`HEAD ${head} does not match origin/main ${originMain}; merge the accepted candidate before release publication`);
434
+ }
435
+ return head;
436
+ }
437
+
438
+ function revalidateReleaseHead(expectedHead) {
439
+ ensureClean();
440
+ fetchRemote();
441
+ const currentHead = exactReleaseHead();
442
+ if (currentHead !== expectedHead) {
443
+ fail(`release source moved from verified main ${expectedHead} to ${currentHead}; restart publication against the new exact main`);
444
+ }
445
+ }
446
+
445
447
  function assertStableSoak(npmCli = process.env.npm_execpath) {
446
448
  try {
447
449
  const result = verifyCurrentStableSoak(root, { npmCli });
@@ -1,6 +1,62 @@
1
+ import { performance } from "node:perf_hooks";
2
+
3
+ const DEFAULT_WORKFLOW_POLL_INTERVAL_MS = 15_000;
4
+ const DEFAULT_WORKFLOW_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
5
+
1
6
  export function requireSuccessfulWorkflowRun(runs, head, workflowName = "CI") {
7
+ const run = latestPushWorkflowRun(runs, head, workflowName);
8
+ if (run.status !== "completed") {
9
+ throw new Error(`${workflowName} run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
10
+ }
11
+ if (run.conclusion !== "success") {
12
+ throw new Error(`${workflowName} run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun the workflow before release`);
13
+ }
14
+ return run;
15
+ }
16
+
17
+ export async function waitForSuccessfulWorkflowRun(loadRuns, head, workflowName = "CI", options = {}) {
18
+ if (typeof loadRuns !== "function") throw new Error("GitHub Actions run loader must be a function");
19
+ validateReleaseHead(head);
20
+ const now = typeof options.now === "function" ? options.now : () => performance.now();
21
+ const wait = typeof options.wait === "function" ? options.wait : defaultWorkflowWait;
22
+ const pollIntervalMs = positiveFinite(options.pollIntervalMs, DEFAULT_WORKFLOW_POLL_INTERVAL_MS, "workflow poll interval");
23
+ const deadlineMs = options.deadlineMs === undefined
24
+ ? now() + DEFAULT_WORKFLOW_WAIT_TIMEOUT_MS
25
+ : finiteDeadline(options.deadlineMs);
26
+ let observed = null;
27
+ for (;;) {
28
+ if (now() >= deadlineMs) throw workflowWaitTimeout(workflowName, head, observed);
29
+ const runs = await loadRuns();
30
+ let run = null;
31
+ try {
32
+ run = latestPushWorkflowRun(runs, head, workflowName);
33
+ } catch (error) {
34
+ if (!isMissingWorkflowRun(error, workflowName, head)) throw error;
35
+ }
36
+ if (run) {
37
+ observed = run;
38
+ if (run.status === "completed") {
39
+ if (run.conclusion !== "success") {
40
+ throw new Error(`${workflowName} run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun the workflow before release`);
41
+ }
42
+ return run;
43
+ }
44
+ } else {
45
+ observed = null;
46
+ }
47
+ const remainingMs = deadlineMs - now();
48
+ if (!(remainingMs > 0)) throw workflowWaitTimeout(workflowName, head, observed);
49
+ await wait(Math.min(pollIntervalMs, remainingMs));
50
+ }
51
+ }
52
+
53
+ export function requireSuccessfulCiRun(runs, head) {
54
+ return requireSuccessfulWorkflowRun(runs, head, "CI");
55
+ }
56
+
57
+ function latestPushWorkflowRun(runs, head, workflowName) {
2
58
  if (!Array.isArray(runs)) throw new Error("GitHub Actions response is not an array");
3
- if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
59
+ validateReleaseHead(head);
4
60
  const label = String(workflowName || "workflow");
5
61
  const matching = runs
6
62
  .filter((run) => run && run.headSha === head && run.event === "push")
@@ -9,15 +65,38 @@ export function requireSuccessfulWorkflowRun(runs, head, workflowName = "CI") {
9
65
  if (!run) {
10
66
  throw new Error(`no push-triggered ${label} run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
11
67
  }
12
- if (run.status !== "completed") {
13
- throw new Error(`${label} run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
14
- }
15
- if (run.conclusion !== "success") {
16
- throw new Error(`${label} run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun the workflow before release`);
17
- }
18
68
  return run;
19
69
  }
20
70
 
21
- export function requireSuccessfulCiRun(runs, head) {
22
- return requireSuccessfulWorkflowRun(runs, head, "CI");
71
+ function validateReleaseHead(head) {
72
+ if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
73
+ }
74
+
75
+ function positiveFinite(value, fallback, label) {
76
+ if (value === undefined) return fallback;
77
+ const number = Number(value);
78
+ if (!Number.isFinite(number) || number <= 0) throw new Error(`${label} must be a positive finite number`);
79
+ return number;
80
+ }
81
+
82
+ function finiteDeadline(value) {
83
+ const number = Number(value);
84
+ if (!Number.isFinite(number)) throw new Error("workflow wait deadline must be finite");
85
+ return number;
86
+ }
87
+
88
+ function isMissingWorkflowRun(error, workflowName, head) {
89
+ return String(error?.message || error).startsWith(`no push-triggered ${String(workflowName || "workflow")} run exists for release commit ${head};`);
90
+ }
91
+
92
+ function workflowWaitTimeout(workflowName, head, run) {
93
+ const label = String(workflowName || "workflow");
94
+ const state = run
95
+ ? `latest run ${run.databaseId || "unknown"} remained ${run.status || "unknown"}`
96
+ : "no exact-commit push run was registered";
97
+ return new Error(`${label} did not succeed for release commit ${head} before the finite CI wait deadline; ${state}`);
98
+ }
99
+
100
+ function defaultWorkflowWait(ms) {
101
+ return new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); });
23
102
  }
@@ -256,7 +256,30 @@ export class AgentContextManager {
256
256
  };
257
257
  }
258
258
 
259
- async discoverState(inputPath, context = {}) {
259
+ async managedJobCommandForLocalInvocation(args = {}, context = {}) {
260
+ const command = await this.resolveLocalCommand(args, context);
261
+ return command.executionMode === "managed_job" ? managedJobCommandSummary(command) : null;
262
+ }
263
+
264
+ async managedJobCommandForDirectInvocation(args = {}, context = {}) {
265
+ const argv = args.argv;
266
+ if (!Array.isArray(argv) || argv.length === 0 || argv.some((value) => typeof value !== "string")) return null;
267
+ const state = await this.discoverState(args.cwd || ".", context, { includeUserGlobalContext: false });
268
+ if (state.target !== state.targetDir) return null;
269
+ for (const command of state.commands.values()) {
270
+ if (command.executionMode !== "managed_job" || !sameArgv(command.argv, argv)) continue;
271
+ let commandCwd;
272
+ try { commandCwd = await realpath(command.cwd); }
273
+ catch (error) {
274
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") continue;
275
+ throw error;
276
+ }
277
+ if (commandCwd === state.target) return managedJobCommandSummary(command);
278
+ }
279
+ return null;
280
+ }
281
+
282
+ async discoverState(inputPath, context = {}, options = {}) {
260
283
  this.throwIfCancelled(context);
261
284
  const effectivePolicy = this.policyForContext(context);
262
285
  this.workspace = await realpath(this.workspace);
@@ -269,14 +292,14 @@ export class AgentContextManager {
269
292
  unrestricted: effectivePolicy.unrestrictedPaths === true,
270
293
  });
271
294
  const directories = directoriesBetween(scopeRoot, targetDir);
272
- const userGlobalContextAllowed = allowsUserGlobalContext(context, effectivePolicy);
295
+ const userGlobalContextAllowed = options.includeUserGlobalContext !== false && allowsUserGlobalContext(context, effectivePolicy);
273
296
  const state = {
274
297
  target,
275
298
  targetDir,
276
299
  scopeRoot,
277
300
  instructionFiles: [...DEFAULT_INSTRUCTION_FILES],
278
301
  instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
279
- skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, effectivePolicy.unrestrictedPaths === true),
302
+ skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, userGlobalContextAllowed && effectivePolicy.unrestrictedPaths === true),
280
303
  commands: new Map(),
281
304
  builtinInstructionsEnabled: true,
282
305
  automaticProjectContextEnabled: true,
@@ -428,6 +451,14 @@ export class AgentContextManager {
428
451
  }
429
452
  }
430
453
 
454
+ function sameArgv(left, right) {
455
+ return left.length === right.length && left.every((value, index) => value === right[index]);
456
+ }
457
+
458
+ function managedJobCommandSummary(command) {
459
+ return Object.freeze({ name: command.name, managedJobTimeoutSeconds: command.managedJobTimeoutSeconds });
460
+ }
461
+
431
462
  async function findScopeRoot({ targetDir, workspace, unrestricted }) {
432
463
  const target = await realpath(targetDir);
433
464
  const canonicalWorkspace = await realpath(workspace);