machine-bridge-mcp 0.8.0 → 0.8.1
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 +12 -0
- package/docs/ENGINEERING.md +1 -0
- package/docs/LOGGING.md +2 -0
- package/docs/OPERATIONS.md +1 -1
- package/docs/TESTING.md +2 -2
- package/package.json +1 -1
- package/src/local/relay-connection.mjs +22 -0
- package/src/local/runtime.mjs +4 -0
- package/src/local/service.mjs +43 -7
- package/src/worker/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 - 2026-07-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Recognize and validate the Worker's pre-handshake `welcome` control message instead of reporting it as an unknown WebSocket warning. A valid welcome remains debug-only and does not imply authenticated readiness; identity or version mismatch still fails immediately.
|
|
8
|
+
- Add a versioned autostart-log schema migration. On the first daemon start after this logging-format change, bounded prior logs are copied to owner-only `daemon.*.legacy.log` snapshots and the active logs are cleared, preventing historical raw close-code lines from appearing to be current behavior.
|
|
9
|
+
|
|
10
|
+
### Tests and documentation
|
|
11
|
+
|
|
12
|
+
- Add relay/runtime regression coverage for valid welcome handling and welcome metadata validation, plus service tests for one-time bounded legacy-log migration and schema-marker idempotence.
|
|
13
|
+
- Record the protocol producer/consumer contract rule and document how current and legacy daemon logs are separated.
|
|
14
|
+
|
|
3
15
|
## 0.8.0 - 2026-07-11
|
|
4
16
|
|
|
5
17
|
### Architecture
|
package/docs/ENGINEERING.md
CHANGED
|
@@ -32,6 +32,7 @@ Rules:
|
|
|
32
32
|
- Transport lifecycle, domain execution, persistence, and presentation are separate modules.
|
|
33
33
|
- Pure classification and normalization functions are exported and tested directly when practical.
|
|
34
34
|
- Adapters may translate data but should not duplicate policy or schemas.
|
|
35
|
+
- Every protocol control message emitted by one side must be explicitly accepted, rejected, or version-gated by the other side, with an end-to-end contract test covering the message name and semantics.
|
|
35
36
|
- State transitions are explicit; readiness is not inferred from a lower-level event. For example, an open WebSocket is not an authenticated relay until `hello_ack` is received.
|
|
36
37
|
- Every externally controlled input is bounded before expensive allocation, traversal, parsing, storage, or execution.
|
|
37
38
|
- Repository text must not contain invisible ASCII controls other than tab, CR, and LF; architecture tests enforce this even when JavaScript syntax remains valid.
|
package/docs/LOGGING.md
CHANGED
|
@@ -97,6 +97,8 @@ logs/daemon.err.log
|
|
|
97
97
|
|
|
98
98
|
Existing files are opened without following symbolic links where supported and tail-trimmed on UTF-8/line boundaries before startup. Background services use `warn`, so ordinary tool traffic and brief relay interruptions do not cause sustained growth.
|
|
99
99
|
|
|
100
|
+
The log format has an explicit schema marker. When the schema changes, the daemon copies a bounded tail of each prior active log to an owner-only `daemon.out.legacy.log` or `daemon.err.legacy.log` snapshot, clears the active file, and advances the marker. The migration is idempotent and keeps historical transport-format lines visibly separate from current behavior.
|
|
101
|
+
|
|
100
102
|
Each managed job has owner-only runner diagnostic logs. Child-step output is retained only in bounded, redacted job results according to `capture_output`; it is not copied into daemon or runner operational logs.
|
|
101
103
|
|
|
102
104
|
## MCP host boundary
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -32,7 +32,7 @@ Use `--verbose` only when close codes, close reasons, heartbeat timeouts, and re
|
|
|
32
32
|
|
|
33
33
|
## Logs
|
|
34
34
|
|
|
35
|
-
Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the Node/CLI directories, the installer's absolute PATH entries, and platform defaults. This avoids versioned Homebrew-style paths becoming invalid after upgrades and prevents launchd/systemd from falling back to a minimal system-only PATH. Re-run `machine-mcp service install` after changing Node installation families or PATH layout. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Files are owner-only where supported and tail-trimmed before daemon startup.
|
|
35
|
+
Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the Node/CLI directories, the installer's absolute PATH entries, and platform defaults. This avoids versioned Homebrew-style paths becoming invalid after upgrades and prevents launchd/systemd from falling back to a minimal system-only PATH. Re-run `machine-mcp service install` after changing Node installation families or PATH layout. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Files are owner-only where supported and tail-trimmed before daemon startup. On a logging-schema upgrade, bounded prior content is moved into `daemon.out.legacy.log` and `daemon.err.legacy.log`; use the active filenames when diagnosing current behavior.
|
|
36
36
|
|
|
37
37
|
Logging is level-based:
|
|
38
38
|
|
package/docs/TESTING.md
CHANGED
|
@@ -36,8 +36,8 @@ The suite includes:
|
|
|
36
36
|
- daemon/startup locking and state corruption recovery;
|
|
37
37
|
- guarded state-root removal, schema migration, policy-origin persistence, and legacy implicit-default migration;
|
|
38
38
|
- no filename-based sensitive-file denial under unrestricted policy;
|
|
39
|
-
- log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug,
|
|
40
|
-
- deterministic relay connection lifecycle coverage for transport construction/error, authenticated `hello_ack` readiness, identity/version mismatch, handshake and heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
|
|
39
|
+
- log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, service warning-level configuration, and idempotent bounded migration of legacy log formats;
|
|
40
|
+
- deterministic relay connection lifecycle coverage for transport construction/error, pre-handshake `welcome` validation, authenticated `hello_ack` readiness, identity/version mismatch, handshake and heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
|
|
41
41
|
- shared no-follow bounded-file reads for normal files, over-limit data, directories, and symbolic links;
|
|
42
42
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
43
43
|
- live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Cross-client MCP bridge for canonical local automation, files, Git, SSH resources, managed jobs, and processes over stdio or an OAuth-protected remote relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -94,6 +94,19 @@ export class RelayConnection {
|
|
|
94
94
|
return this.sendOnSocket(this.socket, value);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
observeWelcome(message = {}) {
|
|
98
|
+
const socket = this.socket;
|
|
99
|
+
if (this.closed || this.ready || !this.isSocketOpen(socket)) return false;
|
|
100
|
+
const mismatch = welcomeMismatch(message, this.expectedServer, this.expectedVersion);
|
|
101
|
+
if (mismatch) {
|
|
102
|
+
this.logger.debug?.("remote relay welcome rejected", { reason: mismatch });
|
|
103
|
+
this.failPermanently("relay_protocol_mismatch");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
this.logger.debug?.("remote relay welcome received");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
acknowledge(message = {}) {
|
|
98
111
|
const socket = this.socket;
|
|
99
112
|
if (this.closed || this.ready || !this.isSocketOpen(socket)) return false;
|
|
@@ -435,6 +448,15 @@ function relayCloseUserCause(category) {
|
|
|
435
448
|
return causes[String(category || "")] || "connection interrupted";
|
|
436
449
|
}
|
|
437
450
|
|
|
451
|
+
export function welcomeMismatch(message, expectedServer = "", expectedVersion = "") {
|
|
452
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return "invalid_welcome";
|
|
453
|
+
if (message.type !== "welcome") return "unexpected_welcome_type";
|
|
454
|
+
if (expectedServer && message.server !== expectedServer) return "server_identity_mismatch";
|
|
455
|
+
if (expectedVersion && message.version !== expectedVersion) return "server_version_mismatch";
|
|
456
|
+
if (typeof message.server !== "string" || !message.server || typeof message.version !== "string" || !message.version) return "incomplete_welcome";
|
|
457
|
+
return "";
|
|
458
|
+
}
|
|
459
|
+
|
|
438
460
|
export function acknowledgementMismatch(message, expectedServer = "", expectedVersion = "") {
|
|
439
461
|
if (!message || typeof message !== "object" || Array.isArray(message)) return "invalid_acknowledgement";
|
|
440
462
|
if (message.type !== "hello_ack") return "unexpected_acknowledgement_type";
|
package/src/local/runtime.mjs
CHANGED
|
@@ -188,6 +188,10 @@ export class LocalRuntime {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
handleRelayControlMessage(message) {
|
|
191
|
+
if (message.type === "welcome") {
|
|
192
|
+
this.relay?.observeWelcome(message);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
191
195
|
if (message.type === "hello_ack") {
|
|
192
196
|
this.relay?.acknowledge(message);
|
|
193
197
|
return true;
|
package/src/local/service.mjs
CHANGED
|
@@ -4,10 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { run } from "./shell.mjs";
|
|
5
5
|
import { ensureOwnerOnlyDir, expandHome, ownerOnlyFile } from "./state.mjs";
|
|
6
6
|
import { replaceFileSync } from "./atomic-fs.mjs";
|
|
7
|
+
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
7
8
|
|
|
8
9
|
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
9
10
|
const WINDOWS_TASK = "MachineBridgeMCP";
|
|
10
11
|
const SERVICE_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
12
|
+
const AUTOSTART_LOG_SCHEMA_VERSION = 2;
|
|
11
13
|
|
|
12
14
|
function serviceRun(command, args) {
|
|
13
15
|
return run(command, args, {
|
|
@@ -53,33 +55,67 @@ export function trimAutostartLogs(stateRoot, options = {}) {
|
|
|
53
55
|
const root = expandHome(stateRoot);
|
|
54
56
|
const maxBytes = Number.isFinite(Number(options.maxBytes)) ? Math.max(1024, Number(options.maxBytes)) : 2 * 1024 * 1024;
|
|
55
57
|
const keepBytes = Math.min(maxBytes, Number.isFinite(Number(options.keepBytes)) ? Math.max(1024, Number(options.keepBytes)) : 1024 * 1024);
|
|
58
|
+
const schemaVersion = String(Number.isInteger(Number(options.schemaVersion)) && Number(options.schemaVersion) > 0
|
|
59
|
+
? Number(options.schemaVersion)
|
|
60
|
+
: AUTOSTART_LOG_SCHEMA_VERSION);
|
|
56
61
|
const logs = path.join(root, "logs");
|
|
62
|
+
const schemaFile = path.join(logs, ".log-schema");
|
|
63
|
+
const migrate = readLogSchema(schemaFile) !== schemaVersion;
|
|
64
|
+
let migrationComplete = true;
|
|
65
|
+
|
|
57
66
|
for (const name of ["daemon.out.log", "daemon.err.log"]) {
|
|
58
67
|
const file = path.join(logs, name);
|
|
59
68
|
let fd;
|
|
60
69
|
try {
|
|
61
70
|
if (!existsSync(file)) continue;
|
|
62
71
|
const before = lstatSync(file);
|
|
63
|
-
if (before.isSymbolicLink() || !before.isFile())
|
|
72
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
73
|
+
if (migrate) migrationComplete = false;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
64
76
|
const noFollow = Number(fsConstants.O_NOFOLLOW || 0);
|
|
65
77
|
fd = openSync(file, Number(fsConstants.O_RDWR) | noFollow);
|
|
66
78
|
const info = fstatSync(fd);
|
|
67
|
-
if (!info.isFile())
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
79
|
+
if (!info.isFile()) {
|
|
80
|
+
if (migrate) migrationComplete = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (migrate && info.size > 0) {
|
|
84
|
+
const legacy = readLogTail(fd, info.size, maxBytes);
|
|
85
|
+
if (legacy.length) writePrivateServiceFile(path.join(logs, legacyLogName(name)), legacy);
|
|
86
|
+
ftruncateSync(fd, 0);
|
|
87
|
+
} else if (info.size > maxBytes) {
|
|
88
|
+
const tail = readLogTail(fd, info.size, keepBytes);
|
|
73
89
|
ftruncateSync(fd, 0);
|
|
74
90
|
if (tail.length) writeSync(fd, tail, 0, tail.length, 0);
|
|
75
91
|
}
|
|
76
92
|
try { chmodSync(file, 0o600); } catch {}
|
|
77
93
|
} catch {
|
|
94
|
+
if (migrate) migrationComplete = false;
|
|
78
95
|
// Operational log maintenance is best effort and must not stop startup.
|
|
79
96
|
} finally {
|
|
80
97
|
if (fd !== undefined) try { closeSync(fd); } catch {}
|
|
81
98
|
}
|
|
82
99
|
}
|
|
100
|
+
|
|
101
|
+
if (migrate && migrationComplete) {
|
|
102
|
+
try { writePrivateServiceFile(schemaFile, `${schemaVersion}\n`); } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readLogSchema(file) {
|
|
107
|
+
try { return readBoundedRegularFileSync(file, 64).toString("utf8").trim(); } catch { return ""; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readLogTail(fd, size, limit) {
|
|
111
|
+
const length = Math.min(limit, size);
|
|
112
|
+
const buffer = Buffer.alloc(length);
|
|
113
|
+
readSync(fd, buffer, 0, length, Math.max(0, size - length));
|
|
114
|
+
return lineSafeTail(buffer);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function legacyLogName(name) {
|
|
118
|
+
return name.endsWith(".log") ? `${name.slice(0, -4)}.legacy.log` : `${name}.legacy`;
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
function lineSafeTail(buffer) {
|
package/src/worker/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
|
|
|
3
3
|
import serverMetadata from "../shared/server-metadata.json";
|
|
4
4
|
|
|
5
5
|
const SERVER_NAME = String(serverMetadata.name);
|
|
6
|
-
const SERVER_VERSION = "0.8.
|
|
6
|
+
const SERVER_VERSION = "0.8.1";
|
|
7
7
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
8
8
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
9
9
|
const JSONRPC_VERSION = "2.0";
|