machine-bridge-mcp 1.2.5 → 1.2.6
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 +8 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT.md +10 -0
- package/docs/OPERATIONS.md +1 -1
- package/docs/UPGRADING.md +3 -3
- package/package.json +1 -1
- package/src/local/daemon-process.mjs +11 -4
- package/src/local/relay-connection.mjs +11 -1
- package/src/local/runtime.mjs +4 -3
- package/src/worker/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.6 - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Relay ready-context and implicit service-daemon takeover
|
|
6
|
+
|
|
7
|
+
- Keep end-to-end readiness fail-closed, but stop treating an incomplete inbound relay context that only carries `sessionId` as permanently unready. After a verified ready connection, the runtime consults live relay status when the per-message snapshot omits `ready`; an explicit `ready: false` snapshot still rejects tool calls. `RelayConnection` now always forwards boolean `authenticated` and `ready` with the session generation.
|
|
8
|
+
- Recognize managed service daemons started with only `--daemon-only` (no explicit `--workspace` / `--state-dir` on the process argv) when the lock owner already matches the active workspace state. Partial identity (one of the two path flags) remains rejected so foreign processes cannot be taken over. This allows recovery of source-tree recovery daemons that previously stayed orphaned across CLI upgrades.
|
|
9
|
+
- Add regression coverage for pre-ready vs ready inbound message contexts, sessionId-only dispatch after readiness, explicit `ready: false` fail-closed behavior, and implicit daemon-only stop/takeover.
|
|
10
|
+
|
|
3
11
|
## 1.2.5 - 2026-07-17
|
|
4
12
|
|
|
5
13
|
### End-to-end relay readiness and safe daemon handover
|
|
@@ -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.6",
|
|
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.6"
|
|
34
34
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -36,7 +36,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
|
|
|
36
36
|
|
|
37
37
|
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 session generation. Every relayed tool result is bound to the generation that delivered its call; a disconnected generation cannot send a late result over its replacement socket. Inbound WebSocket dispatch captures that generation at receive time and passes it to the runtime handler, so a successful local tool execution can publish only on the same generation. Stdio mode invokes `LocalRuntime` directly without that adapter.
|
|
38
38
|
|
|
39
|
-
`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 sends `SIGTERM` only to a verified same-workspace
|
|
39
|
+
`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, accepts lock-backed `--daemon-only` recovery processes that omit repeated path flags, and sends `SIGTERM` only to a verified same-workspace service daemon. POSIX daemons may ignore that signal and reach the bounded non-escalating timeout; Node's Windows signal mapping terminates the verified process directly. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
|
|
40
40
|
|
|
41
41
|
### Agent context and capability resolver
|
|
42
42
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# Security and privacy audit notes
|
|
2
|
+
|
|
3
|
+
## 2026-07-17 version 1.2.6 relay ready-context and daemon-takeover audit
|
|
4
|
+
|
|
5
|
+
Version 1.2.5 introduced fail-closed rejection of ordinary tool calls before end-to-end readiness. That boundary is correct, but an incomplete inbound dispatch object that carried only `sessionId` (the 1.2.4 session-binding shape) evaluated as not ready and converted the first real tool call into a permanent local protocol error. Version 1.2.6 keeps the explicit `ready: false` fail-closed path, requires RelayConnection to forward boolean `authenticated`/`ready`, and allows a live ready relay status to satisfy readiness only when the snapshot omitted the field.
|
|
6
|
+
|
|
7
|
+
Service-daemon takeover previously required both `--workspace` and `--state-dir` on the live process command line. Recovery daemons started with only `--daemon-only` from a source tree therefore remained unverified and blocked upgrade takeover. Identity verification now accepts that implicit form when the lock owner already matches the active workspace and state root, while partial path identity remains rejected.
|
|
8
|
+
|
|
9
|
+
No schema, policy revision, credential, or browser-extension protocol change is included. This source change does not deploy the Worker, replace a running daemon, rotate secrets, publish npm, or mutate live operator state.
|
|
10
|
+
|
|
1
11
|
# Engineering and security audit
|
|
2
12
|
|
|
3
13
|
## 2026-07-17 version 1.2.5 end-to-end relay-readiness audit
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -95,7 +95,7 @@ Application UI inspection/actions require Accessibility permission for the Node/
|
|
|
95
95
|
|
|
96
96
|
`machine-mcp` is a foreground command. It remains attached to the terminal, defaults to `info` logging, and stops on `Ctrl+C`. `machine-mcp service start` launches the installed platform service in the background and returns to the shell; that service uses `warn` logging.
|
|
97
97
|
|
|
98
|
-
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,
|
|
98
|
+
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. It waits up to 15 seconds for both the PID and lock to disappear and does not add a later forced-kill escalation. On POSIX, a daemon that handles and ignores `SIGTERM` therefore reaches the bounded timeout and remains alive. Node maps the supported termination signals to immediate process termination on Windows, so the same verified-daemon stop normally completes rather than exercising the POSIX timeout branch. A foreground or unverifiable process is left untouched; stop a foreground instance with `Ctrl+C`.
|
|
99
99
|
|
|
100
100
|
`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:
|
|
101
101
|
|
package/docs/UPGRADING.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Machine Bridge does not retain parallel implementations for obsolete MCP protocol dates, policy revisions, state schemas, lock formats, or browser-extension protocols. The supported path is a direct upgrade from the immediately preceding published package while its state already uses the current schema.
|
|
6
6
|
|
|
7
|
-
Version 1.2.
|
|
7
|
+
Version 1.2.6 keeps local state schema version 6 and policy revision 5 unchanged. Existing 1.2.5 workspaces, named accounts, resource registrations, managed-job history, Worker identity, and browser pairing state are reused without a schema conversion. The package change hardens relay ready-context propagation and verified service-daemon takeover for recovery daemons; it does not alter stored authority or credential records.
|
|
8
8
|
|
|
9
|
-
Version 1.2.
|
|
9
|
+
Version 1.2.6 still requires the Worker, local daemon, and browser extension to converge on the same package version. The 1.2.5 candidate → probing → ready negotiation remains mandatory; mixed components fail closed. Normal startup redeploys the versioned Worker when required and can now stop a verified same-workspace service daemon even when its process argv used only `--daemon-only` without repeating `--workspace` and `--state-dir`. Reload the unpacked extension after convergence even though this release does not change its browser protocol.
|
|
10
10
|
|
|
11
11
|
Version 1.2.0 could accept prototype-shaped account roles through malformed administration input. On the first 1.2.1 or later Worker access, such an account is preserved for recovery but repaired fail-closed: its role becomes `reviewer`, it is disabled, its account version advances, and its authorization codes and tokens are removed. An operator can then assign a valid role, enable the account, and rotate its password through the normal account administration flow. A local policy record with an unknown profile label is normalized to `custom` while retaining its explicit capability fields; an invalid explicit `--profile` is rejected.
|
|
12
12
|
|
|
@@ -29,6 +29,6 @@ Machine Bridge never treats an unreadable or foreign-schema state file as empty
|
|
|
29
29
|
|
|
30
30
|
## Rollback
|
|
31
31
|
|
|
32
|
-
Rollback is supported only when the older package understands every persisted schema and protocol already written by the newer package. Version 1.2.
|
|
32
|
+
Rollback is supported only when the older package understands every persisted schema and protocol already written by the newer package. Version 1.2.6 does not advance local state or policy schemas, so a complete rollback to 1.2.5 is structurally possible. The Worker, daemon, and extension must be rolled back together; restoring only one component recreates an unavailable mixed-version system. The preferred recovery is still to fix forward so incomplete ready-context dispatch and orphan recovery-daemon takeover remain corrected.
|
|
33
33
|
|
|
34
34
|
Never roll back by copying only selected state files or changing version fields. Restore one complete verified state backup, package version, Worker build, and browser extension as a single operational unit.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
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",
|
|
@@ -168,11 +168,18 @@ function inspectWorkspaceDaemonOwner(state, owner) {
|
|
|
168
168
|
const name = path.basename(value);
|
|
169
169
|
return name === entryName || name === "machine-mcp" || name === "machine-mcp.mjs";
|
|
170
170
|
});
|
|
171
|
-
const
|
|
171
|
+
const explicitIdentity = Boolean(workspaceArg && stateRootArg)
|
|
172
172
|
&& sameCanonicalPath(workspaceArg, state.workspace.path)
|
|
173
|
-
&& sameCanonicalPath(stateRootArg, state.paths.stateRoot)
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
&& sameCanonicalPath(stateRootArg, state.paths.stateRoot);
|
|
174
|
+
const implicitIdentity = !workspaceArg && !stateRootArg;
|
|
175
|
+
const commonIdentity = argv.includes("--daemon-only") && entryMatches;
|
|
176
|
+
if (commonIdentity && explicitIdentity) {
|
|
177
|
+
return { verified_service_daemon: true, reason: "service_command" };
|
|
178
|
+
}
|
|
179
|
+
if (commonIdentity && implicitIdentity) {
|
|
180
|
+
return { verified_service_daemon: true, reason: "implicit_service_command" };
|
|
181
|
+
}
|
|
182
|
+
return { verified_service_daemon: false, reason: "command_mismatch" };
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
function commandFlagValue(argv, name) {
|
|
@@ -319,7 +319,11 @@ export class RelayConnection {
|
|
|
319
319
|
if (this.socket !== socket || this.closed) return;
|
|
320
320
|
this.lastInboundAt = this.now();
|
|
321
321
|
// Bind results to the generation that received this message.
|
|
322
|
-
const relayContext = {
|
|
322
|
+
const relayContext = {
|
|
323
|
+
sessionId: this.activeSessionId,
|
|
324
|
+
authenticated: this.authenticated === true,
|
|
325
|
+
ready: this.ready === true,
|
|
326
|
+
};
|
|
323
327
|
try {
|
|
324
328
|
const outcome = this.onMessage(data, relayContext);
|
|
325
329
|
if (outcome && typeof outcome.catch === "function") {
|
|
@@ -638,6 +642,12 @@ export function readinessMismatch(message, expectedServer = "", expectedVersion
|
|
|
638
642
|
return "";
|
|
639
643
|
}
|
|
640
644
|
|
|
645
|
+
export function isRelayReadyContext(relayContext = {}, relay = null) {
|
|
646
|
+
if (relayContext?.ready === true) return true;
|
|
647
|
+
if (relayContext?.ready === false) return false;
|
|
648
|
+
return Number(relayContext?.sessionId) > 0 && relay?.status?.()?.ready === true;
|
|
649
|
+
}
|
|
650
|
+
|
|
641
651
|
export function isSupersededClose(code, reason) {
|
|
642
652
|
return Number(code) === 1012 && String(reason || "") === "replaced by verified daemon";
|
|
643
653
|
}
|
package/src/local/runtime.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { lstat, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { RelayConnection } from "./relay-connection.mjs";
|
|
5
|
+
import { isRelayReadyContext, RelayConnection } from "./relay-connection.mjs";
|
|
6
6
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
7
7
|
export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
|
|
8
8
|
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, PolicyGate, SERVER_NAME } from "./tools.mjs";
|
|
@@ -303,12 +303,12 @@ export class LocalRuntime {
|
|
|
303
303
|
}
|
|
304
304
|
if (this.handleRelayControlMessage(message)) return;
|
|
305
305
|
if (message.type === "relay_probe") {
|
|
306
|
-
if (relayContext.
|
|
306
|
+
if (isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("unexpected_relay_probe");
|
|
307
307
|
this.handleRelayProbe(message, relayContext);
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
if (message.type !== "tool_call") return this.handleRelayProtocolViolation("unexpected_server_message_type");
|
|
311
|
-
if (relayContext.
|
|
311
|
+
if (!isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("tool_call_before_ready");
|
|
312
312
|
await this.handleRelayToolCall(message, relayContext);
|
|
313
313
|
}
|
|
314
314
|
|
|
@@ -742,6 +742,7 @@ function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, on
|
|
|
742
742
|
});
|
|
743
743
|
}
|
|
744
744
|
|
|
745
|
+
|
|
745
746
|
function handleRelayData(runtime, data, relayContext = {}) {
|
|
746
747
|
const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
747
748
|
if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
|
package/src/worker/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./http.ts";
|
|
28
28
|
|
|
29
29
|
const SERVER_NAME = String(serverMetadata.name);
|
|
30
|
-
const SERVER_VERSION = "1.2.
|
|
30
|
+
const SERVER_VERSION = "1.2.6";
|
|
31
31
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
32
32
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
33
33
|
const JSONRPC_VERSION = "2.0";
|