machine-bridge-mcp 3.0.0-beta.197 → 3.0.0-beta.198
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 +29 -9
- package/README.md +16 -2
- package/SECURITY.md +1 -1
- package/bin/machine-mcp.mjs +2 -2
- package/browser-extension/browser-operations.js +57 -37
- package/browser-extension/manifest.json +1 -1
- package/browser-extension/page-automation.js +49 -27
- package/browser-extension/service-worker.js +3 -9
- package/docs/AGENT_CONTEXT.md +3 -16
- package/docs/AUDIT.md +28 -5
- package/docs/CLIENTS.md +2 -11
- package/docs/COMPUTER_USE.md +3 -3
- package/docs/GETTING_STARTED.md +1 -10
- package/docs/LOCAL_AUTHORIZATION.md +1 -1
- package/docs/LOGGING.md +10 -7
- package/docs/MANAGED_JOBS.md +3 -3
- package/docs/OPERATIONS.md +32 -14
- package/docs/THREAT_MODEL.md +2 -2
- package/docs/UPGRADING.md +17 -3
- package/mbm.cmd +3 -3
- package/native/macos/MachineBridgeTrustBroker.swift +52 -8
- package/package.json +5 -4
- package/scripts/consumer-package-security.mjs +73 -48
- package/scripts/github-release.mjs +4 -4
- package/scripts/hardened-npm-session.mjs +2 -0
- package/scripts/install-published-prerelease.mjs +1 -1
- package/scripts/publish-npm.mjs +12 -12
- package/scripts/release-channel.mjs +1 -1
- package/scripts/start-release-candidate.mjs +1 -1
- package/src/local/account-admin.mjs +2 -1
- package/src/local/agent-context-projection.mjs +4 -1
- package/src/local/agent-context.mjs +11 -10
- package/src/local/agent-skill-discovery.mjs +49 -14
- package/src/local/agent-text-file.mjs +38 -13
- package/src/local/app-automation.mjs +71 -120
- package/src/local/browser-broker-server.mjs +8 -2
- package/src/local/browser-command.mjs +3 -1
- package/src/local/browser-computer-observation-service.mjs +10 -17
- package/src/local/browser-operation-service.mjs +8 -13
- package/src/local/cf-authentication.mjs +8 -7
- package/src/local/cli-local-admin.mjs +4 -9
- package/src/local/cli.mjs +16 -22
- package/src/local/computer-use-observation-contract.mjs +53 -13
- package/src/local/computer-use-observation.mjs +19 -9
- package/src/local/computer-use.mjs +6 -3
- package/src/local/daemon-http-relay-connection.mjs +7 -4
- package/src/local/daemon-http-relay-sequence.mjs +6 -4
- package/src/local/delegated-process-sandbox.mjs +28 -6
- package/src/local/device-identity.mjs +1 -1
- package/src/local/durable-process-spec.mjs +2 -6
- package/src/local/exclusive-publication-recovery.mjs +44 -1
- package/src/local/full-access-test.mjs +11 -5
- package/src/local/git-metadata-tree-safety.mjs +20 -20
- package/src/local/hardened-npm-launcher.mjs +48 -0
- package/src/local/job-runner.mjs +7 -2
- package/src/local/loopback-health.mjs +5 -0
- package/src/local/macos-trust-broker.mjs +15 -15
- package/src/local/managed-job-output-redaction.mjs +21 -10
- package/src/local/managed-job-runner.mjs +11 -11
- package/src/local/npm-audit-report.mjs +21 -0
- package/src/local/npm-environment.mjs +19 -25
- package/src/local/observability.mjs +4 -7
- package/src/local/process-contract.mjs +9 -0
- package/src/local/process-execution.mjs +14 -12
- package/src/local/process-sessions.mjs +7 -4
- package/src/local/relay-connection.mjs +1 -2
- package/src/local/resource-operations.mjs +2 -2
- package/src/local/runtime-activation.mjs +13 -8
- package/src/local/runtime-relay-connection-options.mjs +13 -6
- package/src/local/runtime.mjs +6 -1
- package/src/local/secure-file.mjs +4 -4
- package/src/local/service-convergence.mjs +1 -1
- package/src/local/service-ownership.mjs +1 -1
- package/src/local/shell.mjs +2 -1
- package/src/local/ssh-key.mjs +15 -19
- package/src/local/state.mjs +6 -5
- package/src/local/stdio.mjs +2 -2
- package/src/local/windows-service.mjs +13 -7
- package/src/local/worker-deployment-fingerprint.mjs +32 -12
- package/src/local/worker-deployment.mjs +40 -18
- package/src/local/workspace-file-service.mjs +4 -4
- package/src/local/workspace-file-transaction.mjs +5 -5
- package/src/local/workspace-profile-migration.mjs +2 -0
- package/src/local/wrangler-toolchain.mjs +39 -43
- package/src/shared/activation-recovery.mjs +2 -2
- package/src/shared/authority-revocation.mjs +8 -7
- package/src/shared/log-redaction.mjs +15 -2
- package/src/shared/sensitive-value-patterns.mjs +2 -2
- package/src/shared/tool-argument-validation.d.mts +1 -1
- package/src/worker/authority-revocations.ts +50 -16
- package/src/worker/daemon-http-channel.ts +18 -9
- package/src/worker/daemon-http-controller.ts +24 -7
- package/src/worker/daemon-ready-dispatch.ts +2 -0
- package/src/worker/daemon-ready-waiters.ts +1 -1
- package/src/worker/daemon-registry.ts +1 -1
- package/src/worker/daemon-relay-diagnostics.ts +1 -0
- package/src/worker/device-session-verifier.ts +1 -1
- package/src/worker/dpop.ts +11 -6
- package/src/worker/http.ts +5 -1
- package/src/worker/index.ts +42 -41
- package/src/worker/mcp-job-monitor-store.ts +2 -2
- package/src/worker/mcp-job-monitor-ui.ts +5 -5
- package/src/worker/mcp-response-stream.ts +13 -14
- package/src/worker/oauth-controller.ts +30 -37
- package/src/worker/oauth-refresh-exchange.ts +6 -2
- package/src/worker/oauth-refresh-families.ts +18 -5
- package/src/worker/oauth-state.ts +7 -1
- package/src/worker/oauth-token-issuance.ts +6 -9
- package/src/worker/oauth-tokens.ts +37 -1
- package/src/worker/observability.ts +5 -10
- package/src/worker/worker-entry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.0.0-beta.
|
|
3
|
+
## 3.0.0-beta.198 - 2026-10-03
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
5
|
+
- Redact macOS temporary-path aliases and truncated Windows case aliases from managed-job output.
|
|
6
|
+
- Decode sensitive URL parameter names before log redaction; redact complete and truncated private-key bodies.
|
|
7
|
+
- Reject coerced Cloudflare account identifiers and malformed token-validity evidence.
|
|
8
|
+
- Stream deployment-source enumeration with entry, depth, file-count and byte budgets.
|
|
9
|
+
- Persist confirmed Worker upload evidence before endpoint parsing or staging cleanup can fail; reconcile unknown endpoints instead of automatically uploading again.
|
|
10
|
+
- Consolidate duplicate engineering release guidance under the canonical releasing procedure.
|
|
11
|
+
- Drain durable authority revocations in bounded HTTPS batches without losing acknowledgements or invalidating healthy channels; include sequence/envelope overhead in response budgets and atomically persist replay-capacity family eviction with its revocation.
|
|
12
|
+
- Preserve replacement WebSocket connection deadlines when stale events arrive, and settle asynchronous gateway/signature failures through typed responses.
|
|
13
|
+
- Reject malformed revocation/OAuth/DPoP inputs, non-boolean account activation values and inherited enum keys; revalidate current authority and respect pre-existing cancellation before daemon dispatch.
|
|
14
|
+
- Preserve monitor transitions across sequence rollover and bound observability overflow without recursive failure.
|
|
15
|
+
- Bind instruction/skill reads, file rollback, lock recovery, directory traversal, SSH publication and state cleanup to verified identities; validate config paths before reading and preserve unrelated replacements and failure evidence.
|
|
16
|
+
- Keep process, session and runtime ownership through asynchronous settlement and retain candidate daemon ownership after failed shutdown; close late-spawn shutdown races, terminate SSE encoding/projection failures, bound invocation inputs, and give isolated jobs private runtime directories.
|
|
17
|
+
- Fence browser renderer mutations by cancellation/deadline authority, including asynchronous actionability waits and socket replacement.
|
|
18
|
+
- Verify Secure Enclave provenance before native export, signing or deletion; reject oversized signing input before EOF or Keychain access.
|
|
19
|
+
- Sanitize default CLI errors and reject incomplete, failed, inconsistent or scalar-coerced SARIF evidence; prevent conformance credentials from reaching child environments and release proxy readers after disconnect.
|
|
20
|
+
- Implement the declared empty resource-template list, remove stale resource conformance exclusions, and keep unknown-resource errors bounded without reflecting their URI.
|
|
21
|
+
- Apply path-display policy before skill warning sanitization/truncation and preserve dollar signs as literal text in path projections and file edits.
|
|
22
|
+
- Harden generated Worker type recovery with bounded no-follow reads; share strict consumer/deployment audit validation and fixed Worker persistence error classes without copying arbitrary exception names.
|
|
23
|
+
- Wait for the full-access fixture's verified runner exit before deleting job state, restore its environment on shutdown failure, and use the production Swift driver for native provenance tests.
|
|
24
|
+
- Remove arbitrary peer-supplied WebSocket close reasons from debug logs while retaining fixed protocol classifications and outage recovery summaries.
|
|
25
|
+
- Use locked dependency installation in the Windows checkout launcher and fail on setup errors without stale exit-code expansion; share private npm launchers across CI and temporary toolchains so nested lifecycle calls retain the pinned CLI.
|
|
26
|
+
- Correct the public validator's readonly-array declaration and inspect dependencies through one syntax-aware source graph, including escaped paths and declaration files.
|
|
27
|
+
- Share bounded child readiness, capture and exit cleanup across managed-job/process-lock fixtures, with one deadline for preparation and settlement. Prove recovery-lock handoff before corruption assertions and descendant readiness before publication timeout assertions; await runtime teardown and consolidate screenshot rejection setup while retaining distinct cleanup assertions and browser limits.
|
|
28
|
+
- Require both markers in source-order guards and share exact script checks/source reads. Reject event-derived shell expressions with a bounded scan and alternate YAML mappings before dependency-free workflow policy reads.
|
|
29
|
+
- Recheck Git metadata directory identity after opening and traversal, and preserve cancellation during empty-directory opening.
|
|
30
|
+
- Share bounded browser screenshot validation and one observation decode; assert fixture outcomes and clear failed-handshake deadlines.
|
|
31
|
+
- Reject coerced or malformed consumer dependency/SBOM evidence and duplicate graph edges without string normalization.
|
|
32
|
+
- Replace the repeated test-case inventory with a current regression map, and remove repeated overview/release/install instructions while retaining canonical recovery, authority and privacy contracts.
|
|
13
33
|
|
|
14
34
|
## Historical releases
|
|
15
35
|
|
package/README.md
CHANGED
|
@@ -176,7 +176,13 @@ The shared source of truth is `src/shared/policy-contract.json`. The generated m
|
|
|
176
176
|
|
|
177
177
|
For routine remote health checks, prefer `server_info` with `detail: "summary"`; the empty/default call remains full diagnostics and, for owner/full callers, includes a privacy-bounded durable continuity summary that survives Worker isolate replacement without retaining identities, call IDs, tool arguments, results, endpoints, or close reasons. For routine workspace inventory, `project_overview` also accepts `detail: "summary"`; it preserves policy/tool counts and top-level names/types without repeating exact tool arrays, account identity, routing fingerprints, or per-entry paths/sizes. Its empty/default call likewise remains full for compatibility. For remote calls, `server_info.authorization.effective_policy` and, when exact membership is needed, the full projection's `effective_tools` are authoritative. Daemon policy and tools describe only the local capability ceiling before account-role and host-side filtering.
|
|
178
178
|
|
|
179
|
-
`tools/list` is the authenticated account's
|
|
179
|
+
`tools/list` is the authenticated account's discovery catalog. Both `server/discover` and `tools/list` advertise `ttlMs=0`, and every host-visible tool description carries `Tool schema generation N`. Remote discovery supports `tools.listChanged=true`: `subscriptions/listen` acknowledges `toolsListChanged`, emits a level-trigger `notifications/tools/list_changed`, and stays open until cancellation or its bounded lease expires. Clients re-fetch the catalog; the server cannot prove which generation a host cached. Discovery does not grant authority: calls still require the current account/ready-daemon intersection.
|
|
180
|
+
|
|
181
|
+
WebSocket is preferred. An actually dispatched Ping has a ten-second Pong deadline, followed by one independent fifteen-second application-confirmation window. A protocol Pong or explicit application `pong` during that second stage preserves WSS. Local event-loop stalls use a separate recovery path. WSS connect attempts have a thirty-second outer budget. Healthy WSS keeps signed HTTPS fallback stopped; suspicion permits standby prewarming, and actual loss permits takeover of that exact generation. The daemon sends `resume_calls_ack.missing_ids` only after replacement readiness and only with fail-closed proof that absent ownership means non-execution. A possibly executed call is never automatically replayed. See [architecture](https://github.com/YuLeiFuYun/machine-bridge-mcp/blob/main/docs/ARCHITECTURE.md#daemon-reconnect-and-replacement) for transport sequencing, deadlines, and diagnostics.
|
|
182
|
+
|
|
183
|
+
Planned shutdown sends `daemon_draining` before relay close. An interrupted `read_job` returns `recovery.mode=read_same_job`; preserve its `job_id` and `recovery_key` and read that same job after reconnect. Replacement does not transfer ordinary executing calls to a new daemon.
|
|
184
|
+
|
|
185
|
+
Hosted synchronous calls otherwise retain their ordinary 20-second execution plus separate five-second Worker settlement margin. Configurable browser/application tools use 20-second defaults; compound `computer_observe` / `computer_act` retain 30-second defaults, with a 45-second explicit maximum. Durable process acceptance and continuation have separate budgets below. These limits do not establish external host receipt or a whole-assistant-turn deadline.
|
|
180
186
|
|
|
181
187
|
`full` is the daemon capability ceiling. An authenticated owner may exercise it without per-operation approval IDs. Delegated reviewer, editor, and operator accounts remain inside immutable role ceilings; out-of-role operations are denied rather than converted into a temporary elevation workflow. Process sessions, retained output, and managed jobs are additionally bound to account, client, and refresh-token family. See [local authorization](docs/LOCAL_AUTHORIZATION.md).
|
|
182
188
|
|
|
@@ -197,7 +203,15 @@ For stateful GUI trajectories, owner/full callers can use the higher-level `comp
|
|
|
197
203
|
|
|
198
204
|
## Durable work and local resources
|
|
199
205
|
|
|
200
|
-
Remote request-owned foreground work uses the hosted reply-safe budgets described above; configurable browser/application calls may explicitly request at most 45 seconds, while remote `exec_command`, `run_process`, and `run_local_command` are durable one-step jobs with a 10-second acceptance envelope and an independent 1–600-second child execution budget after admission. A continuous process that legitimately needs more than 600 seconds must use `start_job`: managed-job main/finally steps default to 600 seconds and may explicitly request up to 21,600 seconds (six hours), with resource admission occurring before that execution timer begins. The Worker retains separate settlement ownership for five additional seconds, but neither that margin nor its internal stream metrics prove that an external MCP host consumed the terminal frame. Keep unrelated mutations and validation independently terminal, but batch one coherent non-interactive command sequence into a repository umbrella command or multi-step `start_job` instead of creating one host-visible one-step job per tiny probe. A timeout is a protocol result, not proof that descendant cleanup has already completed; a remote owner can inspect `diagnose_runtime.runtime.processes`, while local stdio exposes `server_info.runtime.processes`. Non-owner accounts receive authority-scoped readiness rather than machine-wide process activity. Remote process sessions are for interactive stdin or incremental output, not a substitute for ordinary durable work: hosted `read_process` reports `status_polling_mode=paced_followup`, caps the actual output/exit blocking wait at one second, and paces a repeated would-block read inside the fifteen-second cooldown within that same MCP call until output/exit or the cooldown boundary instead of returning a rapid running checkpoint. When the current task needs more output or terminal state, the same session may be read again in the same assistant response without busy-looping. Non-interactive work should use durable `run_process`/`read_job`; multi-step, cleanup-sensitive, or daemon-restart-surviving workflows should use managed jobs, which persist ordered argv steps and `finally_steps` under owner-only local state and continue across an MCP disconnect. Hosted durable acceptance returns `job_id`, `recovery_key`, and `control_key`: preserve them together, use the read capability for `read_job` and `depends_on`, and use the control capability for `cancel_job`; a bare job ID is not remote recovery authority. On an MCP Apps-capable host, an active `start_job` may report `ui_monitor_candidate=true`, `ui_monitor_render_tool=render_job_monitor`, and a fresh random `ui_monitor_id`, but it remains a data/execution tool and carries no UI template. **Job polling ownership is not task ownership:** if remaining same-response work needs that job's terminal result, do not render the monitor merely to shorten the response; preserve `job_id` + `recovery_key` and continue bounded server-paced `read_job` calls. Render the monitor only when the current task can deliberately transfer that job's status polling without needing its terminal result for remaining same-response work. When intentionally rendering, preserve the start-result ID, call `render_job_monitor` once with the exact accepted `job_id` + `recovery_key` + `ui_monitor_id`, and do not depend on the render result being planner-visible. Then issue one model-visible `read_job` with the same three values. A matching claimed View on an ordinary job reports `ui_monitor_claimed=true`, `status_polling_mode=ui_monitor`, `host_turn_handoff_recommended=false`, and `follow_up_read_required=false`; those fields suppress duplicate model polling for that job only and do not require ending unrelated or still-actionable assistant work. The sole host-turn exception is an explicitly persisted `continuation_mode=task_supervisor`: only when that durable job's own reviewed plan already encapsulates all remaining noninteractive task work may a later active claimed read report `host_turn_handoff_recommended=true`. Machine Bridge can validate the persisted continuation declaration, active job state, and current monitor claim, but it cannot infer conversation-level task completeness; that completeness remains an owner/caller semantic assertion. That handoff is owned by the durable supervisor, not by the Job Monitor, and grants the View no execution authority. The monitor then uses app-only `read_job_monitor` for fixed 40-second status waits instead of emitting background model-visible `read_job` events. Each app-origin status call has a 60-second response watchdog; missing/transport-failed and explicitly retryable status responses receive at most six automatic retries with 1/2/4/8/15/30-second backoff. Exhausted or non-retryable failures pause for inspection, and no retry resubmits the durable job. An older View claim cannot transfer a newer turn because its monitor ID differs. Unsupported hosts, or hosts that can render MCP Apps but cannot proxy app-origin server tools, retain the existing contract: active relay-origin `read_job` reports `status_polling_mode=bounded_followup` and `host_turn_handoff_recommended=false`. Its hosted default is a 40-second server-side long-poll, so an unchanged long job occupies one bounded live MCP response rather than forcing rapid host-side checkpoints. Terminal settlement returns on the next bounded five-second internal poll; nonterminal status/phase/dependency progress is coalesced for at least 30 seconds by default, and `current_step`-only churn does not wake the hosted call. `wait_ms=0` is available only when an immediate checkpoint is actually wanted, and public hosted `wait_ms` is capped at 60 seconds; longer work continues through another paced read of the same `job_id` plus its preserved `recovery_key`. At the default, a synthetic 100-minute unchanged job has an anti-amplification ceiling of 150 status reads, while continuously changing nonterminal progress is separately bounded by the 30-second coalescing floor; those are density estimates rather than proof of aggregate same-response host lifetime. A known job may be followed through paced same-response `read_job` calls while those calls continue to be accepted and the task still needs the result; after an actual host/tool boundary, later recovery must continue from the same `job_id`. Completed one-step process carriers are lower-priority terminal retention than explicit managed jobs, so removable helper history is reclaimed first under the 512-state durable cap. A hosted helper whose current response still requires `read_job` continuation is non-evictable during the fixed 24-hour undelivered-result grace because Machine Bridge explicitly told the caller to follow that durable handle. If the 512-state store is full and every retained slot is non-evictable, the existing typed retryable `limit_exceeded` backpressure rejects the incoming job instead of deleting promised recovery evidence. Once Machine Bridge produces a terminal hosted response for the helper—during initial settlement or a later `read_job`—the private marker drops to the shorter thirty-minute bounded newest-16 transient delivery reserve because only outer response-delivery uncertainty remains; an undelivered marker older than 24 hours likewise stops hard protection and becomes ordinary eviction material. For relay non-owner one-step process carriers, acceptance additionally caps unresolved pending recovery at 16 records per account ID plus account version across all OAuth clients and refresh families; changing client/family does not reset that fairness bound. The check is serialized by the same root capacity transaction before new status publication or process launch and uses retained job state rather than a separate counter. A securely valid persisted same-key job can still be recovered at quota because it adds no reservation, but an empty/statusless deterministic directory is only an incomplete publication and must pass quota again. Hosted terminal delivery or the existing 24-hour terminal expiry releases the account slot. Owner/local work is not subject to this account quota, and the hosted inventory does not expose raw account-quota counts or principal identifiers. The daemon/local `list_jobs.jobs` inventory keeps a 50-record durable-first primary window and `recent_process_recovery` remains capped at 16 additional authority-visible handles for local administration. The hosted Worker projection deliberately removes those job IDs/names/handles and exposes aggregate retained/capacity/activity state only, so one hosted conversation cannot discover another workflow's durable handle through shared owner inventory. Hosted recovery therefore depends on the capability returned by the original acceptance; legacy or lost-capability jobs require local CLI/stdio administration. Owner/local `capacity` diagnostics expose only coarse `durable_terminal` and `transient_terminal` counts; this improves recovery visibility without pretending that Worker acknowledgement proves an external host rendered the final assistant message. Long cross-job workflows can declare `depends_on`: the dependent job remains pre-execution `queued/dependency_wait` without spawning its main child until all upstream jobs succeed, and an upstream failure settles `dependency_failed` instead of leaving a file-poll loop waiting for an artifact that can never appear. Active/staged dependency plans pin referenced retained results until the dependency-bearing plan is terminal. A valid `job_id` that is no longer retained returns typed `not_found`; that absence is not proof that its underlying side effect never executed. `list_jobs` remains inventory rather than a substitute polling loop, and `server_info`/`diagnose_runtime` remain diagnostic surfaces rather than alternate wait channels. Elapsed minutes are not evidence that an external host deadline is near; return the durable recovery identifier for a later turn only after an actual host/tool boundary is observed, external input or authorization is required, or the user explicitly requested a checkpoint.
|
|
206
|
+
Remote `exec_command`, `run_process`, and `run_local_command` require a caller-held `idempotency_key` and become principal-bound one-step jobs before execution. Their acceptance envelope is ten seconds; child execution is independently bounded to 1–600 seconds after resource admission. Use `start_job` for a continuous command needing more time: main/finally steps default to 600 seconds and may request up to 21,600 seconds (six hours). Batch coherent non-interactive work into one plan or repository umbrella command. Process sessions serve interactive stdin/incremental output and retain daemon-lifetime ownership; hosted `read_process` reports `status_polling_mode=paced_followup`, with a one-second actual blocking cap and a fifteen-second would-block cooldown.
|
|
207
|
+
|
|
208
|
+
Preserve `job_id`, `recovery_key`, and `control_key` from acceptance. Reads/dependencies require the recovery capability; cancellation requires the control capability. A bare ID is not hosted authority. When terminal state is needed, continue bounded same-response `read_job` follow-up: active reads report `status_polling_mode=bounded_followup` and `host_turn_handoff_recommended=false`. The default long-poll is 40 seconds, terminal checks are bounded to five-second intervals, and nonterminal progress is coalesced for at least 30 seconds; step-name changes alone do not wake a hosted read. Explicit `wait_ms=0` requests a checkpoint and the public maximum is 60 seconds. Do not busy-loop or substitute diagnostic calls for known-job reads. Elapsed minutes do not justify handoff; after an actual host/tool boundary, recover the same job instead of repeating its side effect.
|
|
209
|
+
|
|
210
|
+
An active `start_job` may offer a Job Monitor on an MCP Apps-capable host. Polling ownership is separate from task ownership: if remaining work needs the result, keep following `read_job`. Otherwise render once with the accepted `job_id`, `recovery_key`, and `ui_monitor_id`, then read with those same values to verify the View claim. A claimed ordinary job reports `status_polling_mode=ui_monitor`, `follow_up_read_required=false`, and `host_turn_handoff_recommended=false`. Only an explicit `continuation_mode=task_supervisor` plan that already owns all remaining noninteractive work may recommend handoff; completeness remains an owner/caller semantic assertion. Monitor failures never resubmit execution.
|
|
211
|
+
|
|
212
|
+
The retained-state cap is 512. A hosted helper awaiting its promised terminal response is non-evictable during the fixed 24-hour undelivered-result grace; a store with no removable slot rejects new work with retryable `limit_exceeded`. After terminal delivery, helpers have a bounded newest-16/thirty-minute reserve and lower eviction priority than explicit durable history. Delegated one-step pending recovery is capped at 16 records per account ID plus account version across all OAuth clients and refresh families, checked before new state or process launch; a valid same-key replay adds no slot, while a statusless directory must pass admission again. Local CLI/stdio retains detailed inventory; hosted `list_jobs` exposes aggregates, so lost capabilities require local administration. A retained-record `not_found` does not prove non-execution.
|
|
213
|
+
|
|
214
|
+
Jobs can declare `depends_on`: the dependent remains `queued/dependency_wait` before main execution, upstream failure yields `dependency_failed`, and active/staged dependencies protect referenced results. Register secrets as local resource aliases and put idempotent cleanup in `finally_steps`. Timeout or cancellation alone does not prove process-tree settlement. See [managed jobs](docs/MANAGED_JOBS.md) for examples, retention, recovery, and privacy boundaries.
|
|
201
215
|
|
|
202
216
|
The repository-tracked `workflow-bundle.json` is the project-native lifecycle authority for local readiness. Its `project-native:npm:check:fast` and `project-native:npm:check:full` stages must remain `sandbox: required`; a machine-local or untracked lifecycle file must not silently lower high-risk containment. Release architecture checks enforce both Git tracking and those required Seatbelt modes, so the job-local continuation contract above is validated under the same repository-owned verification authority used for the candidate.
|
|
203
217
|
|
package/SECURITY.md
CHANGED
|
@@ -113,7 +113,7 @@ Remote daemon authentication uses a long-term device root and a root-certified e
|
|
|
113
113
|
|
|
114
114
|
The default provider on every platform, including macOS, is an owner-only portable P-256 root. Its private JWK is exportable by the daemon OS user and is reported as such in `server_info`. The root signs a 24-hour ephemeral session certificate at daemon startup; the ephemeral private key remains in memory and signs WebSocket preflight, Worker challenge, reconnect, and account-administration requests. Portable startup does not access Keychain and does not request user presence.
|
|
115
115
|
|
|
116
|
-
macOS can opt into a non-exportable Secure Enclave root only through `MBM_MACOS_TRUST_BROKER`. The configured executable must be an app-like broker signed by an Apple development or distribution identity with provisioning-profile-validated data-protection Keychain entitlements. Machine Bridge validates its canonical path, file mutability, strict code signature, signing identifier, Team ID, protocol responses, and a real create/delete Secure Enclave key probe before enrollment. The broker identity remains bound to the root and is revalidated before later use.
|
|
116
|
+
macOS can opt into a non-exportable Secure Enclave root only through `MBM_MACOS_TRUST_BROKER`. The configured executable must be an app-like broker signed by an Apple development or distribution identity with provisioning-profile-validated data-protection Keychain entitlements. Machine Bridge validates its canonical path, file mutability, strict code signature, signing identifier, Team ID, protocol responses, and a real create/delete Secure Enclave key probe before enrollment. The broker identity remains bound to the root and is revalidated before later use. The native broker verifies the returned key's Secure Enclave token, private-key class, EC type, and 256-bit size before use or export. Only an explicit key-not-found result permits creation; other lookup failures preserve existing state. Deletion binds the validated key reference, and signing input is bounded before Keychain access.
|
|
117
117
|
|
|
118
118
|
The prompt frequency is therefore:
|
|
119
119
|
|
package/bin/machine-mcp.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { sanitizeLogText } from "../src/local/log.mjs";
|
|
|
4
4
|
|
|
5
5
|
main().catch(error => {
|
|
6
6
|
const message = process.env.MBM_DEBUG === "1"
|
|
7
|
-
?
|
|
7
|
+
? (error?.stack || error?.message || String(error))
|
|
8
8
|
: (error?.message || String(error));
|
|
9
|
-
console.error(`Error: ${message}`);
|
|
9
|
+
console.error(`Error: ${sanitizeLogText(message)}`);
|
|
10
10
|
process.exitCode = 1;
|
|
11
11
|
});
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
if (globalThis.__machineBridgeBrowserOperations) return;
|
|
3
3
|
|
|
4
4
|
const MAX_ACCESSIBLE_FRAMES = 64;
|
|
5
|
-
const PAGE_AUTOMATION_VERSION =
|
|
5
|
+
const PAGE_AUTOMATION_VERSION = 5;
|
|
6
|
+
const rendererRequests = new Map();
|
|
7
|
+
const MAX_RENDERER_REQUESTS = 64;
|
|
6
8
|
const MUTATING_METHODS = new Set([
|
|
7
9
|
"manage_tabs", "point_action", "backend_node_action", "action", "fill_form", "upload_files", "screenshot",
|
|
8
10
|
]);
|
|
@@ -158,16 +160,16 @@
|
|
|
158
160
|
}
|
|
159
161
|
throwIfCancelled(state);
|
|
160
162
|
if (action === "new") {
|
|
161
|
-
const tab = await
|
|
163
|
+
const tab = await invokeBrowserMutation(
|
|
162
164
|
() => chrome.tabs.create({ url: newTabUrl || "about:blank", active: newTabActive }),
|
|
163
|
-
"create",
|
|
165
|
+
"during create", "browser tab mutation",
|
|
164
166
|
);
|
|
165
167
|
return { action: "new", ...publicTab(tab) };
|
|
166
168
|
}
|
|
167
169
|
const tab = await chrome.tabs.get(params.tabId);
|
|
168
170
|
throwIfCancelled(state);
|
|
169
171
|
if (action === "activate") {
|
|
170
|
-
const activated = await
|
|
172
|
+
const activated = await invokeBrowserMutation(() => chrome.tabs.update(tab.id, { active: true }), "during activate_tab", "browser tab mutation");
|
|
171
173
|
throwIfCancelled(state);
|
|
172
174
|
if (!Number.isInteger(activated?.windowId) || activated.windowId < 1) {
|
|
173
175
|
throw new Error("browser tab activation completed but its current window is unavailable before focus; inspect tabs before retrying");
|
|
@@ -183,7 +185,7 @@
|
|
|
183
185
|
throw new Error("browser tab activation completed but the target tab was no longer active before focus; inspect tabs before retrying");
|
|
184
186
|
}
|
|
185
187
|
const focusWindowId = focusTarget.windowId;
|
|
186
|
-
await
|
|
188
|
+
await invokeBrowserMutation(() => chrome.windows.update(focusWindowId, { focused: true }), "during focus_window", "browser tab mutation");
|
|
187
189
|
let settled;
|
|
188
190
|
try { settled = await chrome.tabs.get(tab.id); }
|
|
189
191
|
catch { throw new Error("browser tab activation and window focus completed but the target tab could not be verified; inspect tabs before retrying"); }
|
|
@@ -196,7 +198,7 @@
|
|
|
196
198
|
return { action: "activate", ...publicTab(settled) };
|
|
197
199
|
}
|
|
198
200
|
const result = { action: "close", closed: true, ...publicTab(tab) };
|
|
199
|
-
await
|
|
201
|
+
await invokeBrowserMutation(() => chrome.tabs.remove(tab.id), "during close", "browser tab mutation");
|
|
200
202
|
return result;
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -252,7 +254,7 @@
|
|
|
252
254
|
if (urlMatched && page.matched === true) {
|
|
253
255
|
return { ok: true, tab_id: current.id, title: current.title || "", url: current.url || "", condition: last };
|
|
254
256
|
}
|
|
255
|
-
await
|
|
257
|
+
await new Promise((resolve) => { setTimeout(resolve, 200); });
|
|
256
258
|
}
|
|
257
259
|
throw new Error(`browser wait timed out; last condition: ${JSON.stringify(last || {})}`);
|
|
258
260
|
}
|
|
@@ -1360,7 +1362,7 @@
|
|
|
1360
1362
|
try {
|
|
1361
1363
|
await assertExpectedNavigationTab(tab.id, params.expectedTabUrl);
|
|
1362
1364
|
throwIfCancelled(state);
|
|
1363
|
-
const updated = await
|
|
1365
|
+
const updated = await invokeBrowserMutation(() => chrome.tabs.update(tab.id, { url: params.url }), "because the navigation mutation API failed");
|
|
1364
1366
|
await awaitPostDispatchWait(waiter);
|
|
1365
1367
|
return postActionTabMetadata(updated.id);
|
|
1366
1368
|
} catch (error) {
|
|
@@ -1378,7 +1380,7 @@
|
|
|
1378
1380
|
tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
|
|
1379
1381
|
);
|
|
1380
1382
|
throwIfCancelled(state);
|
|
1381
|
-
await
|
|
1383
|
+
await invokeBrowserMutation(() => chrome.tabs.reload(tab.id), "because the navigation mutation API failed");
|
|
1382
1384
|
}
|
|
1383
1385
|
await awaitPostDispatchWait(waiter);
|
|
1384
1386
|
return postActionTabMetadata(tab.id);
|
|
@@ -1397,8 +1399,8 @@
|
|
|
1397
1399
|
tab.id, params.expectedTabUrl, params.expectedDocumentEpoch, params.expectedHistoryEntryKey,
|
|
1398
1400
|
);
|
|
1399
1401
|
throwIfCancelled(state);
|
|
1400
|
-
if (params.action === "back") await
|
|
1401
|
-
else await
|
|
1402
|
+
if (params.action === "back") await invokeBrowserMutation(() => chrome.tabs.goBack(tab.id), "because the navigation mutation API failed");
|
|
1403
|
+
else await invokeBrowserMutation(() => chrome.tabs.goForward(tab.id), "because the navigation mutation API failed");
|
|
1402
1404
|
}
|
|
1403
1405
|
await awaitPostDispatchWait(waiter);
|
|
1404
1406
|
return postActionTabMetadata(tab.id);
|
|
@@ -1760,55 +1762,51 @@
|
|
|
1760
1762
|
return { promise, cancel: () => settle() };
|
|
1761
1763
|
}
|
|
1762
1764
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
await waiter.promise;
|
|
1766
|
-
} catch (error) {
|
|
1767
|
-
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1768
|
-
throw new Error(`browser action may have been dispatched; the action outcome is unknown because post-dispatch wait failed. Inspect the page before retrying. (${detail})`);
|
|
1769
|
-
}
|
|
1765
|
+
function awaitPostDispatchWait(waiter) {
|
|
1766
|
+
return invokeBrowserMutation(() => waiter.promise, "because post-dispatch wait failed");
|
|
1770
1767
|
}
|
|
1771
1768
|
|
|
1772
|
-
async function
|
|
1769
|
+
async function invokeBrowserMutation(operation, failure, subject = "browser action") {
|
|
1773
1770
|
try {
|
|
1774
1771
|
return await operation();
|
|
1775
1772
|
} catch (error) {
|
|
1776
1773
|
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1777
|
-
throw new Error(
|
|
1774
|
+
throw new Error(`${subject} may have been dispatched; the ${subject === "browser action" ? "action " : ""}outcome is unknown ${failure}. Inspect ${subject === "browser action" ? "the page" : "tabs"} before retrying. (${detail})`);
|
|
1778
1775
|
}
|
|
1779
1776
|
}
|
|
1780
1777
|
|
|
1781
|
-
async function invokeBrowserTabMutation(operation, phase) {
|
|
1782
|
-
try {
|
|
1783
|
-
return await operation();
|
|
1784
|
-
} catch (error) {
|
|
1785
|
-
const detail = String(error?.message || error).replace(/\s+/g, " ").slice(0, 500);
|
|
1786
|
-
throw new Error(`browser tab mutation may have been dispatched; the outcome is unknown during ${phase}. Inspect tabs before retrying. (${detail})`);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
1778
|
async function executePageMutation(target, method, params, state) {
|
|
1791
1779
|
await chrome.scripting.executeScript({ target, files: ["page-automation.js"] });
|
|
1792
1780
|
throwIfCancelled(state);
|
|
1781
|
+
if (rendererRequests.size >= MAX_RENDERER_REQUESTS) throw new Error("too many active renderer requests");
|
|
1782
|
+
const token = crypto.randomUUID();
|
|
1783
|
+
if (rendererRequests.has(token)) throw new Error("renderer request identity collision");
|
|
1784
|
+
const lease = {
|
|
1785
|
+
state, tabId: target.tabId, frameId: target.frameIds?.[0] ?? 0,
|
|
1786
|
+
deadline: performance.now() + boundedRequestTimeout(state?.timeoutMs),
|
|
1787
|
+
};
|
|
1788
|
+
rendererRequests.set(token, lease);
|
|
1793
1789
|
let executions;
|
|
1794
1790
|
try {
|
|
1795
1791
|
executions = await chrome.scripting.executeScript({
|
|
1796
1792
|
target,
|
|
1797
|
-
func: async (operation, payload, expectedVersion) => {
|
|
1793
|
+
func: async (operation, payload, expectedVersion, requestToken) => {
|
|
1798
1794
|
const protocol = "machine_bridge_page_mutation_v1";
|
|
1799
1795
|
try {
|
|
1800
1796
|
const api = globalThis.__machineBridgePageAutomation;
|
|
1801
1797
|
if (!api || api.version !== expectedVersion) throw new Error("page automation module version mismatch");
|
|
1802
1798
|
if (typeof api[operation] !== "function") throw new Error("page automation module is unavailable");
|
|
1803
|
-
return { protocol, ok: true, result: await api[operation](payload) };
|
|
1799
|
+
return { protocol, ok: true, result: await api[operation](payload, { token: requestToken }) };
|
|
1804
1800
|
} catch (error) {
|
|
1805
1801
|
return { protocol, ok: false, error: String(error?.message || error).replace(/\s+/g, " ").slice(0, 1000) };
|
|
1806
1802
|
}
|
|
1807
1803
|
},
|
|
1808
|
-
args: [method, params, PAGE_AUTOMATION_VERSION],
|
|
1804
|
+
args: [method, params, PAGE_AUTOMATION_VERSION, token],
|
|
1809
1805
|
});
|
|
1810
1806
|
} catch (error) {
|
|
1811
1807
|
throw pageMutationDispatchUnknown(error);
|
|
1808
|
+
} finally {
|
|
1809
|
+
if (rendererRequests.get(token) === lease) rendererRequests.delete(token);
|
|
1812
1810
|
}
|
|
1813
1811
|
const settlement = Array.isArray(executions) && executions.length === 1 ? executions[0]?.result : null;
|
|
1814
1812
|
if (settlement?.protocol !== "machine_bridge_page_mutation_v1") {
|
|
@@ -1824,6 +1822,32 @@
|
|
|
1824
1822
|
return [{ ...executions[0], result: settlement.result }];
|
|
1825
1823
|
}
|
|
1826
1824
|
|
|
1825
|
+
function rendererProgress(token, sender) {
|
|
1826
|
+
const lease = typeof token === "string" ? rendererRequests.get(token) : null;
|
|
1827
|
+
const active = Boolean(lease && sender?.tab?.id === lease.tabId
|
|
1828
|
+
&& (sender?.frameId ?? 0) === lease.frameId
|
|
1829
|
+
&& lease.state?.cancelled !== true && performance.now() < lease.deadline);
|
|
1830
|
+
return { ok: true, active };
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function handleRendererProgressMessage(message, sender, sendResponse) {
|
|
1834
|
+
if (message.type === "machine_bridge_internal_progress") {
|
|
1835
|
+
sendResponse(rendererProgress(message.request_token, sender));
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
const delayMs = message.delay_ms;
|
|
1839
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 1 || delayMs > 250) {
|
|
1840
|
+
sendResponse({ ok: false });
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
const scoped = message.request_token !== undefined;
|
|
1844
|
+
const response = () => scoped ? rendererProgress(message.request_token, sender) : { ok: true };
|
|
1845
|
+
const current = response();
|
|
1846
|
+
if (scoped && current.active !== true) { sendResponse(current); return false; }
|
|
1847
|
+
setTimeout(() => sendResponse(response()), delayMs);
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1827
1851
|
function pageMutationSettlementUnknown() {
|
|
1828
1852
|
return new Error("browser action may have been dispatched; the action outcome is unknown because the page mutation settlement was malformed. Inspect the page before retrying.");
|
|
1829
1853
|
}
|
|
@@ -1846,10 +1870,6 @@
|
|
|
1846
1870
|
}));
|
|
1847
1871
|
}
|
|
1848
1872
|
|
|
1849
|
-
function delay(ms) {
|
|
1850
|
-
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
1873
|
function boundedRequestTimeout(value) {
|
|
1854
1874
|
if (!Number.isSafeInteger(value)) return 30000;
|
|
1855
1875
|
return Math.max(1000, Math.min(185000, value));
|
|
@@ -1871,7 +1891,7 @@
|
|
|
1871
1891
|
}
|
|
1872
1892
|
|
|
1873
1893
|
Object.defineProperty(globalThis, "__machineBridgeBrowserOperations", {
|
|
1874
|
-
value: Object.freeze({ dispatch, methodMayMutate, responsePayload, boundedRequestTimeout, boundedDocumentSource }),
|
|
1894
|
+
value: Object.freeze({ dispatch, methodMayMutate, responsePayload, boundedRequestTimeout, boundedDocumentSource, rendererProgress, handleRendererProgressMessage }),
|
|
1875
1895
|
configurable: false,
|
|
1876
1896
|
});
|
|
1877
1897
|
})();
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "3.0.0-beta.
|
|
33
|
+
"version_name": "3.0.0-beta.198",
|
|
34
34
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
|
|
35
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
const PAGE_AUTOMATION_VERSION =
|
|
2
|
+
const PAGE_AUTOMATION_VERSION = 5;
|
|
3
3
|
if (globalThis.__machineBridgePageAutomation?.version === PAGE_AUTOMATION_VERSION) return;
|
|
4
4
|
|
|
5
5
|
const INTERACTIVE_SELECTOR = "a,button,input,select,textarea,[role],[contenteditable]:not([contenteditable='false']),summary";
|
|
@@ -486,8 +486,8 @@
|
|
|
486
486
|
return { selector, timeoutMs, files };
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
async function prepareAction(params) {
|
|
490
|
-
const element = await prepareElementForAction(params);
|
|
489
|
+
async function prepareAction(params, progress = null) {
|
|
490
|
+
const element = await prepareElementForAction(params, progress);
|
|
491
491
|
try {
|
|
492
492
|
const result = actionTarget(element);
|
|
493
493
|
return { ok: true, element: describeElement(element, 0, false), point: result.point };
|
|
@@ -496,11 +496,12 @@
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
async function action(params) {
|
|
499
|
+
async function action(params, progress = null) {
|
|
500
500
|
const prepared = validateActionPayload(params);
|
|
501
|
-
const element = await waitForActionable(prepared.selector, prepared.action, prepared.timeoutMs);
|
|
501
|
+
const element = await waitForActionable(prepared.selector, prepared.action, prepared.timeoutMs, progress);
|
|
502
|
+
await checkProgress(progress);
|
|
502
503
|
assertSnapshotIdentity(element, prepared.expectedIdentity);
|
|
503
|
-
const mutationStarted = await applyOne(element, prepared.action, prepared.value, prepared.key, prepared.timeoutMs);
|
|
504
|
+
const mutationStarted = await applyOne(element, prepared.action, prepared.value, prepared.key, prepared.timeoutMs, progress);
|
|
504
505
|
try {
|
|
505
506
|
return { ok: true, element: describeElement(element, 0, false), input_mode: "dom" };
|
|
506
507
|
} catch (error) {
|
|
@@ -509,9 +510,10 @@
|
|
|
509
510
|
}
|
|
510
511
|
}
|
|
511
512
|
|
|
512
|
-
async function prepareElementForAction(params) {
|
|
513
|
+
async function prepareElementForAction(params, progress = null) {
|
|
513
514
|
const prepared = validateActionPayload(params);
|
|
514
|
-
const element = await waitForActionable(prepared.selector, prepared.action, prepared.timeoutMs);
|
|
515
|
+
const element = await waitForActionable(prepared.selector, prepared.action, prepared.timeoutMs, progress);
|
|
516
|
+
await checkProgress(progress);
|
|
515
517
|
assertSnapshotIdentity(element, prepared.expectedIdentity);
|
|
516
518
|
let mutationStarted = false;
|
|
517
519
|
try {
|
|
@@ -522,8 +524,8 @@
|
|
|
522
524
|
if (["click", "double_click", "hover"].includes(prepared.action)) {
|
|
523
525
|
mutationStarted = true;
|
|
524
526
|
scrollElementIntoView(element);
|
|
525
|
-
await waitForStableBox(element, prepared.timeoutMs);
|
|
526
|
-
await waitForPointerTarget(element, prepared.timeoutMs);
|
|
527
|
+
await waitForStableBox(element, prepared.timeoutMs, progress);
|
|
528
|
+
await waitForPointerTarget(element, prepared.timeoutMs, progress);
|
|
527
529
|
} else if (prepared.action === "scroll_into_view") {
|
|
528
530
|
mutationStarted = true;
|
|
529
531
|
scrollElementIntoView(element);
|
|
@@ -535,15 +537,15 @@
|
|
|
535
537
|
return element;
|
|
536
538
|
}
|
|
537
539
|
|
|
538
|
-
async function fillForm(params) {
|
|
540
|
+
async function fillForm(params, progress = null) {
|
|
539
541
|
const prepared = validateFormPayload(params);
|
|
540
542
|
const results = [];
|
|
541
543
|
const timeoutMs = prepared.timeoutMs;
|
|
542
544
|
for (let index = 0; index < prepared.fields.length; index += 1) {
|
|
543
545
|
const field = prepared.fields[index];
|
|
544
546
|
try {
|
|
545
|
-
const element = await waitForActionable(field.selector, field.action, timeoutMs);
|
|
546
|
-
const mutationStarted = await applyOne(element, field.action, field.value, "", timeoutMs);
|
|
547
|
+
const element = await waitForActionable(field.selector, field.action, timeoutMs, progress);
|
|
548
|
+
const mutationStarted = await applyOne(element, field.action, field.value, "", timeoutMs, progress);
|
|
547
549
|
let description;
|
|
548
550
|
try { description = describeElement(element, index, false); }
|
|
549
551
|
catch (error) { if (mutationStarted) throw domMutationUnknown("fill_form", error); throw error; }
|
|
@@ -559,7 +561,8 @@
|
|
|
559
561
|
if (prepared.submit) {
|
|
560
562
|
let submissionStarted = false;
|
|
561
563
|
try {
|
|
562
|
-
const submitter = prepared.submitSelector ? await waitForActionable(prepared.submitSelector, "click", timeoutMs) : deepQuerySelectorAll("button[type='submit'],input[type='submit']", 1)[0];
|
|
564
|
+
const submitter = prepared.submitSelector ? await waitForActionable(prepared.submitSelector, "click", timeoutMs, progress) : deepQuerySelectorAll("button[type='submit'],input[type='submit']", 1)[0];
|
|
565
|
+
await checkProgress(progress);
|
|
563
566
|
if (submitter) {
|
|
564
567
|
submissionStarted = true;
|
|
565
568
|
submitter.click();
|
|
@@ -580,10 +583,11 @@
|
|
|
580
583
|
return { ok: true, fields: results, submitted: prepared.submit, values_exposed: false };
|
|
581
584
|
}
|
|
582
585
|
|
|
583
|
-
async function uploadFiles(params) {
|
|
586
|
+
async function uploadFiles(params, progress = null) {
|
|
584
587
|
const prepared = validateUploadPayload(params);
|
|
585
|
-
const input = await waitForActionable(prepared.selector, "upload", prepared.timeoutMs);
|
|
588
|
+
const input = await waitForActionable(prepared.selector, "upload", prepared.timeoutMs, progress);
|
|
586
589
|
if (!(input instanceof HTMLInputElement) || input.type !== "file") throw new Error("matched element is not a file input");
|
|
590
|
+
await checkProgress(progress);
|
|
587
591
|
const transfer = new DataTransfer();
|
|
588
592
|
for (const item of prepared.files) {
|
|
589
593
|
const binary = atob(item.data);
|
|
@@ -717,18 +721,19 @@
|
|
|
717
721
|
return /(?:password|passwd|secret|token|api[-_ ]?key|otp|one[-_ ]?time|verification|cvc|cvv|security[-_ ]?code|card[-_ ]?number)/.test(identity);
|
|
718
722
|
}
|
|
719
723
|
|
|
720
|
-
async function waitForActionable(selector, actionName, timeoutMs) {
|
|
724
|
+
async function waitForActionable(selector, actionName, timeoutMs, progress) {
|
|
721
725
|
const boundedTimeoutMs = Math.max(1, timeoutMs);
|
|
722
726
|
const startedAt = performance.now();
|
|
723
727
|
let lastProblem = "no element matched selector";
|
|
724
728
|
while (performance.now() - startedAt <= boundedTimeoutMs) {
|
|
729
|
+
await checkProgress(progress);
|
|
725
730
|
const element = findOne(selector);
|
|
726
731
|
if (!element && selector?.ref) throw new Error("element reference is stale; inspect the page again");
|
|
727
732
|
if (element) {
|
|
728
733
|
lastProblem = actionabilityProblem(element, actionName);
|
|
729
734
|
if (!lastProblem) return element;
|
|
730
735
|
}
|
|
731
|
-
await delay(100);
|
|
736
|
+
await delay(100, progress);
|
|
732
737
|
}
|
|
733
738
|
throw new Error(`element was not actionable before timeout: ${lastProblem}`);
|
|
734
739
|
}
|
|
@@ -745,12 +750,12 @@
|
|
|
745
750
|
return "";
|
|
746
751
|
}
|
|
747
752
|
|
|
748
|
-
async function waitForStableBox(element, timeoutMs) {
|
|
753
|
+
async function waitForStableBox(element, timeoutMs, progress) {
|
|
749
754
|
const boundedTimeoutMs = Math.max(1, timeoutMs);
|
|
750
755
|
const startedAt = performance.now();
|
|
751
756
|
let previous = boundingBox(element);
|
|
752
757
|
while (performance.now() - startedAt <= boundedTimeoutMs) {
|
|
753
|
-
await delay(50);
|
|
758
|
+
await delay(50, progress);
|
|
754
759
|
const current = boundingBox(element);
|
|
755
760
|
if (current && previous && boxDistance(previous, current) <= 0.5) return current;
|
|
756
761
|
previous = current;
|
|
@@ -758,17 +763,18 @@
|
|
|
758
763
|
throw new Error("element did not become geometrically stable before timeout");
|
|
759
764
|
}
|
|
760
765
|
|
|
761
|
-
async function waitForPointerTarget(element, timeoutMs) {
|
|
766
|
+
async function waitForPointerTarget(element, timeoutMs, progress) {
|
|
762
767
|
const boundedTimeoutMs = Math.max(1, timeoutMs);
|
|
763
768
|
const startedAt = performance.now();
|
|
764
769
|
let lastProblem = "element does not receive pointer events";
|
|
765
770
|
while (performance.now() - startedAt <= boundedTimeoutMs) {
|
|
771
|
+
await checkProgress(progress);
|
|
766
772
|
const point = actionTarget(element).point;
|
|
767
773
|
if (!point) lastProblem = "element has no usable viewport box";
|
|
768
774
|
else if (point.x < 0 || point.y < 0 || point.x >= innerWidth || point.y >= innerHeight) lastProblem = "element is outside the viewport";
|
|
769
775
|
else if (receivesPointerEvents(element, point)) return;
|
|
770
776
|
else lastProblem = "element is obscured by another element";
|
|
771
|
-
await delay(100);
|
|
777
|
+
await delay(100, progress);
|
|
772
778
|
}
|
|
773
779
|
throw new Error(`element was not clickable before timeout: ${lastProblem}`);
|
|
774
780
|
}
|
|
@@ -809,7 +815,8 @@
|
|
|
809
815
|
return { point: { x: left + (right - left) / 2, y: top + (bottom - top) / 2 } };
|
|
810
816
|
}
|
|
811
817
|
|
|
812
|
-
async function applyOne(element, operation, value, key, timeoutMs = 10000) {
|
|
818
|
+
async function applyOne(element, operation, value, key, timeoutMs = 10000, progress = null) {
|
|
819
|
+
await checkProgress(progress);
|
|
813
820
|
const wanted = operation === "check" ? true : operation === "uncheck" ? false : null;
|
|
814
821
|
if (wanted !== null && Boolean(element.checked) === wanted) return false;
|
|
815
822
|
let selectedOption = null;
|
|
@@ -824,8 +831,9 @@
|
|
|
824
831
|
if (["click", "double_click", "hover"].includes(operation)) {
|
|
825
832
|
mutationStarted = true;
|
|
826
833
|
scrollElementIntoView(element);
|
|
827
|
-
await waitForStableBox(element, timeoutMs);
|
|
828
|
-
await waitForPointerTarget(element, timeoutMs);
|
|
834
|
+
await waitForStableBox(element, timeoutMs, progress);
|
|
835
|
+
await waitForPointerTarget(element, timeoutMs, progress);
|
|
836
|
+
await checkProgress(progress);
|
|
829
837
|
if (operation === "click") element.click();
|
|
830
838
|
else if (operation === "double_click") {
|
|
831
839
|
element.click();
|
|
@@ -1153,16 +1161,30 @@
|
|
|
1153
1161
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1154
1162
|
}
|
|
1155
1163
|
|
|
1156
|
-
function
|
|
1164
|
+
async function checkProgress(progress) {
|
|
1165
|
+
if (progress === null || progress === undefined) return;
|
|
1166
|
+
if (typeof progress?.token !== "string" || !/^[a-f0-9-]{36}$/.test(progress.token)) {
|
|
1167
|
+
throw new Error("browser request progress identity is invalid");
|
|
1168
|
+
}
|
|
1169
|
+
const response = await globalThis.chrome?.runtime?.sendMessage?.({
|
|
1170
|
+
type: "machine_bridge_internal_progress", request_token: progress.token,
|
|
1171
|
+
});
|
|
1172
|
+
if (response?.ok !== true || response.active !== true) throw new Error("browser request cancelled");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function delay(ms, progress = null) {
|
|
1157
1176
|
const runtime = globalThis.chrome?.runtime;
|
|
1158
1177
|
if (runtime && typeof runtime.sendMessage === "function") {
|
|
1159
1178
|
const delayMs = Number.isFinite(ms) ? Math.ceil(ms) : 0;
|
|
1160
1179
|
if (delayMs < 1 || delayMs > 250) return Promise.reject(new Error("browser action delay is invalid"));
|
|
1161
|
-
return Promise.resolve(runtime.sendMessage({ type: "machine_bridge_internal_delay", delay_ms: delayMs
|
|
1180
|
+
return Promise.resolve(runtime.sendMessage({ type: "machine_bridge_internal_delay", delay_ms: delayMs,
|
|
1181
|
+
...(progress ? { request_token: progress.token } : {}) }))
|
|
1162
1182
|
.then((response) => {
|
|
1163
1183
|
if (response?.ok !== true) throw new Error("browser action timing service unavailable");
|
|
1184
|
+
if (progress && response.active !== true) throw new Error("browser request cancelled");
|
|
1164
1185
|
});
|
|
1165
1186
|
}
|
|
1187
|
+
if (progress) return Promise.reject(new Error("browser request progress service unavailable"));
|
|
1166
1188
|
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
1167
1189
|
}
|
|
1168
1190
|
|
|
@@ -15,15 +15,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
15
15
|
if (alarm.name === "machine-bridge-reconnect") void connectFromStorage();
|
|
16
16
|
});
|
|
17
17
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
18
|
-
if (message?.type === "machine_bridge_internal_delay") {
|
|
18
|
+
if (message?.type === "machine_bridge_internal_progress" || message?.type === "machine_bridge_internal_delay") {
|
|
19
19
|
if (sender?.id !== chrome.runtime.id) return false;
|
|
20
|
-
|
|
21
|
-
if (!Number.isSafeInteger(delayMs) || delayMs < 1 || delayMs > 250) {
|
|
22
|
-
sendResponse({ ok: false });
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
setTimeout(() => sendResponse({ ok: true }), delayMs);
|
|
26
|
-
return true;
|
|
20
|
+
return browserOperations().handleRendererProgressMessage(message, sender, sendResponse);
|
|
27
21
|
}
|
|
28
22
|
if (message?.type !== "pair_bootstrap") return false;
|
|
29
23
|
pairFromBootstrap(message.port, message.grant, { replace: false })
|
|
@@ -288,7 +282,7 @@ async function handleMessage(ws, raw, onReady = () => {}) {
|
|
|
288
282
|
if (message?.type === "pong") return brokerLiveness.handlePong(ws, message, closeSocketQuietly);
|
|
289
283
|
if (message?.type === "cancel" && typeof message.id === "string") {
|
|
290
284
|
const state = activeRequests.get(message.id);
|
|
291
|
-
if (state) state.cancelled = true;
|
|
285
|
+
if (state?.socket === ws) state.cancelled = true;
|
|
292
286
|
return;
|
|
293
287
|
}
|
|
294
288
|
if (message?.type !== "request" || typeof message.id !== "string" || typeof message.method !== "string") return;
|