machine-bridge-mcp 3.0.0-beta.26 → 3.0.0-beta.28
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 +23 -0
- package/README.md +3 -2
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +1 -1
- package/docs/ARCHITECTURE.md +9 -6
- package/docs/AUDIT.md +13 -1
- package/docs/LOGGING.md +8 -4
- package/docs/OPERATIONS.md +10 -10
- package/docs/TESTING.md +6 -3
- package/docs/TOOL_REFERENCE.md +1 -1
- package/package.json +2 -1
- package/scripts/check-plan.mjs +1 -0
- package/scripts/coverage-check.mjs +12 -0
- package/src/local/call-capacity.mjs +45 -0
- package/src/local/call-registry.mjs +9 -6
- package/src/local/execution-limits.mjs +7 -0
- package/src/local/process-tracker.mjs +50 -5
- package/src/local/process-tree-ownership-types.d.ts +2 -2
- package/src/local/process-tree-ownership.mjs +21 -43
- package/src/local/process-tree-signal.mjs +33 -0
- package/src/local/process-tree-snapshot.mjs +77 -0
- package/src/local/process-tree-supervisor.mjs +48 -0
- package/src/local/process-tree.mjs +7 -57
- package/src/local/relay-connection.mjs +28 -24
- package/src/local/relay-diagnostics.mjs +1 -0
- package/src/local/relay-heartbeat.mjs +106 -0
- package/src/local/runtime-diagnostic-state.mjs +12 -0
- package/src/local/runtime-diagnostics.mjs +4 -4
- package/src/local/runtime.mjs +11 -1
- package/src/local/security-audit-dispatch.mjs +35 -0
- package/src/local/security-audit-log.mjs +164 -173
- package/src/local/security-audit-storage.mjs +197 -0
- package/src/local/security-audit-warning.mjs +31 -0
- package/src/local/security-audit-worker.mjs +88 -0
- package/src/local/service-runtime-convergence.mjs +52 -0
- package/src/local/service-runtime.mjs +16 -56
- package/src/local/tool-executor.mjs +5 -20
- package/src/shared/tool-call-capacity.d.mts +41 -0
- package/src/shared/tool-call-capacity.mjs +59 -0
- package/src/shared/tool-catalog.json +1 -1
- package/src/worker/durable-stream-calls.ts +7 -5
- package/src/worker/index.ts +7 -8
- package/src/worker/mcp-pending-call-inspection.ts +0 -4
- package/src/worker/mcp-pending-call-store.ts +14 -11
- package/src/worker/mcp-stream-dispatch.ts +4 -2
- package/src/worker/pending-call-capacity.ts +122 -0
- package/src/worker/pending-calls.ts +20 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0-beta.28 - 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Verified service restart semantics
|
|
6
|
+
|
|
7
|
+
- Fix `service restart` returning `already_running` without invoking launchd, systemd, or Task Scheduler. Start remains idempotent, while restart now always reaches the provider when the committed service is active.
|
|
8
|
+
- Require an active-service restart to return explicit provider restart evidence and to converge on a replacement daemon PID before reporting success. A still-ready pre-restart daemon is now `daemon_replacement_not_observed`, not successful convergence.
|
|
9
|
+
- Extract service daemon convergence into a focused module and add regressions for provider invocation, old-PID rejection, replacement readiness, and missing restart evidence.
|
|
10
|
+
|
|
11
|
+
## 3.0.0-beta.27 - 2026-07-31
|
|
12
|
+
|
|
13
|
+
### Control-plane resilience under host I/O pressure
|
|
14
|
+
|
|
15
|
+
- Remove synchronous process-table inspection from foreground timeout and cancellation paths. Process-group identity capture, post-`SIGTERM` refresh, and pre-`SIGKILL` PID/start-time revalidation now use bounded asynchronous `ps` execution with a fixed minimal `PATH`/locale environment, while ambiguous ownership still fails closed. Windows taskkill fallback is idempotent across an `error`/nonzero-`exit` race.
|
|
16
|
+
- Preserve process ownership after a tool result has timed out or been cancelled. Runtime status now distinguishes active calls from draining calls, terminating processes, and pending escalation checks instead of implying that a returned timeout means all operating-system work has stopped.
|
|
17
|
+
- Extend `diagnose_runtime` with privacy-safe local lifecycle, call-capacity, draining-process, execution-guardrail, relay-heartbeat, and audit-health snapshots so remote operators can observe the repaired control plane; local stdio `server_info` retains the equivalent detailed runtime view.
|
|
18
|
+
- Reserve control-plane capacity at both relay layers: two of thirty-two Worker pending-call slots and two of sixteen local runtime slots are restricted to bounded diagnosis/recovery tools. Ordinary transient and durable-stream work share the same admission contract and cannot consume those slots.
|
|
19
|
+
- Split relay heartbeat policy from WebSocket transport. The daemon measures local event-loop lag, reports bounded `runtime.event_loop.stall` warnings, sends a fresh heartbeat, and grants a short recovery interval before classifying remote silence. A locally stalled daemon no longer immediately destroys a healthy relay socket and amplifies one slow operation into a reconnect outage.
|
|
20
|
+
- Move security-audit startup verification, hash-chain updates, atomic replacement, and `fsync` into a dedicated Worker thread; construction now reports `audit_initializing` without synchronously reading persistent state, and stale post-failure Worker events cannot overwrite the original failure class. Tool results no longer wait for audit disk persistence; events are privacy-projected before transfer, batched, queue-bounded, cross-process serialized, and exposed through health/queue/drop diagnostics. Persistent audit failures emit rate-limited warnings with suppressed-count reporting rather than one warning per tool call.
|
|
21
|
+
- Reduce audit Worker message amplification by acknowledging each persisted batch once instead of sending one duplicate snapshot per record. The existing bounded SHA-256 chain, owner-only state, tamper detection, and prohibition on command text, paths, values, and results remain intact.
|
|
22
|
+
- Add the fast-plan `control-plane-resilience:test` gate and deterministic regressions for local event-loop stalls versus genuine relay silence, capture-before-signal ordering, asynchronous process-tree supervision, draining-process visibility, end-to-end Worker/local reserved control capacity, non-blocking audit dispatch, audit warning suppression, batch persistence, and privacy-safe audit projection.
|
|
23
|
+
- Bound headless OAuth browser startup, DevTools HTTP discovery, WebSocket connection, and individual CDP commands. A wedged Chrome process under extreme host pressure now fails with bounded diagnostics instead of hanging release verification indefinitely.
|
|
24
|
+
- Refactor heartbeat, call-capacity, process signaling/supervision/snapshotting, and audit dispatch/storage/warnings into focused modules. Architecture line budgets and import-direction checks were retained rather than relaxed.
|
|
25
|
+
|
|
3
26
|
## 3.0.0-beta.26 - 2026-07-29
|
|
4
27
|
|
|
5
28
|
### Explicit GitHub publication ownership
|
package/README.md
CHANGED
|
@@ -25,7 +25,8 @@ Support boundaries are defined in [SUPPORT.md](SUPPORT.md). Repository participa
|
|
|
25
25
|
- bounded file, patch, Git, process, diagnostic, application, browser, and managed-job tools;
|
|
26
26
|
- account roles whose authority is intersected with the connected daemon policy;
|
|
27
27
|
- root-certified ephemeral daemon sessions, trusted OAuth client binding, refresh-family ownership, and non-escalatable account roles;
|
|
28
|
-
- structured, privacy-conscious lifecycle events and stable error codes;
|
|
28
|
+
- structured, privacy-conscious lifecycle events, a worker-thread-isolated tamper-evident audit chain, and stable error codes;
|
|
29
|
+
- control-plane resilience through end-to-end reserved diagnostic capacity, event-loop-aware relay liveness, and explicit draining-process accounting;
|
|
29
30
|
- fail-closed state, lock, release, package, and supply-chain checks.
|
|
30
31
|
|
|
31
32
|
The remote Worker authenticates and relays requests. It cannot directly read local files or start local processes. Local-user authority remains in the daemon process.
|
|
@@ -187,7 +188,7 @@ Machine Bridge does not launch or identify a separate browser profile. It contro
|
|
|
187
188
|
|
|
188
189
|
## Durable work and local resources
|
|
189
190
|
|
|
190
|
-
Remote foreground process, shell, browser, and application calls are bounded to 85 seconds. Keep mutations and validation in independently terminal calls. Long, cleanup-sensitive, or remotely initiated workflows should use process sessions or managed jobs; managed jobs persist ordered argv steps and `finally_steps` under owner-only local state and continue across an MCP disconnect.
|
|
191
|
+
Remote foreground process, shell, browser, and application calls are bounded to 85 seconds. Keep mutations and validation in independently terminal calls. A timeout is a protocol result, not proof that descendant cleanup has already completed; inspect `diagnose_runtime.runtime.processes` remotely (or `server_info.runtime.processes` over local stdio) when a heavy filesystem or process operation is still draining. Long, cleanup-sensitive, or remotely initiated workflows should use process sessions or managed jobs; managed jobs persist ordered argv steps and `finally_steps` under owner-only local state and continue across an MCP disconnect.
|
|
191
192
|
|
|
192
193
|
Credentials and files can be registered by alias without returning their contents through MCP:
|
|
193
194
|
|
|
@@ -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.28",
|
|
34
34
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
|
|
35
35
|
}
|
package/docs/AGENT_CONTEXT.md
CHANGED
|
@@ -14,7 +14,7 @@ This approximates a local coding agent without pretending that the MCP server ow
|
|
|
14
14
|
- `list_local_commands` returns effective registered commands.
|
|
15
15
|
- `run_local_command` executes a registered direct-argv command when policy permits.
|
|
16
16
|
|
|
17
|
-
Both stdio and remote Worker connection initialization attempt `session_bootstrap`. Its instruction text is appended to the MCP `initialize` result. Because a host may reuse one MCP connection across conversations, the explicit tool and per-task `resolve_task_capabilities` call remain necessary to refresh and reapply instructions reliably. A host may return the previous `refresh.fingerprint` as `known_refresh_fingerprint`; a match suppresses only unchanged static instruction metadata, not the fresh task-specific scan or routing. `server_info.observability.capability_routing` and `project_overview.capabilityRouting` report whether those calls reached the local runtime, their counts and timestamps, loaded-source flags, selected capability metadata, primary route, ambiguity class, score gap, and a runtime-keyed HMAC task fingerprint. Raw task text is not retained.
|
|
17
|
+
Both stdio and remote Worker connection initialization attempt `session_bootstrap`. Its instruction text is appended to the MCP `initialize` result. Because a host may reuse one MCP connection across conversations, the explicit tool and per-task `resolve_task_capabilities` call remain necessary to refresh and reapply instructions reliably. A host may return the previous `refresh.fingerprint` as `known_refresh_fingerprint`; a match suppresses only unchanged static instruction metadata, not the fresh task-specific scan or routing. Local stdio `server_info.observability.capability_routing` and both transports’ `project_overview.capabilityRouting` report whether those calls reached the local runtime, their counts and timestamps, loaded-source flags, selected capability metadata, primary route, ambiguity class, score gap, and a runtime-keyed HMAC task fingerprint. Raw task text is not retained.
|
|
18
18
|
|
|
19
19
|
## Useful defaults without configuration
|
|
20
20
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -30,19 +30,22 @@ A canonical workspace receives an independent profile, Worker name, secret set,
|
|
|
30
30
|
`LocalRuntime` is the transport-independent local tool orchestrator. It owns the shared authorization/execution pipeline, manager construction, mutation serialization, cancellation, and the narrow delegation surface used by stdio and relay transports. Domain behavior remains in focused services:
|
|
31
31
|
|
|
32
32
|
- `workspace-file-service.mjs` and `git-service.mjs` own canonical filesystem/Git operations;
|
|
33
|
-
- `process-contract.mjs` owns argv shape/size validation
|
|
34
|
-
- `runtime-reporting.mjs` builds privacy-aware runtime and project snapshots;
|
|
35
|
-
- `runtime-diagnostics.mjs` owns fixed local probes and their stable interpretation;
|
|
33
|
+
- `process-contract.mjs` owns argv shape/size validation; `process-tree-signal.mjs`, `process-tree-supervisor.mjs`, `process-tree-snapshot.mjs`, and `process-tree-ownership.mjs` separate cross-platform signaling, asynchronous escalation, bounded process-group observation, and PID/start-time ownership; `process-execution.mjs` and `process-sessions.mjs` own one-shot and interactive execution; and `process-tracker.mjs` retains active and draining process ownership until close;
|
|
34
|
+
- `shared/tool-call-capacity.mjs` defines the control-tool set and generic admission algebra; local `call-capacity.mjs` and Worker `pending-call-capacity.ts` apply it independently, while `runtime-reporting.mjs` builds privacy-aware runtime and project snapshots;
|
|
35
|
+
- `runtime-diagnostics.mjs` owns fixed local probes and their stable interpretation, while `runtime-diagnostic-state.mjs` projects privacy-safe control-plane state for remote diagnosis;
|
|
36
36
|
- `runtime-capabilities.mjs` composes agent, application, browser, and effective-policy-filtered routing results, while `execution-routing.mjs` owns bounded set-level route scoring, ambiguity, fallbacks, and advisory tool projection;
|
|
37
37
|
- `runtime-tool-handlers.mjs` owns catalog-to-handler registration;
|
|
38
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
|
- `runtime-resource-service.mjs` owns registered-resource lookup, bounded binary/UTF-8 reads for browser/application injection, and SSH-resource registration/result projection;
|
|
41
|
+
- `security-audit-log.mjs` owns only the bounded main-thread queue and cached initializing/health projection; `security-audit-worker.mjs`, `security-audit-storage.mjs`, `security-audit-dispatch.mjs`, and `security-audit-warning.mjs` isolate startup verification, all disk/hash work, batch persistence, privacy projection, and warning suppression from result delivery;
|
|
41
42
|
- `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;
|
|
42
43
|
- `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;
|
|
43
44
|
- managed jobs, local resources, application automation, and browser automation remain separate managers.
|
|
44
45
|
|
|
45
|
-
Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic,
|
|
46
|
+
Architecture tests cap the orchestration module and each extracted service independently and reject a return of low-level process, patch, diagnostic, capability-scoring, heartbeat-policy, or audit-storage logic to `LocalRuntime`. `RelayConnection` owns remote WebSocket transport, `hello_ack` authentication, end-to-end `relay_probe`/`ready_ack` readiness, reconnect backoff, outage logging, and a monotonically increasing in-memory transport generation. `RelayHeartbeatMonitor` separately owns liveness timing, local event-loop-lag detection, recovery grace, and heartbeat diagnostics, so delayed local scheduling is not conflated with remote silence. 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 the shared two-minute reconnect grace; 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.
|
|
47
|
+
|
|
48
|
+
The control plane has explicit availability budgets at both admission layers. The Worker admits thirty ordinary pending daemon calls and reserves two additional slots for `diagnose_runtime`/`list_roots`; its transient Promise registry and durable stream store merge per-tool counts under one serialized admission gate. The local runtime independently admits fourteen ordinary tools and reserves two control slots; the total ceilings remain thirty-two and sixteen. A timed-out or cancelled process call settles at the protocol boundary before operating-system cleanup necessarily completes, so `process-tracker.mjs` keeps that process under a draining call until `close` and reports pending escalation supervision. Neither process ownership inspection nor security-audit persistence performs synchronous process creation or disk `fsync` on the daemon event loop.
|
|
46
49
|
|
|
47
50
|
`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.
|
|
48
51
|
|
|
@@ -148,7 +151,7 @@ The service layer emits launchd, systemd-user, or Windows Scheduled Task definit
|
|
|
148
151
|
|
|
149
152
|
The Windows adapter owns a short private restart launcher because Task Scheduler's `/TR` action is substantially smaller than the Windows process command-line limit and the full installed Node/CLI/workspace argv can exceed it. The scheduled action therefore contains only the launcher path. The launcher performs the full quoted invocation, redirects to service logs, and restarts only nonzero exits. A fixed PowerShell object query supplies language-independent `Ready`/`Running` state; provider installation is not equated with process activity. The trigger is least-privilege current-user logon, not boot-time `SYSTEM` execution.
|
|
150
153
|
|
|
151
|
-
The platform adapters normalize launchd, systemd, and Windows Scheduled Task operations to one `{ok, provider}` result contract. Removal is not a provider-specific sequence: `service-lifecycle.mjs` first stops the provider, then every verified workspace daemon in scope, and only then removes the definition. A failed stop or unverifiable process prevents definition/state deletion.
|
|
154
|
+
The platform adapters normalize launchd, systemd, and Windows Scheduled Task operations to one `{ok, provider}` result contract. `service-runtime.mjs` owns committed-owner/provider orchestration, while `service-runtime-convergence.mjs` owns daemon readiness and replacement evidence. Start may return idempotently for an already-ready owner; restart may not, and an active restart requires provider evidence plus a different ready daemon PID. Removal is not a provider-specific sequence: `service-lifecycle.mjs` first stops the provider, then every verified workspace daemon in scope, and only then removes the definition. A failed stop or unverifiable process prevents definition/state deletion.
|
|
152
155
|
|
|
153
156
|
The autostart provider name is machine-global, but daemon locks are state/workspace-scoped. Foreground startup therefore does not stop the provider merely because a Machine Bridge service exists. It first verifies that the current state's daemon lock identifies a live `service` process whose command line matches the same canonical workspace and state root. Only that proven owner may trigger provider stop; isolated installs, another workspace, another state root, and foreground locks leave the existing service untouched.
|
|
154
157
|
|
|
@@ -242,7 +245,7 @@ Fixed implementation-owned metadata commands are not routed through that arbitra
|
|
|
242
245
|
|
|
243
246
|
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.
|
|
244
247
|
|
|
245
|
-
`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. `
|
|
248
|
+
`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. `diagnose_runtime.runtime.execution_guardrails` reports those enforced limits remotely, while local stdio `server_info.runtime.execution_guardrails` exposes the same contract 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.
|
|
246
249
|
|
|
247
250
|
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.
|
|
248
251
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Security and privacy audit notes
|
|
2
2
|
|
|
3
|
+
## 2026-07-31 version 3.0.0-beta.27 control-plane resilience audit
|
|
4
|
+
|
|
5
|
+
A host-pressure incident exposed a causal chain that ordinary timeout tests had not covered: a metadata-heavy directory deletion exceeded its foreground deadline; subsequent minimal calls were delayed; and the daemon later classified the relay as unresponsive. Review of production diagnostics and the exact installed source showed that host filesystem/endpoint-security load was the trigger, but Machine Bridge amplified it. Timeout/cancellation performed synchronous process-table snapshots on the daemon thread, every remote call synchronously reread, rehashed, rewrote, and `fsync`ed the complete bounded audit file before returning a result, the process could remain alive after the timeout response, and heartbeat policy could not distinguish remote silence from a locally stalled event loop. Existing tests proved that `cancel_call` was sent and the Promise rejected; they did not prove post-timeout control-plane availability or completed process drain.
|
|
6
|
+
|
|
7
|
+
Beta.27 removes that amplification path. Process-group identity capture and revalidation use bounded asynchronous `ps` execution and retain exact PID/start-time/PGID fail-closed semantics. A capture is initiated before `SIGTERM`; ambiguous identity prevents escalation. Runtime process accounting now keeps calls in a draining state after their protocol owner finishes and exposes pending escalation supervision. The shared admission algebra reserves two of thirty-two Worker pending-call slots and two of sixteen local runtime slots for bounded diagnosis/recovery. Worker admission merges transient Promise calls with durable stream calls by tool under one serialized gate, so ordinary saturation cannot consume the control path before a request reaches the daemon.
|
|
8
|
+
|
|
9
|
+
Relay heartbeat policy is now a focused state machine. It measures expected-versus-actual timer delivery, records local event-loop stalls, sends a fresh heartbeat, and applies a bounded recovery interval before treating continued silence as a remote liveness failure. Diagnostics expose only timing/count classes, not network endpoints or message content. Genuine silence still terminates the half-open socket and enters the existing bounded reconnect path.
|
|
10
|
+
|
|
11
|
+
Security-audit persistence and startup chain verification are isolated in a dedicated Worker thread. The main runtime begins with an explicit `audit_initializing` projection, projects an allowlisted metadata record, queues it with a fixed capacity, and returns the tool result without waiting for disk I/O. The Worker batches records, serializes cross-process writers, verifies the existing SHA-256 chain, and performs owner-only atomic replacement and `fsync`. Batch acknowledgements avoid per-record duplicate snapshots. Health reports queue depth/capacity, worker readiness, retained events, chain status, and dropped-record count. Failures are warning-rate-limited and include only tool/coarse error class. Command text, arguments, paths, principal identifiers, content, form values, and results remain prohibited; account/client/family identifiers are salted and hashed only inside the audit storage boundary.
|
|
12
|
+
|
|
13
|
+
The review also retained strict module budgets by extracting call-capacity, relay-heartbeat, process signal/supervision/snapshot/ownership, and audit dispatch/storage/warning responsibilities rather than raising architecture thresholds. New behavior tests cover local stall recovery, true relay silence, capture-before-signal ordering, asynchronous escalation, draining ownership, end-to-end Worker/local reserved control capacity, mixed transient/durable admission, audit non-blocking dispatch/batching/tamper detection, warning suppression, and privacy projection. Remaining residual risk is explicit: the asynchronous audit queue is not a write-ahead log, so a process or operating-system crash before persistence may lose queued events and the in-process dropped counter cannot observe that loss; CPU, memory, filesystem latency, and network isolation are still OS/container responsibilities; a severely stalled kernel can delay all user processes, but one Machine Bridge call no longer performs known synchronous process enumeration or audit `fsync` on the daemon event loop.
|
|
14
|
+
|
|
3
15
|
## 2026-07-29 version 3.0.0-beta.26 GitHub publication ownership audit
|
|
4
16
|
|
|
5
17
|
After beta.25 was accepted and merged, several non-interactive paths attempted to start its guarded GitHub prerelease command: foreground MCP execution, detached shell/session variants, launchd handoff, and a managed job. Host lifecycle ownership terminated the long-running descendants before publication, and the managed job preserved a terminal `SIGTERM` result. A later calendar-triggered launchd attempt reached the concurrently prepared beta.26 guard and was rejected before fetch. No beta.25 tag, GitHub Release, or npm version was created. The event showed that candidate acceptance and green exact-commit CI are durable evidence but not proof of fresh operator intent for a later remote publication.
|
|
@@ -349,7 +361,7 @@ Version 1.2.7 extracts `process-contract.mjs`, `process-tree.mjs`, and `executio
|
|
|
349
361
|
|
|
350
362
|
A separate lifecycle race existed in the browser broker. `stop()` could run while pairing/listen/proxy startup was awaiting I/O; the old start could then finish and repopulate a listener after shutdown. Startup now carries a monotonically increasing generation through every asynchronous boundary. Stop invalidates that generation, and stale completion closes all HTTP/WebSocket transports, pending calls, and routed proxy requests. Browser status also no longer relies on global `fetch` for loopback health: a strict bounded direct-HTTP adapter accepts only canonical `127.0.0.1` and cannot be diverted by environment proxy configuration. Remote Worker health and relay traffic continue to use the documented proxy resolver.
|
|
351
363
|
|
|
352
|
-
The resource review distinguished application bounds from kernel controls. The implementation has real concurrency, timeout, stdin, output, retention, and job-size limits, but it does not create CPU quotas, resident-memory ceilings, syscall sandboxes, or egress filters. `server_info.runtime.execution_guardrails` now
|
|
364
|
+
The resource review distinguished application bounds from kernel controls. The implementation has real concurrency, timeout, stdin, output, retention, and job-size limits, but it does not create CPU quotas, resident-memory ceilings, syscall sandboxes, or egress filters. Remote `diagnose_runtime.runtime.execution_guardrails` and local stdio `server_info.runtime.execution_guardrails` now state both categories explicitly so a timeout cannot be misread as a CPU quota and a proxy route cannot be misread as network isolation. Hard isolation remains an external deployment responsibility: dedicated low-privilege account, container, or VM.
|
|
353
365
|
|
|
354
366
|
Managed-job launch previously assumed `spawn()` returned a usable child. A system-level launch failure could emit an unhandled child `error` after the job had been reported accepted. Launch now attaches the error observer before returning, refuses acceptance without a positive PID, records a coarse structured failure class, and keeps shell parsing disabled explicitly. The audit also corrected stale `hello_ack`-only readiness wording, incorrect `server_info` field paths, a duplicated plan-retention guard, and documentation that still described daemon takeover as non-escalating.
|
|
355
367
|
|
package/docs/LOGGING.md
CHANGED
|
@@ -12,7 +12,7 @@ Logs should answer:
|
|
|
12
12
|
4. Is an infrastructure, protocol, deployment, or local service problem requiring action?
|
|
13
13
|
5. When debug logging is explicitly enabled, which bounded implementation event should be correlated?
|
|
14
14
|
|
|
15
|
-
Logs are not a command history or content transcript. The local security audit provides a bounded SHA-256 hash chain over coarse operation metadata without recording command text, paths, contents, form values, or output.
|
|
15
|
+
Logs are not a command history or content transcript. The local security audit provides a bounded SHA-256 hash chain over coarse operation metadata without recording command text, paths, contents, form values, or output. Audit inputs are projected to an allowlist before crossing to a dedicated Worker thread; chain verification, batching, atomic replacement, and `fsync` never delay tool-result delivery on the daemon event loop. Remote `diagnose_runtime.runtime.security_audit` reports worker readiness, queue depth/capacity, dropped records, retained entries, and chain health; local stdio exposes the same snapshot as `server_info.security_audit`. Before the Worker reports readiness, health is explicitly `audit_initializing`; the daemon thread does not synchronously read the chain. The queue is not a write-ahead log: a process or operating-system crash before persistence can lose queued events, and `dropped_records` counts only failures observed during the current process lifetime. The chain detects local alteration but is not a remote immutable ledger and is not a substitute for OS isolation.
|
|
16
16
|
|
|
17
17
|
## Levels
|
|
18
18
|
|
|
@@ -62,7 +62,8 @@ Brief network interruptions are expected on laptop network changes, Worker deplo
|
|
|
62
62
|
- recovery after a visible outage produces one information summary with a human-readable duration and attempt count; exact seconds and error classes remain debug-only;
|
|
63
63
|
- a verified replacement is a distinct warning and permanently stops the older daemon;
|
|
64
64
|
- failure to receive `hello_ack` within the handshake deadline, or `ready_ack` within the independent end-to-end readiness deadline, terminates the candidate socket and retries;
|
|
65
|
-
- lack of inbound heartbeat activity terminates a half-open socket and reconnects
|
|
65
|
+
- lack of inbound heartbeat activity terminates a half-open socket and reconnects;
|
|
66
|
+
- a late local heartbeat tick is classified as `runtime.event_loop.stall`, sends a fresh probe, and defers disconnect for a bounded recovery interval instead of being mislabeled as immediate remote failure.
|
|
66
67
|
|
|
67
68
|
A WebSocket close code such as `1006` means the transport ended without a normal close handshake. It is useful for debug diagnosis but not useful as the default user message. It is not evidence that the daemon process restarted. Worker `daemon_transport_error` / `daemon_liveness_timeout` messages and their 1012 close frames are likewise retryable connection conditions, not upgrade instructions. Only an unknown/incompatible Worker error, authentication failure, or identity/version mismatch may produce the fatal protocol/configuration log and daemon exit. Default logs therefore describe the affected layer, duration, classification, and recovery behavior rather than printing raw close envelopes.
|
|
68
69
|
|
|
@@ -86,7 +87,7 @@ The layered repository check runner follows the same noise rule. Green child-tas
|
|
|
86
87
|
|
|
87
88
|
A completed local result is normally sent on the ready relay connection. If that daemon WebSocket disappears, the runtime may queue the bounded result envelope during the shared two-minute same-daemon reconnect window; this relay-layer queue is independent of the public MCP protocol era. 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. For modern MCP `2026-07-28`, closing the public HTTP response stream cancels that request through a random internal capability; the control request contains no Authorization or DPoP header, and the capability is not logged. For legacy MCP `2025-11-25`, disposing the public response does not cancel the session-bound operation because the host may recover it with `Last-Event-ID`; explicit legacy cancellation suppresses eventual delivery. Tool arguments, mirrored header values, validation values, and result content are never logged.
|
|
88
89
|
|
|
89
|
-
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. Authorization failures
|
|
90
|
+
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. Authorization failures return a stable denial code and bounded guidance; the current runtime never creates approval IDs or temporary elevation leases. Daemon logs still omit normalized targets and request arguments.
|
|
90
91
|
|
|
91
92
|
## Data that is never logged
|
|
92
93
|
|
|
@@ -122,6 +123,9 @@ This is defense in depth, not content classification. Unknown, split, transforme
|
|
|
122
123
|
|
|
123
124
|
## Structured lifecycle events
|
|
124
125
|
|
|
126
|
+
|
|
127
|
+
Security-audit enqueue/persistence failures are warning-level operational faults, but repeated failures are rate-limited per event class. The first warning is emitted immediately; duplicates within one minute are suppressed, and the next emitted warning reports the suppressed count. Warning fields contain only the tool name and a coarse error class, never the rejected audit payload or principal identifiers.
|
|
128
|
+
|
|
125
129
|
The local execution middleware emits bounded events such as `tool.call.started`, `tool.call.completed`, `tool.call.failed`, `tool.call.slow`, and `tool.call.cancel_requested`. Stable fields include a shortened call ID, tool name, origin, duration, error code, and retryability. The Worker emits JSON events for HTTP failures and daemon socket errors. Structured values still pass through field-name and value redaction; JSON format is not permission to log arguments or results.
|
|
126
130
|
|
|
127
131
|
`server_info` is the operational metrics surface. Local metrics include lifecycle state, active and maximum calls, oldest-call age, active-process ownership, per-tool duration buckets, and error-code counts. Worker metrics include HTTP status classes, pending internal/request-key indexes, per-tool outcomes, daemon candidate/authenticated/ready/disconnected event counts, current authenticated/probing/ready socket counts, and protocol-error counts. Metrics contain counts and bounded identifiers, not request arguments or result contents.
|
|
@@ -145,7 +149,7 @@ Each managed job has owner-only runner diagnostic logs. Child-step output is ret
|
|
|
145
149
|
|
|
146
150
|
`network_route` describes only Machine Bridge's application-level proxy decision. `system-network-stack` does **not** mean a direct physical path: an operating-system VPN, TUN, packet tunnel, DNS interceptor, or endpoint-security product may still carry the connection. `network_route_scope` therefore remains `application-proxy-selection-only`.
|
|
147
151
|
|
|
148
|
-
During an outage, `
|
|
152
|
+
During an outage, remote `diagnose_runtime.runtime.relay` and local stdio `server_info.runtime.relay` expose bounded operational fields: outage count/start/duration, last close category/code, coarse transport error class, last disconnect/ready time, prior ready duration, and next retry timing. On macOS, `diagnose_runtime` may also return a coarse default-route class and `operating_system_interception` boolean. That diagnostic is returned on demand and is not promoted to default logs; interface names, IP addresses, DNS answers, proxy endpoints/credentials, Worker endpoints, tool arguments, and results remain absent. `relay.outage.active` and `relay.outage.recovered` carry the existing safe relay fields.
|
|
149
153
|
|
|
150
154
|
Schema 4 is strict NDJSON. Before daemon startup, both active log files are opened as owner-only regular single-link files. A schema change clears both only after validation and commits the marker only after the transition succeeds. A symlink, multiple-hard-link inode, permission error, or marker-write failure blocks startup rather than mixing formats or repeatedly erasing evidence.
|
|
151
155
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -59,7 +59,7 @@ A successful diagnostic result applies only to that probe. An MCP host can still
|
|
|
59
59
|
|
|
60
60
|
### Concurrent chat windows and pending calls
|
|
61
61
|
|
|
62
|
-
Machine Bridge supports concurrent calls: the Worker admits
|
|
62
|
+
Machine Bridge supports concurrent calls: the Worker admits 32 pending daemon calls (30 ordinary plus two reserved control calls), and the local runtime admits 16 active tool calls (14 ordinary plus two reserved control calls). The same `diagnose_runtime`/`list_roots` control set is enforced at both layers. These are capacity limits, not a single global execution queue. Modern MCP `2026-07-28` HTTP requests are independent: JSON-RPC IDs are scoped to each request/response stream, so separate clients may reuse the same numeric ID even when they share one OAuth account and token. Legacy MCP `2025-11-25` initialization still receives a signed `Mcp-Session-Id`; duplicate detection, explicit cancellation, and replay for that compatibility path remain session-scoped.
|
|
63
63
|
|
|
64
64
|
`server_info.worker.pending_calls` reports `active`, `detached`, `request_keys`, `maximum`, `oldest_ms`, `by_tool`, `transient`, and `durable_streams`. `worker.sockets_live` separately reports `authenticated`, `probing`, `ready`, and `candidates`; only `ready` sockets contribute to `daemon.connected` and `authorization.effective_tools`. A nonzero `active` count means work is in flight, not that the bridge is globally locked. `detached > 0` means the daemon WebSocket was lost and calls are inside the bounded same-daemon reconnect interval. This relay-layer state exists below both MCP eras.
|
|
65
65
|
|
|
@@ -83,11 +83,11 @@ After the host path recovers, compare `server_info`, `machine-mcp doctor`, and `
|
|
|
83
83
|
|
|
84
84
|
A reconnect warning proves a transport interruption, not a daemon crash. Compare daemon PID and process start time with `connected_at`, `last_seen_at`, relay close category/code, outage count, and the coarse system-route diagnostic. A VPN/TUN UI may remain “connected” while its upstream route is unusable. Machine Bridge reports only coarse route/proxy classes and never logs interface names, addresses, DNS answers, proxy credentials, or Worker secrets.
|
|
85
85
|
|
|
86
|
-
Brief retryable outages reconnect automatically. A persistent outage emits bounded summaries; identity/version mismatch, authentication rejection, and unexpected protocol messages remain permanent failures requiring version convergence or credential repair. Compare outage intervals with sleep/wake records before classifying them as active network faults. Use `--verbose` only when close codes, heartbeat deadlines, and retry delays are required.
|
|
86
|
+
Brief retryable outages reconnect automatically. A persistent outage emits bounded summaries; identity/version mismatch, authentication rejection, and unexpected protocol messages remain permanent failures requiring version convergence or credential repair. Compare outage intervals with sleep/wake records and `diagnose_runtime.runtime.relay.heartbeat` before classifying them as active network faults; local stdio `server_info.runtime.relay.heartbeat` exposes the same state. A nonzero `event_loop_stall_count` with a large `max_event_loop_lag_ms` means the local daemon was not scheduled promptly; during recovery grace it sends a new heartbeat and deliberately postpones disconnect. That is distinct from a relay that remains silent after local scheduling has recovered. Use `--verbose` only when close codes, heartbeat deadlines, and retry delays are required.
|
|
87
87
|
|
|
88
88
|
A foreground MCP tool is not a durable job. Hosted clients can impose a request ceiling below the local/stdio schema, so the remote catalog accepts at most 85 seconds and reserves terminal-delivery margin. Longer work belongs in `start_process` plus bounded `read_process`, or in a managed job. Keep mutation and verification in independently terminal calls when a host exposes only a foreground shell tool.
|
|
89
89
|
|
|
90
|
-
The daemon honors `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` through standard environment-proxy resolution for remote Worker health and relay traffic. `wss:` targets use HTTPS proxy selection and `ws:` targets use HTTP proxy selection. Only HTTP and HTTPS proxy URLs are accepted. Invalid URLs or unsupported protocols fail startup with corrective guidance instead of entering the reconnect loop. `server_info.runtime.relay.network_route` reports `system-network-stack`, `application-http-proxy`, or `invalid-application-proxy-configuration`. This field describes only Machine Bridge application-level proxy selection: an operating-system VPN/TUN may still intercept `system-network-stack` traffic. `network_route_scope`, outage timestamps/durations, close category/code, transport error class, and next retry timing make that distinction explicit; proxy endpoints and credentials are never returned or logged. The browser-broker CLI health probe is a separate loopback-only path: it accepts only canonical `127.0.0.1`, uses direct Node HTTP with no proxy agent, and does not depend on `NO_PROXY`.
|
|
90
|
+
The daemon honors `HTTPS_PROXY`/`HTTP_PROXY` and `NO_PROXY` through standard environment-proxy resolution for remote Worker health and relay traffic. `wss:` targets use HTTPS proxy selection and `ws:` targets use HTTP proxy selection. Only HTTP and HTTPS proxy URLs are accepted. Invalid URLs or unsupported protocols fail startup with corrective guidance instead of entering the reconnect loop. `diagnose_runtime.runtime.relay.network_route` reports remotely, while local stdio `server_info.runtime.relay.network_route` reports `system-network-stack`, `application-http-proxy`, or `invalid-application-proxy-configuration`. This field describes only Machine Bridge application-level proxy selection: an operating-system VPN/TUN may still intercept `system-network-stack` traffic. `network_route_scope`, outage timestamps/durations, close category/code, transport error class, and next retry timing make that distinction explicit; proxy endpoints and credentials are never returned or logged. The browser-broker CLI health probe is a separate loopback-only path: it accepts only canonical `127.0.0.1`, uses direct Node HTTP with no proxy agent, and does not depend on `NO_PROXY`.
|
|
91
91
|
|
|
92
92
|
## Browser extension setup and diagnosis
|
|
93
93
|
|
|
@@ -123,7 +123,7 @@ Application UI inspection/actions require Accessibility permission for the Node/
|
|
|
123
123
|
|
|
124
124
|
A global npm install changes the CLI files on disk but does not replace an already running Node process. Startup and other state-changing CLI operations use a token/process-identity lock and wait up to 30 seconds for a normal concurrent startup to finish; duration limits use monotonic elapsed time, so NTP or manual wall-clock correction does not lengthen or shorten the wait; a short launchd/systemd overlap is therefore serialized rather than reported immediately as an error. On a normal foreground start, Machine Bridge unloads the platform service and then independently examines the workspace daemon lock. This second path handles a detached/orphan `--daemon-only` process that launchd, systemd, or Task Scheduler no longer tracks. Only current lock records containing service mode, version, PID, process start time, entrypoint, workspace, and state root are eligible for takeover. Before sending `SIGTERM`, Machine Bridge verifies PID and process start time plus the live command line, entrypoint, and daemon-only flag. Explicit `--workspace` and `--state-dir` must both match the active state when present; a recovery daemon started with only `--daemon-only` is accepted when the lock owner already records that workspace and state root. Partial path identity (only one of the two flags) is rejected. If the verified daemon ignores graceful termination, Machine Bridge waits for the grace period, then repeats process-instance and full daemon-identity verification before sending `SIGKILL`. PID reuse, identity drift, foreground mode, or any ambiguity blocks escalation. The total stop remains bounded at 15 seconds and stale lock reclamation still uses token-aware ownership. A foreground or unverifiable process is left untouched; stop a foreground instance with `Ctrl+C`.
|
|
125
125
|
|
|
126
|
-
Foreground shell, process-session, and managed-job timeouts terminate the complete process group rather than only the direct child. On macOS, ownership capture and revalidation use `ps -g <PGID>` so unrelated system-wide process-table load cannot erase all identity evidence.
|
|
126
|
+
Foreground shell, process-session, and managed-job timeouts terminate the complete process group rather than only the direct child. On macOS, ownership capture and revalidation use asynchronous `ps -g <PGID>` so unrelated system-wide process-table load cannot erase all identity evidence or block the daemon event loop. The identity snapshot begins before `SIGTERM`; post-signal refresh and pre-`SIGKILL` revalidation share one three-second monotonic inspection budget and compare exact PID, start time, and PGID. Empty or ambiguous ownership still fails closed; the operation may require manual cleanup rather than risk signaling a reused process.
|
|
127
127
|
|
|
128
128
|
`machine-mcp service status [WORKSPACE]` reports two independent layers: the platform service (`active`) and `workspace_daemon`, plus `effective_active` and `orphaned_workspace_daemon` summary flags. On macOS it is possible for launchd to report inactive while a prior Node process remains alive with parent PID 1; that is an orphan-daemon condition, not proof that the daemon stopped. `service stop` unloads the provider when present and then terminates only a verified service-style workspace daemon. `service uninstall` and full uninstall are ordered fail-closed operations: provider stop → verified daemon stop(s) → definition removal. A failed or ambiguous stop leaves definitions and state intact. If takeover reaches its deadline, run:
|
|
129
129
|
|
|
@@ -157,7 +157,7 @@ After global installation, Windows users may open any `cmd.exe` window and run `
|
|
|
157
157
|
|
|
158
158
|
`service start` is an idempotent ensure-running operation, but provider state is only an intermediate observation. It loads the committed owner, verifies that no foreground, unverifiable, or orphan service daemon conflicts with it, starts the provider, and waits for the exact service-mode daemon to publish its token-protected startup-readiness checkpoint. The daemon publishes that checkpoint once, only after device authentication, relay probe, and `ready_ack`; a stable launchd PID or a Windows task that briefly reports `Running` is not sufficient. Failed readiness triggers a bounded provider stop and reports whether cleanup itself was verified. A Windows task that completes and returns to `Ready` remains `completed_without_persistence`, even when its process exit code was zero.
|
|
159
159
|
|
|
160
|
-
`service restart` uses the same owner/readiness convergence after its detached service-manager handoff. Windows restart remains fail closed from inside the running task because Task Scheduler `/End` may terminate the helper with the daemon; use an independent terminal stop/start sequence until a behaviorally verified Windows handoff exists.
|
|
160
|
+
`service restart` uses the same owner/readiness convergence after its detached service-manager handoff, but unlike idempotent `service start` it must invoke the provider. When the service was already active, success requires both explicit provider restart evidence and a ready replacement daemon PID; the old daemon remaining ready is `daemon_replacement_not_observed`, not success. Windows restart remains fail closed from inside the running task because Task Scheduler `/End` may terminate the helper with the daemon; use an independent terminal stop/start sequence until a behaviorally verified Windows handoff exists.
|
|
161
161
|
|
|
162
162
|
A workspace/state selector does not grant authority over the machine-global service label. All service mutations share one fixed per-user machine-service lock. Operations that also need a workspace startup lock acquire machine-service first; foreground startup releases the machine lock after service takeover and daemon-lock acquisition rather than holding it for the lifetime of the foreground runtime. Stop, restart, foreground takeover, secret rotation, and uninstall still require exact live ownership evidence. Service status returns only provider state, bounded owner metadata, verified daemon state, PID/run counters, readiness, and classified termination state; provider environment dumps and owner paths are never returned.
|
|
163
163
|
|
|
@@ -199,7 +199,7 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
|
|
|
199
199
|
|
|
200
200
|
### Lifecycle and pending-call diagnosis
|
|
201
201
|
|
|
202
|
-
`
|
|
202
|
+
Remote `diagnose_runtime.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`; `diagnose_runtime.observability.in_flight_calls` reports ordinary versus reserved local capacity; and `diagnose_runtime.runtime.processes` distinguishes active calls, draining calls whose protocol result already settled, currently terminating processes, and pending escalation checks. It also returns `runtime.execution_guardrails` and `runtime.security_audit`. Local stdio `server_info` exposes the equivalent fields under `server_info.runtime`, `server_info.observability`, and `server_info.security_audit`. A returned timeout therefore does not claim that all kernel or descendant work has already stopped. Worker `server_info.worker.pending_calls` reports the internal-call index, legacy request-key index, detached-call count, ordinary/control capacity, and current ordinary/control occupancy across transient and durable calls. Modern HTTP stream closure should remove its transient stream owner and pending daemon call; there is no modern replay record or request-key entry. Legacy terminal result, explicit cancellation, timeout, or reconnect-grace expiry must return active/detached/request-key counts to zero. During a brief daemon interruption, legacy `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 legacy 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.
|
|
203
203
|
|
|
204
204
|
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.
|
|
205
205
|
|
|
@@ -286,7 +286,7 @@ The daemon sends heartbeats and reconnects with bounded exponential backoff and
|
|
|
286
286
|
|
|
287
287
|
The Worker tracks inbound `lastSeenAt` and reclaims ready or probing sockets that go silent past their applicable liveness/readiness windows. Diagnose with `daemon.readiness_verified`, `worker.sockets_live.ready`, `worker.sockets_live.probing`, and `daemon.last_seen_at`; cumulative `worker.observability.sockets.authenticated` is an event counter, not current liveness. A state with no ready daemon is unavailable by construction rather than falsely online. Restart the daemon only after inspecting the classified timeout/protocol error; refreshing an MCP client does not repair the local transport.
|
|
288
288
|
|
|
289
|
-
Pending calls are bound to the socket that received them. Results from another socket are ignored. A lost or replaced socket rejects only its own pending calls and
|
|
289
|
+
Pending calls are bound to the socket that received them. Results from another socket are ignored. A lost or replaced socket rejects only its own pending calls and begins termination of locally tracked child process trees. The result/cancellation boundary and the operating-system drain boundary are separate: cleanup ownership remains visible until child close or bounded escalation settlement. Process sessions are in-memory and do not survive daemon restart or replacement.
|
|
290
290
|
|
|
291
291
|
## Limits
|
|
292
292
|
|
|
@@ -304,8 +304,8 @@ Defense-in-depth limits include:
|
|
|
304
304
|
- 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;
|
|
305
305
|
- process sessions: 8 retained per runtime;
|
|
306
306
|
- process stdin write: 64 KiB per call;
|
|
307
|
-
- local simultaneous tool calls: 16;
|
|
308
|
-
- Worker pending daemon calls: 32;
|
|
307
|
+
- local simultaneous tool calls: 16 total, with 14 ordinary slots and two reserved for bounded control-plane diagnosis/recovery;
|
|
308
|
+
- Worker pending daemon calls: 32 total, with 30 ordinary slots and two reserved for bounded control-plane diagnosis/recovery;
|
|
309
309
|
- local/stdio command timeout schema: 1–600 seconds; the remote Worker schema is 1–85 seconds with tool-specific 30- or 60-second defaults, reserves five seconds for terminal delivery, and rejects larger values before dispatch;
|
|
310
310
|
- process-session read wait: at most 30 seconds, measured with monotonic elapsed time;
|
|
311
311
|
- direct directory result: 10,000 entries and 4 MiB of path metadata;
|
|
@@ -320,7 +320,7 @@ Defense-in-depth limits include:
|
|
|
320
320
|
- application Accessibility inspection: 500 elements and depth 12; action text: 4,000 characters;
|
|
321
321
|
- job-scoped temporary files: 16 files, 512 KiB total content.
|
|
322
322
|
|
|
323
|
-
The list above describes bounded application resources, not OS quotas. CPU time shares, resident-memory ceilings, syscall sandboxes, and egress policy must be imposed by the account/container/VM that runs Machine Bridge. Check `server_info.runtime.execution_guardrails.operating_system_enforcement`; current in-process values are intentionally `not-enforced` rather than inferred from timeouts or output limits.
|
|
323
|
+
The list above describes bounded application resources, not OS quotas. CPU time shares, resident-memory ceilings, syscall sandboxes, and egress policy must be imposed by the account/container/VM that runs Machine Bridge. Check `diagnose_runtime.runtime.execution_guardrails.operating_system_enforcement` remotely or local stdio `server_info.runtime.execution_guardrails.operating_system_enforcement`; current in-process values are intentionally `not-enforced` rather than inferred from timeouts or output limits.
|
|
324
324
|
|
|
325
325
|
## Upgrade behavior
|
|
326
326
|
|
package/docs/TESTING.md
CHANGED
|
@@ -86,7 +86,7 @@ The suite includes:
|
|
|
86
86
|
- prototype-shaped command, action, role, profile, form-field, keyboard, and resource names proving that inherited object properties are never interpreted as dispatch or authority; current-schema malformed OAuth roles are repaired to disabled reviewer accounts with credential revocation;
|
|
87
87
|
- canonical Worker deployment URL extraction proving unrelated `/mcp`, `/healthz`, path-bearing, and wrong-name URLs cannot be persisted as upload evidence;
|
|
88
88
|
- byte-exact UTF-8 DOM-source truncation across emoji and Chinese partial-code-point boundaries, including equality between the reported byte count and the encoded returned source;
|
|
89
|
-
- independently injected service CLI status/install/start/stop/uninstall/remove paths, including provider failure, no selected workspace, no deployed Worker, aliases,
|
|
89
|
+
- independently injected service CLI status/install/start/stop/restart/uninstall/remove paths, including provider failure, no selected workspace, no deployed Worker, aliases, default output/exit adapters, active restart provider invocation, old-PID rejection, and replacement-readiness evidence;
|
|
90
90
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
91
91
|
- installed-package zero-argument startup with an isolated HOME/state/workspace and a service-manager trap, proving the smoke test reaches the controlled Wrangler boundary without querying, stopping, or replacing the operator's machine-global launchd/systemd/Scheduled Task service;
|
|
92
92
|
- live stdio MCP initialization with session instructions, capability resolution, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
@@ -95,7 +95,7 @@ The suite includes:
|
|
|
95
95
|
- root-certified ephemeral P-256 account-administration requests with origin/method/path/body/key/time/nonce binding, transactional one-time nonce consumption, removal of the long-lived administration secret, certificate/signature/body tamper rejection, nonce replay rejection, malformed nonce-state fail-closed behavior, one-megabyte response bounds, immediate oversized-response cancellation, and strict successful JSON-object validation;
|
|
96
96
|
- live local Worker OAuth registration and authorization metadata; PKCE, DCR, refresh rotation/replay, account/client/family revocation, DPoP, actual `/mcp` Origin checks, bounded CORS/CSP, exact callback handling, and bounded OAuth persistence; modern MCP `2026-07-28` per-request `_meta`, open-JSON structural budgets, bounded resource subscriptions, strict dual-media `Accept` quality values, mirrored header/body validation, `server/discover`, result identity, cache hints, same-token/same-request-ID concurrency, role-hidden/unknown/schema `-32602` non-dispatch, credential-free private cancellation (including public-header forgery and DPoP replay controls), request-scoped streaming, filtered `subscriptions/listen`, removed-method 404 behavior, and no session/replay leakage; plus the complete legacy MCP `2025-11-25` initialize, pre-persistence raw-argument validation, signed-session cancellation, sequence event, recovery GET, `Last-Event-ID`, duplicate-domain, and replay-isolation suite. The same integration covers shared Worker/local timeout ceilings, daemon candidate/probing/ready replacement, malformed daemon messages, rich content, account-role projection, and stable catalog behavior before/during/after daemon availability.
|
|
97
97
|
- local runtime proof that one blocked tool handler does not serialize an independent handler, plus relay fault injection proving an undeliverable terminal result interrupts the ambiguous socket and enters reconnect backoff.
|
|
98
|
-
- a real headless-Chrome OAuth navigation regression with four cases: `form-action 'self'` blocks the first cross-origin callback, allowing only the registered callback blocks the regional redirect, allowing the registered and regional callbacks blocks the final Copilot Studio redirect, and the complete policy preserves `code` and `state` through all three cross-origin hops. Linux CI fails if Chrome is unavailable; other environments skip only this browser executable check while retaining the Worker CSP assertions.
|
|
98
|
+
- a real headless-Chrome OAuth navigation regression with bounded browser startup, DevTools discovery, WebSocket connection, and per-command deadlines, covering four cases: `form-action 'self'` blocks the first cross-origin callback, allowing only the registered callback blocks the regional redirect, allowing the registered and regional callbacks blocks the final Copilot Studio redirect, and the complete policy preserves `code` and `state` through all three cross-origin hops. Linux CI fails if Chrome is unavailable; other environments skip only this browser executable check while retaining the Worker CSP assertions.
|
|
99
99
|
|
|
100
100
|
`npm run ssh-key:test` exercises Ed25519/RSA generation, private-file permissions, public/private matching, existing-pair reuse, incomplete-pair rejection, symbolic-link denial, atomic registration under the startup lock, registry conflicts/capacity, complete two-file rollback after state-write failure, and explicit failure when sensitive-key cleanup remains incomplete.
|
|
101
101
|
|
|
@@ -112,6 +112,9 @@ It opens Calculator, activates it through the fixed JXA helper, verifies structu
|
|
|
112
112
|
For deterministic release validation, perform an isolated-profile smoke test with the packaged unpacked extension; a Playwright persistent Chromium context is acceptable only as that isolated harness. When the requirement is specifically to prove control of the user's ordinary browser, the user must load the same unpacked directory into that known daily Chromium profile; status can verify the extension version/protocol and that Machine Bridge did not launch a browser, but cannot infer profile identity. Then use a localhost no-store fixture in a newly created tab. Do not enumerate, read, or mutate unrelated existing tabs. In both modes, inspect and reuse refs, exercise waits/forms/trusted input/open Shadow DOM/screenshots, verify final live DOM, and close the fixture tab.
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
`npm run control-plane-resilience:test` is the focused accident-regression gate. It exercises synchronous/asynchronous audit failures, persistent-state corruption and retention anchoring, POSIX/Windows process-tree fallbacks, escalation-supervisor exception isolation, mixed transient/durable Worker capacity, and the shared 30+2 / 14+2 control-plane admission contract. It is part of the fast plan rather than coverage-only evidence.
|
|
116
|
+
|
|
117
|
+
- control-plane resilience under host pressure: local event-loop stalls versus genuine relay silence, fresh-heartbeat recovery grace, asynchronous process-group identity capture before `SIGTERM`, bounded post-signal revalidation, draining-process visibility after result settlement, two reserved diagnostic slots at both Worker and local layers under mixed transient/durable ordinary-call saturation, non-blocking audit dispatch, batched Worker persistence, queue/drop health, warning suppression, and privacy-safe audit projection;
|
|
115
118
|
- relay outage diagnostics and recovery: application-proxy versus OS-network scope, timestamped close/outage/recovery fields, fifteen-second maximum reconnect delay, heartbeat timeout, same-instance call continuation, Worker pong/welcome send failure, and `diagnose_runtime` relay history;
|
|
116
119
|
- recoverable managed-job terminal commits under injected result/status/delete/confirmation failures, result-only terminal reconstruction, private runtime/plan scrubbing, 24-hour staged-plan expiry, and minimal/full runner-environment inheritance;
|
|
117
120
|
- cross-process security-audit serialization, continuous hash-chain sequence, uninstall blocking for audit/authorization/job transition/recovery locks, workspace recovery-envelope validation, and symlink/multiple-hard-link rejection at owner state and operational logs;
|
|
@@ -119,7 +122,7 @@ For deterministic release validation, perform an isolated-profile smoke test wit
|
|
|
119
122
|
|
|
120
123
|
## Critical-module coverage gate
|
|
121
124
|
|
|
122
|
-
`npm run coverage:test` runs selected in-process and lightweight entrypoint fixtures under V8 coverage and enforces per-module function and branch baselines. The measured set includes policy, typed errors, call registration, bounded child-process settlement, execution middleware, lifecycle/observability, logging, Runtime/CLI orchestration, persistent candidate activation, state persistence, relay lifecycle, managed-job lifecycle/runner/storage/projection, browser broker/direct-request/runtime-client routing and loopback upgrade handling, the independently injected service CLI adapter, Agent configuration/projection/skill discovery and bounded text reads, capability ranking, runtime path redaction, browser protocol/operation boundaries, runtime reporting/diagnostics/capability composition, and Worker OAuth state/authorization-page/pending/policy/error/JSON-RPC/WebSocket protocol modules. The full stdio and workerd OAuth/MCP integration still runs separately.
|
|
125
|
+
`npm run coverage:test` runs selected in-process and lightweight entrypoint fixtures under V8 coverage and enforces per-module function and branch baselines. The measured set includes policy, typed errors, call registration, bounded child-process settlement, execution middleware, lifecycle/observability, logging, Runtime/CLI orchestration, persistent candidate activation, state persistence, relay lifecycle, managed-job lifecycle/runner/storage/projection, browser broker/direct-request/runtime-client routing and loopback upgrade handling, the independently injected service CLI adapter, Agent configuration/projection/skill discovery and bounded text reads, capability ranking, runtime path redaction, browser protocol/operation boundaries, runtime reporting/diagnostics/control-plane projection/capability composition, shared and Worker/local call-capacity algebra, relay-heartbeat/process-tree/audit isolation, and Worker OAuth state/authorization-page/pending/policy/error/JSON-RPC/WebSocket protocol modules. The full stdio and workerd OAuth/MCP integration still runs separately.
|
|
123
126
|
|
|
124
127
|
Coverage belongs to the selected top-level fixtures. Concurrency, process-identity, and daemon-takeover helpers explicitly set `NODE_V8_COVERAGE` to an empty value: Node reinjects the parent coverage directory when the variable is merely deleted, which would start a profiler in every helper and distort both runtime and host load without improving the gated module evidence. Helper scripts assert this isolation where practical.
|
|
125
128
|
Managed-job runners remain instrumented because their lifecycle modules are gated; only test-plan business steps clear coverage, preventing nested profiler startup from replacing the behavior the test is intended to observe. The suite also holds a queued status beyond the recovery grace period while a provisional runner claim is live, verifies that no recovery occurs, and checks that the child upgrades the one-time-token claim to an exact token-free process identity.
|
package/docs/TOOL_REFERENCE.md
CHANGED
|
@@ -2211,7 +2211,7 @@ Terminate a live server-managed process tree with graceful or forced termination
|
|
|
2211
2211
|
|
|
2212
2212
|
**Diagnose runtime layers**
|
|
2213
2213
|
|
|
2214
|
-
Run fixed, non-user-controlled local probes to distinguish MCP policy, local filesystem, process-spawn, shell, managed-job storage, and registered-resource failures. A successful response proves the request reached the local daemon; it cannot diagnose a host refusal that blocks the tool call itself.
|
|
2214
|
+
Run fixed, non-user-controlled local probes and return privacy-safe control-plane state (call capacity, draining processes, execution guardrails, relay liveness, and audit health) to distinguish MCP policy, local filesystem, process-spawn, shell, managed-job storage, and registered-resource failures. A successful response proves the request reached the local daemon; it cannot diagnose a host refusal that blocks the tool call itself.
|
|
2215
2215
|
|
|
2216
2216
|
| Contract field | Value |
|
|
2217
2217
|
|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.28",
|
|
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",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"service-platform:test": "node tests/service-platform-test.mjs",
|
|
96
96
|
"policy:test": "node tests/policy-test.mjs",
|
|
97
97
|
"runtime-infrastructure:test": "node tests/runtime-infrastructure-test.mjs",
|
|
98
|
+
"control-plane-resilience:test": "node --test tests/control-plane-resilience-test.mjs",
|
|
98
99
|
"worker-runtime-infrastructure:test": "node tests/worker-runtime-infrastructure-test.mjs",
|
|
99
100
|
"logging-structure:test": "node tests/logging-structure-test.mjs",
|
|
100
101
|
"policy-docs:generate": "node scripts/generate-policy-reference.mjs",
|
package/scripts/check-plan.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const coverageDir = mkdtempSync(resolve(tmpdir(), "machine-bridge-coverage-"));
|
|
|
10
10
|
const tests = [
|
|
11
11
|
"tests/policy-test.mjs",
|
|
12
12
|
"tests/runtime-infrastructure-test.mjs",
|
|
13
|
+
"tests/control-plane-resilience-test.mjs",
|
|
13
14
|
"tests/runtime-boundaries-test.mjs",
|
|
14
15
|
"tests/worker-runtime-infrastructure-test.mjs",
|
|
15
16
|
"tests/mcp-resumption-test.mjs",
|
|
@@ -74,14 +75,23 @@ try {
|
|
|
74
75
|
"src/local/operation-risk.mjs": [85, 60],
|
|
75
76
|
"src/local/operation-state-lock.mjs": [100, 7],
|
|
76
77
|
"src/local/security-audit-log.mjs": [85, 55],
|
|
78
|
+
"src/local/security-audit-dispatch.mjs": [100, 70],
|
|
79
|
+
"src/local/security-audit-warning.mjs": [100, 75],
|
|
80
|
+
"src/local/security-audit-storage.mjs": [85, 60],
|
|
77
81
|
"src/local/delegated-process-sandbox.mjs": [80, 45],
|
|
78
82
|
"src/shared/device-session-auth.mjs": [100, null],
|
|
79
83
|
"src/shared/mcp-protocol.mjs": [90, 70],
|
|
80
84
|
"src/shared/mcp-subscriptions.mjs": [95, 75],
|
|
81
85
|
"src/shared/tool-argument-validation.mjs": [90, 70],
|
|
86
|
+
"src/shared/tool-call-capacity.mjs": [100, 75],
|
|
82
87
|
"src/local/policy.mjs": [90, 65],
|
|
83
88
|
"src/local/errors.mjs": [70, 50],
|
|
84
89
|
"src/local/call-registry.mjs": [85, 55],
|
|
90
|
+
"src/local/call-capacity.mjs": [100, 70],
|
|
91
|
+
"src/local/process-tree-signal.mjs": [90, 55],
|
|
92
|
+
"src/local/process-tree-supervisor.mjs": [100, 70],
|
|
93
|
+
"src/local/process-tree-snapshot.mjs": [90, 65],
|
|
94
|
+
"src/local/relay-heartbeat.mjs": [90, 65],
|
|
85
95
|
"src/local/tool-executor.mjs": [90, 40],
|
|
86
96
|
"src/local/tool-result-boundary.mjs": [100, 75],
|
|
87
97
|
"src/local/observability.mjs": [95, 40],
|
|
@@ -100,6 +110,7 @@ try {
|
|
|
100
110
|
"src/local/service-restart-handoff.mjs": [100, 60],
|
|
101
111
|
"src/local/service-owner.mjs": [100, 85],
|
|
102
112
|
"src/local/service-runtime.mjs": [100, 80],
|
|
113
|
+
"src/local/service-runtime-convergence.mjs": [100, 80],
|
|
103
114
|
"src/local/windows-service-convergence.mjs": [100, 95],
|
|
104
115
|
"src/local/runtime-activation.mjs": [90, 70],
|
|
105
116
|
"scripts/release-publication-guard.mjs": [100, 80],
|
|
@@ -150,6 +161,7 @@ try {
|
|
|
150
161
|
"src/worker/oauth-controller.ts": [84, 65],
|
|
151
162
|
"src/worker/oauth-authorization-page.ts": [90, 60],
|
|
152
163
|
"src/worker/pending-calls.ts": [90, 35],
|
|
164
|
+
"src/worker/pending-call-capacity.ts": [100, 70],
|
|
153
165
|
"src/worker/policy.ts": [100, 25],
|
|
154
166
|
"src/worker/errors.ts": [100, 40],
|
|
155
167
|
"src/worker/mcp-jsonrpc.ts": [95, 55],
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toolCallAdmission, toolCallCapacityConfig, toolCallCapacityUsage } from "../shared/tool-call-capacity.mjs";
|
|
2
|
+
import { BridgeError } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
/** @param {unknown} maximumValue @param {unknown} reservedValue @param {Iterable<string>} [reservedTools] */
|
|
5
|
+
export function callCapacityConfig(maximumValue, reservedValue, reservedTools = []) {
|
|
6
|
+
const maximum = Number(maximumValue);
|
|
7
|
+
return toolCallCapacityConfig(
|
|
8
|
+
Number.isFinite(maximum) && maximum > 0 ? Math.floor(maximum) : 16,
|
|
9
|
+
reservedValue,
|
|
10
|
+
reservedTools,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {Map<string, any>} calls @param {ReturnType<typeof callCapacityConfig>} config @param {string} toolName */
|
|
15
|
+
export function assertCallCapacity(calls, config, toolName) {
|
|
16
|
+
const decision = toolCallAdmission(callSnapshot(calls), config, toolName);
|
|
17
|
+
if (decision.allowed) return;
|
|
18
|
+
if (decision.reason === "total_capacity") {
|
|
19
|
+
throw new BridgeError("limit_exceeded", `too many concurrent tool calls (${config.maximum})`, { retryable: true });
|
|
20
|
+
}
|
|
21
|
+
throw new BridgeError(
|
|
22
|
+
"limit_exceeded",
|
|
23
|
+
`ordinary tool-call capacity reached (${config.ordinaryMaximum}); control-plane capacity is reserved for diagnosis and recovery`,
|
|
24
|
+
{ retryable: true },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {Map<string, any>} calls @param {ReturnType<typeof callCapacityConfig>} config */
|
|
29
|
+
export function callCapacitySnapshot(calls, config) {
|
|
30
|
+
const usage = toolCallCapacityUsage(callSnapshot(calls), config);
|
|
31
|
+
return {
|
|
32
|
+
maximum: usage.maximum,
|
|
33
|
+
ordinary_capacity: usage.ordinaryMaximum,
|
|
34
|
+
reserved_capacity: usage.reserved,
|
|
35
|
+
active_reserved: usage.activeReserved,
|
|
36
|
+
active_ordinary: usage.activeOrdinary,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {Map<string, any>} calls */
|
|
41
|
+
function callSnapshot(calls) {
|
|
42
|
+
const byTool = Object.create(null);
|
|
43
|
+
for (const call of calls.values()) byTool[call.tool] = (byTool[call.tool] || 0) + 1;
|
|
44
|
+
return { active: calls.size, byTool };
|
|
45
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
5
|
import { BridgeError } from "./errors.mjs";
|
|
6
|
+
import { assertCallCapacity, callCapacityConfig, callCapacitySnapshot } from "./call-capacity.mjs";
|
|
6
7
|
|
|
7
8
|
/** @typedef {ReturnType<typeof setTimeout>} TimerHandle */
|
|
8
9
|
/**
|
|
@@ -20,6 +21,8 @@ import { BridgeError } from "./errors.mjs";
|
|
|
20
21
|
/**
|
|
21
22
|
* @typedef {{
|
|
22
23
|
* maximum?: unknown,
|
|
24
|
+
* reserved?: unknown,
|
|
25
|
+
* reservedTools?: Iterable<string>,
|
|
23
26
|
* now?: () => number,
|
|
24
27
|
* scheduler?: {setTimeout: typeof setTimeout, clearTimeout: typeof clearTimeout},
|
|
25
28
|
* onCancel?: (record: CallRecord) => void,
|
|
@@ -34,7 +37,8 @@ import { BridgeError } from "./errors.mjs";
|
|
|
34
37
|
export class CallRegistry {
|
|
35
38
|
/** @param {CallRegistryOptions} [options] */
|
|
36
39
|
constructor(options = {}) {
|
|
37
|
-
this.
|
|
40
|
+
this.capacity = callCapacityConfig(options.maximum, options.reserved, options.reservedTools);
|
|
41
|
+
this.maximum = this.capacity.maximum;
|
|
38
42
|
this.now = typeof options.now === "function" ? options.now : () => performance.now();
|
|
39
43
|
this.scheduler = options.scheduler || { setTimeout, clearTimeout };
|
|
40
44
|
this.onCancel = typeof options.onCancel === "function" ? options.onCancel : () => {};
|
|
@@ -46,17 +50,16 @@ export class CallRegistry {
|
|
|
46
50
|
/** @param {OpenCallInput} [input] */
|
|
47
51
|
open({ callId = "", tool = "", origin = "local", timeoutMs = 0 } = {}) {
|
|
48
52
|
const id = String(callId || `call_${randomBytes(16).toString("hex")}`);
|
|
53
|
+
const toolName = String(tool || "");
|
|
49
54
|
if (this.calls.has(id)) throw new BridgeError("conflict", "duplicate in-flight call id");
|
|
50
|
-
|
|
51
|
-
throw new BridgeError("limit_exceeded", `too many concurrent tool calls (${this.maximum})`, { retryable: true });
|
|
52
|
-
}
|
|
55
|
+
assertCallCapacity(this.calls, this.capacity, toolName);
|
|
53
56
|
const controller = new AbortController();
|
|
54
57
|
const startedAt = this.now();
|
|
55
58
|
const timeout = positiveInteger(timeoutMs, 0);
|
|
56
59
|
/** @type {CallRecord} */
|
|
57
60
|
const record = {
|
|
58
61
|
id,
|
|
59
|
-
tool:
|
|
62
|
+
tool: toolName,
|
|
60
63
|
origin: String(origin || "local"),
|
|
61
64
|
startedAt,
|
|
62
65
|
deadlineAt: timeout ? startedAt + timeout : null,
|
|
@@ -159,7 +162,7 @@ export class CallRegistry {
|
|
|
159
162
|
}
|
|
160
163
|
return {
|
|
161
164
|
active: this.calls.size,
|
|
162
|
-
|
|
165
|
+
...callCapacitySnapshot(this.calls, this.capacity),
|
|
163
166
|
by_origin: byOrigin,
|
|
164
167
|
oldest_ms: oldestMs,
|
|
165
168
|
};
|