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

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 (147) hide show
  1. package/CHANGELOG.md +120 -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 +28 -13
  8. package/docs/AUDIT.md +101 -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 +14 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +34 -23
  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 +21 -9
  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 +37 -6
  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 +87 -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 +119 -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 +18 -44
  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-resource-service.mjs +77 -0
  101. package/src/local/runtime-tool-handlers.mjs +8 -8
  102. package/src/local/runtime.mjs +85 -95
  103. package/src/local/secure-file.mjs +5 -1
  104. package/src/local/security-audit-log.mjs +204 -0
  105. package/src/local/service-convergence.mjs +17 -1
  106. package/src/local/service-ownership.mjs +17 -0
  107. package/src/local/service-restart-handoff.mjs +50 -0
  108. package/src/local/service-restart-scheduler.mjs +49 -0
  109. package/src/local/service-status.mjs +47 -0
  110. package/src/local/service.mjs +142 -43
  111. package/src/local/state-inventory.mjs +21 -3
  112. package/src/local/state.mjs +155 -17
  113. package/src/local/stdio.mjs +5 -0
  114. package/src/local/tool-executor.mjs +39 -6
  115. package/src/local/tool-result-boundary.mjs +51 -0
  116. package/src/local/tools.mjs +1 -0
  117. package/src/local/trusted-executable.mjs +53 -0
  118. package/src/local/trusted-git-executable.mjs +34 -0
  119. package/src/local/trusted-github-cli.mjs +24 -0
  120. package/src/local/windows-service.mjs +20 -0
  121. package/src/local/worker-deployment.mjs +3 -4
  122. package/src/local/worker-secret-file.mjs +2 -2
  123. package/src/local/workspace-file-service.mjs +34 -31
  124. package/src/shared/admin-auth.d.mts +2 -1
  125. package/src/shared/admin-auth.mjs +3 -3
  126. package/src/shared/device-session-auth.d.mts +20 -0
  127. package/src/shared/device-session-auth.mjs +51 -0
  128. package/src/shared/log-redaction.d.mts +9 -0
  129. package/src/shared/log-redaction.mjs +51 -0
  130. package/src/shared/server-metadata.json +1 -1
  131. package/src/shared/tool-catalog.json +2 -2
  132. package/src/worker/account-admin.ts +23 -22
  133. package/src/worker/authority.ts +23 -4
  134. package/src/worker/daemon-auth.ts +45 -67
  135. package/src/worker/daemon-sockets.ts +12 -2
  136. package/src/worker/device-session-verifier.ts +129 -0
  137. package/src/worker/dpop.ts +151 -0
  138. package/src/worker/http.ts +47 -16
  139. package/src/worker/index.ts +53 -16
  140. package/src/worker/nonce-store.ts +1 -5
  141. package/src/worker/oauth-client-admin.ts +45 -0
  142. package/src/worker/oauth-controller.ts +55 -6
  143. package/src/worker/oauth-refresh-families.ts +21 -7
  144. package/src/worker/oauth-state.ts +6 -0
  145. package/src/worker/oauth-tokens.ts +28 -8
  146. package/src/worker/observability.ts +4 -2
  147. package/src/worker/websocket-protocol.ts +8 -2
@@ -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
+ }
@@ -0,0 +1,106 @@
1
+ import { BridgeError } from "./errors.mjs";
2
+ import { intersectPolicies, normalizePolicy, policyProfile } from "./policy.mjs";
3
+
4
+ const ACCOUNT_ID = /^acct_[A-Za-z0-9_-]{20,96}$/;
5
+ const CLIENT_ID = /^mcp_client_[A-Za-z0-9_-]{43}$/;
6
+ const FAMILY_ID = /^mcp_family_[A-Za-z0-9_-]{43}$/;
7
+
8
+ export function buildAuthorityContext({ authorization = {}, daemonPolicy, origin = "local" } = {}) {
9
+ if (origin !== "relay") {
10
+ const policy = normalizePolicy(daemonPolicy || {});
11
+ return Object.freeze({
12
+ origin: "local",
13
+ principal: Object.freeze({ kind: "local", role: "owner" }),
14
+ daemonPolicy: policy,
15
+ accountPolicy: policy,
16
+ effectivePolicy: policy,
17
+ owner: true,
18
+ });
19
+ }
20
+
21
+ const accountId = String(authorization.account_id || "");
22
+ const clientId = String(authorization.client_id || "");
23
+ const familyId = String(authorization.family_id || "");
24
+ const role = String(authorization.role || "").trim().toLowerCase();
25
+ const accountVersion = Number(authorization.account_version);
26
+ if (!ACCOUNT_ID.test(accountId) || !CLIENT_ID.test(clientId) || !FAMILY_ID.test(familyId) || !Number.isSafeInteger(accountVersion) || accountVersion <= 0) {
27
+ throw new BridgeError("authorization_denied", "relay operation is missing authenticated principal identity");
28
+ }
29
+ const accountPolicy = policyProfile(roleProfile(role), "explicit");
30
+ const normalizedDaemonPolicy = normalizePolicy(daemonPolicy || {});
31
+ const effectivePolicy = intersectPolicies(normalizedDaemonPolicy, accountPolicy, { origin: "effective" });
32
+ const principal = Object.freeze({
33
+ kind: "account",
34
+ accountId,
35
+ accountVersion,
36
+ clientId,
37
+ familyId,
38
+ role,
39
+ });
40
+ return Object.freeze({
41
+ origin: "relay",
42
+ principal,
43
+ daemonPolicy: normalizedDaemonPolicy,
44
+ accountPolicy,
45
+ effectivePolicy,
46
+ owner: role === "owner",
47
+ });
48
+ }
49
+
50
+ export function policyForContext(context, fallbackPolicy) {
51
+ return normalizePolicy(context?.authority?.effectivePolicy || fallbackPolicy || {});
52
+ }
53
+
54
+ export function principalForContext(context) {
55
+ const principal = context?.authority?.principal;
56
+ if (principal?.kind === "account") return principal;
57
+ return Object.freeze({ kind: "local", role: "owner" });
58
+ }
59
+
60
+ export function principalBinding(context) {
61
+ const principal = principalForContext(context);
62
+ if (principal.kind !== "account") return { owner_kind: "local" };
63
+ return {
64
+ owner_kind: "account",
65
+ owner_account_id: principal.accountId,
66
+ owner_account_version: principal.accountVersion,
67
+ owner_client_id: principal.clientId,
68
+ owner_family_id: principal.familyId,
69
+ owner_role: principal.role,
70
+ };
71
+ }
72
+
73
+ export function assertOwnedByContext(record, context, label = "object") {
74
+ const principal = principalForContext(context);
75
+ if (principal.kind !== "account" || principal.role === "owner") return;
76
+ if (
77
+ record?.owner_kind !== "account"
78
+ || record.owner_account_id !== principal.accountId
79
+ || record.owner_account_version !== principal.accountVersion
80
+ || record.owner_client_id !== principal.clientId
81
+ || record.owner_family_id !== principal.familyId
82
+ ) {
83
+ throw new BridgeError("authorization_denied", `${label} belongs to another account or an obsolete account session`);
84
+ }
85
+ }
86
+
87
+ export function visibleToContext(record, context) {
88
+ try {
89
+ assertOwnedByContext(record, context);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function roleProfile(role) {
97
+ const mapping = {
98
+ reviewer: "review",
99
+ editor: "edit",
100
+ operator: "agent",
101
+ owner: "full",
102
+ };
103
+ const profile = mapping[role];
104
+ if (!profile) throw new BridgeError("authorization_denied", `unknown account role: ${role}`);
105
+ return profile;
106
+ }
@@ -1,5 +1,6 @@
1
1
  import { EXPECTED_EXTENSION_VERSION } from "./browser-extension-protocol.mjs";
2
- import { isAllowedLoopbackHost, pairingHtml, securityHeaders, sendJson } from "./browser-pairing-store.mjs";
2
+ import { EXPECTED_EXTENSION_ID } from "./browser-extension-identity.mjs";
3
+ import { isAllowedLoopbackHost, pairingHtml, securityHeaders, sendJson } from "./browser-pairing-http.mjs";
3
4
 
4
5
  export function handleBrowserBridgeHttp(request, response, {
5
6
  port,
@@ -26,6 +27,8 @@ export function handleBrowserBridgeHttp(request, response, {
26
27
  connected: extensionConnected(),
27
28
  broker: "machine-bridge-browser",
28
29
  expected_extension_version: EXPECTED_EXTENSION_VERSION,
30
+ expected_extension_id: EXPECTED_EXTENSION_ID,
31
+ extension_id: extension?.extension_id || "",
29
32
  extension_protocol: extension?.protocol || null,
30
33
  extension_version: extension?.version || "",
31
34
  extension_capabilities: extension?.capabilities || [],
@@ -13,6 +13,7 @@ import { BrowserRequestRegistry } from "./browser-request-registry.mjs";
13
13
  import { handleBrowserBridgeHttp } from "./browser-bridge-http.mjs";
14
14
  import { BrowserBrokerRoutes } from "./browser-broker-routes.mjs";
15
15
  import { startBrowserBrokerServer } from "./browser-broker-server.mjs";
16
+ import { EXPECTED_EXTENSION_ID } from "./browser-extension-identity.mjs";
16
17
 
17
18
  const MAX_PORT_ATTEMPTS = 10;
18
19
  const MAX_PENDING = 32;
@@ -49,13 +50,11 @@ export class BrowserBridgeManager {
49
50
  extensionStatusInfo: () => this.extensionStatusInfo(),
50
51
  extensionReloadRequired: () => this.extensionReloadRequired(),
51
52
  });
52
- // Public aliases remain for diagnostics and compatibility with existing behavioral tests.
53
- this.runtimeClients = this.brokerRoutes.clients;
54
- this.proxyRoutes = this.brokerRoutes.routes;
55
53
  this.startPromise = null;
56
54
  this.startGeneration = 0;
57
55
  this.port = 0;
58
- this.token = "";
56
+ this.extensionToken = "";
57
+ this.runtimeToken = "";
59
58
  this.extensionPath = resolve(packageRoot, "browser-extension");
60
59
  this.operationService = new BrowserOperationService({
61
60
  authorizeTool: (tool) => this.authorizeTool(tool),
@@ -67,15 +66,19 @@ export class BrowserBridgeManager {
67
66
  extensionConnected: this.extensionConnected(),
68
67
  extensionInfo: this.extensionStatusInfo(),
69
68
  extensionReloadRequired: this.extensionReloadRequired(),
69
+ ...this.brokerRoutes.snapshot(),
70
70
  }),
71
71
  extensionPath: this.extensionPath,
72
72
  expectedExtensionVersion: EXPECTED_EXTENSION_VERSION,
73
+ expectedExtensionId: EXPECTED_EXTENSION_ID,
73
74
  runProcess: (...args) => this.runProcess(...args),
74
75
  readResourceText: (name) => this.readResourceText(name),
75
76
  readResourceBinary: (name) => this.readResourceBinary(name),
76
77
  });
77
78
  }
78
79
 
80
+ brokerDiagnostics() { return this.brokerRoutes.snapshot(); }
81
+
79
82
  status(context = {}) { return this.operationService.status(context); }
80
83
 
81
84
  pair(args = {}, context = {}) { return this.operationService.pair(args, context); }
@@ -133,14 +136,15 @@ export class BrowserBridgeManager {
133
136
  try {
134
137
  const pairing = await loadOrCreatePairing(this.stateRoot);
135
138
  this.assertStartCurrent(generation);
136
- this.token = pairing.token;
139
+ this.extensionToken = pairing.extensionToken;
140
+ this.runtimeToken = pairing.runtimeToken;
137
141
  for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
138
142
  const port = pairing.port + offset;
139
143
  try {
140
144
  await this.listen(port);
141
145
  this.assertStartCurrent(generation);
142
146
  if (port !== pairing.port && this.stateRoot) {
143
- await savePairing(this.stateRoot, { token: this.token, port });
147
+ await savePairing(this.stateRoot, { schemaVersion: pairing.schemaVersion, extensionToken: this.extensionToken, runtimeToken: this.runtimeToken, port });
144
148
  this.assertStartCurrent(generation);
145
149
  }
146
150
  return;
@@ -176,7 +180,8 @@ export class BrowserBridgeManager {
176
180
  this.port = port;
177
181
  const { server, wss } = await startBrowserBrokerServer({
178
182
  port,
179
- token: this.token,
183
+ extensionToken: this.extensionToken,
184
+ runtimeToken: this.runtimeToken,
180
185
  maxPayload: MAX_BROWSER_MESSAGE_BYTES,
181
186
  onHttp: (request, response) => this.handleHttp(request, response),
182
187
  onSocket: (socket, role) => this.acceptSocket(socket, role),
@@ -189,7 +194,7 @@ export class BrowserBridgeManager {
189
194
  const url = `ws://127.0.0.1:${port}/runtime`;
190
195
  return new Promise((resolvePromise) => {
191
196
  let settled = false;
192
- const ws = new WebSocket(url, [`mbm-runtime.${this.token}`], { maxPayload: MAX_BROWSER_MESSAGE_BYTES });
197
+ const ws = new WebSocket(url, [`mbm-runtime.${this.runtimeToken}`], { maxPayload: MAX_BROWSER_MESSAGE_BYTES });
193
198
  const timer = setTimeout(() => finish(false), 1500);
194
199
  timer.unref?.();
195
200
  const finish = (ok) => {
@@ -372,7 +377,7 @@ export class BrowserBridgeManager {
372
377
  handleHttp(request, response) {
373
378
  return handleBrowserBridgeHttp(request, response, {
374
379
  port: this.port,
375
- token: this.token,
380
+ token: this.extensionToken,
376
381
  extensionConnected: () => this.extensionConnected(),
377
382
  extensionStatusInfo: () => this.extensionStatusInfo(),
378
383
  extensionReloadRequired: () => this.extensionReloadRequired(),
@@ -13,6 +13,10 @@ export class BrowserBrokerRoutes {
13
13
  this.routes = new Map();
14
14
  }
15
15
 
16
+ snapshot() {
17
+ return Object.freeze({ runtime_clients: this.clients.size, routed_requests: this.routes.size });
18
+ }
19
+
16
20
  acceptClient(socket) {
17
21
  this.clients.add(socket);
18
22
  socket.on("message", (data) => this.handleClientMessage(socket, data));
@@ -1,8 +1,9 @@
1
1
  import { createServer } from "node:http";
2
2
  import { WebSocketServer } from "ws";
3
- import { isAllowedExtensionOrigin, isAllowedLoopbackHost } from "./browser-pairing-store.mjs";
3
+ import { isAllowedExtensionOrigin, isAllowedLoopbackHost } from "./browser-pairing-http.mjs";
4
+ import { EXPECTED_EXTENSION_ID } from "./browser-extension-identity.mjs";
4
5
 
5
- export async function startBrowserBrokerServer({ port, token, maxPayload, onHttp, onSocket }) {
6
+ export async function startBrowserBrokerServer({ port, extensionToken, runtimeToken, maxPayload, onHttp, onSocket }) {
6
7
  const server = createServer(onHttp);
7
8
  const wss = new WebSocketServer({ noServer: true, maxPayload });
8
9
  server.on("upgrade", (request, socket, head) => {
@@ -16,8 +17,8 @@ export async function startBrowserBrokerServer({ port, token, maxPayload, onHttp
16
17
  const protocol = String(request.headers["sec-websocket-protocol"] || "");
17
18
  const origin = String(request.headers.origin || "");
18
19
  let role = "";
19
- if (url.pathname === "/extension" && protocol === `mbm.${token}` && isAllowedExtensionOrigin(origin)) role = "extension";
20
- if (url.pathname === "/runtime" && protocol === `mbm-runtime.${token}` && !origin) role = "runtime";
20
+ if (url.pathname === "/extension" && protocol === `mbm.${extensionToken}` && isAllowedExtensionOrigin(origin, EXPECTED_EXTENSION_ID)) role = "extension";
21
+ if (url.pathname === "/runtime" && protocol === `mbm-runtime.${runtimeToken}` && !origin) role = "runtime";
21
22
  if (!role) {
22
23
  rejectUpgrade(socket, "401 Unauthorized");
23
24
  return;
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { packageRoot } from "./state.mjs";
5
+
6
+ const EXTENSION_ID_PATTERN = /^[a-p]{32}$/;
7
+ const PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
8
+
9
+ const manifest = readExtensionManifest();
10
+ export const EXPECTED_EXTENSION_PUBLIC_KEY = normalizeManifestPublicKey(manifest.key);
11
+ export const EXPECTED_EXTENSION_ID = extensionIdFromPublicKey(EXPECTED_EXTENSION_PUBLIC_KEY);
12
+
13
+ export function extensionIdFromPublicKey(value) {
14
+ const publicKey = normalizeManifestPublicKey(value);
15
+ const bytes = Buffer.from(publicKey, "base64");
16
+ const hexadecimal = createHash("sha256").update(bytes).digest("hex").slice(0, 32);
17
+ const extensionId = [...hexadecimal]
18
+ .map((character) => String.fromCharCode(97 + Number.parseInt(character, 16)))
19
+ .join("");
20
+ if (!EXTENSION_ID_PATTERN.test(extensionId)) throw new Error("browser extension public key produced an invalid extension id");
21
+ return extensionId;
22
+ }
23
+
24
+ export function normalizeExtensionId(value) {
25
+ const extensionId = String(value || "").trim().toLowerCase();
26
+ return EXTENSION_ID_PATTERN.test(extensionId) ? extensionId : "";
27
+ }
28
+
29
+ function normalizeManifestPublicKey(value) {
30
+ const publicKey = String(value || "").trim();
31
+ if (!PUBLIC_KEY_PATTERN.test(publicKey) || publicKey.length < 128 || publicKey.length > 8192) {
32
+ throw new Error("browser extension manifest must contain a bounded base64 public key");
33
+ }
34
+ let bytes;
35
+ try { bytes = Buffer.from(publicKey, "base64"); }
36
+ catch { throw new Error("browser extension manifest public key is invalid base64"); }
37
+ if (bytes.length < 96 || bytes.length > 6144 || bytes.toString("base64") !== publicKey) {
38
+ throw new Error("browser extension manifest public key is not canonical DER base64");
39
+ }
40
+ return publicKey;
41
+ }
42
+
43
+ function readExtensionManifest() {
44
+ const path = resolve(packageRoot, "browser-extension", "manifest.json");
45
+ const manifestValue = JSON.parse(readFileSync(path, "utf8"));
46
+ if (!manifestValue || typeof manifestValue !== "object" || Array.isArray(manifestValue)) {
47
+ throw new Error("browser extension manifest is invalid");
48
+ }
49
+ return manifestValue;
50
+ }
@@ -3,6 +3,7 @@
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { packageRoot } from "./state.mjs";
6
+ import { EXPECTED_EXTENSION_ID, normalizeExtensionId } from "./browser-extension-identity.mjs";
6
7
 
7
8
  export const BROWSER_EXTENSION_PROTOCOL = 3;
8
9
  export const EXPECTED_EXTENSION_VERSION = extensionVersion();
@@ -11,13 +12,13 @@ const REQUIRED_EXTENSION_CAPABILITIES = Object.freeze([
11
12
  ]);
12
13
  export const MAX_BROWSER_MESSAGE_BYTES = 8 * 1024 * 1024;
13
14
 
14
- /** @typedef {{protocol: number, version: string, capabilities: string[]}} ExtensionInfo */
15
+ /** @typedef {{protocol: number, version: string, extension_id: string, capabilities: string[]}} ExtensionInfo */
15
16
  /** @typedef {{readyState: number, close: (code?: number, reason?: string) => unknown, send: (value: string) => unknown}} ProtocolSocket */
16
17
 
17
18
  /** @param {unknown} value @returns {ExtensionInfo | null} */
18
19
  export function normalizeCompatibleExtensionInfo(value) {
19
20
  const info = normalizeExtensionInfo(value);
20
- if (!info || info.protocol !== BROWSER_EXTENSION_PROTOCOL || info.version !== EXPECTED_EXTENSION_VERSION) return null;
21
+ if (!info || info.protocol !== BROWSER_EXTENSION_PROTOCOL || info.version !== EXPECTED_EXTENSION_VERSION || info.extension_id !== EXPECTED_EXTENSION_ID) return null;
21
22
  if (REQUIRED_EXTENSION_CAPABILITIES.some((capability) => !info.capabilities.includes(capability))) return null;
22
23
  return info;
23
24
  }
@@ -32,6 +33,9 @@ export function parseExtensionHello(message) {
32
33
  if (info.version !== EXPECTED_EXTENSION_VERSION) {
33
34
  throw new Error(`extension version mismatch; expected ${EXPECTED_EXTENSION_VERSION}; reload the extension`);
34
35
  }
36
+ if (info.extension_id !== EXPECTED_EXTENSION_ID) {
37
+ throw new Error(`extension identity mismatch; expected ${EXPECTED_EXTENSION_ID}; reload the packaged extension`);
38
+ }
35
39
  const missing = REQUIRED_EXTENSION_CAPABILITIES.filter((capability) => !info.capabilities.includes(capability));
36
40
  if (missing.length) throw new Error(`extension capability mismatch; reload the extension (${missing.join(",")})`);
37
41
  return info;
@@ -77,11 +81,12 @@ function normalizeExtensionInfo(value) {
77
81
  const record = /** @type {Record<string, unknown>} */ (value);
78
82
  const protocol = Number(record.protocol);
79
83
  const version = typeof record.version === "string" && record.version.length <= 100 ? record.version : "";
84
+ const extension_id = normalizeExtensionId(record.extension_id);
80
85
  const capabilities = Array.isArray(record.capabilities)
81
86
  ? [...new Set(record.capabilities.filter((entry) => typeof entry === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(entry)))].slice(0, 32)
82
87
  : [];
83
- if (!Number.isInteger(protocol) || protocol < 1 || !version) return null;
84
- return { protocol, version, capabilities };
88
+ if (!Number.isInteger(protocol) || protocol < 1 || !version || !extension_id) return null;
89
+ return { protocol, version, extension_id, capabilities };
85
90
  }
86
91
 
87
92
  function extensionVersion() {