machine-bridge-mcp 2.0.0 → 3.0.0-beta.9

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/CONTRIBUTING.md +20 -25
  3. package/README.md +21 -14
  4. package/SECURITY.md +147 -123
  5. package/browser-extension/manifest.json +3 -2
  6. package/browser-extension/service-worker.js +31 -12
  7. package/docs/ARCHITECTURE.md +27 -13
  8. package/docs/AUDIT.md +85 -2
  9. package/docs/CLIENTS.md +2 -2
  10. package/docs/ENGINEERING.md +9 -3
  11. package/docs/GETTING_STARTED.md +10 -9
  12. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  13. package/docs/LOGGING.md +12 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +33 -22
  17. package/docs/OVERVIEW.md +7 -7
  18. package/docs/PROJECT_STANDARDS.md +9 -5
  19. package/docs/RELEASING.md +171 -64
  20. package/docs/TESTING.md +19 -7
  21. package/docs/THREAT_MODEL.md +138 -67
  22. package/docs/TOOL_REFERENCE.md +2 -2
  23. package/docs/UPGRADING.md +102 -18
  24. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  25. package/package.json +36 -5
  26. package/scripts/candidate-runtime-store.mjs +38 -0
  27. package/scripts/check-plan.mjs +19 -0
  28. package/scripts/check-runner.mjs +4 -1
  29. package/scripts/commit-message-check.mjs +5 -2
  30. package/scripts/coverage-check.mjs +30 -2
  31. package/scripts/github-push.mjs +11 -0
  32. package/scripts/github-release.mjs +38 -7
  33. package/scripts/install-published-prerelease.mjs +167 -0
  34. package/scripts/local-release-acceptance.mjs +23 -12
  35. package/scripts/npm-publication-policy.mjs +16 -0
  36. package/scripts/prerelease-activation.mjs +79 -0
  37. package/scripts/privacy-check.mjs +7 -5
  38. package/scripts/promotion-digest.mjs +121 -0
  39. package/scripts/publish-npm.mjs +51 -0
  40. package/scripts/published-release.mjs +80 -0
  41. package/scripts/release-acceptance.mjs +30 -1
  42. package/scripts/release-candidate-manifest.mjs +40 -0
  43. package/scripts/release-channel.mjs +106 -0
  44. package/scripts/release-impact-check.mjs +12 -22
  45. package/scripts/release-soak.mjs +237 -0
  46. package/scripts/start-release-candidate.mjs +113 -7
  47. package/src/local/account-access.mjs +6 -0
  48. package/src/local/account-admin.mjs +34 -16
  49. package/src/local/agent-context.mjs +45 -42
  50. package/src/local/app-automation.mjs +9 -4
  51. package/src/local/authority-context.mjs +106 -0
  52. package/src/local/browser-bridge-http.mjs +4 -1
  53. package/src/local/browser-bridge.mjs +14 -9
  54. package/src/local/browser-broker-routes.mjs +4 -0
  55. package/src/local/browser-broker-server.mjs +5 -4
  56. package/src/local/browser-extension-identity.mjs +50 -0
  57. package/src/local/browser-extension-protocol.mjs +9 -4
  58. package/src/local/browser-operation-service.mjs +7 -0
  59. package/src/local/browser-pairing-http.mjs +36 -0
  60. package/src/local/browser-pairing-store.mjs +56 -47
  61. package/src/local/cli-account-admin.mjs +41 -7
  62. package/src/local/cli-activate.mjs +90 -0
  63. package/src/local/cli-approval.mjs +14 -58
  64. package/src/local/cli-local-admin.mjs +5 -15
  65. package/src/local/cli-options.mjs +11 -7
  66. package/src/local/cli-service.mjs +35 -5
  67. package/src/local/cli.mjs +112 -58
  68. package/src/local/daemon-process.mjs +6 -0
  69. package/src/local/delegated-process-sandbox.mjs +147 -0
  70. package/src/local/device-identity.mjs +157 -48
  71. package/src/local/device-root-provider.mjs +87 -0
  72. package/src/local/errors.mjs +25 -3
  73. package/src/local/git-service.mjs +16 -15
  74. package/src/local/job-runner.mjs +35 -27
  75. package/src/local/log.mjs +14 -1
  76. package/src/local/macos-trust-broker.mjs +320 -0
  77. package/src/local/managed-job-lock.mjs +18 -0
  78. package/src/local/managed-job-projection.mjs +9 -1
  79. package/src/local/managed-job-runner.mjs +13 -1
  80. package/src/local/managed-job-storage.mjs +2 -1
  81. package/src/local/managed-job-terminal.mjs +138 -0
  82. package/src/local/managed-jobs.mjs +88 -32
  83. package/src/local/operation-authorization.mjs +75 -239
  84. package/src/local/operation-risk.mjs +28 -0
  85. package/src/local/operation-state-lock.mjs +7 -86
  86. package/src/local/owner-state-lock.mjs +104 -0
  87. package/src/local/policy.mjs +19 -0
  88. package/src/local/process-execution.mjs +21 -9
  89. package/src/local/process-sessions.mjs +27 -17
  90. package/src/local/process-tracker.mjs +30 -6
  91. package/src/local/process-tree-ownership.mjs +59 -0
  92. package/src/local/process-tree.mjs +22 -18
  93. package/src/local/relay-connection.mjs +47 -35
  94. package/src/local/relay-diagnostics.mjs +65 -0
  95. package/src/local/remote-configuration.mjs +48 -0
  96. package/src/local/runtime-activation.mjs +141 -0
  97. package/src/local/runtime-diagnostics.mjs +21 -0
  98. package/src/local/runtime-relay.mjs +9 -8
  99. package/src/local/runtime-reporting.mjs +8 -3
  100. package/src/local/runtime-tool-handlers.mjs +8 -8
  101. package/src/local/runtime.mjs +73 -39
  102. package/src/local/secure-file.mjs +5 -1
  103. package/src/local/security-audit-log.mjs +204 -0
  104. package/src/local/service-convergence.mjs +17 -1
  105. package/src/local/service-ownership.mjs +17 -0
  106. package/src/local/service-restart-handoff.mjs +50 -0
  107. package/src/local/service-restart-scheduler.mjs +49 -0
  108. package/src/local/service-status.mjs +47 -0
  109. package/src/local/service.mjs +142 -43
  110. package/src/local/state-inventory.mjs +21 -3
  111. package/src/local/state.mjs +155 -17
  112. package/src/local/stdio.mjs +5 -0
  113. package/src/local/tool-executor.mjs +39 -6
  114. package/src/local/tool-result-boundary.mjs +51 -0
  115. package/src/local/tools.mjs +1 -0
  116. package/src/local/trusted-executable.mjs +53 -0
  117. package/src/local/trusted-git-executable.mjs +34 -0
  118. package/src/local/trusted-github-cli.mjs +24 -0
  119. package/src/local/windows-service.mjs +20 -0
  120. package/src/local/worker-deployment.mjs +3 -4
  121. package/src/local/worker-secret-file.mjs +2 -2
  122. package/src/local/workspace-file-service.mjs +34 -31
  123. package/src/shared/admin-auth.d.mts +2 -1
  124. package/src/shared/admin-auth.mjs +3 -3
  125. package/src/shared/device-session-auth.d.mts +20 -0
  126. package/src/shared/device-session-auth.mjs +51 -0
  127. package/src/shared/server-metadata.json +1 -1
  128. package/src/shared/tool-catalog.json +2 -2
  129. package/src/worker/account-admin.ts +23 -22
  130. package/src/worker/authority.ts +9 -2
  131. package/src/worker/daemon-auth.ts +45 -67
  132. package/src/worker/daemon-sockets.ts +12 -2
  133. package/src/worker/device-session-verifier.ts +129 -0
  134. package/src/worker/dpop.ts +151 -0
  135. package/src/worker/http.ts +47 -16
  136. package/src/worker/index.ts +53 -16
  137. package/src/worker/nonce-store.ts +1 -5
  138. package/src/worker/oauth-client-admin.ts +45 -0
  139. package/src/worker/oauth-controller.ts +55 -6
  140. package/src/worker/oauth-refresh-families.ts +21 -7
  141. package/src/worker/oauth-state.ts +6 -0
  142. package/src/worker/oauth-tokens.ts +28 -8
  143. package/src/worker/websocket-protocol.ts +8 -2
@@ -1,22 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn, spawnSync } from "node:child_process";
4
- import { mkdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync, rmSync } from "node:fs";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
8
+ import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
9
+ import { ensureOwnerOnlyDirectorySync } from "../src/local/secure-file.mjs";
10
+ import { createCandidateRuntimePrefix, pruneInactiveCandidateRuntimes } from "./candidate-runtime-store.mjs";
11
+ import { writePrereleaseActivation } from "./prerelease-activation.mjs";
7
12
  import { verifyTarball } from "./release-acceptance.mjs";
13
+ import { parseReleaseVersion } from "./release-channel.mjs";
14
+ import { validateCandidateManifest } from "./release-candidate-manifest.mjs";
8
15
 
9
16
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
17
  const candidateDirectory = join(root, ".release-candidate");
11
18
  const manifestPath = join(candidateDirectory, "manifest.json");
12
- const installPrefix = join(candidateDirectory, "runtime");
19
+ const foregroundInstallPrefix = join(candidateDirectory, "runtime");
13
20
  const npmCli = process.env.npm_execpath;
14
21
 
15
22
  if (!npmCli) fail("candidate startup must run through npm so npm_execpath is available");
16
23
 
17
24
  try {
18
25
  const manifest = readJson(manifestPath, "release candidate manifest");
19
- if (manifest.result !== "pending") throw new Error("release candidate manifest is not pending");
26
+ validateCandidateManifest(manifest);
20
27
  const tarball = join(candidateDirectory, String(manifest.filename || ""));
21
28
  verifyTarball(tarball, manifest);
22
29
 
@@ -25,8 +32,13 @@ try {
25
32
  throw new Error(`candidate startup requires npm 12 or newer; current ${npmVersion}`);
26
33
  }
27
34
 
28
- rmSync(installPrefix, { recursive: true, force: true });
29
- mkdirSync(installPrefix, { recursive: true });
35
+ const activateService = process.argv.includes("--activate-service");
36
+ const stateRoot = resolve(expandHome(argumentValue("--state-dir") || defaultStateRoot()));
37
+ const installPrefix = activateService
38
+ ? createCandidateRuntimePrefix({ stateRoot, version: manifest.package_version, shasum: manifest.shasum })
39
+ : foregroundInstallPrefix;
40
+ if (!activateService) rmSync(installPrefix, { recursive: true, force: true });
41
+ ensureOwnerOnlyDirectorySync(installPrefix);
30
42
  runNpm([
31
43
  "install",
32
44
  "--global",
@@ -53,10 +65,17 @@ try {
53
65
  }
54
66
 
55
67
  if (!allowWorkerDeploy) {
56
- throw new Error("foreground candidate startup may update the configured same-name Worker; rerun with --allow-worker-deploy to authorize that live candidate deployment");
68
+ throw new Error("candidate activation may update the configured same-name Worker; rerun with --allow-worker-deploy to authorize that live candidate deployment");
69
+ }
70
+
71
+ const forwardedArgs = process.argv.slice(2).filter((value) => ![
72
+ "--install-only", "--allow-worker-deploy", "--activate-service",
73
+ ].includes(value));
74
+ if (activateService) {
75
+ activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs });
76
+ process.exit(0);
57
77
  }
58
78
 
59
- const forwardedArgs = process.argv.slice(2).filter((value) => value !== "--install-only" && value !== "--allow-worker-deploy");
60
79
  const cli = join(installedPackage, "bin", "machine-mcp.mjs");
61
80
  console.log("Authorized live candidate deployment: startup may update the configured same-name Worker when its version or deployment hash differs.");
62
81
  console.log("Starting the exact candidate in the foreground. Leave this process running while the coding agent verifies the Worker, relay, and local runtime end to end.");
@@ -86,6 +105,93 @@ try {
86
105
  fail(error?.message || error);
87
106
  }
88
107
 
108
+ function activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs }) {
109
+ const releaseVersion = parseReleaseVersion(manifest.package_version);
110
+ const cli = join(installedPackage, "bin", "machine-mcp.mjs");
111
+ const args = ["activate", ...withoutManagedFlags(forwardedArgs), "--state-dir", stateRoot, "--json"];
112
+ const previous = currentGlobalInstallation(manifest.package_name);
113
+ console.log("Activating the exact prerelease as the persistent login daemon. Portable-root startup does not prompt; a separately provisioned Secure Enclave broker may request one user-presence operation.");
114
+ const result = spawnSync(process.execPath, [cli, ...args], {
115
+ cwd: root,
116
+ env: process.env,
117
+ encoding: "utf8",
118
+ timeout: 300_000,
119
+ windowsHide: true,
120
+ });
121
+ if (result.error) throw result.error;
122
+ if (result.status !== 0) throw new Error(`persistent candidate activation failed: ${result.stderr || result.stdout}`);
123
+ let activation;
124
+ try { activation = JSON.parse(result.stdout); } catch { throw new Error("persistent candidate activation did not return valid JSON"); }
125
+ if (
126
+ activation.ok !== true
127
+ || activation.version !== manifest.package_version
128
+ || activation.daemon?.version !== manifest.package_version
129
+ || activation.worker?.health?.version !== manifest.package_version
130
+ ) {
131
+ throw new Error("persistent candidate activation did not converge on the exact candidate version");
132
+ }
133
+
134
+ let recordPath = "";
135
+ if (releaseVersion.prerelease) {
136
+ recordPath = writePrereleaseActivation({
137
+ schema_version: 1,
138
+ package_name: manifest.package_name,
139
+ package_version: manifest.package_version,
140
+ source: "local-candidate",
141
+ shasum: manifest.shasum,
142
+ integrity: manifest.integrity,
143
+ promotion_content_sha256: manifest.promotion_content_sha256,
144
+ activated_at: new Date().toISOString(),
145
+ workspace_hash: workspaceHash(activation.workspace),
146
+ runtime_entry: cli,
147
+ ...(previous ? { previous } : {}),
148
+ }, stateRoot);
149
+ }
150
+ const removedRuntimes = pruneInactiveCandidateRuntimes({ stateRoot, activePrefix: installPrefix });
151
+ console.log(`Persistent release candidate activated: ${manifest.package_version}`);
152
+ if (recordPath) console.log(`Activation record: ${recordPath}`);
153
+ if (removedRuntimes.length) console.log(`Removed ${removedRuntimes.length} inactive candidate runtime(s).`);
154
+ console.log("The Worker and login daemon now run the exact candidate. The terminal may close; the coding agent should verify the live deployment through Machine Bridge.");
155
+ if (previous?.version) console.log(`Rollback baseline retained: globally installed ${previous.version}.`);
156
+ }
157
+
158
+ function currentGlobalInstallation(packageName) {
159
+ try {
160
+ const globalRoot = runNpm(["root", "--global"], root).stdout.trim();
161
+ const packageRoot = join(globalRoot, packageName);
162
+ const packagePath = join(packageRoot, "package.json");
163
+ if (!existsSync(packagePath)) return null;
164
+ const pkg = readJson(packagePath, "globally installed package");
165
+ return { version: String(pkg.version || ""), entry: join(packageRoot, "bin", "machine-mcp.mjs") };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function withoutManagedFlags(args) {
172
+ const out = [];
173
+ for (let index = 0; index < args.length; index += 1) {
174
+ const value = args[index];
175
+ if (value === "--json" || value.startsWith("--json=")) continue;
176
+ if (value === "--state-dir") { index += 1; continue; }
177
+ if (value.startsWith("--state-dir=")) continue;
178
+ out.push(value);
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function argumentValue(name) {
184
+ const exact = process.argv.find((value) => value.startsWith(`${name}=`));
185
+ if (exact) return exact.slice(name.length + 1);
186
+ const index = process.argv.indexOf(name);
187
+ return index >= 0 ? process.argv[index + 1] : "";
188
+ }
189
+
190
+ function workspaceHash(workspace) {
191
+ const value = process.platform === "win32" ? String(workspace).toLowerCase() : String(workspace);
192
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
193
+ }
194
+
89
195
  function runNpm(args, cwd) {
90
196
  const result = spawnSync(process.execPath, [npmCli, ...args], {
91
197
  cwd,
@@ -1,5 +1,6 @@
1
1
  import accessContract from "../shared/access-contract.json" with { type: "json" };
2
2
  import { BridgeError } from "./errors.mjs";
3
+ import { buildAuthorityContext } from "./authority-context.mjs";
3
4
  import { policyProfile, toolNamesForPolicy, assertToolAllowed } from "./policy.mjs";
4
5
 
5
6
  export const ACCOUNT_ACCESS_REVISION = Number(accessContract.revision);
@@ -32,6 +33,11 @@ export class AccountAccessGate {
32
33
  return normalized;
33
34
  }
34
35
 
36
+ authority(authorization, daemonPolicy, origin = "relay") {
37
+ const role = origin === "relay" ? normalizeAccountRole(authorization?.role) : "owner";
38
+ return buildAuthorityContext({ authorization: { ...authorization, role }, daemonPolicy, origin });
39
+ }
40
+
35
41
  names(role) {
36
42
  return accountRoleToolNames(role);
37
43
  }
@@ -1,6 +1,7 @@
1
- import { createHash, createHmac, randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { ADMIN_AUTH_SCHEME, adminAuthTranscript } from "../shared/admin-auth.mjs";
3
3
  import { ACCOUNT_ROLES, normalizeAccountRole } from "./account-access.mjs";
4
+ import { encodeDeviceSessionCertificate, signWithDeviceSessionIdentity, validateDeviceSessionIdentity } from "./device-identity.mjs";
4
5
  import { BridgeError } from "./errors.mjs";
5
6
 
6
7
  const REQUEST_TIMEOUT_MS = 15_000;
@@ -10,15 +11,18 @@ export function generateAccountPassword() {
10
11
  }
11
12
 
12
13
  export class AccountAdminClient {
13
- constructor({ workerUrl, adminSecret, fetchImpl = fetch }) {
14
+ constructor({ workerUrl, sessionIdentity, fetchImpl = fetch }) {
14
15
  this.workerUrl = normalizeWorkerUrl(workerUrl);
15
- if (typeof adminSecret !== "string" || adminSecret.length < 24) throw new BridgeError("invalid_request", "account administration secret is missing");
16
- this.adminSecret = adminSecret;
16
+ this.sessionIdentity = validateDeviceSessionIdentity(sessionIdentity);
17
17
  this.fetchImpl = fetchImpl;
18
18
  }
19
19
 
20
- list() {
21
- return this.request("GET", "/admin/accounts");
20
+ list() { return this.request("GET", "/admin/accounts"); }
21
+ listClients() { return this.request("GET", "/admin/clients"); }
22
+
23
+ removeClient({ clientId }) {
24
+ if (!/^mcp_client_[A-Za-z0-9_-]{43}$/.test(String(clientId || ""))) throw new BridgeError("invalid_request", "OAuth client id is invalid");
25
+ return this.request("DELETE", "/admin/clients", { client_id: clientId });
22
26
  }
23
27
 
24
28
  create({ name, role, password, displayName = "" }) {
@@ -61,7 +65,7 @@ export class AccountAdminClient {
61
65
  async request(method, pathname, body) {
62
66
  const serializedBody = body === undefined ? "" : JSON.stringify(body);
63
67
  const headers = accountAdminRequestHeaders({
64
- secret: this.adminSecret,
68
+ sessionIdentity: this.sessionIdentity,
65
69
  origin: this.workerUrl,
66
70
  method,
67
71
  pathname,
@@ -89,25 +93,39 @@ export class AccountAdminClient {
89
93
  }
90
94
  }
91
95
 
92
-
93
- export function accountAdminRequestHeaders({ secret, origin, method, pathname, body = "", now = Date.now(), nonce = randomBytes(24).toString("base64url") }) {
94
- if (typeof secret !== "string" || secret.length < 24) throw new BridgeError("invalid_request", "account administration secret is missing");
96
+ export function accountAdminRequestHeaders({
97
+ sessionIdentity,
98
+ origin,
99
+ method,
100
+ pathname,
101
+ body = "",
102
+ now = Date.now(),
103
+ nonce = randomBytes(24).toString("base64url"),
104
+ }) {
105
+ validateDeviceSessionIdentity(sessionIdentity, now);
95
106
  const issuedAt = Math.floor(Number(now) / 1000);
96
107
  const bodyHash = createHash("sha256").update(String(body)).digest("hex");
97
- const transcript = adminAuthTranscript({ origin, method: String(method).toUpperCase(), pathname, bodyHash, issuedAt, nonce });
98
- const signature = createHmac("sha256", secret).update(transcript).digest("base64url");
108
+ const transcript = adminAuthTranscript({
109
+ origin,
110
+ method: String(method).toUpperCase(),
111
+ pathname,
112
+ bodyHash,
113
+ keyId: sessionIdentity.keyId,
114
+ issuedAt,
115
+ nonce,
116
+ });
99
117
  return {
100
118
  "X-Bridge-Admin-Scheme": ADMIN_AUTH_SCHEME,
101
119
  "X-Bridge-Admin-Time": String(issuedAt),
102
120
  "X-Bridge-Admin-Nonce": nonce,
103
121
  "X-Bridge-Admin-Body-SHA256": bodyHash,
104
- "X-Bridge-Admin-Signature": signature,
122
+ "X-Bridge-Admin-Key": sessionIdentity.keyId,
123
+ "X-Bridge-Admin-Signature": signWithDeviceSessionIdentity(sessionIdentity, transcript),
124
+ "X-Bridge-Device-Certificate": encodeDeviceSessionCertificate(sessionIdentity),
105
125
  };
106
126
  }
107
127
 
108
- export function accountRoleNames() {
109
- return Object.keys(ACCOUNT_ROLES);
110
- }
128
+ export function accountRoleNames() { return Object.keys(ACCOUNT_ROLES); }
111
129
 
112
130
  function normalizeWorkerUrl(value) {
113
131
  const url = new URL(String(value || ""));
@@ -29,9 +29,10 @@ const MAX_INSTRUCTION_FILE_BYTES = 512 * 1024;
29
29
  const MAX_INSTRUCTION_FILES = 64;
30
30
 
31
31
  export class AgentContextManager {
32
- constructor({ workspace, policy, displayPath, resolveExistingPath, throwIfCancelled = () => {}, home = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "" }) {
32
+ constructor({ workspace, policy, policyForContext = null, displayPath, resolveExistingPath, throwIfCancelled = () => {}, home = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "" }) {
33
33
  this.workspace = resolve(workspace);
34
34
  this.policy = policy || {};
35
+ this.policyForContext = typeof policyForContext === "function" ? policyForContext : () => this.policy;
35
36
  this.displayPath = displayPath;
36
37
  this.resolveExistingPath = resolveExistingPath;
37
38
  this.throwIfCancelled = throwIfCancelled;
@@ -44,23 +45,23 @@ export class AgentContextManager {
44
45
  const includeContent = args.include_instruction_content !== false;
45
46
  const skillLimit = clampInteger(args.max_skills, 100, 1, MAX_SKILL_RESULTS);
46
47
  const discoveredSkills = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
47
- const contextSkills = contextSkillSummaries(discoveredSkills.skills, this.displayPath, skillLimit, MAX_CONTEXT_SKILL_SUMMARY_CHARS);
48
+ const contextSkills = contextSkillSummaries(discoveredSkills.skills, (value) => this.displayPath(value, context), skillLimit, MAX_CONTEXT_SKILL_SUMMARY_CHARS);
48
49
  const result = {
49
- target: this.displayPath(state.target),
50
- scope_root: this.displayPath(state.scopeRoot),
50
+ target: this.displayPath(state.target, context),
51
+ scope_root: this.displayPath(state.scopeRoot, context),
51
52
  precedence: "built-in defaults, automatic project facts, user-global guidance, then scope root to target directory; explicit files loaded later have higher precedence",
52
- config_files: state.configFiles.map((file) => this.displayPath(file)),
53
+ config_files: state.configFiles.map((file) => this.displayPath(file, context)),
53
54
  builtin_instructions: publicVirtualInstruction(state.builtinInstructions, includeContent),
54
55
  automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, includeContent),
55
56
  model_instructions_file: state.modelInstructions ? {
56
- path: this.displayPath(state.modelInstructions.path),
57
+ path: this.displayPath(state.modelInstructions.path, context),
57
58
  bytes: state.modelInstructions.bytes,
58
59
  sha256: state.modelInstructions.sha256,
59
60
  ...(includeContent ? { content: state.modelInstructions.content } : {}),
60
61
  } : null,
61
62
  instruction_files: state.instructions.map((item) => ({
62
63
  scope: item.scope,
63
- path: this.displayPath(item.path),
64
+ path: this.displayPath(item.path, context),
64
65
  bytes: item.bytes,
65
66
  sha256: item.sha256,
66
67
  precedence: item.precedence,
@@ -68,9 +69,9 @@ export class AgentContextManager {
68
69
  })),
69
70
  skills: contextSkills.skills,
70
71
  skills_truncated: discoveredSkills.truncated || contextSkills.truncated,
71
- skill_warnings: publicSkillWarnings(discoveredSkills.warnings, this.displayPath),
72
+ skill_warnings: publicSkillWarnings(discoveredSkills.warnings, (value) => this.displayPath(value, context)),
72
73
  instructions_truncated: state.instructionsTruncated,
73
- commands: publicCommands(state.commands, this.displayPath),
74
+ commands: publicCommands(state.commands, (value) => this.displayPath(value, context)),
74
75
  guidance: [
75
76
  "Apply built-in instructions and automatic project context as lower-precedence defaults; explicit global/project instruction files loaded later take precedence.",
76
77
  "Treat instruction_files as authoritative workspace guidance in the returned precedence order.",
@@ -78,7 +79,7 @@ export class AgentContextManager {
78
79
  "Prefer run_local_command for registered repeatable commands. Use run_process or exec_command only when no registered command fits and policy permits it.",
79
80
  ],
80
81
  };
81
- if (includeContent) result.effective_instructions = renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath);
82
+ if (includeContent) result.effective_instructions = renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context));
82
83
  return result;
83
84
  }
84
85
 
@@ -86,11 +87,11 @@ export class AgentContextManager {
86
87
  const state = await this.discoverState(args.path || ".", context);
87
88
  const fingerprint = capabilityFingerprint(state, []);
88
89
  return {
89
- target: this.displayPath(state.target),
90
- instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
90
+ target: this.displayPath(state.target, context),
91
+ instructions: renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context)),
91
92
  builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
92
93
  automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
93
- model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
94
+ model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path, context) : null,
94
95
  capability_refresh: {
95
96
  strategy: "resolve_task_capabilities-rescans-on-every-call",
96
97
  instruction_and_command_fingerprint: fingerprint,
@@ -125,7 +126,7 @@ export class AgentContextManager {
125
126
  let selectedSkill = null;
126
127
  if (selected && args.include_selected_skill !== false) {
127
128
  const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
128
- selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
129
+ selectedSkill = { ...publicSkill(selected, (value) => this.displayPath(value, context)), instructions: content.text };
129
130
  }
130
131
  const recommendedTools = recommendTools(task, {
131
132
  commandsAvailable: state.commands.size > 0,
@@ -135,20 +136,20 @@ export class AgentContextManager {
135
136
  const refresh = capabilityFingerprint(state, discovered.skills);
136
137
  return {
137
138
  task,
138
- target: this.displayPath(state.target),
139
- effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
139
+ target: this.displayPath(state.target, context),
140
+ effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context)),
140
141
  builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
141
142
  automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
142
- model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
143
- instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
143
+ model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path, context) : null,
144
+ instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path, context), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
144
145
  instructions_truncated: state.instructionsTruncated,
145
146
  refresh: { strategy: "rescan-on-every-call", fingerprint: refresh, generated_at: new Date().toISOString() },
146
147
  selected_skill: selectedSkill,
147
- skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, this.displayPath), score })),
148
- command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), this.displayPath)[0], score })),
148
+ skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, (value) => this.displayPath(value, context)), score })),
149
+ command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), (value) => this.displayPath(value, context))[0], score })),
149
150
  recommended_tools: recommendedTools,
150
151
  host_semantics: "Machine Bridge can discover, rank, and load capabilities automatically. The MCP host remains responsible for deciding whether to call the recommended tools.",
151
- warnings: publicSkillWarnings(discovered.warnings, this.displayPath),
152
+ warnings: publicSkillWarnings(discovered.warnings, (value) => this.displayPath(value, context)),
152
153
  truncated: discovered.truncated,
153
154
  };
154
155
  }
@@ -171,11 +172,11 @@ export class AgentContextManager {
171
172
  maxResults: clampInteger(args.max_results, 100, 1, MAX_SKILL_RESULTS),
172
173
  }, context);
173
174
  return {
174
- target: this.displayPath(state.target),
175
- scope_root: this.displayPath(state.scopeRoot),
176
- skill_roots: state.skillRoots.map((root) => this.displayPath(root)),
177
- skills: result.skills.map((skill) => publicSkill(skill, this.displayPath)),
178
- warnings: publicSkillWarnings(result.warnings, this.displayPath),
175
+ target: this.displayPath(state.target, context),
176
+ scope_root: this.displayPath(state.scopeRoot, context),
177
+ skill_roots: state.skillRoots.map((root) => this.displayPath(root, context)),
178
+ skills: result.skills.map((skill) => publicSkill(skill, (value) => this.displayPath(value, context))),
179
+ warnings: publicSkillWarnings(result.warnings, (value) => this.displayPath(value, context)),
179
180
  truncated: result.truncated,
180
181
  };
181
182
  }
@@ -184,14 +185,14 @@ export class AgentContextManager {
184
185
  const requested = requiredString(args.skill, "skill");
185
186
  const state = await this.discoverState(args.path || ".", context);
186
187
  const result = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
187
- const matches = result.skills.filter((skill) => skill.id === requested || skill.name === requested || this.displayPath(skill.entrypoint) === requested);
188
+ const matches = result.skills.filter((skill) => skill.id === requested || skill.name === requested || this.displayPath(skill.entrypoint, context) === requested);
188
189
  if (!matches.length) throw new Error(`local skill not found: ${requested}`);
189
190
  if (matches.length > 1) throw new Error(`local skill name is ambiguous; use its id: ${requested}`);
190
191
  const skill = matches[0];
191
192
  const content = await readRegularUtf8(skill.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
192
193
  const inventory = await listSkillFiles(skill.directory, clampInteger(args.max_files, 200, 1, MAX_SKILL_FILES), context, this.throwIfCancelled);
193
194
  return {
194
- skill: publicSkill(skill, this.displayPath),
195
+ skill: publicSkill(skill, (value) => this.displayPath(value, context)),
195
196
  instructions: content.text,
196
197
  files: inventory.files,
197
198
  files_truncated: inventory.truncated,
@@ -202,9 +203,9 @@ export class AgentContextManager {
202
203
  async listLocalCommands(args = {}, context = {}) {
203
204
  const state = await this.discoverState(args.path || ".", context);
204
205
  return {
205
- target: this.displayPath(state.target),
206
- scope_root: this.displayPath(state.scopeRoot),
207
- commands: publicCommands(state.commands, this.displayPath),
206
+ target: this.displayPath(state.target, context),
207
+ scope_root: this.displayPath(state.scopeRoot, context),
208
+ commands: publicCommands(state.commands, (value) => this.displayPath(value, context)),
208
209
  };
209
210
  }
210
211
 
@@ -227,14 +228,15 @@ export class AgentContextManager {
227
228
 
228
229
  async discoverState(inputPath, context = {}) {
229
230
  this.throwIfCancelled(context);
231
+ const effectivePolicy = this.policyForContext(context);
230
232
  this.workspace = await realpath(this.workspace);
231
- const target = await realpath(await this.resolveExistingPath(inputPath));
233
+ const target = await realpath(await this.resolveExistingPath(inputPath, context));
232
234
  const targetInfo = await stat(target);
233
235
  const targetDir = targetInfo.isDirectory() ? target : dirname(target);
234
236
  const scopeRoot = await findScopeRoot({
235
237
  targetDir,
236
238
  workspace: this.workspace,
237
- unrestricted: this.policy.unrestrictedPaths === true,
239
+ unrestricted: effectivePolicy.unrestrictedPaths === true,
238
240
  });
239
241
  const directories = directoriesBetween(scopeRoot, targetDir);
240
242
  const state = {
@@ -243,7 +245,7 @@ export class AgentContextManager {
243
245
  scopeRoot,
244
246
  instructionFiles: [...DEFAULT_INSTRUCTION_FILES],
245
247
  instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
246
- skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, this.policy.unrestrictedPaths === true),
248
+ skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, effectivePolicy.unrestrictedPaths === true),
247
249
  commands: new Map(),
248
250
  builtinInstructionsEnabled: true,
249
251
  automaticProjectContextEnabled: true,
@@ -264,7 +266,7 @@ export class AgentContextManager {
264
266
  const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
265
267
  const config = await readOptionalConfig(globalConfig, this.home, false);
266
268
  if (config) {
267
- if (this.policy.unrestrictedPaths === true) this.applyConfig(state, config, globalConfig, this.home, { global: true });
269
+ if (effectivePolicy.unrestrictedPaths === true) this.applyConfig(state, config, globalConfig, this.home, { global: true, unrestricted: true });
268
270
  else {
269
271
  state.configFiles.push(globalConfig);
270
272
  if (config.builtinInstructions !== null) state.builtinInstructionsEnabled = config.builtinInstructions;
@@ -284,20 +286,20 @@ export class AgentContextManager {
284
286
 
285
287
  if (this.home) {
286
288
  if (state.modelInstructionsFile) await this.collectModelInstructions(state, context);
287
- if (this.policy.unrestrictedPaths === true && this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
289
+ if (effectivePolicy.unrestrictedPaths === true && this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
288
290
  }
289
291
 
290
292
  for (const directory of directories) {
291
293
  this.throwIfCancelled(context);
292
294
  const configPath = join(directory, CONFIG_RELATIVE_PATH);
293
295
  const config = await readOptionalConfig(configPath, directory, true);
294
- if (config) this.applyConfig(state, config, configPath, directory, { global: false });
296
+ if (config) this.applyConfig(state, config, configPath, directory, { global: false, unrestricted: effectivePolicy.unrestrictedPaths === true });
295
297
  await this.collectDirectoryInstruction(state, directory, context, "project");
296
298
  }
297
299
  return state;
298
300
  }
299
301
 
300
- applyConfig(state, config, configPath, baseDir, { global = false } = {}) {
302
+ applyConfig(state, config, configPath, baseDir, { global = false, unrestricted = false } = {}) {
301
303
  state.configFiles.push(configPath);
302
304
  if (config.builtinInstructions !== null) {
303
305
  if (!global) throw new Error(`builtin_instructions is only allowed in the global agent config: ${configPath}`);
@@ -317,14 +319,14 @@ export class AgentContextManager {
317
319
  if (state.instructionBytes >= state.instructionMaxBytes) state.instructionsTruncated = true;
318
320
  }
319
321
  if (config.skillRoots) {
320
- state.skillRoots = config.skillRoots.map((value) => resolveConfiguredPath(value, baseDir, this.home, this.workspace, this.policy.unrestrictedPaths === true));
322
+ state.skillRoots = config.skillRoots.map((value) => resolveConfiguredPath(value, baseDir, this.home, this.workspace, unrestricted));
321
323
  }
322
324
  for (const [name, definition] of config.commands) {
323
325
  if (definition === null) {
324
326
  state.commands.delete(name);
325
327
  continue;
326
328
  }
327
- const cwd = resolveConfiguredPath(definition.cwd, baseDir, this.home, this.workspace, this.policy.unrestrictedPaths === true);
329
+ const cwd = resolveConfiguredPath(definition.cwd, baseDir, this.home, this.workspace, unrestricted);
328
330
  state.commands.set(name, {
329
331
  name,
330
332
  description: definition.description,
@@ -379,13 +381,14 @@ export class AgentContextManager {
379
381
  }
380
382
 
381
383
  async discoverSkills(state, options = {}, context = {}) {
384
+ const effectivePolicy = this.policyForContext(context);
382
385
  return discoverLocalSkills({
383
386
  skillRoots: state.skillRoots,
384
387
  query: String(options.query || ""),
385
388
  maxResults: clampInteger(options.maxResults, 100, 1, MAX_SKILL_RESULTS),
386
389
  workspace: this.workspace,
387
- unrestricted: this.policy.unrestrictedPaths === true,
388
- displayPath: this.displayPath,
390
+ unrestricted: effectivePolicy.unrestrictedPaths === true,
391
+ displayPath: (value) => this.displayPath(value, context),
389
392
  context,
390
393
  throwIfCancelled: this.throwIfCancelled,
391
394
  });
@@ -102,9 +102,9 @@ export class AppAutomationManager {
102
102
  if (target) argv.push(target);
103
103
  command = { cmd: "open", argv };
104
104
  } else if (this.platform === "win32") {
105
- const escaped = resolvedApplication.replace(/'/g, "''");
106
- const targetFragment = target ? ` -ArgumentList '${target.replace(/'/g, "''")}'` : "";
107
- command = { cmd: "powershell.exe", argv: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath '${escaped}'${targetFragment}`] };
105
+ const fileLiteral = powershellSingleQuotedLiteral(resolvedApplication);
106
+ const targetFragment = target ? ` -ArgumentList ${powershellSingleQuotedLiteral(target)}` : "";
107
+ command = { cmd: "powershell.exe", argv: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath ${fileLiteral}${targetFragment}`] };
108
108
  } else if (resolvedApplication.toLowerCase().endsWith(".desktop")) {
109
109
  command = { cmd: "gio", argv: ["launch", resolvedApplication, ...(target ? [target] : [])] };
110
110
  } else {
@@ -113,7 +113,8 @@ export class AppAutomationManager {
113
113
  : { cmd: resolvedApplication, argv: [] };
114
114
  }
115
115
  const result = await this.runProcess(command.cmd, command.argv, clampInt(args.timeout_seconds, 30, 1, 120) * 1000, false, 256 * 1024, context, undefined, null);
116
- return { application, resolved_application: resolvedApplication, target, platform: this.platform, ...result };
116
+ const publicResolvedApplication = /[\\/]/.test(resolvedApplication) ? this.displayPath(resolvedApplication) : resolvedApplication;
117
+ return { application, resolved_application: publicResolvedApplication, target, platform: this.platform, ...result };
117
118
  }
118
119
 
119
120
  async inspectApplication(args = {}, context = {}) {
@@ -417,3 +418,7 @@ function main() {
417
418
  }
418
419
  (() => { try { return JSON.stringify(main()); } catch (error) { return JSON.stringify({ error: String(error.message || error) }); } })()
419
420
  `;
421
+
422
+ export function powershellSingleQuotedLiteral(value) {
423
+ return `'${String(value).replaceAll("'", "''")}'`;
424
+ }