machine-bridge-mcp 1.1.5 → 1.2.0

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 (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CODE_OF_CONDUCT.md +24 -0
  3. package/CONTRIBUTING.md +3 -1
  4. package/GOVERNANCE.md +50 -0
  5. package/README.md +26 -0
  6. package/SUPPORT.md +31 -0
  7. package/browser-extension/browser-operations.js +1 -1
  8. package/browser-extension/manifest.json +2 -2
  9. package/browser-extension/page-automation.js +1 -1
  10. package/docs/ARCHITECTURE.md +14 -17
  11. package/docs/AUDIT.md +14 -0
  12. package/docs/ENGINEERING.md +5 -2
  13. package/docs/PROJECT_STANDARDS.md +3 -2
  14. package/docs/TESTING.md +4 -2
  15. package/docs/UPGRADING.md +30 -0
  16. package/package.json +11 -4
  17. package/scripts/coverage-check.mjs +24 -9
  18. package/src/local/agent-context.mjs +7 -148
  19. package/src/local/agent-contract.mjs +206 -0
  20. package/src/local/browser-bridge.mjs +30 -281
  21. package/src/local/browser-extension-protocol.mjs +19 -4
  22. package/src/local/browser-operation-service.mjs +325 -0
  23. package/src/local/call-registry.mjs +44 -1
  24. package/src/local/capability-ranking.mjs +13 -0
  25. package/src/local/cli.mjs +4 -4
  26. package/src/local/daemon-process.mjs +1 -1
  27. package/src/local/full-access-test.mjs +1 -1
  28. package/src/local/job-runner.mjs +9 -3
  29. package/src/local/monotonic-deadline.mjs +7 -0
  30. package/src/local/numbers.mjs +8 -0
  31. package/src/local/policy.mjs +88 -12
  32. package/src/local/project-metadata.mjs +7 -1
  33. package/src/local/records.mjs +6 -0
  34. package/src/local/runtime-capabilities.mjs +66 -0
  35. package/src/local/runtime-diagnostics.mjs +91 -0
  36. package/src/local/runtime-reporting.mjs +113 -0
  37. package/src/local/runtime.mjs +53 -190
  38. package/src/local/service-convergence.mjs +1 -1
  39. package/src/local/service.mjs +1 -1
  40. package/src/local/state.mjs +1 -1
  41. package/src/local/windows-service.mjs +1 -1
  42. package/src/local/worker-health.mjs +1 -1
  43. package/src/worker/access.ts +4 -4
  44. package/src/worker/account-admin.ts +2 -2
  45. package/src/worker/authority.ts +3 -3
  46. package/src/worker/index.ts +29 -337
  47. package/src/worker/oauth-controller.ts +334 -0
  48. package/src/worker/oauth-state.ts +1 -1
  49. package/src/worker/oauth-tokens.ts +2 -2
  50. package/src/worker/tool-catalog.ts +1 -1
  51. package/tsconfig.json +2 -1
  52. package/tsconfig.local.json +27 -0
@@ -1,12 +1,11 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
3
- import { lstat, realpath, rm, stat, writeFile } from "node:fs/promises";
2
+ import { lstat, realpath, stat } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
5
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
5
  import { RelayConnection } from "./relay-connection.mjs";
7
6
  import { ProcessSessionManager } from "./process-sessions.mjs";
8
7
  export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
9
- import { allToolNames, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, POLICY_PROFILES, PolicyGate, SERVER_NAME } from "./tools.mjs";
8
+ import { MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, PolicyGate, SERVER_NAME } from "./tools.mjs";
10
9
  import { publicError } from "./errors.mjs";
11
10
  import { ProcessTracker } from "./process-tracker.mjs";
12
11
  import { CallRegistry } from "./call-registry.mjs";
@@ -15,7 +14,7 @@ import { ToolExecutor } from "./tool-executor.mjs";
15
14
  import { boundedErrorMessage, ProcessExecutionService } from "./process-execution.mjs";
16
15
  import { GitService } from "./git-service.mjs";
17
16
  import { LifecycleController } from "./lifecycle.mjs";
18
- import { MAX_WRITE_BYTES, readBoundedFile, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
17
+ import { MAX_WRITE_BYTES, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
19
18
  export { MAX_WRITE_BYTES, sha256 } from "./workspace-file-service.mjs";
20
19
  import { classifyOperationalError } from "./log.mjs";
21
20
  import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
@@ -29,6 +28,12 @@ import { readBoundedRegularFileSync } from "./secure-file.mjs";
29
28
  import { clampInteger } from "./numbers.mjs";
30
29
  import { isPlainRecord } from "./records.mjs";
31
30
  import { AccountAccessGate, normalizeAccountRole } from "./account-access.mjs";
31
+ import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
32
+ import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
33
+ import {
34
+ resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
35
+ sessionBootstrap as buildRuntimeSessionBootstrap,
36
+ } from "./runtime-capabilities.mjs";
32
37
 
33
38
  const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
34
39
  const MAX_CONCURRENT_TOOL_CALLS = 16;
@@ -232,53 +237,21 @@ export class LocalRuntime {
232
237
  }
233
238
 
234
239
  runtimeInfo() {
235
- return {
236
- name: SERVER_NAME,
237
- protocol_version: MCP_PROTOCOL_VERSION,
238
- supported_protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
239
- workspace: this.displayPath(this.workspace),
240
- workspace_name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
240
+ return buildRuntimeInfo({
241
+ workspace: this.workspace,
242
+ displayPath: (value) => this.displayPath(value),
241
243
  policy: this.policy,
242
- policy_contract: {
243
- named_profile_is_canonical: this.policy.profile === "custom" || policyMatchesNamedProfile(this.policy),
244
- full_catalog_complete: this.policy.profile === "full" ? isCanonicalFullPolicy(this.policy) && this.tools().length + 1 === allToolNames().length : null,
245
- machine_bridge_internal_denials_under_full: this.policy.profile === "full" && isCanonicalFullPolicy(this.policy) ? false : null,
246
- },
247
- enforcement: {
248
- filesystem_scope: this.policy.unrestrictedPaths ? "local-user-accessible" : "workspace",
249
- sensitive_filename_filter: false,
250
- operating_system_permissions_apply: true,
251
- host_policy_is_independent: true,
252
- },
253
- tool_delivery: {
254
- full_profile_scope: "local-daemon-and-relay-advertisement",
255
- daemon_advertised_tool_count: this.tools().length + 1,
256
- host_exposed_tools_known_to_server: false,
257
- host_may_expose_subset: true,
258
- },
259
- tools: ["server_info", ...this.tools()],
260
- observability: {
261
- relay_readiness: "authenticated-hello-acknowledged",
262
- brief_relay_interruptions: "debug-only",
263
- raw_transport_details: "debug-only",
264
- per_tool_events: "structured-debug-events",
265
- default_logs_include_tool_failures: false,
266
- tool_arguments_or_results_logged: false,
267
- capability_routing: this.capabilityObserver.snapshot(),
268
- tool_calls: this.observability.snapshot(),
269
- in_flight_calls: this.callRegistry.snapshot(),
270
- },
271
- runtime: {
272
- environment: this.policy.minimalEnv ? "isolated-minimal" : "full-parent",
273
- lifecycle: this.lifecycle.snapshot(),
274
- relay: this.relay?.status?.() || null,
275
- runtime_dir: this.policy.exposeAbsolutePaths ? this.runtimeDir : "<private-runtime-dir>",
276
- processes: this.processTracker.snapshot(),
277
- process_sessions: this.processSessionManager.status(),
278
- managed_jobs: this.managedJobManager.status(),
279
- local_resources: this.managedJobManager.resourceInfo(),
280
- },
281
- };
244
+ toolNames: this.tools(),
245
+ capabilityObserver: this.capabilityObserver,
246
+ observability: this.observability,
247
+ callRegistry: this.callRegistry,
248
+ lifecycle: this.lifecycle,
249
+ relayStatus: () => this.relay?.status?.() || null,
250
+ runtimeDir: this.runtimeDir,
251
+ processTracker: this.processTracker,
252
+ processSessionManager: this.processSessionManager,
253
+ managedJobManager: this.managedJobManager,
254
+ });
282
255
  }
283
256
 
284
257
  async start() {
@@ -426,18 +399,17 @@ export class LocalRuntime {
426
399
  }
427
400
 
428
401
  async projectOverview(context = {}) {
429
- this.throwIfCancelled(context);
430
- const top = await this.listDir(".", context).catch(error => ({ error: this.safeErrorMessage(error), entries: [] }));
431
- const git = await this.runProcess("git", ["-c", "core.fsmonitor=false", "-C", this.workspace, "rev-parse", "--show-toplevel"], 10_000, true, 512 * 1024, context);
432
- return {
433
- workspace: this.displayPath(this.workspace),
434
- workspaceName: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
435
- gitRoot: git.code === 0 ? this.displayPath(git.stdout.trim()) : "",
402
+ return buildProjectOverview({
403
+ workspace: this.workspace,
404
+ displayPath: (value) => this.displayPath(value),
436
405
  policy: this.policy,
437
- tools: ["server_info", ...this.tools()],
438
- capabilityRouting: this.capabilityObserver.snapshot(),
439
- topLevel: top.entries || [],
440
- };
406
+ toolNames: this.tools(),
407
+ capabilityObserver: this.capabilityObserver,
408
+ listTopLevel: (callContext) => this.listDir(".", callContext),
409
+ runProcess: (...args) => this.runProcess(...args),
410
+ safeErrorMessage: (error) => this.safeErrorMessage(error),
411
+ throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
412
+ }, context);
441
413
  }
442
414
 
443
415
  listRoots() { return this.workspaceFileService.listRoots(); }
@@ -475,82 +447,15 @@ export class LocalRuntime {
475
447
  }
476
448
 
477
449
  async diagnoseRuntime(context = {}) {
478
- this.throwIfCancelled(context);
479
- const checks = [];
480
- checks.push({
481
- layer: "mcp-host-to-daemon",
482
- ok: true,
483
- detail: "This diagnostic request reached the local Machine Bridge runtime.",
484
- });
485
- checks.push({
486
- layer: "machine-bridge-policy",
487
- ok: this.policy.execMode === "direct" || this.policy.execMode === "shell",
488
- detail: `profile=${this.policy.profile}; exec_mode=${this.policy.execMode}; unrestricted_paths=${this.policy.unrestrictedPaths}`,
489
- });
490
-
491
- const probe = join(this.runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
492
- try {
493
- await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
494
- const { buffer } = await readBoundedFile(probe, 64, "diagnostic file");
495
- checks.push({ layer: "local-filesystem", ok: buffer.toString("utf8") === "ok\n", error_class: null });
496
- } catch (error) {
497
- checks.push({ layer: "local-filesystem", ok: false, error_class: classifyOperationalError(error) });
498
- } finally {
499
- await rm(probe, { force: true }).catch(() => {});
500
- }
501
-
502
- if (this.policy.execMode === "direct" || this.policy.execMode === "shell") {
503
- const direct = await this.runProcess(
504
- process.execPath,
505
- ["-e", "process.stdout.write('ok')"],
506
- 5000,
507
- true,
508
- 1024,
509
- context,
510
- this.workspace,
511
- ).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
512
- checks.push({
513
- layer: "local-process-spawn",
514
- ok: direct.code === 0 && direct.stdout === "ok",
515
- error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
516
- });
517
- } else {
518
- checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
519
- }
520
-
521
- if (this.policy.execMode === "shell") {
522
- const result = await this.processExecutionService.probeShell(context)
523
- .catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
524
- checks.push({
525
- layer: "local-shell",
526
- ok: result.code === 0,
527
- error_class: result.error_class || (result.code === 0 ? null : classifyOperationalError(result.stderr || result.stdout || "execution failed")),
528
- });
529
- } else {
530
- checks.push({ layer: "local-shell", ok: false, skipped: true, error_class: "policy_denied" });
531
- }
532
-
533
- const storage = this.managedJobManager.diagnoseStorage();
534
- checks.push({ layer: "managed-job-storage", ...storage });
535
- const resources = this.managedJobManager.listResources();
536
- checks.push({
537
- layer: "local-resource-registry",
538
- ok: resources.resources.every((resource) => resource.available),
539
- registered: resources.count,
540
- unavailable: resources.resources.filter((resource) => !resource.available).map((resource) => ({ name: resource.name, error_class: resource.error_class })),
541
- });
542
-
543
- return {
544
- request_reached_local_runtime: true,
545
- interpretation: {
546
- tool_call_blocked_before_response: "host/platform or connector gateway",
547
- diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
548
- managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
549
- },
450
+ return runRuntimeDiagnostics({
550
451
  policy: this.policy,
551
- checks,
552
- ok: checks.filter((check) => !check.skipped).every((check) => check.ok),
553
- };
452
+ runtimeDir: this.runtimeDir,
453
+ workspace: this.workspace,
454
+ runProcess: (...args) => this.runProcess(...args),
455
+ probeShell: (callContext) => this.processExecutionService.probeShell(callContext),
456
+ managedJobManager: this.managedJobManager,
457
+ throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
458
+ }, context);
554
459
  }
555
460
 
556
461
  async generateSshKeyResource(args = {}, context = {}) {
@@ -588,43 +493,22 @@ export class LocalRuntime {
588
493
  };
589
494
  }
590
495
 
591
-
592
496
  async sessionBootstrap(args = {}, context = {}) {
593
- const bootstrap = await this.agentContextManager.sessionBootstrap(args, context);
594
- bootstrap.local_automation = {
595
- applications: this.appAutomationManager.capabilities(),
596
- browser: this.policy.profile === "full" ? {
597
- existing_profile: true,
598
- extension_bridge: true,
599
- status_tool: "browser_status",
600
- } : null,
601
- };
602
- this.capabilityObserver.recordBootstrap(bootstrap);
603
- return bootstrap;
497
+ return buildRuntimeSessionBootstrap({
498
+ agentContextManager: this.agentContextManager,
499
+ appAutomationManager: this.appAutomationManager,
500
+ capabilityObserver: this.capabilityObserver,
501
+ policy: this.policy,
502
+ }, args, context);
604
503
  }
605
504
 
606
505
  async resolveTaskCapabilities(args = {}, context = {}) {
607
- const result = await this.agentContextManager.resolveTaskCapabilities(args, context);
608
- const task = String(args.task || "");
609
- if (this.policy.profile === "full") {
610
- const applications = await this.appAutomationManager.listApplications({ query: "", max_results: 500 }, context).catch(() => ({ applications: [] }));
611
- const lower = task.toLowerCase();
612
- result.application_matches = applications.applications
613
- .map((application) => ({ application, score: applicationMatchScore(lower, application) }))
614
- .filter((item) => item.score > 0)
615
- .sort((left, right) => right.score - left.score || left.application.name.localeCompare(right.application.name))
616
- .slice(0, 20)
617
- .map(({ application, score }) => ({ ...application, score }));
618
- } else {
619
- result.application_matches = [];
620
- }
621
- if (result.application_matches.length) {
622
- result.recommended_tools = [...new Set([...result.recommended_tools, "list_local_applications", "open_local_application", "inspect_local_application", "operate_local_application"])];
623
- }
624
- result.browser_backend = this.policy.profile === "full" ? { tool: "browser_status", existing_profile: true, extension_bridge: true } : null;
625
- result.routing_observability = "Call server_info or project_overview to verify that bootstrap and task capability resolution reached the local runtime.";
626
- this.capabilityObserver.recordResolution(task, result);
627
- return result;
506
+ return resolveRuntimeTaskCapabilities({
507
+ agentContextManager: this.agentContextManager,
508
+ appAutomationManager: this.appAutomationManager,
509
+ capabilityObserver: this.capabilityObserver,
510
+ policy: this.policy,
511
+ }, args, context);
628
512
  }
629
513
 
630
514
  readLocalResourceBinary(name) {
@@ -749,25 +633,6 @@ export class LocalRuntime {
749
633
  }
750
634
  }
751
635
 
752
- function applicationMatchScore(task, application) {
753
- const name = String(application.name || "").toLowerCase();
754
- const id = String(application.id || "").toLowerCase();
755
- if (!name) return 0;
756
- if (task.includes(name)) return 10 + Math.min(name.length, 20);
757
- const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
758
- return words.reduce((score, word) => score + (task.includes(word) ? 2 : 0), id && task.includes(id) ? 5 : 0);
759
- }
760
-
761
- function policyMatchesNamedProfile(policy) {
762
- const named = POLICY_PROFILES[policy.profile];
763
- if (!named) return false;
764
- return policy.allowWrite === named.allowWrite
765
- && policy.execMode === named.execMode
766
- && policy.unrestrictedPaths === named.unrestrictedPaths
767
- && policy.minimalEnv === named.minimalEnv
768
- && policy.exposeAbsolutePaths === named.exposeAbsolutePaths;
769
- }
770
-
771
636
  function stateRootFromProfileStatePath(statePath) {
772
637
  const absolute = resolve(statePath);
773
638
  if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
@@ -854,8 +719,6 @@ function normalizeRelayAuthorization(value) {
854
719
  return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
855
720
  }
856
721
 
857
-
858
-
859
722
  function collectToolPathCandidates(error, toolArgs, workspace) {
860
723
  const candidates = new Set();
861
724
  for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
@@ -16,5 +16,5 @@ export async function waitForInactiveStatus(
16
16
  }
17
17
 
18
18
  function delay(milliseconds) {
19
- return new Promise(resolve => setTimeout(resolve, milliseconds));
19
+ return new Promise(resolve => { setTimeout(resolve, milliseconds); });
20
20
  }
@@ -48,7 +48,7 @@ export async function uninstallAutostart({ stateRoot, logger = console } = {}) {
48
48
  return uninstallSystemd(logger);
49
49
  }
50
50
 
51
- export async function autostartStatus({ logger = console } = {}) {
51
+ export async function autostartStatus() {
52
52
  if (process.platform === "darwin") return statusLaunchd();
53
53
  if (process.platform === "win32") return statusWindowsTask({ run: serviceRun });
54
54
  return statusSystemd();
@@ -279,7 +279,7 @@ export async function acquireStartupLockWithWait(state, options = {}) {
279
279
  logger.info?.(`waiting for ${ownerPid} to finish the current startup/state operation`);
280
280
  const deadline = createMonotonicDeadline(timeoutMs);
281
281
  while (!deadline.expired()) {
282
- await new Promise((resolvePromise) => setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))));
282
+ await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
283
283
  lock = acquireStartupLock(state, metadata);
284
284
  if (lock.acquired) {
285
285
  logger.info?.("the previous startup/state operation finished; continuing");
@@ -220,7 +220,7 @@ function windowsServiceFailureReason(command, status) {
220
220
  async function waitForWindowsStatus(predicate, options) {
221
221
  const attempts = boundedPositiveInteger(options.statusAttempts, 10);
222
222
  const delayMs = boundedPositiveInteger(options.statusDelayMs, 100);
223
- const sleep = typeof options.sleep === "function" ? options.sleep : milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
223
+ const sleep = typeof options.sleep === "function" ? options.sleep : milliseconds => new Promise(resolve => { setTimeout(resolve, milliseconds); });
224
224
  let status = await statusWindowsTask(options);
225
225
  for (let index = 1; index < attempts && !predicate(status); index += 1) {
226
226
  await sleep(delayMs);
@@ -242,5 +242,5 @@ function boundedTimeout(value) {
242
242
  }
243
243
 
244
244
  function sleep(ms) {
245
- return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
245
+ return new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); });
246
246
  }
@@ -1,7 +1,7 @@
1
- import accessContract from "../shared/access-contract.json";
2
- import policyContract from "../shared/policy-contract.json";
3
- import toolCatalog from "../shared/tool-catalog.json";
4
- import { policyAllowsAvailability, type DaemonPolicy } from "./policy";
1
+ import accessContract from "../shared/access-contract.json" with { type: "json" };
2
+ import policyContract from "../shared/policy-contract.json" with { type: "json" };
3
+ import toolCatalog from "../shared/tool-catalog.json" with { type: "json" };
4
+ import { policyAllowsAvailability, type DaemonPolicy } from "./policy.ts";
5
5
 
6
6
  export type AccountRole = keyof typeof accessContract.roles;
7
7
 
@@ -1,8 +1,8 @@
1
- import { json, methodNotAllowed, parseRequestBody } from "./http";
1
+ import { json, methodNotAllowed, parseRequestBody } from "./http.ts";
2
2
  import {
3
3
  accountByName, createAccount, publicAccount, replaceAccountPassword, revokeAccountCredentials,
4
4
  safeEqual, updateAccount, type AccountRecord, type OAuthStore,
5
- } from "./oauth-state";
5
+ } from "./oauth-state.ts";
6
6
 
7
7
  const BODY_LIMIT_BYTES = 64 * 1024;
8
8
  const MAX_ACCOUNTS = 64;
@@ -1,6 +1,6 @@
1
- import policyContract from "../shared/policy-contract.json";
2
- import { accountRolePolicy, accountRoleToolNames, type AccountRole } from "./access";
3
- import type { DaemonPolicy } from "./policy";
1
+ import policyContract from "../shared/policy-contract.json" with { type: "json" };
2
+ import { accountRolePolicy, accountRoleToolNames, type AccountRole } from "./access.ts";
3
+ import type { DaemonPolicy } from "./policy.ts";
4
4
 
5
5
  type EffectivePolicy = {
6
6
  scope: "authenticated_account_effective_authority";