machine-bridge-mcp 1.2.9 → 1.2.11
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 +17 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +15 -8
- package/docs/AUDIT.md +22 -0
- package/docs/LOGGING.md +4 -2
- package/docs/MULTI_ACCOUNT.md +2 -2
- package/docs/OPERATIONS.md +5 -4
- package/docs/RELEASING.md +1 -1
- package/docs/TESTING.md +5 -1
- package/docs/TOOL_REFERENCE.md +4 -4
- package/package.json +6 -2
- package/scripts/check-plan.mjs +3 -0
- package/scripts/check-runner.mjs +75 -0
- package/scripts/coverage-check.mjs +1 -1
- package/scripts/github-backlog.mjs +116 -0
- package/scripts/github-push.mjs +4 -0
- package/scripts/release-acceptance.mjs +29 -13
- package/scripts/run-checks.mjs +11 -21
- package/src/local/bounded-output.mjs +20 -3
- package/src/local/errors.mjs +5 -1
- package/src/local/execution-limits.mjs +6 -1
- package/src/local/process-execution.mjs +94 -14
- package/src/local/process-output-stream.mjs +82 -0
- package/src/local/process-result-projection.mjs +25 -0
- package/src/local/process-sessions.mjs +37 -51
- package/src/local/relay-call-recovery.mjs +148 -0
- package/src/local/relay-connection.mjs +5 -0
- package/src/local/runtime-relay.mjs +16 -0
- package/src/local/runtime.mjs +60 -39
- package/src/local/secure-file.mjs +24 -4
- package/src/local/tools.mjs +4 -9
- package/src/shared/result-projection.d.mts +6 -0
- package/src/shared/result-projection.json +4 -0
- package/src/shared/result-projection.mjs +50 -0
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +4 -4
- package/src/worker/daemon-sockets.ts +10 -1
- package/src/worker/errors.ts +18 -4
- package/src/worker/index.ts +45 -9
- package/src/worker/mcp-jsonrpc.ts +4 -2
- package/src/worker/pending-call-contract.ts +26 -0
- package/src/worker/pending-calls.ts +41 -25
- package/tsconfig.local.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.11 - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Bounded output continuation and repository backlog gate
|
|
6
|
+
|
|
7
|
+
- Stop large one-shot process responses from overwhelming MCP hosts. `run_process`, `run_local_command`, and `exec_command` now inline only a bounded stdout/stderr preview, retain up to 1 MiB per stream in a temporary completed process session, and return an `output_session_id` for paged `read_process` continuation. Nonzero exits keep their human error message bounded while preserving structured continuation details.
|
|
8
|
+
- Avoid protocol-level payload duplication by replacing the text mirror of large object results with a compact field summary while retaining the authoritative object in `structuredContent`. The fast/platform/full check runner suppresses successful child noise, preserves bounded head/tail diagnostics on failure, and supports explicit `MBM_CHECK_VERBOSE=1` streaming. Coverage cleanup now retries concurrent late V8 coverage-file writes instead of failing after the thresholds already passed.
|
|
9
|
+
- Resolve the release-acceptance file race by opening no-follow descriptors first, checking descriptor/path identity, and reading acceptance records and tarballs through bounded descriptors. Remove the temporary CodeQL exception and add deterministic path-replacement and symbolic-link regressions.
|
|
10
|
+
- Add a guarded GitHub backlog pre-push check. `npm run github:push` now blocks unrelated open pull requests and open issues not covered by a standard closing keyword in the current branch commits; the current branch PR remains updateable. Add unit and integration coverage for output paging, compact MCP projection, bounded check diagnostics, and backlog enforcement.
|
|
11
|
+
|
|
12
|
+
## 1.2.10 - 2026-07-20
|
|
13
|
+
|
|
14
|
+
### Relay interruption recovery
|
|
15
|
+
|
|
16
|
+
- Preserve an in-flight MCP tool call across a brief daemon WebSocket interruption instead of converting every transient proxy or network reset into an immediate cancellation. The Worker now detaches pending calls for a bounded 30-second grace period and rebinds them only when the same local daemon process instance completes the full authenticated readiness probe on its replacement socket.
|
|
17
|
+
- Keep the local operation running during that grace period and queue a completed result until the relay is ready again. Before `ready_ack`, the Worker sends an authoritative `resume_calls` set so the runtime cancels work whose client disappeared during the outage; results are replayed only for same-instance calls that still have a receiver. A different daemon instance cannot claim them, and an unrecovered outage cancels ordinary calls and process trees when the grace period expires.
|
|
18
|
+
- Add deterministic registry/runtime regressions and a real Worker/OAuth/MCP fault-injection test that starts a tool call, forcibly drops the daemon WebSocket, reconnects the same daemon instance, and proves the original HTTP request completes. Expose the bounded `pending_calls.detached` count for diagnosis, require a validated ephemeral daemon instance identifier in the current-version hello contract, and update architecture, logging, operations, audit, and multi-account documentation.
|
|
19
|
+
|
|
3
20
|
## 1.2.9 - 2026-07-18
|
|
4
21
|
|
|
5
22
|
- Repair cross-platform release infrastructure found by PR CI: the layered check runner now invokes the pinned npm CLI through Node instead of spawning `npm.cmd`, and `release:accept` computes and locally validates the portable Git-content digest through a temporary index so CI can verify accepted package content across merge commits without mutating the maintainer index.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -44,7 +44,7 @@ Repository-only infrastructure changes, such as a `.github/` workflow update, do
|
|
|
44
44
|
6. give the repository owner `npm run release:candidate:start -- --allow-worker-deploy`; the owner explicitly authorizes the in-place candidate Worker deployment, starts the exact candidate locally, and leaves it running;
|
|
45
45
|
7. verify the live candidate through Machine Bridge, including Worker version/hash, remote health, relay readiness, exact local version, and representative functionality relevant to the change;
|
|
46
46
|
8. after observed verification succeeds, have the coding agent run the exact `release:accept` command printed by the candidate tool, creating `release-acceptance/v<version>.json`;
|
|
47
|
-
9.
|
|
47
|
+
9. resolve every open issue and pull request before pushing. The current branch may cover an issue only with a standard closing keyword such as `Closes #47`; unrelated open PRs block the push. Commit the acceptance record and push the clean non-`main` branch only with `npm run github:push`, which paginates and enforces that backlog contract.
|
|
48
48
|
|
|
49
49
|
Automated checks do not authorize step 8. The coding agent may record acceptance only after it has observed the owner-started candidate operating successfully. Any packaged-file change after acceptance changes the npm tarball hash and requires a regenerated candidate and another observed live verification.
|
|
50
50
|
|
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ Major groups include:
|
|
|
217
217
|
|
|
218
218
|
```sh
|
|
219
219
|
npm ci
|
|
220
|
-
npm run check:fast # local feedback
|
|
220
|
+
npm run check:fast # quiet-success local feedback; set MBM_CHECK_VERBOSE=1 only for live child logs
|
|
221
221
|
npm run check # complete suite, equivalent to check:full
|
|
222
222
|
npm run worker:dry-run
|
|
223
223
|
npm audit --audit-level=high
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Machine Bridge Browser",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.11",
|
|
5
5
|
"description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "1.2.
|
|
33
|
+
"version_name": "1.2.11"
|
|
34
34
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -35,13 +35,13 @@ A canonical workspace receives an independent profile, Worker name, secret set,
|
|
|
35
35
|
- `runtime-diagnostics.mjs` owns fixed local probes and their stable interpretation;
|
|
36
36
|
- `runtime-capabilities.mjs` composes agent, application, and browser capability results;
|
|
37
37
|
- `runtime-tool-handlers.mjs` owns catalog-to-handler registration;
|
|
38
|
-
- `runtime-relay.mjs` owns relay construction and inbound envelope normalization;
|
|
38
|
+
- `runtime-relay.mjs` owns relay construction and inbound envelope normalization, while `relay-call-recovery.mjs` owns the bounded disconnect grace, result queue, authoritative resumed-call reconciliation, replay, and expiry cleanup;
|
|
39
39
|
- `runtime-paths.mjs` owns runtime-directory creation, containment checks, and error-path redaction;
|
|
40
40
|
- `managed-job-lock.mjs`, `managed-job-runner.mjs`, `managed-job-storage.mjs`, and `managed-job-projection.mjs` separate transition ownership, detached runner identity, private persistence/diagnostics, and public result shaping from the managed-job lifecycle;
|
|
41
41
|
- `browser-request-registry.mjs`, `browser-broker-routes.mjs`, `browser-broker-server.mjs`, and `browser-bridge-http.mjs` separate direct request ownership, runtime-client proxy routing, authenticated loopback WebSocket upgrades/listening, and loopback HTTP handling from broker startup and extension handover;
|
|
42
42
|
- managed jobs, local resources, application automation, and browser automation remain separate managers.
|
|
43
43
|
|
|
44
|
-
Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic, or capability-scoring logic to `LocalRuntime`. `RelayConnection` owns remote WebSocket transport, `hello_ack` authentication, end-to-end `relay_probe`/`ready_ack` readiness, heartbeat liveness, reconnect backoff, outage logging, and a monotonically increasing in-memory
|
|
44
|
+
Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic, or capability-scoring logic to `LocalRuntime`. `RelayConnection` owns remote WebSocket transport, `hello_ack` authentication, end-to-end `relay_probe`/`ready_ack` readiness, heartbeat liveness, reconnect backoff, outage logging, and a monotonically increasing in-memory transport generation. The generation still protects the pre-ready probe and prevents arbitrary use of a stale socket. Ordinary tool calls additionally bind to an ephemeral identifier generated once per local daemon process. If a ready socket drops, the Worker detaches its pending calls for at most thirty seconds; only a replacement socket that presents the same daemon-process identifier and completes the full readiness probe can reclaim them. The local runtime keeps those calls alive and queues completed results during the same bounded interval, then replays them after readiness. A different process instance, an explicit cancellation, or grace expiry cannot receive those results. Stdio mode invokes `LocalRuntime` directly without that adapter.
|
|
45
45
|
|
|
46
46
|
`daemon-process.mjs` owns workspace-daemon inspection and takeover. It distinguishes platform service state from the lock-owning Node process, validates PID and process-start identity, canonicalizes workspace/state paths before comparison, parses bounded process command lines without executing them, and accepts lock-backed `--daemon-only` recovery processes that omit repeated path flags. Stop/takeover sends `SIGTERM` only to a verified same-workspace service daemon. If it remains alive after the grace period, the code revalidates PID, process-start identity, command line, entrypoint, daemon mode, workspace, and state root before sending `SIGKILL`; a foreground, replaced-PID, or otherwise unverifiable process remains untouched. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
|
|
47
47
|
|
|
@@ -160,11 +160,12 @@ Remote OAuth binds each code, access token, and refresh token to a named Machine
|
|
|
160
160
|
7. The MCP client initializes against the sole current protocol version; an obsolete client must upgrade rather than enter a legacy execution path. The Worker returns a stateless HMAC-bound `MCP-Session-Id`, and later request/cancellation correlation is scoped by OAuth token, MCP session, JSON-RPC id type, and id value. Two clients may therefore reuse the same JSON-RPC id concurrently without collision. Sessionless POSTs remain independent and are not inserted into a token-global cancellation index. When the daemon advertises `session_bootstrap`, the Worker requests bounded local instructions and appends them to the initialization result; failure degrades to static instructions.
|
|
161
161
|
8. A new daemon first authenticates as a bounded `probing` socket. The Worker sends a random `relay_probe`; the local runtime returns it through the normal session-bound result-delivery path; only the matching result produces `ready_ack`, promotion to the active daemon, and safe replacement of an incumbent connection.
|
|
162
162
|
9. `tools/list` is derived only from the active end-to-end-verified daemon; without one, only `server_info` is advertised.
|
|
163
|
-
10. `tools/call` receives a random relay call ID and is bound to the current daemon socket, authenticated client request key, incoming HTTP abort signal
|
|
163
|
+
10. `tools/call` receives a random relay call ID and is bound to the current daemon socket, that daemon process's ephemeral instance identifier, the authenticated client request key, and the incoming HTTP abort signal.
|
|
164
164
|
11. The runtime validates policy and arguments, executes the tool, and returns a bounded result.
|
|
165
|
-
12.
|
|
166
|
-
13. A matching cancellation notification or incoming HTTP client disconnect removes the pending indexes
|
|
167
|
-
14.
|
|
165
|
+
12. If the socket remains ready, the Durable Object accepts the result only from that socket. If it drops, the Worker detaches the pending call for at most thirty seconds and accepts completion only after a replacement socket with the same daemon-process identifier has passed the end-to-end readiness probe. The local runtime preserves the operation and queues a completion over the same interval.
|
|
166
|
+
13. A matching cancellation notification or incoming HTTP client disconnect removes the pending indexes and sends best-effort cancellation to a connected daemon. Local completion that races with cancellation is discarded. On every readiness handover, the Worker first sends an authoritative bounded `resume_calls` set; the runtime cancels active calls and queued results absent from that set before accepting `ready_ack`. A request cancelled while disconnected therefore cannot be revived by a fast reconnect.
|
|
167
|
+
14. If same-instance readiness does not return before the grace deadline, the Worker rejects the detached request and the local runtime cancels ordinary calls, terminates their process trees, and discards queued results. A newly started daemon has a different instance identifier and cannot inherit prior calls.
|
|
168
|
+
15. `start_job` is different: after durable acceptance, the detached runner is no longer bound to the relay call or socket. Later cancellation uses `cancel_job` or the local CLI.
|
|
168
169
|
|
|
169
170
|
Duplicate in-flight JSON-RPC IDs are rejected only within the same authenticated MCP session. The request key includes OAuth token identity, the HMAC-bound MCP session, JSON-RPC id type, and id value, so separate initialized clients may safely reuse the same numeric id.
|
|
170
171
|
|
|
@@ -212,7 +213,11 @@ This is a process-level transaction, not a filesystem-wide atomic transaction ac
|
|
|
212
213
|
|
|
213
214
|
The default `full` profile passes the complete parent environment. Isolated environment mode, used by the narrower named profiles unless overridden, creates private runtime HOME, temporary, and cache directories and passes only a small set of path/locale/platform variables. It reduces accidental credential inheritance but cannot prevent explicit access to known filesystem paths, credential stores, network services, or other user resources.
|
|
214
215
|
|
|
215
|
-
`execution-limits.mjs` is the shared source for local tool-call concurrency, one-shot process timeout/stdin/output limits, and process-session count/stdin/output/retention limits. `server_info.runtime.execution_guardrails` reports those enforced limits together with explicit `not-enforced` values for CPU quota, memory quota, and network isolation.
|
|
216
|
+
`execution-limits.mjs` is the shared source for local tool-call concurrency, one-shot process timeout/stdin/output limits, and process-session count/stdin/output/retention limits. `server_info.runtime.execution_guardrails` reports those enforced limits together with explicit `not-enforced` values for CPU quota, memory quota, and network isolation. Public one-shot commands inline at most 32 KiB per stream. When either stream exceeds that preview, the runtime keeps up to 1 MiB per stream in a closed in-memory process session for thirty minutes and returns an `output_session_id`; `read_process` then reads monotonic byte-offset pages. The oldest exited session is evicted before an active session is refused, so continuation retention is explicitly best effort rather than durable. The continuation stores command basename and cwd metadata but not argv or shell text. It is memory-only and disappears on runtime stop or daemon replacement.
|
|
217
|
+
|
|
218
|
+
Large object results use `structuredContent` as the authoritative representation. The human text mirror is complete only below the shared 16 KiB projection threshold; above it, both local stdio and the Worker return the same compact byte-count/field summary instead of serializing the object a second time. `process-result-projection.mjs`, `process-output-stream.mjs`, and the shared result projector keep lifecycle, byte retention, and MCP presentation as separate boundaries.
|
|
219
|
+
|
|
220
|
+
Startup-lock waits, daemon takeover, process-session reads, managed-job recovery handoff, browser/page waits, application-cache freshness, and in-memory duration metrics use monotonic elapsed time, so wall-clock correction cannot extend or prematurely terminate their configured duration. Persisted timestamps and retention/credential expiry continue to use wall time. Process sessions retain bounded byte buffers with monotonic offsets, accept bounded stdin, support short output/exit waits, and are capped per runtime. Valid UTF-8 is returned as text; byte slices that are not valid UTF-8 also include lossless base64 data. Head/tail previews trim incomplete UTF-8 boundary code points instead of introducing replacement characters. Session IDs are random. Running sessions are killed on runtime stop, remote disconnect, or daemon replacement.
|
|
216
221
|
|
|
217
222
|
Child processes run in a separate process group where supported. Timeout, cancellation, disconnect, and replacement send termination to process trees, with a referenced forced-escalation timer that remains alive even when the direct child exits before a resistant descendant. Windows uses tree-aware task termination.
|
|
218
223
|
|
|
@@ -234,7 +239,9 @@ A connection-attempt deadline terminates sockets stuck in `CONNECTING`. After op
|
|
|
234
239
|
|
|
235
240
|
Reconnect uses bounded exponential backoff with jitter. Brief self-healing interruptions are debug-only. An unresolved outage is promoted to a rate-limited warning after a grace period, and recovery produces one summary. Raw close codes and reason strings remain debug-only.
|
|
236
241
|
|
|
237
|
-
The Worker stores socket transitions in `DaemonSocketRegistry`: `candidate` before hello, `probing` after authentication, `daemon` only after the end-to-end result probe, and `expired` after terminal failure. Durable Object alarms enforce separate hello, readiness, and steady-state liveness deadlines across hibernation. A healthy incumbent remains active while a replacement is probed; a malformed, silent, or
|
|
242
|
+
The Worker stores socket transitions in `DaemonSocketRegistry`: `candidate` before hello, `probing` after authentication, `daemon` only after the end-to-end result probe, and `expired` after terminal failure. Durable Object alarms enforce separate hello, readiness, and steady-state liveness deadlines across hibernation. A healthy incumbent remains active while a replacement is probed; a malformed, silent, incompatible, or identity-mismatched replacement is closed without displacing it. Only a verified candidate receives `ready_ack` and then replaces the old socket. Ready daemons stay live only while inbound traffic refreshes `lastSeenAt`; silent half-open or hibernation-restored sockets are reclaimed instead of advertising `daemon.connected` while tool calls time out.
|
|
243
|
+
|
|
244
|
+
Each daemon process generates a random bounded `instance_id` at startup and includes it in every reconnect hello. Pending calls normally retain their assigned socket. On an unexpected socket loss, only those records are detached and a thirty-second timer bounds recovery. A verified socket with the same instance ID rebinds them; another process or socket cannot resolve them. The local runtime mirrors that state machine by preserving active calls and completed-result envelopes until relay readiness returns. Before `ready_ack`, the Worker sends the exact IDs that still have remote waiters; the runtime cancels everything else and only then replays retained results through the verified socket. Grace expiry restores the terminal behavior: reject remote waiters, cancel local ordinary calls, terminate process trees, and discard undeliverable results. This does not make calls durable across daemon restart or machine failure; managed jobs remain the separate durable mechanism.
|
|
238
245
|
|
|
239
246
|
## Persistence
|
|
240
247
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Security and privacy audit notes
|
|
2
2
|
|
|
3
|
+
## 2026-07-20 version 1.2.11 bounded-result and release-file audit
|
|
4
|
+
|
|
5
|
+
The reported web truncation was not one defect. One-shot command tools retained up to 512 KiB independently for stdout and stderr, returned no continuation handle after dropping their middle, and generic MCP framing serialized object results once as text and again as `structuredContent`. The layered verification runner then inherited every child stream, so a long successful gate could accumulate output even though only its final status mattered. Nonzero process exits also promoted the retained stderr preview into the human error message. These independent amplification points made host-side response truncation frequent and made locally truncated command output unrecoverable.
|
|
6
|
+
|
|
7
|
+
Version 1.2.11 separates preview, retention, and presentation. Public one-shot commands inline 32 KiB per stream and retain up to 1 MiB per stream in an exited in-memory process session. The response supplies a random `output_session_id` and bounded `read_process` parameters; reads use monotonic offsets, disclose aged-out data, and preserve invalid byte slices with base64. Failed commands keep a bounded message and carry the same continuation only in exposed structured details. Large object results have one authoritative `structuredContent` value and a short shared text projection rather than a second full JSON serialization. UTF-8 head/tail previews no longer split Chinese or emoji code points. Green verification tasks suppress child noise; failures retain bounded head and tail, while explicit verbose mode remains available.
|
|
8
|
+
|
|
9
|
+
The only open security issue concerned `lstat` followed by path-based reads of release acceptance JSON and candidate tarballs. Both now open with no-follow semantics, validate the descriptor as a regular file, compare descriptor and path identity, enforce a pre-allocation size ceiling, and read through the descriptor. Deterministic tests replace the path after open and prove the descriptor remains authoritative; symlink records and tarballs fail closed. The temporary CodeQL file-race exception is removed. The guarded push path now also paginates the complete GitHub issue/PR backlog and refuses unrelated open PRs or issues not closed by the current branch, making repository backlog completion an executable release condition rather than a conversational promise.
|
|
10
|
+
|
|
11
|
+
Repeated full-gate execution also exposed a separate test-infrastructure race: V8 could finish writing a coverage file just after the last fixture process exited, causing recursive cleanup to fail with `ENOTEMPTY` even though every threshold passed. Coverage cleanup now uses bounded retry/backoff, the contract is architecture-tested, and three consecutive coverage runs pass.
|
|
12
|
+
|
|
13
|
+
Privacy review found no new durable output store. Continuation bytes live only in the daemon heap, share the existing eight-session/30-minute bounds, are omitted from logs and acceptance records, and disappear on runtime stop. Complete and production dependency audits remain at zero vulnerabilities; registry signatures, attestations, package file modes, generated references, and reachable Git history privacy checks are independently verified.
|
|
14
|
+
|
|
15
|
+
## 2026-07-20 version 1.2.10 transient-relay recovery audit
|
|
16
|
+
|
|
17
|
+
Repeated ChatGPT web executions were terminating even though the local daemon and Worker were healthy immediately afterward. The causal defect was not the tool implementation or its configured timeout. A brief WebSocket loss—made more likely when the daemon relay traverses an environment-selected local proxy—was promoted into a terminal request failure at both ends: the Worker rejected every pending call assigned to the closed socket, while the local runtime cancelled all relay-origin calls and killed their ordinary child processes. Automatic WebSocket reconnection restored future traffic but could not recover the request already abandoned by both state machines.
|
|
18
|
+
|
|
19
|
+
The correction introduces a bounded continuation identity rather than weakening cancellation globally. Every local daemon process creates one random ephemeral `instance_id` and presents it in the current-version hello. On socket loss, the Worker detaches only calls owned by that socket and retains their OAuth/session request indexes for thirty seconds. A replacement socket may rebind them only after presenting the same process identifier and completing the existing end-to-end readiness probe. The local runtime mirrors this interval: active calls continue, completed result envelopes are retained in memory, and readiness triggers replay. A different process instance cannot claim the pending IDs; daemon restart remains terminal rather than pretending to provide durable execution.
|
|
20
|
+
|
|
21
|
+
The recovery path remains bounded and fail closed. Explicit MCP cancellation and HTTP client abandonment still remove the remote receiver immediately. A connected daemon receives cancellation as before. If cancellation occurs while disconnected, the next same-instance handshake carries an authoritative `resume_calls` set before `ready_ack`; the local runtime cancels any active or completed call absent from that set, so a fast reconnect cannot revive a request whose HTTP receiver disappeared. Timeout remains authoritative. At thirty seconds without same-instance readiness, the Worker rejects detached requests and the runtime cancels calls, discards queued results, and enters the existing persistent-outage reporting path. No tool arguments, commands, paths, outputs, proxy endpoint, or result content are added to logs or durable storage.
|
|
22
|
+
|
|
23
|
+
Regression evidence covers three layers: deterministic pending-registry detach/rebind/expiry tests; local runtime queue/replay, resumed-call reconciliation, cancellation, and grace behavior; and real workerd OAuth/MCP/WebSocket fault injection that both completes a detached request after same-instance reconnect and proves a request cancelled while detached is excluded from the next `resume_calls` set. Malformed or missing daemon-instance identifiers are rejected as protocol errors. The version remains a current-only Worker/daemon protocol unit and requires normal candidate convergence before live use.
|
|
24
|
+
|
|
3
25
|
## 2026-07-18 version 1.2.9 interactive release-handoff correction
|
|
4
26
|
|
|
5
27
|
The release-integrity mechanism correctly bound acceptance to exact package bytes, but the operational role split was wrong. Documentation and repository guards required the owner to run the acceptance command personally and prohibited the coding agent from recording it. That contradicted the established working process: the owner should perform the one action the agent cannot perform honestly—start the exact candidate on the maintainer machine—while the agent should connect to that candidate, verify its version, readiness, and representative functionality, then record acceptance and complete source release operations.
|
package/docs/LOGGING.md
CHANGED
|
@@ -77,9 +77,11 @@ With `--verbose`, the same incident additionally includes bounded structured fie
|
|
|
77
77
|
|
|
78
78
|
## Tool and result-delivery events
|
|
79
79
|
|
|
80
|
-
All per-tool starts, successes, failures, cancellations, timing, and expected late-result disposal are debug-only. The MCP response already reports the outcome to the caller; duplicating routine tool traffic at default levels creates noise and can reveal activity patterns.
|
|
80
|
+
All per-tool starts, successes, failures, cancellations, timing, and expected late-result disposal are debug-only. The MCP response already reports the outcome to the caller; duplicating routine tool traffic at default levels creates noise and can reveal activity patterns. Completed one-shot output continuations are not written to daemon logs or disk; their bounded stdout/stderr remains only in the daemon process-session memory and expires with normal session retention.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
The layered repository check runner follows the same noise rule. Green child-task output is discarded after the child exits; only task name and elapsed time are printed. Failed tasks expose bounded head/tail stdout and stderr diagnostics. `MBM_CHECK_VERBOSE=1` is an explicit operator choice to stream raw child output and is not used by default or CI.
|
|
83
|
+
|
|
84
|
+
A completed local result is normally sent on the ready relay connection. If that socket disappears, the runtime queues the bounded result envelope during the thirty-second same-daemon reconnect window rather than logging a terminal delivery failure. Debug output records only a shortened call ID and queue/reconnect counts. After the same daemon process completes readiness, replay emits one recovery event; a different process cannot inherit the result. Explicit caller cancellation suppresses eventual delivery. If the relay does not recover before the grace deadline, ordinary calls are cancelled, queued results are discarded, and the existing outage state machine determines whether the persistent failure warrants a warning. Tool arguments, commands, and result content are never logged.
|
|
83
85
|
|
|
84
86
|
Debug per-tool fields may include tool name, duration, coarse outcome class, and a shortened random call identifier. The identifier is for correlating adjacent local events and is not a stable audit identifier.
|
|
85
87
|
|
package/docs/MULTI_ACCOUNT.md
CHANGED
|
@@ -93,9 +93,9 @@ The first start of a new deployment creates an `owner` account automatically and
|
|
|
93
93
|
|
|
94
94
|
## Concurrency and revocation
|
|
95
95
|
|
|
96
|
-
Pending calls remain bound to the OAuth access token and JSON-RPC request ID. Duplicate in-flight IDs under one token are rejected. Account revocation blocks new requests immediately; calls already relayed remain subject to ordinary cancellation, deadlines,
|
|
96
|
+
Pending calls remain bound to the OAuth access token and JSON-RPC request ID. Duplicate in-flight IDs under one token are rejected. Account revocation blocks new requests immediately; calls already relayed remain subject to ordinary cancellation, deadlines, local role validation, and the bounded same-daemon reconnect state machine. A replacement process cannot inherit a detached call.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
A relay interruption keeps ordinary relay-owned calls alive only inside the bounded same-daemon reconnect window. Reconciliation or grace expiry cancels calls that no longer have a remote receiver and terminates their child process trees. Process promises settle on cancellation even when a child does not emit `close`; process ownership remains tracked until actual exit.
|
|
99
99
|
|
|
100
100
|
## Audit and privacy
|
|
101
101
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -51,7 +51,7 @@ A successful diagnostic result applies only to that probe. An MCP host can still
|
|
|
51
51
|
|
|
52
52
|
Machine Bridge supports concurrent calls: the Worker admits up to 32 pending daemon calls and the local runtime admits up to 16 active tool calls. These are capacity limits, not a single global execution queue. Each successful MCP initialization receives a signed `MCP-Session-Id`; JSON-RPC ids and cancellation are scoped to that session, so separate chat windows may reuse the same numeric ids safely even when they share one OAuth account and token.
|
|
53
53
|
|
|
54
|
-
`server_info.worker.pending_calls` reports `active`, `request_keys`, `maximum`, `oldest_ms`, and `by_tool`. `worker.sockets_live` separately reports `authenticated`, `probing`, `ready`, and `candidates`; only `ready` sockets contribute to `daemon.connected` and tool advertisement. A nonzero `active` count means work is in flight, not that the bridge is locked. Calls for simple reads and probes should continue while another independent process call runs. Explicit MCP cancellation, an incoming HTTP client disconnect,
|
|
54
|
+
`server_info.worker.pending_calls` reports `active`, `detached`, `request_keys`, `maximum`, `oldest_ms`, and `by_tool`. `worker.sockets_live` separately reports `authenticated`, `probing`, `ready`, and `candidates`; only `ready` sockets contribute to `daemon.connected` and tool advertisement. A nonzero `active` count means work is in flight, not that the bridge is locked. `detached > 0` means a daemon socket was lost and those requests are inside the bounded thirty-second same-instance reconnect window. Calls for simple reads and probes should continue while another independent process call runs. Explicit MCP cancellation, an incoming HTTP client disconnect, and timeout remove the pending record and its session request key. A daemon-socket closure detaches only calls assigned to that socket; the same daemon process can reclaim them after completing readiness, while another process cannot. Grace expiry rejects the request and cancels the local ordinary operation. Refreshing a chat page is not the recovery mechanism and should not be required.
|
|
55
55
|
|
|
56
56
|
`server_info.worker.observability.calls.unmatched_results` counts results that reached the Worker after their pending record was already removed. A small increase can accompany cancellation or timeout races, especially during mixed-version upgrade convergence; sustained growth together with old pending calls indicates incompatible components or a lifecycle defect. The counter contains no tool arguments or result data.
|
|
57
57
|
|
|
@@ -147,7 +147,7 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
|
|
|
147
147
|
|
|
148
148
|
### Lifecycle and pending-call diagnosis
|
|
149
149
|
|
|
150
|
-
`server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `server_info.observability.in_flight_calls` and `server_info.runtime.processes` distinguish a blocked call from a surviving process. `server_info.runtime.execution_guardrails` reports the enforced local concurrency/timeout/stdin/output/session limits and explicitly states that CPU quota, memory quota, and network isolation are not enforced in process. Worker `server_info.worker.pending_calls` reports
|
|
150
|
+
`server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `server_info.observability.in_flight_calls` and `server_info.runtime.processes` distinguish a blocked call from a surviving process. `server_info.runtime.execution_guardrails` reports the enforced local concurrency/timeout/stdin/output/session limits and explicitly states that CPU quota, memory quota, and network isolation are not enforced in process. Worker `server_info.worker.pending_calls` reports the internal-call index, client request-key index, and detached-call count. All three must return to zero after a terminal result, explicit cancellation, client disconnect, timeout, or reconnect-grace expiry. During a brief daemon interruption, `active` and `request_keys` may remain nonzero while `detached` identifies the recoverable subset; after same-instance readiness, `detached` returns to zero without losing those requests. Nonzero request-key counts after active calls reach zero indicate a lifecycle defect rather than normal load. `worker.observability.calls.unmatched_results` is the bounded counter for late results that no longer have a receiver.
|
|
151
151
|
|
|
152
152
|
Stable errors include `policy_denied`, `invalid_request`, `timeout`, `cancelled`, `network_error`, `unavailable`, `limit_exceeded`, and `integrity_error`, with retryability metadata. Diagnose by code first; free-form messages are guidance, not an API contract.
|
|
153
153
|
|
|
@@ -248,8 +248,9 @@ Defense-in-depth limits include:
|
|
|
248
248
|
- text writes and patch envelopes: 5 MiB;
|
|
249
249
|
- images: 4 MiB before base64 encoding;
|
|
250
250
|
- shell/argv envelope: 64 KiB;
|
|
251
|
-
-
|
|
252
|
-
-
|
|
251
|
+
- public one-shot output preview: 32 KiB per stream; larger output returns an `output_session_id` and remains readable through `read_process`;
|
|
252
|
+
- internal bounded subprocess capture: 512 KiB per stream by default;
|
|
253
|
+
- running or completed process-session retained output: 1 MiB per stream for up to 30 minutes, best effort subject to the eight-session capacity, with monotonic offsets and lossless base64 fallback for non-UTF-8 slices;
|
|
253
254
|
- process sessions: 8 retained per runtime;
|
|
254
255
|
- process stdin write: 64 KiB per call;
|
|
255
256
|
- local simultaneous tool calls: 16;
|
package/docs/RELEASING.md
CHANGED
|
@@ -73,7 +73,7 @@ Commit the candidate changes and acceptance record, then push the branch only th
|
|
|
73
73
|
npm run github:push
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
The command requires a clean non-`main` branch, verifies that the acceptance record is tracked, rebuilds the npm package, compares both hashes, and only then executes a non-force push of the current branch. Direct pushes to `main` remain prohibited. Any packaged-file change after acceptance invalidates the hash and blocks the next push until the owner retests a regenerated candidate.
|
|
76
|
+
The command requires a clean non-`main` branch, fetches `origin/main`, paginates all open GitHub issues and pull requests, and refuses to push while an unrelated PR remains open or an issue is not covered by a standard closing keyword in `origin/main..HEAD`. An already-open PR for the current branch is allowed so it can be updated. The command then verifies that the acceptance record is tracked, rebuilds the npm package, compares both hashes, and only then executes a non-force push of the current branch. Direct pushes to `main` remain prohibited. Any packaged-file change after acceptance invalidates the hash and blocks the next push until the owner retests a regenerated candidate.
|
|
77
77
|
|
|
78
78
|
Open a pull request, satisfy the required checks, and squash-merge. Pull-request CI repeats the acceptance verification. A content-preserving squash changes the Git commit but not the package bytes, so the accepted hash remains valid.
|
|
79
79
|
|
package/docs/TESTING.md
CHANGED
|
@@ -12,12 +12,16 @@ npm run check # complete suite; equivalent to check:full
|
|
|
12
12
|
|
|
13
13
|
The explicit task lists live in `scripts/check-plan.mjs`. The fast plan retains static analysis, policy, architecture, contracts, and focused unit behavior. The platform plan adds process, service, browser/application, state, managed-job, cryptographic, and real-machine behavior tests needed on macOS and Windows. The full-only plan adds coverage, browser-broker integration, package/install, stdio, Worker/OAuth, and real-browser navigation suites. `npm run check` remains the authoritative complete gate.
|
|
14
14
|
|
|
15
|
+
Successful child-task stdout/stderr is intentionally suppressed so a long green plan does not overwhelm an MCP response or hide the final status behind host truncation. Progress and timing remain visible. A failed task returns bounded head/tail diagnostics for both streams. Set `MBM_CHECK_VERBOSE=1` only when an operator explicitly needs live child output; verbose mode can be large and should be run through a process session when used remotely.
|
|
16
|
+
|
|
15
17
|
The repository requires Node.js 26 and npm 12. `.node-version`, `.nvmrc`, `packageManager`, `devEngines`, and strict engine checks keep local and CI execution on the same baseline.
|
|
16
18
|
|
|
17
19
|
The suite includes:
|
|
18
20
|
|
|
19
21
|
- package-impact classification derived from the npm package manifest, allowing repository-only GitHub workflow maintenance without weakening version requirements for shipped content;
|
|
20
|
-
- interactive candidate acceptance hashing, including owner-authorized in-place Worker deployment plus owner-started candidate verification, exact tarball SHA-1/SHA-512 matching, acceptance-record exclusion from the package, legacy 1.2.8 marker compatibility, and invalidation after any packaged-byte change;
|
|
22
|
+
- interactive candidate acceptance hashing, including owner-authorized in-place Worker deployment plus owner-started candidate verification, exact tarball SHA-1/SHA-512 matching, descriptor-first no-follow reads, path-identity/symlink regressions, acceptance-record exclusion from the package, legacy 1.2.8 marker compatibility, and invalidation after any packaged-byte change;
|
|
23
|
+
- one-shot process output preview/continuation, completed-session paging, bounded nonzero-exit details, UTF-8-safe head/tail diagnostics, compact local/Worker MCP text mirrors, and quiet-success/bounded-failure verification-runner behavior;
|
|
24
|
+
- GitHub backlog enforcement that paginates all open issues and pull requests, permits only the current branch PR, and requires standard closing keywords for every open issue before a guarded push;
|
|
21
25
|
- release-impact enforcement requiring a new package version and CHANGELOG section for release-relevant changes;
|
|
22
26
|
- release-state diagnostics distinguishing missing local/remote version tags from tags that point to the wrong commit, plus a release-CI gate that rejects missing, pending, failed, pull-request-only, stale, or wrong-commit runs;
|
|
23
27
|
- generated Cloudflare Worker types under ignored `.wrangler/` state and strict TypeScript checking, including unused-local and unused-parameter rejection; packaging rejects generated declarations;
|
package/docs/TOOL_REFERENCE.md
CHANGED
|
@@ -1455,7 +1455,7 @@ List effective direct-argv commands from project manifests and safe automatic pa
|
|
|
1455
1455
|
|
|
1456
1456
|
**Run registered local command**
|
|
1457
1457
|
|
|
1458
|
-
Run an effective manifest or automatic package-script command through its fixed argv, cwd, timeout ceiling, and extra-argument policy.
|
|
1458
|
+
Run an effective manifest or automatic package-script command through its fixed argv, cwd, timeout ceiling, and extra-argument policy. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.
|
|
1459
1459
|
|
|
1460
1460
|
| Contract field | Value |
|
|
1461
1461
|
|---|---|
|
|
@@ -1988,7 +1988,7 @@ Return bounded metadata and patch output for one revision without running reposi
|
|
|
1988
1988
|
|
|
1989
1989
|
**Run process directly**
|
|
1990
1990
|
|
|
1991
|
-
Execute an argv array without a command shell. This avoids shell parsing but does not sandbox the executable or code it launches.
|
|
1991
|
+
Execute an argv array without a command shell. This avoids shell parsing but does not sandbox the executable or code it launches. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.
|
|
1992
1992
|
|
|
1993
1993
|
| Contract field | Value |
|
|
1994
1994
|
|---|---|
|
|
@@ -2074,7 +2074,7 @@ Start a direct argv process without a shell and retain bounded stdout, stderr, a
|
|
|
2074
2074
|
|
|
2075
2075
|
**Read process session**
|
|
2076
2076
|
|
|
2077
|
-
Read bounded stdout and stderr deltas from a
|
|
2077
|
+
Read bounded stdout and stderr deltas from a running process session or a recently completed one-shot command continuation, optionally waiting briefly for new output.
|
|
2078
2078
|
|
|
2079
2079
|
| Contract field | Value |
|
|
2080
2080
|
|---|---|
|
|
@@ -2796,7 +2796,7 @@ Request cancellation of a detached managed job. The runner terminates the active
|
|
|
2796
2796
|
|
|
2797
2797
|
**Execute shell command**
|
|
2798
2798
|
|
|
2799
|
-
Execute a shell command with workspace cwd. This is not a sandbox and has the operating-system authority of the local user.
|
|
2799
|
+
Execute a shell command with workspace cwd. This is not a sandbox and has the operating-system authority of the local user. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.
|
|
2800
2800
|
|
|
2801
2801
|
| Contract field | Value |
|
|
2802
2802
|
|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.11",
|
|
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",
|
|
@@ -129,7 +129,11 @@
|
|
|
129
129
|
"check-plan:test": "node tests/check-plan-test.mjs",
|
|
130
130
|
"check:platform": "node scripts/run-checks.mjs platform",
|
|
131
131
|
"release": "node scripts/github-release.mjs --publish",
|
|
132
|
-
"release:candidate:start": "node scripts/start-release-candidate.mjs"
|
|
132
|
+
"release:candidate:start": "node scripts/start-release-candidate.mjs",
|
|
133
|
+
"check-runner:test": "node tests/check-runner-test.mjs",
|
|
134
|
+
"process-output:test": "node tests/process-output-continuation-test.mjs",
|
|
135
|
+
"github-backlog:test": "node tests/github-backlog-test.mjs",
|
|
136
|
+
"github:backlog": "node scripts/github-backlog.mjs"
|
|
133
137
|
},
|
|
134
138
|
"dependencies": {
|
|
135
139
|
"https-proxy-agent": "9.1.0",
|
package/scripts/check-plan.mjs
CHANGED
|
@@ -5,6 +5,9 @@ export const FAST_CHECK_TASKS = Object.freeze([
|
|
|
5
5
|
"release-state:test",
|
|
6
6
|
"release-ci:test",
|
|
7
7
|
"network-retry:test",
|
|
8
|
+
"check-runner:test",
|
|
9
|
+
"process-output:test",
|
|
10
|
+
"github-backlog:test",
|
|
8
11
|
"secure-file:test",
|
|
9
12
|
"worker-secret-file:test",
|
|
10
13
|
"sarif-security:test",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { BoundedOutput } from "../src/local/bounded-output.mjs";
|
|
4
|
+
|
|
5
|
+
const FAILURE_OUTPUT_BYTES_PER_STREAM = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
export async function runVerificationPlan(options) {
|
|
8
|
+
const {
|
|
9
|
+
mode,
|
|
10
|
+
tasks,
|
|
11
|
+
npmCli,
|
|
12
|
+
cwd = process.cwd(),
|
|
13
|
+
env = process.env,
|
|
14
|
+
verbose = false,
|
|
15
|
+
stdout = process.stdout,
|
|
16
|
+
stderr = process.stderr,
|
|
17
|
+
spawnProcess = spawn,
|
|
18
|
+
} = options;
|
|
19
|
+
if (!npmCli) throw new Error("check runner must run through npm so npm_execpath is available");
|
|
20
|
+
const planStartedAt = performance.now();
|
|
21
|
+
stdout.write(`running ${mode} verification plan (${tasks.length} tasks)\n`);
|
|
22
|
+
for (const [index, task] of tasks.entries()) {
|
|
23
|
+
const taskStartedAt = performance.now();
|
|
24
|
+
stdout.write(`\n[${index + 1}/${tasks.length}] npm run ${task}\n`);
|
|
25
|
+
const result = await runTask({ task, npmCli, cwd, env, verbose, spawnProcess });
|
|
26
|
+
const elapsedSeconds = ((performance.now() - taskStartedAt) / 1000).toFixed(1);
|
|
27
|
+
if (result.error) throw result.error;
|
|
28
|
+
if (result.code !== 0) {
|
|
29
|
+
stderr.write(`verification task failed after ${elapsedSeconds}s: ${task}\n`);
|
|
30
|
+
emitFailureDiagnostics(result, stderr);
|
|
31
|
+
const error = new Error(`verification task failed: ${task}`);
|
|
32
|
+
error.exitCode = result.code || 1;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
stdout.write(`completed ${task} in ${elapsedSeconds}s\n`);
|
|
36
|
+
}
|
|
37
|
+
const totalSeconds = ((performance.now() - planStartedAt) / 1000).toFixed(1);
|
|
38
|
+
stdout.write(`\n${mode} verification plan passed in ${totalSeconds}s\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runTask({ task, npmCli, cwd, env, verbose, spawnProcess }) {
|
|
42
|
+
return new Promise((resolvePromise) => {
|
|
43
|
+
const stdout = verbose ? null : new BoundedOutput(FAILURE_OUTPUT_BYTES_PER_STREAM);
|
|
44
|
+
const stderr = verbose ? null : new BoundedOutput(FAILURE_OUTPUT_BYTES_PER_STREAM);
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawnProcess(process.execPath, [npmCli, "run", "--silent", task], {
|
|
48
|
+
cwd,
|
|
49
|
+
env: { ...env, NO_COLOR: env.NO_COLOR || "1" },
|
|
50
|
+
stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
shell: false,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
resolvePromise({ code: 1, error, stdout, stderr });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
child.stdout?.on?.("data", (chunk) => stdout?.append(chunk));
|
|
59
|
+
child.stderr?.on?.("data", (chunk) => stderr?.append(chunk));
|
|
60
|
+
child.once("error", (error) => resolvePromise({ code: 1, error, stdout, stderr }));
|
|
61
|
+
child.once("close", (code) => resolvePromise({ code: Number.isInteger(code) ? code : 1, stdout, stderr }));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emitFailureDiagnostics(result, output) {
|
|
66
|
+
const stdoutText = result.stdout?.text?.() || "";
|
|
67
|
+
const stderrText = result.stderr?.text?.() || "";
|
|
68
|
+
if (stdoutText) output.write(`\n--- task stdout ---\n${ensureNewline(stdoutText)}`);
|
|
69
|
+
if (stderrText) output.write(`\n--- task stderr ---\n${ensureNewline(stderrText)}`);
|
|
70
|
+
if (!stdoutText && !stderrText) output.write("task produced no captured output\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureNewline(value) {
|
|
74
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
75
|
+
}
|
|
@@ -115,7 +115,7 @@ try {
|
|
|
115
115
|
if (failures.length) throw new Error(`coverage thresholds failed:\n- ${failures.join("\n- ")}`);
|
|
116
116
|
console.log("critical-module coverage thresholds passed");
|
|
117
117
|
} finally {
|
|
118
|
-
rmSync(coverageDir, { recursive: true, force: true });
|
|
118
|
+
rmSync(coverageDir, { recursive: true, force: true, maxRetries: 8, retryDelay: 50 });
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function collectCoverage(directory) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
|
|
9
|
+
export function closingIssueNumbers(messages) {
|
|
10
|
+
const numbers = new Set();
|
|
11
|
+
const pattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?[ \t]+#(\d+)\b/gi;
|
|
12
|
+
for (const match of String(messages || "").matchAll(pattern)) numbers.add(Number(match[1]));
|
|
13
|
+
return numbers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function backlogBlockers({ issues = [], pullRequests = [], branch = "", commitMessages = "" }) {
|
|
17
|
+
const closing = closingIssueNumbers(commitMessages);
|
|
18
|
+
const issueBlockers = issues.filter((issue) => !closing.has(Number(issue.number)));
|
|
19
|
+
const pullRequestBlockers = pullRequests.filter((pull) => !(
|
|
20
|
+
String(pull.headRefName || "") === branch
|
|
21
|
+
&& pull.headRepository
|
|
22
|
+
&& pull.headRepository === pull.baseRepository
|
|
23
|
+
));
|
|
24
|
+
return { issueBlockers, pullRequestBlockers, closing: [...closing].sort((left, right) => left - right) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assertGitHubBacklogReady(options = {}) {
|
|
28
|
+
const run = options.run || runCommand;
|
|
29
|
+
const cwd = options.cwd || root;
|
|
30
|
+
const branch = options.branch || output(run, "git", ["branch", "--show-current"], cwd);
|
|
31
|
+
if (!branch) throw new Error("cannot inspect GitHub backlog from a detached HEAD");
|
|
32
|
+
const commitMessages = output(run, "git", ["log", "--format=%B%x00", "origin/main..HEAD"], cwd);
|
|
33
|
+
const issueRows = pagedJson(output(run, "gh", [
|
|
34
|
+
"api", "--paginate", "--slurp", "repos/{owner}/{repo}/issues?state=open&per_page=100",
|
|
35
|
+
], cwd), "open GitHub issues");
|
|
36
|
+
const pullRows = pagedJson(output(run, "gh", [
|
|
37
|
+
"api", "--paginate", "--slurp", "repos/{owner}/{repo}/pulls?state=open&per_page=100",
|
|
38
|
+
], cwd), "open GitHub pull requests");
|
|
39
|
+
const issues = issueRows
|
|
40
|
+
.filter((item) => !item.pull_request)
|
|
41
|
+
.map((item) => ({ number: item.number, title: item.title, url: item.html_url }));
|
|
42
|
+
const pullRequests = pullRows.map((item) => ({
|
|
43
|
+
number: item.number,
|
|
44
|
+
title: item.title,
|
|
45
|
+
headRefName: item.head?.ref,
|
|
46
|
+
headRepository: item.head?.repo?.full_name,
|
|
47
|
+
baseRepository: item.base?.repo?.full_name,
|
|
48
|
+
url: item.html_url,
|
|
49
|
+
}));
|
|
50
|
+
const blockers = backlogBlockers({ issues, pullRequests, branch, commitMessages });
|
|
51
|
+
if (!blockers.issueBlockers.length && !blockers.pullRequestBlockers.length) {
|
|
52
|
+
const covered = blockers.closing.length ? `; closing issue(s): ${blockers.closing.map((number) => `#${number}`).join(", ")}` : "";
|
|
53
|
+
return { branch, issues, pullRequests, ...blockers, message: `GitHub backlog gate passed${covered}.` };
|
|
54
|
+
}
|
|
55
|
+
throw new Error(formatBlockers(blockers, branch));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatBlockers(blockers, branch) {
|
|
59
|
+
const lines = ["GitHub backlog is not ready for another push."];
|
|
60
|
+
if (blockers.issueBlockers.length) {
|
|
61
|
+
lines.push("Open issues not closed by a commit in origin/main..HEAD:");
|
|
62
|
+
for (const issue of blockers.issueBlockers) lines.push(`- #${issue.number} ${boundedTitle(issue.title)}${issue.url ? ` (${issue.url})` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
if (blockers.pullRequestBlockers.length) {
|
|
65
|
+
lines.push(`Open pull requests other than the current branch (${branch}):`);
|
|
66
|
+
for (const pull of blockers.pullRequestBlockers) lines.push(`- #${pull.number} ${boundedTitle(pull.title)} [${pull.headRefName || "unknown branch"}]${pull.url ? ` (${pull.url})` : ""}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("Resolve or close every blocker before pushing. The current branch may cover an issue with a standard closing keyword such as 'Closes #47'.");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function boundedTitle(value) {
|
|
73
|
+
return String(value || "untitled").replace(/[\r\n]+/g, " ").slice(0, 200);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pagedJson(text, label) {
|
|
77
|
+
let pages;
|
|
78
|
+
try { pages = JSON.parse(text || "[]"); }
|
|
79
|
+
catch (error) { throw new Error(`${label} response was not valid JSON: ${error.message}`); }
|
|
80
|
+
if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page))) {
|
|
81
|
+
throw new Error(`${label} response was not a slurped page array`);
|
|
82
|
+
}
|
|
83
|
+
return pages.flat();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function output(run, command, args, cwd) {
|
|
87
|
+
const result = run(command, args, cwd);
|
|
88
|
+
return String(result.stdout || "").trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runCommand(command, args, cwd) {
|
|
92
|
+
const result = spawnSync(command, args, {
|
|
93
|
+
cwd,
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
env: process.env,
|
|
96
|
+
stdio: "pipe",
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
shell: false,
|
|
99
|
+
});
|
|
100
|
+
if (result.error) throw result.error;
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
103
|
+
throw new Error(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
109
|
+
try {
|
|
110
|
+
const result = assertGitHubBacklogReady();
|
|
111
|
+
console.log(result.message);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`GitHub backlog gate failed: ${error?.message || error}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/scripts/github-push.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
|
|
7
|
+
import { assertGitHubBacklogReady } from "./github-backlog.mjs";
|
|
7
8
|
|
|
8
9
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
|
|
@@ -13,6 +14,9 @@ try {
|
|
|
13
14
|
const branch = output("git", ["branch", "--show-current"]);
|
|
14
15
|
if (!branch) throw new Error("cannot push from a detached HEAD");
|
|
15
16
|
if (branch === "main") throw new Error("direct pushes to main are prohibited; push a reviewed branch and merge through a pull request");
|
|
17
|
+
run("git", ["fetch", "origin", "main", "--prune"]);
|
|
18
|
+
const backlog = assertGitHubBacklogReady({ cwd: root, branch });
|
|
19
|
+
console.log(backlog.message);
|
|
16
20
|
|
|
17
21
|
const acceptance = verifyCurrentReleaseAcceptance(root);
|
|
18
22
|
if (acceptance.required) {
|