machine-bridge-mcp 1.1.3 → 1.1.4

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.4 - 2026-07-15
4
+
5
+ ### File integrity and contract corrections
6
+
7
+ - Preserve exact UTF-8 line endings in `read_file` whole-file and line-range results. CRLF files no longer return LF-normalized content paired with a SHA-256 value for different text.
8
+ - Flush workspace write and patch staging files through their open descriptors before atomic commit, remove partial staging files after failed writes, and report incomplete staging cleanup instead of leaving hidden residual data silently. This aligns `write_file`, `edit_file`, and `apply_patch` with the documented durability contract while retaining exact POSIX mode application.
9
+ - Surface incomplete `apply_patch` rollback as an explicit recovery error, and return a warning when a committed transaction cannot remove an internal staging or backup artifact instead of silently hiding residual workspace state.
10
+ - Enforce the documented 3-64 character account-name rule for newly created accounts in both the local administration client and Worker. Existing one-character accounts created by older versions remain discoverable for login and administration.
11
+ - Correct CLI `--json` guidance to state that a newly generated account password is intentionally included once during initial creation or rotation; stored administration, daemon, and token-version secrets remain omitted.
12
+
3
13
  ## 1.1.3 - 2026-07-15
4
14
 
5
15
  ### Copilot Studio final OAuth callback
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "1.1.3",
4
+ "version": "1.1.4",
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.1.3"
33
+ "version_name": "1.1.4"
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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",
@@ -94,7 +94,7 @@ function normalizeWorkerUrl(value) {
94
94
 
95
95
  function normalizeAccountName(value) {
96
96
  const name = String(value || "").trim().toLowerCase();
97
- if (!/^[a-z0-9](?:[a-z0-9._-]{1,62}[a-z0-9])?$/.test(name)) throw new BridgeError("invalid_request", "account name must contain 3-64 lowercase letters, digits, dots, underscores, or hyphens");
97
+ if (!/^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$/.test(name)) throw new BridgeError("invalid_request", "account name must contain 3-64 lowercase letters, digits, dots, underscores, or hyphens");
98
98
  return name;
99
99
  }
100
100
 
package/src/local/cli.mjs CHANGED
@@ -843,7 +843,7 @@ Start options:
843
843
  --unrestricted-paths Allow filesystem tools outside the workspace
844
844
  --absolute-paths Return absolute local paths (enabled by the full profile)
845
845
  --state-dir DIR Override state root
846
- --json Print machine-readable output; secrets are never included
846
+ --json Print machine-readable output; newly generated account passwords are included once
847
847
  --log-level LEVEL error, warn, info (default), or debug
848
848
  --log-format FORMAT text (default) or newline-delimited json
849
849
  --verbose Alias for --log-level debug; includes per-tool success/correlation logs
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
- import { chmod, link, lstat, mkdir, open, opendir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { link, lstat, mkdir, open, opendir, rename, rm, stat } from "node:fs/promises";
4
4
  import path, { basename, dirname, join, resolve } from "node:path";
5
5
  import { applyUpdateHunks, parsePatchEnvelope } from "./patch.mjs";
6
6
  import { clampInteger } from "./numbers.mjs";
@@ -92,14 +92,18 @@ export class WorkspaceFileService {
92
92
  this.throwIfCancelled(context);
93
93
  const maxBytes = clampInteger(args.max_bytes, 1024 * 1024, 1, MAX_WRITE_BYTES);
94
94
  const startLine = args.start_line === undefined ? 1 : clampInteger(args.start_line, 1, 1, Number.MAX_SAFE_INTEGER);
95
- const rawLines = content.split(/\r?\n/);
96
- const totalLines = content.endsWith("\n") ? Math.max(1, rawLines.length - 1) : rawLines.length;
95
+ const lineStarts = [0];
96
+ for (let index = 0; index < content.length; index += 1) {
97
+ if (content[index] === "\n" && index + 1 < content.length) lineStarts.push(index + 1);
98
+ }
99
+ const totalLines = lineStarts.length;
97
100
  const endLine = args.end_line === undefined ? totalLines : clampInteger(args.end_line, totalLines, 1, Number.MAX_SAFE_INTEGER);
98
101
  if (endLine < startLine) throw new Error("end_line must be greater than or equal to start_line");
99
102
  if (startLine > totalLines) throw new Error(`start_line exceeds total lines (${startLine} > ${totalLines})`);
100
103
  const selectedEnd = Math.min(endLine, totalLines);
101
- let selected = rawLines.slice(startLine - 1, selectedEnd).join("\n");
102
- if (selectedEnd < totalLines || content.endsWith("\n")) selected += "\n";
104
+ const selectedStartOffset = lineStarts[startLine - 1];
105
+ const selectedEndOffset = selectedEnd < totalLines ? lineStarts[selectedEnd] : content.length;
106
+ const selected = content.slice(selectedStartOffset, selectedEndOffset);
103
107
  const selectedBytes = Buffer.byteLength(selected);
104
108
  if (selectedBytes > maxBytes) throw new Error(`selected content exceeds max_bytes (${selectedBytes} > ${maxBytes})`);
105
109
  return {
@@ -221,9 +225,10 @@ export class WorkspaceFileService {
221
225
  }
222
226
  assertNoResolvedPatchCollisions(prepared);
223
227
  this.throwIfCancelled(context);
224
- await commitPatchTransaction(prepared);
228
+ const transaction = await commitPatchTransaction(prepared);
225
229
  return {
226
230
  ok: true,
231
+ ...(transaction.warnings.length ? { warnings: transaction.warnings } : {}),
227
232
  files: prepared.map((item) => ({
228
233
  operation: item.kind,
229
234
  path: this.displayPath(item.target || item.source),
@@ -331,11 +336,22 @@ async function readUtf8File(filePath) {
331
336
  return decodeUtf8(buffer);
332
337
  }
333
338
 
334
- async function applyExactMode(filePath, mode) {
339
+ async function writeFlushedText(filePath, content, mode) {
340
+ const handle = await open(filePath, "wx", mode);
341
+ let failure = null;
335
342
  try {
336
- await chmod(filePath, mode);
343
+ await handle.writeFile(content, { encoding: "utf8" });
344
+ try { await handle.chmod(mode); } catch (error) { if (process.platform !== "win32") throw error; }
345
+ await handle.sync();
337
346
  } catch (error) {
338
- if (process.platform !== "win32") throw error;
347
+ failure = error;
348
+ }
349
+ try { await handle.close(); } catch (error) { failure ||= error; }
350
+ if (failure) {
351
+ try { await rm(filePath, { force: true }); } catch {
352
+ throw new Error("staged file write failed and cleanup was incomplete; inspect the destination directory before retrying", { cause: failure });
353
+ }
354
+ throw failure;
339
355
  }
340
356
  }
341
357
 
@@ -343,8 +359,7 @@ async function atomicWriteText(full, content, existing = null, options = {}) {
343
359
  await mkdir(dirname(full), { recursive: true });
344
360
  const temp = join(dirname(full), `.${basename(full)}.mbm-${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
345
361
  try {
346
- await writeFile(temp, content, { encoding: "utf8", flag: "wx", mode: existing ? existing.mode & 0o777 : 0o600 });
347
- if (existing) await applyExactMode(temp, existing.mode & 0o777);
362
+ await writeFlushedText(temp, content, existing ? existing.mode & 0o777 : 0o600);
348
363
  if (options.expectedHash) {
349
364
  const current = await readUtf8File(full).catch(() => null);
350
365
  if (current === null || sha256(current) !== options.expectedHash) throw new Error("file changed before atomic commit");
@@ -376,7 +391,9 @@ function assertNoResolvedPatchCollisions(operations) {
376
391
  }
377
392
  }
378
393
 
379
- async function commitPatchTransaction(operations) {
394
+ export async function commitPatchTransaction(operations, options = {}) {
395
+ const move = options.rename || rename;
396
+ const remove = options.remove || rm;
380
397
  const staged = [];
381
398
  const committed = [];
382
399
  try {
@@ -384,8 +401,7 @@ async function commitPatchTransaction(operations) {
384
401
  if (operation.content === undefined) continue;
385
402
  await mkdir(dirname(operation.target), { recursive: true });
386
403
  const temp = join(dirname(operation.target), `.${basename(operation.target)}.mbm-patch-${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
387
- await writeFile(temp, operation.content, { encoding: "utf8", flag: "wx", mode: operation.mode });
388
- await applyExactMode(temp, operation.mode);
404
+ await writeFlushedText(temp, operation.content, operation.mode);
389
405
  staged.push({ operation, temp });
390
406
  }
391
407
 
@@ -403,26 +419,50 @@ async function commitPatchTransaction(operations) {
403
419
  let backup = null;
404
420
  if (operation.source) {
405
421
  backup = join(dirname(operation.source), `.${basename(operation.source)}.mbm-backup-${process.pid}-${randomBytes(6).toString("hex")}`);
406
- await rename(operation.source, backup);
422
+ await move(operation.source, backup);
407
423
  }
408
424
  const record = { operation, backup, targetCreated: false };
409
425
  committed.push(record);
410
426
  const stage = staged.find((item) => item.operation === operation);
411
427
  if (stage) {
412
- await rename(stage.temp, operation.target);
428
+ await move(stage.temp, operation.target);
413
429
  record.targetCreated = true;
414
430
  }
415
431
  }
416
432
  } catch (error) {
417
- for (const item of committed.reverse()) {
418
- if (item.targetCreated) await rm(item.operation.target, { force: true }).catch(() => {});
419
- if (item.backup) await rename(item.backup, item.operation.source).catch(() => {});
433
+ const recoveryFailures = [];
434
+ for (const item of [...committed].reverse()) {
435
+ if (item.targetCreated) {
436
+ try { await remove(item.operation.target, { force: true }); } catch (failure) { recoveryFailures.push(failure); }
437
+ }
438
+ if (item.backup) {
439
+ try { await move(item.backup, item.operation.source); } catch (failure) { recoveryFailures.push(failure); }
440
+ }
441
+ }
442
+ recoveryFailures.push(...await removePatchArtifacts(staged.map((item) => item.temp), remove));
443
+ if (recoveryFailures.length) {
444
+ throw new Error(`patch transaction failed and recovery was incomplete (${recoveryFailures.length} recovery operation(s) failed); inspect the workspace before retrying`, { cause: error });
420
445
  }
421
446
  throw error;
422
- } finally {
423
- for (const item of staged) await rm(item.temp, { force: true }).catch(() => {});
424
447
  }
425
- for (const item of committed) if (item.backup) await rm(item.backup, { force: true }).catch(() => {});
448
+
449
+ const cleanupFailures = await removePatchArtifacts([
450
+ ...staged.map((item) => item.temp),
451
+ ...committed.map((item) => item.backup).filter(Boolean),
452
+ ], remove);
453
+ return {
454
+ warnings: cleanupFailures.length
455
+ ? [`Patch committed, but ${cleanupFailures.length} internal transaction artifact(s) could not be removed; inspect affected directories for .mbm-backup-* or .mbm-patch-* files.`]
456
+ : [],
457
+ };
458
+ }
459
+
460
+ async function removePatchArtifacts(paths, remove) {
461
+ const failures = [];
462
+ for (const path of paths) {
463
+ try { await remove(path, { force: true }); } catch (error) { failures.push(error); }
464
+ }
465
+ return failures;
426
466
  }
427
467
 
428
468
  function assertTextSize(content, label) {
@@ -24,7 +24,7 @@ import {
24
24
  } from "./http";
25
25
 
26
26
  const SERVER_NAME = String(serverMetadata.name);
27
- const SERVER_VERSION = "1.1.3";
27
+ const SERVER_VERSION = "1.1.4";
28
28
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
29
29
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
30
30
  const JSONRPC_VERSION = "2.0";
@@ -4,6 +4,8 @@ const OAUTH_STORE_SCHEMA_VERSION = 1;
4
4
  const OAUTH_REFRESH_STORE_SCHEMA_VERSION = 1;
5
5
  export const OFFLINE_ACCESS_SCOPE = "offline_access";
6
6
  const PASSWORD_TOKEN_PATTERN = /^[a-z][a-z0-9_]{2,31}_[A-Za-z0-9_-]{43}$/;
7
+ const ACCOUNT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$/;
8
+ const LEGACY_ACCOUNT_NAME_PATTERN = /^(?:[a-z0-9]|[a-z0-9][a-z0-9._-]{1,62}[a-z0-9])$/;
7
9
 
8
10
  export interface AccountRecord {
9
11
  account_id: string;
@@ -166,11 +168,12 @@ export function validateAuthorizationRequest(
166
168
 
167
169
  export function normalizeAccountName(value: unknown): string | null {
168
170
  const name = String(value ?? "").trim().toLowerCase();
169
- return /^[a-z0-9](?:[a-z0-9._-]{1,62}[a-z0-9])?$/.test(name) ? name : null;
171
+ return ACCOUNT_NAME_PATTERN.test(name) ? name : null;
170
172
  }
171
173
 
172
174
  export function accountByName(store: OAuthStore, name: unknown): AccountRecord | null {
173
- const normalized = normalizeAccountName(name);
175
+ const candidate = String(name ?? "").trim().toLowerCase();
176
+ const normalized = LEGACY_ACCOUNT_NAME_PATTERN.test(candidate) ? candidate : null;
174
177
  if (!normalized) return null;
175
178
  return Object.values(store.accounts).find((account) => account.name === normalized) ?? null;
176
179
  }