machine-bridge-mcp 3.0.0-beta.196 → 3.0.0-beta.197

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,13 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 3.0.0-beta.196 - 2026-09-21
4
-
5
- - Stop verified-WebSocket operation from maintaining a continuously polled HTTPS standby relay; HTTPS fallback now starts only during startup grace, liveness degradation, or actual WebSocket loss, and verified WebSocket recovery stops it.
6
- - Raise ready HTTPS fallback idle polling from one second to five seconds and standby retry from five seconds to thirty seconds while preserving the 750 ms minimum path for handshake and queued outbound work, reducing idle request and replay-nonce traffic. Scenario tests separately count recurring Durable Object alarm mutations; these are not a whole-account billing guarantee.
7
- - Add regression coverage for healthy-day zero fallback polling, direct HTTP and WebSocket-preflight replay-nonce write accounting, daily request/write budgeting, exact-generation failover, late-ready recovery races, and repeated handover behavior.
8
- - Enforce one monotonic HTTP retry deadline across queued work and lifecycle transitions, honor bounded 429/503 `Retry-After`, and reject exact-expiry authentication and delayed preflight consumption without nonce writes.
9
- - Correct stale initialization-compatibility and subscription documentation to match the current-only protocol implementation.
10
- - Independent review removes obsolete `standbyDelayMs` test fixtures and synchronizes README, architecture, operations, logging, testing, privacy, and audit documentation with the quota-bounded fallback behavior and the absence of a checked-in Workers KV binding.
3
+ ## 3.0.0-beta.197 - 2026-09-30
4
+
5
+ - Pin source tooling and the integrity-verified hardened npm bootstrap to brace-expansion 5.0.12, fixing the newly disclosed parser denial-of-service advisories. Preserve clean global consumer installs without postinstall hooks; declare the audited private Wrangler build provider and supply its executable runner before native cf builds.
6
+
7
+ - Migrate Worker builds and deployments to native cf configuration and prebuilt artifacts; preserve the Worker name, SQLite Durable Object export, rate-limit namespaces, existing variables, and secrets.
8
+ - Require structured cf authentication before unattended activation and bind deployment fingerprints to the exact private source snapshot and pinned toolchain.
9
+
10
+ - Add an isolated, exact cf deployment toolchain with a compatible Wrangler builder and patched Undici.
11
+ - Verify the deterministic compatibility artifact for cf beta.5 before its network factory runs; reject unknown or tampered bundle bytes.
12
+ - Synchronize the package, Worker, and browser extension prerelease version declarations.
11
13
 
12
14
  ## Historical releases
13
15
 
package/README.md CHANGED
@@ -119,7 +119,9 @@ Run the CLI in the workspace to expose:
119
119
  machine-mcp --workspace /path/to/project
120
120
  ```
121
121
 
122
- On first remote start, Machine Bridge creates workspace-scoped state, signs in to Wrangler when needed, deploys one stable Worker, creates the initial `owner` account, installs user-level autostart unless disabled, starts the outbound daemon connection, and prints the MCP URL and one-time owner password.
122
+ On first remote start, Machine Bridge creates workspace-scoped state, signs in with `cf auth login --scopes account-settings.read user-details.read workers-scripts.read workers-scripts.write workers-scripts.bind workers-routes.read workers-routes.write workers-observability.read workers-observability.write offline` when needed, builds and deploys one stable Worker through `cf`, creates the initial `owner` account, installs user-level autostart unless disabled, starts the outbound daemon connection, and prints the MCP URL and one-time owner password.
123
+
124
+ Machine Bridge, including Worker deployment, requires Node.js 26 or later. `cf` has its own login, separate from Wrangler. Before unattended activation, complete `cf auth login` in an ordinary owner terminal; for multiple Cloudflare accounts, set `CLOUDFLARE_ACCOUNT_ID` to the existing Worker's account. The pinned deployment tools stay in Machine Bridge's private toolchain, outside the consumer runtime dependencies. Native `cloudflare.config.ts` controls deployment; the retained `wrangler.jsonc` is used for deterministic runtime type generation and legacy maintenance commands.
123
125
 
124
126
  Use the printed endpoint in the hosted client:
125
127
 
@@ -30,6 +30,6 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "3.0.0-beta.196",
33
+ "version_name": "3.0.0-beta.197",
34
34
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
35
35
  }
@@ -0,0 +1,23 @@
1
+ import { bindings, defineConfig, exports as workerExports } from "cf/config";
2
+
3
+ const name = process.env.MBM_WORKER_NAME || "machine-bridge-mcp";
4
+ export default defineConfig({
5
+ worker: {
6
+ name,
7
+ entrypoint: "src/worker/index.ts",
8
+ compatibilityDate: "2026-07-11",
9
+ compatibilityFlags: ["nodejs_compat", "enable_request_signal", "request_signal_passthrough"],
10
+ workersDev: true,
11
+ previewUrls: false,
12
+ unsafe: { metadata: { keep_bindings: ["plain_text", "json", "secret_text", "secret_key"] } },
13
+ observability: { enabled: true, headSamplingRate: 0.1 },
14
+ exports: { BridgeRoom: workerExports.durableObject({ storage: "sqlite" }) },
15
+ env: {
16
+ MBM_WORKER_MAX_BODY_BYTES: bindings.text("8388608"),
17
+ MBM_ALLOWED_ORIGINS: bindings.text(""),
18
+ BRIDGE: bindings.durableObject({ worker: name, exportName: "BridgeRoom" }),
19
+ STATEFUL_GLOBAL_RATE_LIMITER: bindings.rateLimit({ namespace: "4301702", simple: { limit: 1200, period: 60 } }),
20
+ STATEFUL_RATE_LIMITER: bindings.rateLimit({ namespace: "4301701", simple: { limit: 120, period: 60 } }),
21
+ },
22
+ },
23
+ });
package/docs/AUDIT.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Current audit status
2
2
 
3
+ The source dependency override and private hardened npm bootstrap pin brace-expansion 5.0.12 with the registry SHA-512 artifact integrity, addressing GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, and GHSA-6j4f-fj2g-mc7p. Consumer packages have no postinstall hook: the verified cf network transformation runs before private CLI execution. Real private builds declare the exact audited Wrangler provider and receive the executable runner.
4
+
3
5
  This file is a compact statement of current cross-cutting audit conclusions and residual limits. It is intentionally version-agnostic: release identity, candidate acceptance, publication state, and soak state are authoritative only in package metadata, release records, Git history/tags, and the external release systems they describe. This file is not a release-state authority.
4
6
 
5
7
  ## Current conclusions
@@ -9,19 +11,20 @@ This file is a compact statement of current cross-cutting audit conclusions and
9
11
  - **State publication is atomic and identity-sensitive.** Security-sensitive state readers reject symlinks, unexpected hard links, malformed content, permission failures, and persistent identity changes. Bounded retries are reserved for explicitly classified atomic-publication races; durable state is complete before it becomes visible.
10
12
  - **Durable execution owns continuity explicitly.** Managed jobs persist recovery state independently of the hosted MCP response. Idempotency keys bind uncertain acceptance retries only while the retained record exists; cancellation, dependency state, cleanup, output budgets, and terminal persistence have explicit fail-closed boundaries. Callers recover the same accepted job instead of replaying its side effect.
11
13
  - **Daemon-owned Node children use a verified runtime launcher.** A live concrete runtime executable is preferred; if it disappears after a package-manager upgrade, only the original absolute Node launcher is eligible as fallback. Diagnostics expose provenance booleans rather than executable paths, and an unavailable launcher requires service restart instead of PATH guessing.
12
- - **Relay recovery separates transport from execution semantics and from quota pacing.** WebSocket and signed HTTPS are transport paths, not permission or replay authorities. Their authenticated handshake/resume state is generation-scoped per transport, ended-generation control messages are discarded before reconciliation or interruption, and a protocol violation is routed only to its originating current transport. Ambiguous side effects are not transparently replayed. A verified healthy WSS schedules no recurring HTTPS fallback traffic; fallback starts only for startup grace, transport suspicion, or actual WSS loss, then uses five-second ready-idle polls and thirty-second successful-standby retries while handshake/queued work retains the 750 ms minimum request-start interval. Each accepted signed HTTP request or WebSocket preflight consumes one bounded Durable Object replay-nonce write, while a replay is rejected without another write. The checked-in Wrangler configuration has no Workers KV namespace binding, so repository evidence supports Worker/Durable Object amplification but does not by itself establish a Workers KV quota incident. Same-daemon terminal-result retention is bounded, cancellation removes ownership, and repeated reconnects cannot accumulate execution time or delivery grace. Fatal protocol diagnostics retain only sanitized error/source/generation-class/stage/disposition fields, while fallback takeover history remains privacy-bounded across both control paths.
14
+ - **Relay recovery separates transport from execution semantics and from quota pacing.** WebSocket and signed HTTPS are transport paths, not permission or replay authorities. Their authenticated handshake/resume state is generation-scoped per transport, ended-generation control messages are discarded before reconciliation or interruption, and a protocol violation is routed only to its originating current transport. Ambiguous side effects are not transparently replayed. A verified healthy WSS schedules no recurring HTTPS fallback traffic; fallback starts only for startup grace, transport suspicion, or actual WSS loss, then uses five-second ready-idle polls and thirty-second successful-standby retries while handshake/queued work retains the 750 ms minimum request-start interval. Each accepted signed HTTP request or WebSocket preflight consumes one bounded Durable Object replay-nonce write, while a replay is rejected without another write. The checked-in native Cloudflare deployment configuration has no Workers KV namespace binding, so repository evidence supports Worker/Durable Object amplification but does not by itself establish a Workers KV quota incident. Same-daemon terminal-result retention is bounded, cancellation removes ownership, and repeated reconnects cannot accumulate execution time or delivery grace. Fatal protocol diagnostics retain only sanitized error/source/generation-class/stage/disposition fields, while fallback takeover history remains privacy-bounded across both control paths.
13
15
  - **Operating-system sleep is an observable suspension boundary, not a network guarantee.** On supported macOS hosts, diagnostics correlate bounded sleep history with runtime stalls and recovered relay outages. Machine Bridge cannot keep JavaScript or a socket executing while the machine is suspended, and sleep correlation does not identify unrelated upstream network causes.
14
16
  - **Browser automation remains existing-profile and snapshot-bound.** The extension controls an already installed Chromium profile; hosted calls bind explicit tab identity, and later Computer Use actions bind the exact observation snapshot. Mutation response loss, timeout, extension replacement, or ambiguous trusted-input settlement yields an unknown/non-retryable boundary that must be inspected before any retry.
15
17
  - **Desktop automation is permission- and evidence-bound.** Application actions use structured Accessibility operations or an explicitly enabled snapshot-bound visual backend. The runtime does not silently substitute arbitrary scripting or unverified pixel guessing when required authority/evidence is unavailable.
16
18
  - **Logging and diagnostics are privacy-minimized by contract.** Runtime logs are structured and must not contain tool arguments, command output, credentials, account identity, arbitrary URLs, private paths, or other user content. Explicit local CLI/tool results are a separate user-requested output surface. Security-audit telemetry is content-free, bounded, and hash-chained; diagnostics expose coarse provenance/health rather than secrets or private values.
17
19
  - **Resource admission is cooperative scheduling, not a hostile-code sandbox.** CPU, memory, I/O, disk-reserve, queue, and result budgets protect service continuity, but ordinary execution still has the daemon OS user's ambient authority unless a dedicated low-privilege account or VM/container supplies the stronger boundary.
20
+ - **Deployment audits cover bundled code as well as the dependency graph.** The consumer runtime excludes cf, Wrangler, and Miniflare. Deployment uses an exact private toolchain whose registry signatures, dependency audit, and artifact bytes are checked before execution. The pinned cf bundle includes an older Undici implementation, so a zero-vulnerability dependency audit is insufficient: a hash-bound deterministic transformation routes that bundle to the patched external Undici, and behavior tests cover custom transport and TLS-verification preservation. Unknown artifact bytes fail closed. Structured cf authentication must be positive before unattended activation can mutate the Worker or service; source snapshots, generated manifests, Worker identity, SQLite export, preserved secrets, and rate-limit bindings are validated before upload. These local controls still require candidate-bound deployed-edge acceptance.
18
21
  - **Release and package evidence remain separate from audit prose.** Shipped-source or packed-document changes require synchronized package identity and fresh verification. Candidate acceptance, publication, activation, and stable promotion must be proven by their dedicated records and external systems; no sentence in this audit summary inherits or confers release authority.
19
22
 
20
23
  ## Relay quota incident evidence boundary
21
24
 
22
25
  The quota repair is intentionally split into repository evidence and external-platform evidence. Repository evidence establishes that the prior relay design could generate recurring signed HTTPS traffic even when WebSocket was healthy, that a ready HTTPS fallback polled every second, and that each accepted signed HTTP relay request plus each accepted WebSocket preflight consumed a bounded Durable Object replay-nonce write. The current design keeps HTTPS fallback stopped while WSS is verified healthy, uses a five-second ready-idle HTTPS cadence and thirty-second successful-standby cadence, and keeps handshake/queued traffic on the existing 750 ms minimum start interval. The request/nonce-only regression models idle HTTPS polling alongside continuous failed WSS reconnects; it is not an upper bound across readiness resets or traffic patterns. A separate executed idle-day regression includes the production nonce store and liveness-alarm scheduler, as documented in TESTING.md.
23
26
 
24
- The checked-in Wrangler configuration contains Durable Object bindings and no Workers KV namespace binding. That fact narrows the repository-level causal claim: source and local runtime evidence support Worker request amplification plus Durable Object persistence amplification, but they cannot prove that a Cloudflare dashboard item labeled KV came from this repository. Account-side usage/resource records remain the authority for that label. Likewise, local full verification proves only the frozen source candidate; candidate activation, public Worker identity, relay readiness, representative durable execution, and post-activation quota-sensitive behavior require separate live evidence.
27
+ The checked-in native Cloudflare deployment configuration contains Durable Object bindings and no Workers KV namespace binding. That fact narrows the repository-level causal claim: source and local runtime evidence support Worker request amplification plus Durable Object persistence amplification, but they cannot prove that a Cloudflare dashboard item labeled KV came from this repository. Account-side usage/resource records remain the authority for that label. Likewise, local full verification proves only the frozen source candidate; candidate activation, public Worker identity, relay readiness, representative durable execution, and post-activation quota-sensitive behavior require separate live evidence.
25
28
 
26
29
  A reported account-wide Workers/KV quota event must be attributed using provider counters by UTC day, script, namespace, and deployed bindings before assigning a project root cause. The checked-in and inspected deployed bridge use SQLite-backed Durable Object storage, not a Workers KV namespace; another workload can exhaust the shared account allowance. HTTP polling and nonce/alarm mutations are separate resource-amplification risks that require their own regression budgets. Provider identifiers, account data, raw credentials, and request contents belong outside public repository evidence. Local scenario counts do not establish provider billing or a completed production soak.
27
30
 
@@ -8,7 +8,7 @@ machine-mcp doctor
8
8
  machine-mcp service status
9
9
  ```
10
10
 
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.
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 audited private cf binary, structured 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
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
 
@@ -24,9 +24,9 @@ Process timeout cleanup is ownership-bound rather than parent-liveness-bound. On
24
24
 
25
25
  `/healthz` and `/` are answered by the outer Worker and do not consume Durable Object request volume. If MCP or daemon routes return `503 durable_object_quota_exceeded` (or Cloudflare 1101 with Durable Objects free-tier exhaustion in Worker tails), wait for the daily UTC free-tier reset or move the account off the free DO plan; do not treat that as a failed script deploy when `/healthz` still reports the expected version.
26
26
 
27
- Wrangler upload and public health verification are two separate observations. Once Wrangler reports a successful deployment and supplies the `workers.dev` URL, Machine Bridge immediately records that URL together with the exact deployment fingerprint and package version. Beta.43 upgrades this fingerprint to length-framed v5 and therefore treats earlier hashes as stale once; it verifies or redeploys the same-name Worker rather than creating another Worker. Required Worker/shared/config inputs must be readable ordinary repository files or directories. Missing, inaccessible, symlinked, hard-linked, or special paths stop before upload. It then verifies `/healthz` through the standard `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` environment route. If that secondary probe times out or encounters a proxy, TLS, network, or temporary HTTP 5xx failure, startup stops with an actionable error, but the successful deployment evidence remains. The next ordinary start verifies the same Worker and does not repeat the upload.
27
+ cf upload and public health verification are two separate observations. Once cf reports a successful deployment and supplies the `workers.dev` URL, Machine Bridge immediately records that URL together with the exact deployment fingerprint and package version. The length-framed v6 fingerprint includes native deployment/build configuration and the exact private toolchain templates, so earlier hashes are treated as stale once; the migration verifies or redeploys the same-name Worker rather than creating another Worker. Required Worker/shared/config inputs must be readable ordinary repository files or directories. Missing, inaccessible, symlinked, hard-linked, or special paths stop before upload. One immutable bounded snapshot supplies both the fingerprint and private `cf build`; generated output must retain the same `BridgeRoom` SQLite export, self-binding, rate-limit namespace identities, and binding-preservation metadata. It then verifies `/healthz` through the standard `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` environment route. If that secondary probe times out or encounters a proxy, TLS, network, or temporary HTTP 5xx failure, startup stops with an actionable error, but the successful deployment evidence remains. The next ordinary start verifies the same Worker and does not repeat the upload.
28
28
 
29
- Interactive Wrangler login is deliberately unavailable in machine-readable Worker deployment. A normal non-JSON local startup may run login and then must re-run `whoami` before deployment; JSON mode returns `worker_authentication_required` instead. Persistent candidate activation adds an outer authentication preflight before the service handoff: detached managed-job activation requires an already authenticated Wrangler session and fails before live mutation otherwise, while an ordinary owner terminal may complete login before the old login service is stopped. This prevents a ten-minute interactive authentication flow from becoming part of a service-offline transaction or writing non-JSON text into the activation result channel.
29
+ Interactive cf login is deliberately unavailable in machine-readable Worker deployment. Wrangler credentials alone do not establish cf authentication: `cf auth whoami` must return bounded valid JSON with `authenticated === true`, and an explicitly invalid token is rejected. A normal non-JSON local startup may run `cf auth login` with the required Worker scopes and then must re-run structured `whoami` before deployment; JSON mode returns `worker_authentication_required` instead. Persistent candidate activation adds an outer authentication preflight before the service handoff: detached managed-job activation requires an already authenticated cf session and fails before live mutation otherwise, while an ordinary owner terminal may complete login before the old login service is stopped. The selected account must be explicitly accessible or the only accessible account; ambiguous account selection fails before build/upload. This keeps interactive authentication outside the service-offline transaction and preserves the activation JSON result channel.
30
30
 
31
31
  Automatic redeployment is limited to bounded health evidence that the recorded endpoint is genuinely stale: a persistent package-version mismatch, an unexpected Machine Bridge identity, or a persistent `404`/`410`. Unreachability is not proof of absence. `--force-worker` remains the explicit override when an operator deliberately wants an upload despite matching state.
32
32
 
@@ -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. 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.
174
+ The published runtime package does not install cf, 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.12 registry tarballs, then uses that CLI to install the exact private cf/Wrangler toolchain under its owner-only state root. It audits the resulting production tree, verifies registry signatures, and validates the exact deterministic cf network-compatibility transformation 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
 
@@ -82,6 +82,12 @@ The broker is loopback-only, but loopback is not treated as a per-user authentic
82
82
 
83
83
  The extension still operates inside a real browser profile. It can reach tabs, authenticated sessions, and origins granted by browser permissions. Only the `owner` role may use browser-session control or export local data into it. Computer Use does not increase that role ceiling: its browser observations bind semantic refs, document/frame epochs, optional screenshot evidence, and private backend-node identity to one bounded snapshot, then consume that snapshot as one-shot mutation authority. Transport loss after a browser mutation is attempted is explicitly unknown and never safe replay evidence. Screenshot-bound point actions additionally require exact current screenshot/viewport/document evidence; callers cannot submit raw CDP methods, backend-node IDs, or coordinates that are not bound to the referenced snapshot.
84
84
 
85
+ ### Package runtime to Cloudflare deployment
86
+
87
+ The deployment toolchain has local-owner code execution and Cloudflare upload authority. The published consumer dependency graph excludes cf, Wrangler, and Miniflare; a private owner-only toolchain retains exact registry-backed versions and lockfile bytes. Registry signatures and a dependency audit do not prove bundled-code safety. The pinned cf artifact therefore has a hash-bound, bounded, atomic compatibility transformation that routes its embedded network factory to patched external Undici; unknown bytes or filesystem identity/mode violations stop execution. Regression evidence includes custom connector and TLS-verification behavior without external network access.
88
+
89
+ Unattended activation requires positive structured cf authentication before any live Worker/service mutation. CLI exit status alone, Wrangler login, malformed/truncated JSON, a truthy non-boolean value, or an explicitly invalid token cannot satisfy that gate. Account ambiguity is rejected. A bounded immutable source snapshot supplies both the deployment HMAC and the private build, and the generated manifest must retain the selected Worker name, the `BridgeRoom` SQLite export/self-binding, rate-limit identities, and explicit secret/plain-text/JSON binding preservation. Successful upload evidence is recorded before the subsequent health read; a transient health failure cannot authorize a duplicate upload. Local output checks cannot prove provider-side namespace continuity or successful live authentication, so candidate-bound deployed-edge canary and acceptance remain separate requirements.
90
+
85
91
  ### Local runtime to persistent state
86
92
 
87
93
  State paths, roots, locks, plans, resources, native broker files, and service definitions are security-sensitive.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "3.0.0-beta.196",
3
+ "version": "3.0.0-beta.197",
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",
@@ -86,7 +86,9 @@
86
86
  "docs/THREAT_MODEL.md",
87
87
  "docs/TOOL_REFERENCE.md",
88
88
  "docs/UPGRADING.md",
89
- "LICENSE"
89
+ "LICENSE",
90
+ "cloudflare.config.ts",
91
+ "wrangler.config.ts"
90
92
  ],
91
93
  "scripts": {
92
94
  "start": "node bin/machine-mcp.mjs start",
@@ -262,7 +264,8 @@
262
264
  "browser-pairing-launch:test": "node tests/browser-pairing-launch-test.mjs",
263
265
  "browser-cli-pairing:test": "node tests/browser-cli-pairing-test.mjs",
264
266
  "resource-admission:test": "node tests/resource-admission-test.mjs",
265
- "resource-build-root:test": "node tests/resource-build-root-test.mjs"
267
+ "resource-build-root:test": "node tests/resource-build-root-test.mjs",
268
+ "cf-network:test": "node tests/cf-network-compatibility-test.mjs"
266
269
  },
267
270
  "dependencies": {
268
271
  "https-proxy-agent": "9.1.0",
@@ -275,7 +278,9 @@
275
278
  "fast-check": "4.10.0",
276
279
  "globals": "17.11.0",
277
280
  "typescript": "7.0.2",
278
- "wrangler": "4.131.2"
281
+ "wrangler": "4.144.0",
282
+ "cf": "1.0.0-beta.5",
283
+ "undici": "7.29.1"
279
284
  },
280
285
  "keywords": [
281
286
  "mcp",
@@ -307,7 +312,7 @@
307
312
  "esbuild@0.28.1": true,
308
313
  "fsevents": false,
309
314
  "sharp@0.35.4": true,
310
- "workerd@1.20260911.1": true
315
+ "workerd@1.20260926.1": true
311
316
  },
312
317
  "packageManager": "npm@12.0.2",
313
318
  "devEngines": {
@@ -324,7 +329,7 @@
324
329
  },
325
330
  "overrides": {
326
331
  "sharp": "0.35.4",
327
- "brace-expansion": "5.0.9",
328
- "undici": "7.29.0"
332
+ "brace-expansion": "5.0.12",
333
+ "undici": "7.29.1"
329
334
  }
330
335
  }
@@ -1,6 +1,7 @@
1
1
  import { normalizeActivationRecovery } from "../src/shared/activation-recovery.mjs";
2
2
  import { EXECUTION_SURFACE, executionSurface } from "../src/local/execution-surface.mjs";
3
- import { runWrangler as defaultRunWrangler } from "../src/local/shell.mjs";
3
+ import { runCf as defaultRunCf } from "../src/local/shell.mjs";
4
+ import { ensureCfAuthenticated } from "../src/local/cf-authentication.mjs";
4
5
 
5
6
  export function assertPersistentActivationExecutionSurface(environment = process.env) {
6
7
  const surface = executionSurface(environment);
@@ -27,31 +28,18 @@ export function persistentActivationSpawnOptions({ cwd, env = process.env } = {}
27
28
  }
28
29
 
29
30
  export async function preflightPersistentActivationWorkerAuth({
30
- surface = "local",
31
- stateRoot,
32
- packageRoot,
33
- npmCli,
34
- env = process.env,
35
- runWrangler = defaultRunWrangler,
31
+ surface = "local", stateRoot, packageRoot, npmCli, env = process.env, runCf = defaultRunCf,
36
32
  } = {}) {
37
33
  if (surface !== "local" && surface !== EXECUTION_SURFACE.managedJob) {
38
- throw new TypeError("persistent activation Wrangler preflight requires a local or managed-job execution surface");
34
+ throw new TypeError("persistent activation cf preflight requires a local or managed-job execution surface");
39
35
  }
40
- if (typeof stateRoot !== "string" || !stateRoot) throw new TypeError("persistent activation Wrangler preflight requires stateRoot");
41
- if (typeof packageRoot !== "string" || !packageRoot) throw new TypeError("persistent activation Wrangler preflight requires packageRoot");
42
- if (typeof runWrangler !== "function") throw new TypeError("persistent activation Wrangler preflight requires runWrangler");
43
- const shared = { stateRoot, packageRoot, npmCli, env };
44
- const whoami = await runWrangler(["whoami"], { ...shared, capture: true, allowFailure: true });
45
- if (whoami?.code === 0) return { authenticated: true, login_performed: false };
46
- if (surface === EXECUTION_SURFACE.managedJob) throw workerAuthenticationRequiredError(
47
- "persistent activation cannot start interactive Wrangler login from a detached managed job; authenticate Cloudflare Wrangler in an ordinary owner terminal before retrying",
48
- );
49
- await runWrangler(["login"], shared);
50
- const verified = await runWrangler(["whoami"], { ...shared, capture: true, allowFailure: true });
51
- if (verified?.code !== 0) throw workerAuthenticationRequiredError(
52
- "Cloudflare Wrangler login completed without a verifiable authenticated session; the persistent activation was not started",
53
- );
54
- return { authenticated: true, login_performed: true };
36
+ if (typeof stateRoot !== "string" || !stateRoot) throw new TypeError("persistent activation cf preflight requires stateRoot");
37
+ if (typeof packageRoot !== "string" || !packageRoot) throw new TypeError("persistent activation cf preflight requires packageRoot");
38
+ if (typeof runCf !== "function") throw new TypeError("persistent activation cf preflight requires runCf");
39
+ const auth = await ensureCfAuthenticated({
40
+ runCf, shared: { stateRoot, packageRoot, npmCli, env }, interactive: surface === "local",
41
+ });
42
+ return { authenticated: true, login_performed: auth.login_performed };
55
43
  }
56
44
 
57
45
  export function persistentCandidateFailureMessage(output, { cli, stateRoot, previousRuntime = null } = {}) {
@@ -92,10 +80,3 @@ export function validateActivationRecoveryPayload(value) {
92
80
  throw new Error(`persistent ${String(error?.message || "activation recovery metadata is invalid")}`, { cause: error });
93
81
  }
94
82
  }
95
-
96
- function workerAuthenticationRequiredError(message) {
97
- const error = new Error(message);
98
- error.code = "worker_authentication_required";
99
- error.sideEffectsStarted = false;
100
- return error;
101
- }
@@ -0,0 +1,57 @@
1
+ import { runCf as defaultRunCf } from "./shell.mjs";
2
+
3
+ const CF_DEPLOYMENT_SCOPES = Object.freeze(["account-settings.read","user-details.read","workers-scripts.read","workers-scripts.write","workers-scripts.bind","workers-routes.read","workers-routes.write","workers-observability.read","workers-observability.write","offline"]);
4
+
5
+ export function cfAuthenticationResult(result = {}) {
6
+ if (result.code !== 0 || result.stdout_truncated_bytes || result.stderr_truncated_bytes
7
+ || typeof result.stdout !== "string" || Buffer.byteLength(result.stdout) > 128 * 1024) {
8
+ return { authenticated: false, accounts: [] };
9
+ }
10
+ let value;
11
+ try { value = JSON.parse(result.stdout); }
12
+ catch { return { authenticated: false, accounts: [] }; }
13
+ if (!value || Array.isArray(value) || value.authenticated !== true || value.tokenValid === false) {
14
+ return { authenticated: false, accounts: [] };
15
+ }
16
+ const accounts = Array.isArray(value.accounts) ? value.accounts
17
+ .filter(account => account && /^[a-f0-9]{32}$/i.test(account.id))
18
+ .map(account => ({ id: account.id })) : [];
19
+ return { authenticated: true, accounts };
20
+ }
21
+
22
+ export async function ensureCfAuthenticated({
23
+ runCf = defaultRunCf, shared = {}, interactive = false, logger = console,
24
+ } = {}) {
25
+ const probe = () => runCf(["auth", "whoami"], { ...shared, capture: true, allowFailure: true, maxOutputBytes: 128 * 1024 });
26
+ let auth = cfAuthenticationResult(await probe());
27
+ if (auth.authenticated) return { ...auth, login_performed: false };
28
+ if (!interactive) throw cfAuthenticationRequiredError();
29
+ logger.info?.("Cloudflare cf is not logged in; opening Cloudflare login");
30
+ const login = ["auth", "login"];
31
+ await runCf([...login, "--scopes", ...CF_DEPLOYMENT_SCOPES], shared);
32
+ auth = cfAuthenticationResult(await probe());
33
+ if (!auth.authenticated) throw cfAuthenticationRequiredError();
34
+ return { ...auth, login_performed: true };
35
+ }
36
+
37
+ export function cfDeploymentAccount(auth, environment = process.env) {
38
+ const configured = environment.CLOUDFLARE_ACCOUNT_ID;
39
+ if (configured) {
40
+ if (!/^[a-f0-9]{32}$/i.test(configured)
41
+ || (auth.accounts.length && !auth.accounts.some(account => account.id === configured))) {
42
+ throw new Error("CLOUDFLARE_ACCOUNT_ID does not identify an accessible Cloudflare account");
43
+ }
44
+ return configured;
45
+ }
46
+ if (auth.accounts.length === 1) return auth.accounts[0].id;
47
+ throw new Error("Set CLOUDFLARE_ACCOUNT_ID to the existing Worker's account before deploying with cf");
48
+ }
49
+
50
+ export function cfAuthenticationRequiredError() {
51
+ const error = new Error(
52
+ "Cloudflare cf is not authenticated; unattended activation will not start interactive login. Complete cf auth login in an ordinary owner terminal before retrying.",
53
+ );
54
+ error.code = "worker_authentication_required";
55
+ error.sideEffectsStarted = false;
56
+ return error;
57
+ }
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
6
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
7
+ import { privateToolchainIntegrityError, throwOperationalOrIntegrity } from "./private-toolchain-integrity.mjs";
8
+
9
+ export const CF_NETWORK_COMPATIBILITY = Object.freeze({
10
+ version: "1.0.0-beta.5",
11
+ undici: "7.29.1",
12
+ bundle: "dist/dist-DfMipWKR.mjs",
13
+ originalSha256: "5d55682ff9886ca43946efaea575b4f00438df592ef20db1faaa85493e1929c7",
14
+ patchedSha256: "719025f199195ddd646e67473c406740de8ac2fa38f5de244cceb960459843f1",
15
+ });
16
+ const MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
17
+ const FACTORY_START = 869268;
18
+ const FACTORY_END = 873255;
19
+ const PATCH_IMPORT = 'import __mbmUndici from "undici";\n';
20
+
21
+ export function transformCfNetworkBundle(value) {
22
+ if (typeof value !== "string" && !Buffer.isBuffer(value)) throw new TypeError("cf bundle must be text or bytes");
23
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
24
+ if (bytes.length > MAX_BUNDLE_BYTES) throw privateToolchainIntegrityError("cf network bundle exceeds its byte limit");
25
+ const digest = sha256(bytes);
26
+ if (digest === CF_NETWORK_COMPATIBILITY.patchedSha256) return bytes.toString("utf8");
27
+ if (digest !== CF_NETWORK_COMPATIBILITY.originalSha256) {
28
+ throw privateToolchainIntegrityError("cf network bundle does not match the pinned upstream or patched artifact");
29
+ }
30
+ const source = bytes.toString("utf8");
31
+ const firstLineEnd = source.indexOf("\n") + 1;
32
+ if (!source.startsWith("#!/usr/bin/env node\n")
33
+ || source.slice(FACTORY_START, FACTORY_START + 5) !== "Ry=e("
34
+ || source.slice(FACTORY_END, FACTORY_END + 9) !== ",zy=Ry();") {
35
+ throw privateToolchainIntegrityError("cf network factory layout is not the reviewed layout");
36
+ }
37
+ const replaced = source.slice(0, FACTORY_START) + "Ry=()=>__mbmUndici" + source.slice(FACTORY_END);
38
+ const patched = replaced.slice(0, firstLineEnd) + PATCH_IMPORT + replaced.slice(firstLineEnd);
39
+ if (sha256(Buffer.from(patched, "utf8")) !== CF_NETWORK_COMPATIBILITY.patchedSha256) {
40
+ throw privateToolchainIntegrityError("cf network transformation did not produce the reviewed artifact");
41
+ }
42
+ return patched;
43
+ }
44
+
45
+ export function applyCfNetworkCompatibility(root, { allowAbsent = false } = {}) {
46
+ const canonicalRoot = realpathSync(path.resolve(String(root)));
47
+ const cfRoot = path.join(canonicalRoot, "node_modules", "cf");
48
+ let cfInfo;
49
+ try { cfInfo = lstatSync(cfRoot); }
50
+ catch (error) {
51
+ if (allowAbsent && error?.code === "ENOENT") return { present: false, changed: false };
52
+ throwOperationalOrIntegrity(error, "cf package is missing or structurally invalid");
53
+ }
54
+ if (cfInfo.isSymbolicLink() || !cfInfo.isDirectory()) throw privateToolchainIntegrityError("cf package must be a real directory");
55
+ const manifestPath = path.join(cfRoot, "package.json");
56
+ requireContainedRegularFile(canonicalRoot, manifestPath);
57
+ const manifest = JSON.parse(readBoundedRegularFileSync(manifestPath, 16 * 1024, "cf package manifest", {
58
+ verifyPathIdentity: true, rejectMultipleLinks: true,
59
+ }).toString("utf8"));
60
+ if (manifest.name !== "cf" || manifest.version !== CF_NETWORK_COMPATIBILITY.version) {
61
+ throw privateToolchainIntegrityError("cf package version does not match the reviewed compatibility artifact");
62
+ }
63
+ const file = path.join(cfRoot, CF_NETWORK_COMPATIBILITY.bundle);
64
+ requireContainedRegularFile(canonicalRoot, file);
65
+ const before = readBoundedRegularFileSync(file, MAX_BUNDLE_BYTES, "cf network bundle", {
66
+ verifyPathIdentity: true, rejectMultipleLinks: true,
67
+ });
68
+ const patched = transformCfNetworkBundle(before);
69
+ const changed = !before.equals(Buffer.from(patched, "utf8"));
70
+ if (changed) replaceFileAtomicallySync(file, patched, { mode: 0o600 });
71
+ requireContainedRegularFile(canonicalRoot, file);
72
+ const verified = readBoundedRegularFileSync(file, MAX_BUNDLE_BYTES, "patched cf network bundle", {
73
+ verifyPathIdentity: true, rejectMultipleLinks: true,
74
+ });
75
+ if (sha256(verified) !== CF_NETWORK_COMPATIBILITY.patchedSha256) {
76
+ throw privateToolchainIntegrityError("cf network bundle changed before verification");
77
+ }
78
+ return { present: true, changed, version: manifest.version, bundleSha256: CF_NETWORK_COMPATIBILITY.patchedSha256 };
79
+ }
80
+
81
+ function requireContainedRegularFile(root, file) {
82
+ let info;
83
+ try { info = lstatSync(file); }
84
+ catch (error) { throwOperationalOrIntegrity(error, "cf artifact is missing or structurally invalid"); }
85
+ const relative = path.relative(root, realpathSync(file));
86
+ if (info.isSymbolicLink() || !info.isFile() || Number(info.nlink) !== 1
87
+ || !relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
88
+ || (process.platform !== "win32" && (Number(info.mode) & 0o022) !== 0)) {
89
+ throw privateToolchainIntegrityError("cf artifact must be a contained private regular file");
90
+ }
91
+ }
92
+
93
+ function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
94
+
95
+ // A consumer installation omits cf. Source installations patch the exact dev
96
+ // dependency before its public bin or config imports can run.
97
+ if (process.argv[2] === "--install-if-present" && process.argv[1]
98
+ && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
99
+ applyCfNetworkCompatibility(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."), { allowAbsent: true });
100
+ }
@@ -0,0 +1,81 @@
1
+ import { chmodSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureCloudflareToolchain } from "./wrangler-toolchain.mjs";
4
+ import { runCf as defaultRunCf, runExecutable } from "./shell.mjs";
5
+ import { packageRoot } from "./package-identity.mjs";
6
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
7
+
8
+ const PRESERVED_BINDING_TYPES = ["plain_text", "json", "secret_text", "secret_key"];
9
+
10
+ export async function withCfProject(snapshot, workerName, callback, options = {}) {
11
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(workerName)) throw new Error("Invalid Worker deployment name");
12
+ const toolchainRoot = await (options.ensureToolchain || ensureCloudflareToolchain)({
13
+ stateRoot: options.stateRoot, packageRoot: options.packageRoot || packageRoot,
14
+ env: options.env || process.env, runCommand: options.runCommand || runExecutable,
15
+ });
16
+ const toolchainManifest = JSON.parse(readBoundedRegularFileSync(path.join(toolchainRoot, "package.json"),
17
+ 128 * 1024, "Cloudflare toolchain manifest", { verifyPathIdentity: true, rejectMultipleLinks: true }));
18
+ const builderVersion = toolchainManifest.dependencies.wrangler;
19
+ const directory = mkdtempSync(path.join(toolchainRoot, "cf-project-"));
20
+ chmodSync(directory, 0o700);
21
+ try {
22
+ for (const file of snapshot.files) {
23
+ const target = path.resolve(directory, file.path);
24
+ const relative = path.relative(directory, target);
25
+ if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) {
26
+ throw new Error("Worker deployment snapshot escapes the private project");
27
+ }
28
+ mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
29
+ writeFileSync(target, file.content, { flag: "wx", mode: 0o600 });
30
+ }
31
+ writeFileSync(path.join(directory, "package.json"), JSON.stringify({ private: true, type: "module", devDependencies: { wrangler: builderVersion } }), { flag: "wx", mode: 0o600 });
32
+ symlinkSync(path.join(toolchainRoot, "node_modules"), path.join(directory, "node_modules"), "dir");
33
+ const project = {
34
+ cwd: directory,
35
+ env: { ...(options.env || process.env), MBM_WORKER_NAME: workerName, CI: "1", WRANGLER_SEND_METRICS: "false" },
36
+ };
37
+ const runCf = options.runCf || defaultRunCf;
38
+ await runCf(["build"], { ...project, stateRoot: options.stateRoot, packageRoot: options.packageRoot || packageRoot,
39
+ capture: true, hardTimeout: true, timeoutMs: options.buildTimeoutMs ?? 120_000 });
40
+ validateCfBuildOutput(directory, workerName);
41
+ return await callback(project);
42
+ } finally {
43
+ rmSync(directory, { recursive: true, force: true, maxRetries: 6, retryDelay: 30 });
44
+ }
45
+ }
46
+
47
+ export function validateCfBuildOutput(directory, workerName) {
48
+ const configPath = ".cloudflare/output/v0/workers/default/worker.config.json";
49
+ const config = JSON.parse(readProjectArtifact(directory, configPath, 128 * 1024));
50
+ const kept = config.unsafe?.metadata?.keep_bindings;
51
+ if (config.name !== workerName || config.exports?.BridgeRoom?.storage !== "sqlite"
52
+ || config.exports.BridgeRoom.type !== "durable-object"
53
+ || config.env?.BRIDGE?.worker !== workerName || config.env.BRIDGE.exportName !== "BridgeRoom"
54
+ || config.env.STATEFUL_GLOBAL_RATE_LIMITER?.namespace !== "4301702"
55
+ || config.env.STATEFUL_RATE_LIMITER?.namespace !== "4301701"
56
+ || !Array.isArray(kept) || kept.length !== PRESERVED_BINDING_TYPES.length
57
+ || PRESERVED_BINDING_TYPES.some(type => !kept.includes(type))
58
+ || config.manifest?.type !== "complete" || config.manifest.mainModule !== "index.js"
59
+ || config.manifest.modules?.["index.js"]?.type !== "esm") {
60
+ throw new Error("cf Build Output does not preserve the Machine Bridge deployment contract");
61
+ }
62
+ const prefix = ".cloudflare/output/v0/workers/default/bundle/";
63
+ for (const module of Object.keys(config.manifest.modules)) readProjectArtifact(directory, prefix + module, 16 * 1024 * 1024);
64
+ return config;
65
+ }
66
+
67
+ function readProjectArtifact(directory, relativePath, limit) {
68
+ const root = realpathSync(directory);
69
+ const target = path.resolve(root, relativePath);
70
+ const relative = path.relative(root, target);
71
+ if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) {
72
+ throw new Error("cf Build Output artifact escapes the private project");
73
+ }
74
+ let current = root;
75
+ for (const part of relative.split(path.sep)) {
76
+ current = path.join(current, part);
77
+ if (lstatSync(current).isSymbolicLink()) throw new Error("cf Build Output artifact must not be a symbolic link");
78
+ }
79
+ return readBoundedRegularFileSync(target, limit, "cf Build Output artifact",
80
+ { verifyPathIdentity: true, rejectMultipleLinks: true });
81
+ }
package/src/local/cli.mjs CHANGED
@@ -19,7 +19,8 @@ export { resolvePolicy } from "./cli-policy.mjs";
19
19
  export { parseArgs, validateCommandOptions, validateLoggingOptions, validatePositionals } from "./cli-options.mjs";
20
20
  import { classifyOperationalError, createLogger, sanitizeLogText } from "./log.mjs";
21
21
  import { formatPolicySummary, printMcpConnection, printStartJson } from "./cli-ready-output.mjs";
22
- import { runExecutable, runWrangler } from "./shell.mjs";
22
+ import { runCf, runExecutable, runWrangler } from "./shell.mjs";
23
+ import { cfAuthenticationResult } from "./cf-authentication.mjs";
23
24
  import { runFullAccessTest } from "./full-access-test.mjs";
24
25
  import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
25
26
  import { stopOwnedPlatformService } from "./service-ownership.mjs";
@@ -488,10 +489,11 @@ async function doctorCommand(args) {
488
489
  const npm = await runExecutable(npmCommand.file, npmCommand.args, { capture: true, allowFailure: true, timeoutMs: 10_000 });
489
490
  const npmDetail = sanitizeLines(npm.stdout || npm.stderr);
490
491
  checks.push({ name: "npm", ok: npm.code === 0 && isSupportedNpmVersion(npmDetail), detail: npmDetail || "unavailable" });
491
- const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true, stateRoot: state.paths.stateRoot });
492
- checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
493
- const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true, stateRoot: state.paths.stateRoot });
494
- checks.push({ name: "cloudflare-login", ok: whoami.code === 0, detail: whoami.code === 0 ? "authenticated" : sanitizeLines(whoami.stderr || whoami.stdout) });
492
+ const cf = await runCf(["--version"], { capture: true, allowFailure: true, stateRoot: state.paths.stateRoot });
493
+ checks.push({ name: "cf", ok: cf.code === 0, detail: sanitizeLines(cf.stdout || cf.stderr) });
494
+ const whoami = await runCf(["auth", "whoami"], { capture: true, allowFailure: true, stateRoot: state.paths.stateRoot });
495
+ const authenticated = cfAuthenticationResult(whoami).authenticated;
496
+ checks.push({ name: "cloudflare-login", ok: authenticated, detail: authenticated ? "authenticated" : "cf authentication required" });
495
497
  checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
496
498
  checks.push({
497
499
  name: "authorization-model",
@@ -812,7 +814,7 @@ Commands:
812
814
  service stop Stop the installed autostart service
813
815
  service uninstall Remove only the autostart entry
814
816
  status Print redacted local profile state and Worker health
815
- doctor Check Node, Wrangler, Cloudflare login, Worker health
817
+ doctor Check Node, cf, Cloudflare login, Worker health
816
818
  full-test Run real local full-profile capability tests in a temporary sandbox
817
819
  rotate-secrets Rotate account-admin, device identity, and global token-version secrets
818
820
  account list|clients|revoke-client|add|role|enable|disable|rotate-password|remove