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 +10 -0
- package/browser-extension/manifest.json +2 -2
- package/package.json +1 -1
- package/src/local/account-admin.mjs +1 -1
- package/src/local/cli.mjs +1 -1
- package/src/local/workspace-file-service.mjs +62 -22
- package/src/worker/index.ts +1 -1
- package/src/worker/oauth-state.ts +5 -2
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.
|
|
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.
|
|
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
|
+
"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]
|
|
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;
|
|
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 {
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
339
|
+
async function writeFlushedText(filePath, content, mode) {
|
|
340
|
+
const handle = await open(filePath, "wx", mode);
|
|
341
|
+
let failure = null;
|
|
335
342
|
try {
|
|
336
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
428
|
+
await move(stage.temp, operation.target);
|
|
413
429
|
record.targetCreated = true;
|
|
414
430
|
}
|
|
415
431
|
}
|
|
416
432
|
} catch (error) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (item.
|
|
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
|
-
|
|
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) {
|
package/src/worker/index.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
}
|