machine-bridge-mcp 1.1.2 → 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,23 @@
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
+
13
+ ## 1.1.3 - 2026-07-15
14
+
15
+ ### Copilot Studio final OAuth callback
16
+
17
+ - Complete the Power Platform browser callback chain by allowing `https://copilotstudio.microsoft.com` only when the already validated OAuth redirect URI belongs to Microsoft's HTTPS `consent.azure-apim.net` domain. Copilot Studio redirects global consent to a regional consent endpoint and then to its own `/connection/oauth/redirect` page; Chromium applies the originating authorization page's `form-action` policy across every hop.
18
+ - Preserve the narrow security boundary: ordinary OAuth clients still receive only `'self'` plus their exact validated redirect origin, Microsoft consent callbacks retain the consent-subdomain allowance, and lookalike domains receive neither the regional nor Copilot Studio exception.
19
+ - Extend the real Chrome regression to prove four policy states: self-only blocks the first callback, global-only blocks the regional handoff, global-plus-regional blocks the final Studio handoff, and the complete policy preserves `code` and `state` through the entire chain.
20
+
3
21
  ## 1.1.2 - 2026-07-15
4
22
 
5
23
  ### Copilot Studio regional OAuth callback
package/README.md CHANGED
@@ -150,7 +150,7 @@ The same `/mcp` endpoint supports the current hosted-client paths documented by
150
150
 
151
151
  These are protocol and integration-contract claims, not evidence that every external tenant, plan, administrator policy, or host-side tool filter has been tested. Claude remote connectors originate from Anthropic's cloud, and Copilot Studio reaches MCP servers through Power Platform connectivity; neither requires adding a browser origin to Machine Bridge's CORS list.
152
152
 
153
- The Worker grants browser CORS response access to the exact first-party origins used by ChatGPT (`https://chatgpt.com` and the legacy `https://chat.openai.com`) and Grok (`https://grok.com` and its X-hosted surface at `https://x.com`). Users do not need to run Wrangler or edit Cloudflare variables. `MBM_ALLOWED_ORIGINS` remains an operator-only, comma-separated extension point for other exact browser origins; it is additive and does not replace the built-in set. Wildcards and `null` do not receive CORS permission. Actual OAuth navigations and form submissions are not rejected solely because a browser container supplies an opaque or client-specific `Origin`; authorization still depends on exact redirect/resource binding, PKCE, account authentication, and bearer-token validation. The consent page CSP permits form navigation only to the Worker itself and the validated redirect origin for that request. When the validated callback belongs to Microsoft `consent.azure-apim.net`, it additionally permits HTTPS subdomains of that same Microsoft consent domain so Power Platform can hand the global callback to its regional endpoint without granting arbitrary form destinations.
153
+ The Worker grants browser CORS response access to the exact first-party origins used by ChatGPT (`https://chatgpt.com` and the legacy `https://chat.openai.com`) and Grok (`https://grok.com` and its X-hosted surface at `https://x.com`). Users do not need to run Wrangler or edit Cloudflare variables. `MBM_ALLOWED_ORIGINS` remains an operator-only, comma-separated extension point for other exact browser origins; it is additive and does not replace the built-in set. Wildcards and `null` do not receive CORS permission. Actual OAuth navigations and form submissions are not rejected solely because a browser container supplies an opaque or client-specific `Origin`; authorization still depends on exact redirect/resource binding, PKCE, account authentication, and bearer-token validation. The consent page CSP permits form navigation only to the Worker itself and the validated redirect origin for that request. When the validated callback belongs to Microsoft `consent.azure-apim.net`, it additionally permits HTTPS subdomains of that same Microsoft consent domain and the exact `https://copilotstudio.microsoft.com` origin. This lets Power Platform hand the global callback to its regional endpoint and then return to Copilot Studio without granting arbitrary form destinations.
154
154
 
155
155
  ### Multiple clients and accounts
156
156
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "1.1.2",
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.2"
33
+ "version_name": "1.1.4"
34
34
  }
@@ -243,7 +243,7 @@ Removal first acquires a state-root maintenance lock that blocks new profile/sta
243
243
 
244
244
  OAuth metadata is pruned on access. Expired authorization codes, access tokens, refresh tokens, old throttling records, and inactive clients without active credentials are removed. Account disablement, role or password changes, removal, and deployment-wide token-version rotation make both token classes unusable; stale refresh records are deleted when the refresh store is next read. Source identities are deployment-keyed HMAC values, not stored source addresses or reversible unsalted hashes.
245
245
 
246
- Browser-origin handling separates CORS response sharing from protocol authentication. Preflight succeeds only for the Worker's own origin, a fixed first-party set for ChatGPT (`https://chatgpt.com`, `https://chat.openai.com`) and Grok (`https://grok.com`, `https://x.com`), or optional exact comma-separated additions from `MBM_ALLOWED_ORIGINS`; wildcard and `null` origins are not granted CORS access. Actual requests are routed without using `Origin` as an authentication boundary, and `Access-Control-Allow-Origin` is added only when that exact predicate passes. This permits OAuth top-level navigation and form submission from opaque or client-specific containers while exact redirect/resource binding, PKCE, account credentials, bearer tokens, and private admin/daemon secrets enforce authority. The authorization document's `form-action` policy is generated only after request validation and contains `'self'` plus the exact origin of that validated redirect URI. If that validated origin is Microsoft `consent.azure-apim.net`, the policy also admits `https://*.consent.azure-apim.net`; Power Platform first receives the authorization code at its global consent endpoint and then redirects to a regional endpoint, and Chromium applies `form-action` across that redirect chain. No other callback receives a wildcard source. This allows successful callback navigation without opening form submission to unrelated origins. Hosted Claude remote connectors originate from Anthropic infrastructure and Copilot Studio uses Power Platform connectivity, so their server-to-server requests do not require browser CORS entries; adding those brands to the browser allowlist would expand response sharing without enabling either integration.
246
+ Browser-origin handling separates CORS response sharing from protocol authentication. Preflight succeeds only for the Worker's own origin, a fixed first-party set for ChatGPT (`https://chatgpt.com`, `https://chat.openai.com`) and Grok (`https://grok.com`, `https://x.com`), or optional exact comma-separated additions from `MBM_ALLOWED_ORIGINS`; wildcard and `null` origins are not granted CORS access. Actual requests are routed without using `Origin` as an authentication boundary, and `Access-Control-Allow-Origin` is added only when that exact predicate passes. This permits OAuth top-level navigation and form submission from opaque or client-specific containers while exact redirect/resource binding, PKCE, account credentials, bearer tokens, and private admin/daemon secrets enforce authority. The authorization document's `form-action` policy is generated only after request validation and contains `'self'` plus the exact origin of that validated redirect URI. If that validated origin is Microsoft `consent.azure-apim.net`, the policy also admits `https://*.consent.azure-apim.net` and the exact `https://copilotstudio.microsoft.com` origin; Power Platform receives the authorization code at its global consent endpoint, redirects to a regional endpoint, and then returns to Copilot Studio, while Chromium applies `form-action` across the complete redirect chain. No other callback receives either exception. This allows successful callback navigation without opening form submission to unrelated origins. Hosted Claude remote connectors originate from Anthropic infrastructure and Copilot Studio uses Power Platform connectivity, so their server-to-server requests do not require browser CORS entries; adding those brands to the browser allowlist would expand response sharing without enabling either integration.
247
247
 
248
248
  ## Observability
249
249
 
package/docs/AUDIT.md CHANGED
@@ -16,7 +16,7 @@ The Claude and Microsoft Copilot Studio request was first tested against the exi
16
16
 
17
17
  The Worker now advertises the refresh grant and `offline_access`, stores only hashes of access and refresh tokens, and rotates every refresh token in one Durable Object write with the associated access-token update. Refresh records live under a separate versioned storage key, preserving the current primary OAuth-store schema and avoiding a destructive live migration. Refreshes remain bound to client, account, account version, role, normalized scope, resource, and deployment token version. Old-token replay and account-version changes return `invalid_grant`; account suspension, role/password changes, removal, and global token-version rotation cannot extend authority through refresh. Because both hosted clients may create a new DCR client for a fresh connection, the source throttle now counts only registrations that have not completed authorization; the global retained-client bound is unchanged.
18
18
 
19
- The workerd integration proves the hosted Claude callback, the unauthenticated `resource_metadata` challenge, discovery metadata, DCR grant advertisement, `offline_access`, form-encoded refresh, access/refresh replacement, replay rejection, continued MCP use with the refreshed access token, and targeted account refresh revocation. A live Copilot Studio connection exposed an additional browser boundary: Power Platform redirects the validated global `consent.azure-apim.net` callback to a regional subdomain, while Chromium enforces the originating authorization page's `form-action` policy across that redirect chain. The Worker therefore adds the Microsoft consent-subdomain source only when the already validated redirect URI is itself on that HTTPS domain; all other clients retain the exact-origin policy. Power Platform data policies, plan availability, tenant administration, and host-side tool filtering remain external controls.
19
+ The workerd integration proves the hosted Claude callback, the unauthenticated `resource_metadata` challenge, discovery metadata, DCR grant advertisement, `offline_access`, form-encoded refresh, access/refresh replacement, replay rejection, continued MCP use with the refreshed access token, and targeted account refresh revocation. A live Copilot Studio connection exposed an additional browser boundary: Power Platform redirects the validated global `consent.azure-apim.net` callback to a regional subdomain, while Chromium enforces the originating authorization page's `form-action` policy across that redirect chain. The Worker therefore adds the Microsoft consent-subdomain source and the exact Copilot Studio origin only when the already validated redirect URI is itself on that HTTPS domain; all other clients retain the exact-origin policy. Power Platform data policies, plan availability, tenant administration, and host-side tool filtering remain external controls.
20
20
 
21
21
  This document records the cross-cutting audit initiated for version 0.12.0, its later architecture, browser, lifecycle, authorization, release-gate, and version 1 follow-ups, including the version 1.0.2 elapsed-time review and the version 1.0.3 code-scanning inventory correction. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
22
22
 
package/docs/CLIENTS.md CHANGED
@@ -130,7 +130,7 @@ Supported configuration paths are:
130
130
  - **Claude custom connectors:** add the exact `/mcp` URL under Claude's connector settings and leave static OAuth client credentials empty so Claude can use DCR. Hosted Claude surfaces use `https://claude.ai/api/mcp/auth_callback`; the Worker accepts that exact registered redirect and rotates refresh tokens for the public client. See Anthropic's [remote custom connector guide](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) and [authentication contract](https://claude.com/docs/connectors/building/authentication).
131
131
  - **Microsoft Copilot Studio:** from the agent's **Tools** page, select **Add a tool > New tool > Model Context Protocol**, enter the `/mcp` URL, then select **OAuth 2.0 > Dynamic discovery**. Create the connection and add it to the agent. See Microsoft's [existing MCP server guide](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent).
132
132
 
133
- OAuth navigations and form submissions from opaque or client-specific browser containers are routed normally but receive no cross-origin response-sharing permission unless their exact origin is allowed. The authorization page permits form navigation only to itself and the validated redirect origin. For a validated Microsoft `consent.azure-apim.net` callback, the form policy also permits HTTPS subdomains of that same Microsoft consent domain because Power Platform redirects its global callback to a regional endpoint. Claude remote connectors originate from Anthropic infrastructure, while Copilot Studio connectivity runs through Power Platform connectors, so neither is a reason to broaden the browser CORS allowlist.
133
+ OAuth navigations and form submissions from opaque or client-specific browser containers are routed normally but receive no cross-origin response-sharing permission unless their exact origin is allowed. The authorization page permits form navigation only to itself and the validated redirect origin. For a validated Microsoft `consent.azure-apim.net` callback, the form policy also permits HTTPS subdomains of that same Microsoft consent domain and the exact Copilot Studio origin because Power Platform redirects its global callback to a regional endpoint and then back to Copilot Studio. Claude remote connectors originate from Anthropic infrastructure, while Copilot Studio connectivity runs through Power Platform connectors, so neither is a reason to broaden the browser CORS allowlist.
134
134
 
135
135
  Several OAuth clients and named accounts can coexist. Accounts have independent passwords, roles, active state, versions, authorization codes, access tokens, refresh tokens, and targeted revocation. Their effective tool sets are intersected with the connected daemon policy. All accounts still share one daemon and OS user, so hard tenant isolation requires separate deployments; see [MULTI_ACCOUNT.md](MULTI_ACCOUNT.md).
136
136
 
@@ -233,7 +233,7 @@ Microsoft currently recommends the MCP onboarding wizard:
233
233
  5. Select **Create**, then **Next**.
234
234
  6. Create or select a connection and choose **Add to agent**.
235
235
 
236
- Copilot Studio supports Streamable transport rather than the obsolete SSE transport. Its dynamic-discovery path uses the Worker's protected-resource metadata, authorization-server metadata, DCR endpoint, authorization-code exchange, and refresh-token exchange. The authorization page also permits Power Platform's global `consent.azure-apim.net` callback to hand the browser to its regional consent endpoint. See Microsoft's [existing MCP server guide](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent). Power Platform data policies and tenant administration can still block a connection independently of Machine Bridge.
236
+ Copilot Studio supports Streamable transport rather than the obsolete SSE transport. Its dynamic-discovery path uses the Worker's protected-resource metadata, authorization-server metadata, DCR endpoint, authorization-code exchange, and refresh-token exchange. The authorization page also permits Power Platform's global `consent.azure-apim.net` callback to hand the browser to its regional consent endpoint and then to Copilot Studio's `/connection/oauth/redirect` page. See Microsoft's [existing MCP server guide](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent). Power Platform data policies and tenant administration can still block a connection independently of Machine Bridge.
237
237
 
238
238
  Claude remote connectors originate from Anthropic's cloud, and Copilot Studio reaches the endpoint through Power Platform connectors. Those server-to-server paths do not require adding Claude or Microsoft browser domains to `MBM_ALLOWED_ORIGINS`; widening CORS would not solve tenant, firewall, or Power Platform policy failures.
239
239
 
package/docs/TESTING.md CHANGED
@@ -55,9 +55,9 @@ The suite includes:
55
55
  - deterministic property tests over hostile browser-protocol byte strings, canonical/custom policy combinations, argv bounds/NULs, and a real direct process proving shell metacharacters remain literal argv;
56
56
  - CLI parsing, policy profiles, and client configuration boundaries;
57
57
  - 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;
58
- - live local Worker OAuth registration, the unauthenticated `resource_metadata` challenge, protected-resource and authorization-server discovery, Streamable transport metadata, consent, URL-constructed `303` callbacks including the ChatGPT and hosted Claude redirect URIs with encoded state, PKCE, `offline_access`, form-encoded authorization-code and refresh-token exchanges, access/refresh rotation, stale refresh replay rejection, account-version refresh revocation, authorization-code replay rejection, pending-registration throttling that excludes already authorized DCR clients, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, exact per-request redirect-origin CSP with a Microsoft consent-domain regional handoff exception, accessible credential-error rendering, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure. The metadata/refresh contract is the path used by Claude DCR and Copilot Studio Dynamic discovery. The same integration runs an `editor` account against a canonical `full` daemon and proves that `server_info` and remote `project_overview` report effective `edit` authority while retaining the full daemon ceiling only in explicitly scoped fields.
58
+ - live local Worker OAuth registration, the unauthenticated `resource_metadata` challenge, protected-resource and authorization-server discovery, Streamable transport metadata, consent, URL-constructed `303` callbacks including the ChatGPT and hosted Claude redirect URIs with encoded state, PKCE, `offline_access`, form-encoded authorization-code and refresh-token exchanges, access/refresh rotation, stale refresh replay rejection, account-version refresh revocation, authorization-code replay rejection, pending-registration throttling that excludes already authorized DCR clients, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, exact per-request redirect-origin CSP with narrowly scoped Microsoft regional-consent and final Copilot Studio handoff exceptions, accessible credential-error rendering, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure. The metadata/refresh contract is the path used by Claude DCR and Copilot Studio Dynamic discovery. The same integration runs an `editor` account against a canonical `full` daemon and proves that `server_info` and remote `project_overview` report effective `edit` authority while retaining the full daemon ceiling only in explicitly scoped fields.
59
59
  - 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.
60
- - a real headless-Chrome OAuth navigation regression with three cases: `form-action 'self'` blocks the first cross-origin callback, allowing only the registered callback still blocks a second cross-origin redirect, and allowing both callback origins completes the two-hop chain while preserving `code` and `state`. Linux CI fails if Chrome is unavailable; other environments skip only this browser executable check while retaining the Worker CSP assertions.
60
+ - 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.
61
61
 
62
62
  ## Opt-in live desktop and browser validation
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "1.1.2",
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) {
@@ -87,8 +87,8 @@ function authorizationFormActionSources(value: string): string[] {
87
87
  const sources = [url.origin];
88
88
  const microsoftConsentHost = url.hostname === "consent.azure-apim.net" || url.hostname.endsWith(".consent.azure-apim.net");
89
89
  if (url.protocol === "https:" && !url.port && microsoftConsentHost) {
90
- // Power Platform redirects its global consent callback to a regional subdomain, and CSP checks every hop.
91
- sources.push("https://*.consent.azure-apim.net");
90
+ // Power Platform redirects global consent to a regional endpoint and then back to Copilot Studio; CSP checks every hop.
91
+ sources.push("https://*.consent.azure-apim.net", "https://copilotstudio.microsoft.com");
92
92
  }
93
93
  return sources;
94
94
  } catch {
@@ -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.2";
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
  }