machine-bridge-mcp 1.1.5 → 1.2.1

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 (63) hide show
  1. package/CHANGELOG.md +21 -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 +10 -4
  8. package/browser-extension/devtools-input.js +2 -2
  9. package/browser-extension/manifest.json +2 -2
  10. package/browser-extension/page-automation.js +1 -1
  11. package/docs/ARCHITECTURE.md +14 -17
  12. package/docs/AUDIT.md +28 -0
  13. package/docs/ENGINEERING.md +8 -2
  14. package/docs/PROJECT_STANDARDS.md +4 -2
  15. package/docs/TESTING.md +9 -3
  16. package/docs/UPGRADING.md +32 -0
  17. package/package.json +12 -4
  18. package/scripts/coverage-check.mjs +27 -10
  19. package/src/local/account-access.mjs +7 -5
  20. package/src/local/agent-context.mjs +7 -148
  21. package/src/local/agent-contract.mjs +206 -0
  22. package/src/local/browser-bridge.mjs +30 -281
  23. package/src/local/browser-extension-protocol.mjs +19 -4
  24. package/src/local/browser-operation-service.mjs +325 -0
  25. package/src/local/call-registry.mjs +44 -1
  26. package/src/local/capability-ranking.mjs +13 -0
  27. package/src/local/cli-local-admin.mjs +74 -38
  28. package/src/local/cli-options.mjs +18 -18
  29. package/src/local/cli-policy.mjs +1 -1
  30. package/src/local/cli-service.mjs +132 -0
  31. package/src/local/cli.mjs +27 -109
  32. package/src/local/daemon-process.mjs +1 -1
  33. package/src/local/full-access-test.mjs +1 -1
  34. package/src/local/job-runner.mjs +9 -3
  35. package/src/local/managed-job-plan.mjs +3 -3
  36. package/src/local/monotonic-deadline.mjs +7 -0
  37. package/src/local/numbers.mjs +8 -0
  38. package/src/local/policy.mjs +97 -16
  39. package/src/local/project-metadata.mjs +7 -1
  40. package/src/local/project-package.mjs +1 -1
  41. package/src/local/records.mjs +6 -0
  42. package/src/local/resource-operations.mjs +1 -1
  43. package/src/local/runtime-capabilities.mjs +66 -0
  44. package/src/local/runtime-diagnostics.mjs +91 -0
  45. package/src/local/runtime-reporting.mjs +113 -0
  46. package/src/local/runtime.mjs +54 -191
  47. package/src/local/service-convergence.mjs +1 -1
  48. package/src/local/service.mjs +1 -1
  49. package/src/local/state.mjs +1 -1
  50. package/src/local/windows-service.mjs +1 -1
  51. package/src/local/worker-deployment.mjs +15 -10
  52. package/src/local/worker-health.mjs +8 -4
  53. package/src/worker/access.ts +8 -7
  54. package/src/worker/account-admin.ts +2 -2
  55. package/src/worker/authority.ts +3 -3
  56. package/src/worker/http.ts +2 -2
  57. package/src/worker/index.ts +29 -337
  58. package/src/worker/oauth-controller.ts +343 -0
  59. package/src/worker/oauth-state.ts +1 -1
  60. package/src/worker/oauth-tokens.ts +2 -2
  61. package/src/worker/tool-catalog.ts +1 -1
  62. package/tsconfig.json +2 -1
  63. package/tsconfig.local.json +27 -0
@@ -1,35 +1,98 @@
1
+ // @ts-check
2
+
1
3
  import catalog from "../shared/tool-catalog.json" with { type: "json" };
2
4
  import contract from "../shared/policy-contract.json" with { type: "json" };
3
5
  import { BridgeError } from "./errors.mjs";
4
6
 
7
+ /** @typedef {"off" | "direct" | "shell"} ExecMode */
8
+ /**
9
+ * @typedef {{
10
+ * profile: string,
11
+ * allowWrite: boolean,
12
+ * execMode: ExecMode,
13
+ * unrestrictedPaths: boolean,
14
+ * minimalEnv: boolean,
15
+ * exposeAbsolutePaths: boolean,
16
+ * }} PolicyCapabilities
17
+ */
18
+ /**
19
+ * @typedef {PolicyCapabilities & {
20
+ * origin: string,
21
+ * revision: number,
22
+ * allowExec: boolean,
23
+ * }} NormalizedPolicy
24
+ */
25
+ /**
26
+ * @typedef {{
27
+ * profile?: string,
28
+ * allowWrite?: boolean,
29
+ * execModes?: ExecMode[],
30
+ * unrestrictedPaths?: boolean,
31
+ * minimalEnv?: boolean,
32
+ * exposeAbsolutePaths?: boolean,
33
+ * }} AvailabilityRequirements
34
+ */
35
+ /** @typedef {{name: string, availability: string} & Record<string, unknown>} ToolDefinition */
36
+ /**
37
+ * @typedef {{
38
+ * profile?: unknown,
39
+ * origin?: unknown,
40
+ * revision?: unknown,
41
+ * allowWrite?: unknown,
42
+ * allowExec?: unknown,
43
+ * execMode?: unknown,
44
+ * unrestrictedPaths?: unknown,
45
+ * minimalEnv?: unknown,
46
+ * exposeAbsolutePaths?: unknown,
47
+ * }} PolicyInput
48
+ */
49
+
5
50
  export const DEFAULT_POLICY_PROFILE = String(contract.defaultProfile);
6
51
  export const DEFAULT_POLICY_REVISION = Number(contract.revision);
52
+ /** @type {Readonly<Record<string, Readonly<PolicyCapabilities>>>} */
7
53
  export const POLICY_PROFILES = Object.freeze(Object.fromEntries(
8
- Object.entries(contract.profiles).map(([name, value]) => [name, Object.freeze({ ...value })]),
54
+ Object.entries(contract.profiles).map(([name, value]) => [name, Object.freeze(/** @type {PolicyCapabilities} */ ({ ...value }))]),
9
55
  ));
56
+ const POLICY_PROFILE_NAMES = Object.freeze(new Set(Object.keys(POLICY_PROFILES)));
10
57
  export const POLICY_ORIGINS = Object.freeze(new Set(contract.origins.map(String)));
58
+ /** @type {Readonly<Record<string, Readonly<AvailabilityRequirements>>>} */
11
59
  export const POLICY_AVAILABILITY = Object.freeze(Object.fromEntries(
12
- Object.entries(contract.availability).map(([name, value]) => [name, Object.freeze({ ...value })]),
60
+ Object.entries(contract.availability).map(([name, value]) => [
61
+ name,
62
+ Object.freeze(/** @type {AvailabilityRequirements} */ ({ ...value })),
63
+ ]),
13
64
  ));
14
65
 
15
- const TOOLS = Object.freeze(catalog.map((tool) => Object.freeze({ ...tool })));
66
+ /** @type {ReadonlyArray<Readonly<ToolDefinition>>} */
67
+ const TOOLS = Object.freeze(catalog.map((tool) => Object.freeze(/** @type {ToolDefinition} */ ({ ...tool }))));
16
68
  const TOOL_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
17
69
 
70
+ /** @param {unknown} name @param {unknown} [origin] */
18
71
  export function policyProfile(name, origin = "explicit") {
19
72
  const profile = String(name || "").trim().toLowerCase();
20
- if (!POLICY_PROFILES[profile]) throw new BridgeError("invalid_request", `unknown policy profile: ${profile}`);
21
- return normalizePolicy({ ...POLICY_PROFILES[profile], origin, revision: DEFAULT_POLICY_REVISION });
73
+ if (!POLICY_PROFILE_NAMES.has(profile)) throw new BridgeError("invalid_request", `unknown policy profile: ${profile}`);
74
+ const canonical = POLICY_PROFILES[profile];
75
+ return normalizePolicy({ ...canonical, origin, revision: DEFAULT_POLICY_REVISION });
22
76
  }
23
77
 
78
+ /** @param {PolicyInput} [policy] @returns {Readonly<NormalizedPolicy>} */
24
79
  export function normalizePolicy(policy = {}) {
25
- const execMode = ["off", "direct", "shell"].includes(policy.execMode)
26
- ? policy.execMode
80
+ const requestedExecMode = policy.execMode;
81
+ /** @type {ExecMode} */
82
+ const execMode = requestedExecMode === "off" || requestedExecMode === "direct" || requestedExecMode === "shell"
83
+ ? requestedExecMode
27
84
  : policy.allowExec === true
28
85
  ? "shell"
29
86
  : "off";
30
- const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
31
- const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
32
- const requestedProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
87
+ const requestedOrigin = typeof policy.origin === "string" ? policy.origin : "";
88
+ const origin = POLICY_ORIGINS.has(requestedOrigin) ? requestedOrigin : "custom";
89
+ const requestedRevision = typeof policy.revision === "number" ? policy.revision : Number.NaN;
90
+ const revision = Number.isInteger(requestedRevision) && requestedRevision > 0
91
+ ? requestedRevision
92
+ : DEFAULT_POLICY_REVISION;
93
+ const rawProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
94
+ const requestedProfile = POLICY_PROFILE_NAMES.has(rawProfile) ? rawProfile : "custom";
95
+ /** @type {NormalizedPolicy} */
33
96
  const normalized = {
34
97
  profile: requestedProfile,
35
98
  origin,
@@ -41,11 +104,14 @@ export function normalizePolicy(policy = {}) {
41
104
  minimalEnv: policy.minimalEnv !== false,
42
105
  exposeAbsolutePaths: policy.exposeAbsolutePaths === true,
43
106
  };
44
- const canonical = POLICY_PROFILES[requestedProfile];
45
- if (canonical) Object.assign(normalized, canonical, { allowExec: canonical.execMode !== "off" });
107
+ if (POLICY_PROFILE_NAMES.has(requestedProfile)) {
108
+ const canonical = POLICY_PROFILES[requestedProfile];
109
+ Object.assign(normalized, canonical, { allowExec: canonical.execMode !== "off" });
110
+ }
46
111
  return Object.freeze(normalized);
47
112
  }
48
113
 
114
+ /** @param {PolicyCapabilities} left @param {PolicyCapabilities} right */
49
115
  export function policyCapabilitiesEqual(left, right) {
50
116
  return left.allowWrite === right.allowWrite
51
117
  && left.execMode === right.execMode
@@ -54,10 +120,12 @@ export function policyCapabilitiesEqual(left, right) {
54
120
  && left.exposeAbsolutePaths === right.exposeAbsolutePaths;
55
121
  }
56
122
 
123
+ /** @param {PolicyInput} [policy] */
57
124
  export function isCanonicalFullPolicy(policy = {}) {
58
125
  return policyCapabilitiesEqual(normalizePolicy(policy), POLICY_PROFILES.full);
59
126
  }
60
127
 
128
+ /** @param {PolicyInput} [policy] */
61
129
  export function assertCanonicalFullPolicy(policy = {}) {
62
130
  const normalized = normalizePolicy(policy);
63
131
  if (normalized.profile !== "full" || !isCanonicalFullPolicy(normalized)) {
@@ -69,10 +137,12 @@ export function assertCanonicalFullPolicy(policy = {}) {
69
137
  return normalized;
70
138
  }
71
139
 
140
+ /** @param {PolicyInput} policy @param {unknown} availability */
72
141
  export function policyAllowsAvailability(policy, availability) {
73
142
  const normalized = normalizePolicy(policy);
74
- const requirements = POLICY_AVAILABILITY[availability];
75
- if (!requirements) return false;
143
+ const availabilityName = String(availability || "");
144
+ if (!Object.hasOwn(POLICY_AVAILABILITY, availabilityName)) return false;
145
+ const requirements = POLICY_AVAILABILITY[availabilityName];
76
146
  if (requirements.profile && normalized.profile !== requirements.profile) return false;
77
147
  if (requirements.allowWrite === true && normalized.allowWrite !== true) return false;
78
148
  if (Array.isArray(requirements.execModes) && !requirements.execModes.includes(normalized.execMode)) return false;
@@ -82,15 +152,18 @@ export function policyAllowsAvailability(policy, availability) {
82
152
  return true;
83
153
  }
84
154
 
155
+ /** @param {unknown} name */
85
156
  export function toolDefinition(name) {
86
157
  return TOOL_BY_NAME.get(String(name || "")) || null;
87
158
  }
88
159
 
160
+ /** @param {PolicyInput} policy @param {unknown} name */
89
161
  export function policyAllowsTool(policy, name) {
90
162
  const tool = toolDefinition(name);
91
163
  return Boolean(tool && policyAllowsAvailability(policy, tool.availability));
92
164
  }
93
165
 
166
+ /** @param {PolicyInput} policy @param {unknown} name */
94
167
  export function assertToolAllowed(policy, name) {
95
168
  const tool = toolDefinition(name);
96
169
  if (!tool) throw new BridgeError("not_found", `unknown tool: ${String(name || "")}`);
@@ -102,12 +175,14 @@ export function assertToolAllowed(policy, name) {
102
175
  return tool;
103
176
  }
104
177
 
178
+ /** @param {PolicyInput} [policy] */
105
179
  export function toolsForPolicy(policy = {}) {
106
180
  const normalized = normalizePolicy(policy);
107
181
  return TOOLS.filter((tool) => policyAllowsAvailability(normalized, tool.availability))
108
182
  .map(({ availability, ...tool }) => structuredClone(tool));
109
183
  }
110
184
 
185
+ /** @param {PolicyInput} [policy] */
111
186
  export function toolNamesForPolicy(policy = {}) {
112
187
  const normalized = normalizePolicy(policy);
113
188
  return TOOLS.filter((tool) => policyAllowsAvailability(normalized, tool.availability)).map((tool) => tool.name);
@@ -117,23 +192,29 @@ export function allToolNames() {
117
192
  return TOOLS.map((tool) => tool.name);
118
193
  }
119
194
 
120
-
195
+ /**
196
+ * @param {PolicyInput} policy
197
+ * @param {((tool: string) => unknown) | null | undefined} provided
198
+ */
121
199
  export function createToolAuthorizer(policy, provided) {
122
200
  if (typeof provided === "function") return provided;
123
201
  const gate = new PolicyGate(policy);
124
- return (tool) => gate.assert(tool);
202
+ return (/** @type {string} */ tool) => gate.assert(tool);
125
203
  }
126
204
 
127
205
  export class PolicyGate {
206
+ /** @param {PolicyInput} policy */
128
207
  constructor(policy) {
129
208
  this.policy = normalizePolicy(policy);
130
209
  this.allowedNames = new Set(toolNamesForPolicy(this.policy));
131
210
  }
132
211
 
212
+ /** @param {unknown} name */
133
213
  allows(name) {
134
214
  return this.allowedNames.has(String(name || ""));
135
215
  }
136
216
 
217
+ /** @param {unknown} name */
137
218
  assert(name) {
138
219
  return assertToolAllowed(this.policy, name);
139
220
  }
@@ -1,22 +1,28 @@
1
+ // @ts-check
2
+
1
3
  import { constants as fsConstants } from "node:fs";
2
4
  import { lstat, open } from "node:fs/promises";
3
5
 
4
6
  const SKIPPABLE_METADATA_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP", "EBUSY"]);
5
7
 
8
+ /** @param {unknown} value @param {number} maxLength */
6
9
  export function safeSingleLine(value, maxLength) {
7
10
  if (typeof value !== "string") return "";
8
11
  return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
9
12
  }
10
13
 
14
+ /** @param {unknown} error */
11
15
  export function skippableMetadataError(error) {
12
- return SKIPPABLE_METADATA_CODES.has(error?.code);
16
+ return SKIPPABLE_METADATA_CODES.has(/** @type {NodeJS.ErrnoException} */ (error)?.code || "");
13
17
  }
14
18
 
19
+ /** @param {string} filePath */
15
20
  export async function isRegularNonSymlink(filePath) {
16
21
  const info = await lstat(filePath).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
17
22
  return Boolean(info && !info.isSymbolicLink() && info.isFile());
18
23
  }
19
24
 
25
+ /** @param {string} filePath @param {number} maxBytes */
20
26
  export async function readOptionalRegularUtf8(filePath, maxBytes) {
21
27
  const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0))
22
28
  .catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
@@ -138,7 +138,7 @@ export function safeVersionValue(value) {
138
138
  function packageScriptSearchTerms(script) {
139
139
  const normalized = String(script).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
140
140
  const root = normalized.split("-")[0];
141
- return PACKAGE_SCRIPT_INTENTS[root] || "";
141
+ return Object.hasOwn(PACKAGE_SCRIPT_INTENTS, root) ? PACKAGE_SCRIPT_INTENTS[root] : "";
142
142
  }
143
143
 
144
144
  function invalidPackageMetadata(packagePath, lockfiles, packageState) {
@@ -1,3 +1,9 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {unknown} value
5
+ * @returns {value is Record<string, unknown>}
6
+ */
1
7
  export function isPlainRecord(value) {
2
8
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3
9
  const prototype = Object.getPrototypeOf(value);
@@ -14,7 +14,7 @@ export async function generateRegisteredSshKey({ workspace, stateDir, name: rawN
14
14
  let key = null;
15
15
  try {
16
16
  state.resources ||= {};
17
- const existing = state.resources[name];
17
+ const existing = Object.hasOwn(state.resources, name) ? state.resources[name] : null;
18
18
  if (existing?.path && !samePathIdentity(existing.path, target)) {
19
19
  throw new Error(`local resource ${name} is already registered to a different file; remove it first`);
20
20
  }
@@ -0,0 +1,66 @@
1
+ export async function sessionBootstrap({
2
+ agentContextManager,
3
+ appAutomationManager,
4
+ capabilityObserver,
5
+ policy,
6
+ }, args = {}, context = {}) {
7
+ const bootstrap = await agentContextManager.sessionBootstrap(args, context);
8
+ bootstrap.local_automation = {
9
+ applications: appAutomationManager.capabilities(),
10
+ browser: policy.profile === "full" ? {
11
+ existing_profile: true,
12
+ extension_bridge: true,
13
+ status_tool: "browser_status",
14
+ } : null,
15
+ };
16
+ capabilityObserver.recordBootstrap(bootstrap);
17
+ return bootstrap;
18
+ }
19
+
20
+ export async function resolveTaskCapabilities({
21
+ agentContextManager,
22
+ appAutomationManager,
23
+ capabilityObserver,
24
+ policy,
25
+ }, args = {}, context = {}) {
26
+ const result = await agentContextManager.resolveTaskCapabilities(args, context);
27
+ const task = String(args.task || "");
28
+ if (policy.profile === "full") {
29
+ const applications = await appAutomationManager
30
+ .listApplications({ query: "", max_results: 500 }, context)
31
+ .catch(() => ({ applications: [] }));
32
+ const lower = task.toLowerCase();
33
+ result.application_matches = applications.applications
34
+ .map((application) => ({ application, score: applicationMatchScore(lower, application) }))
35
+ .filter((item) => item.score > 0)
36
+ .sort((left, right) => right.score - left.score || left.application.name.localeCompare(right.application.name))
37
+ .slice(0, 20)
38
+ .map(({ application, score }) => ({ ...application, score }));
39
+ } else {
40
+ result.application_matches = [];
41
+ }
42
+ if (result.application_matches.length) {
43
+ result.recommended_tools = [...new Set([
44
+ ...result.recommended_tools,
45
+ "list_local_applications",
46
+ "open_local_application",
47
+ "inspect_local_application",
48
+ "operate_local_application",
49
+ ])];
50
+ }
51
+ result.browser_backend = policy.profile === "full"
52
+ ? { tool: "browser_status", existing_profile: true, extension_bridge: true }
53
+ : null;
54
+ result.routing_observability = "Call server_info or project_overview to verify that bootstrap and task capability resolution reached the local runtime.";
55
+ capabilityObserver.recordResolution(task, result);
56
+ return result;
57
+ }
58
+
59
+ function applicationMatchScore(task, application) {
60
+ const name = String(application.name || "").toLowerCase();
61
+ const id = String(application.id || "").toLowerCase();
62
+ if (!name) return 0;
63
+ if (task.includes(name)) return 10 + Math.min(name.length, 20);
64
+ const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
65
+ return words.reduce((score, word) => score + (task.includes(word) ? 2 : 0), id && task.includes(id) ? 5 : 0);
66
+ }
@@ -0,0 +1,91 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { classifyOperationalError } from "./log.mjs";
5
+ import { readBoundedFile } from "./workspace-file-service.mjs";
6
+
7
+ export async function diagnoseRuntime({
8
+ policy,
9
+ runtimeDir,
10
+ workspace,
11
+ runProcess,
12
+ probeShell,
13
+ managedJobManager,
14
+ throwIfCancelled,
15
+ }, context = {}) {
16
+ throwIfCancelled(context);
17
+ const checks = [{
18
+ layer: "mcp-host-to-daemon",
19
+ ok: true,
20
+ detail: "This diagnostic request reached the local Machine Bridge runtime.",
21
+ }, {
22
+ layer: "machine-bridge-policy",
23
+ ok: policy.execMode === "direct" || policy.execMode === "shell",
24
+ detail: `profile=${policy.profile}; exec_mode=${policy.execMode}; unrestricted_paths=${policy.unrestrictedPaths}`,
25
+ }];
26
+
27
+ const probe = join(runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
28
+ try {
29
+ await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
30
+ const { buffer } = await readBoundedFile(probe, 64, "diagnostic file");
31
+ checks.push({ layer: "local-filesystem", ok: buffer.toString("utf8") === "ok\n", error_class: null });
32
+ } catch (error) {
33
+ checks.push({ layer: "local-filesystem", ok: false, error_class: classifyOperationalError(error) });
34
+ } finally {
35
+ await rm(probe, { force: true }).catch(() => {});
36
+ }
37
+
38
+ if (policy.execMode === "direct" || policy.execMode === "shell") {
39
+ const direct = await runProcess(
40
+ process.execPath,
41
+ ["-e", "process.stdout.write('ok')"],
42
+ 5000,
43
+ true,
44
+ 1024,
45
+ context,
46
+ workspace,
47
+ ).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
48
+ checks.push({
49
+ layer: "local-process-spawn",
50
+ ok: direct.code === 0 && direct.stdout === "ok",
51
+ error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
52
+ });
53
+ } else {
54
+ checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
55
+ }
56
+
57
+ if (policy.execMode === "shell") {
58
+ const result = await probeShell(context)
59
+ .catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
60
+ checks.push({
61
+ layer: "local-shell",
62
+ ok: result.code === 0,
63
+ error_class: result.error_class || (result.code === 0 ? null : classifyOperationalError(result.stderr || result.stdout || "execution failed")),
64
+ });
65
+ } else {
66
+ checks.push({ layer: "local-shell", ok: false, skipped: true, error_class: "policy_denied" });
67
+ }
68
+
69
+ checks.push({ layer: "managed-job-storage", ...managedJobManager.diagnoseStorage() });
70
+ const resources = managedJobManager.listResources();
71
+ checks.push({
72
+ layer: "local-resource-registry",
73
+ ok: resources.resources.every((resource) => resource.available),
74
+ registered: resources.count,
75
+ unavailable: resources.resources
76
+ .filter((resource) => !resource.available)
77
+ .map((resource) => ({ name: resource.name, error_class: resource.error_class })),
78
+ });
79
+
80
+ return {
81
+ request_reached_local_runtime: true,
82
+ interpretation: {
83
+ tool_call_blocked_before_response: "host/platform or connector gateway",
84
+ diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
85
+ managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
86
+ },
87
+ policy,
88
+ checks,
89
+ ok: checks.filter((check) => !check.skipped).every((check) => check.ok),
90
+ };
91
+ }
@@ -0,0 +1,113 @@
1
+ import { basename } from "node:path";
2
+ import {
3
+ allToolNames, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS,
4
+ POLICY_PROFILES, SERVER_NAME,
5
+ } from "./tools.mjs";
6
+
7
+ export function buildRuntimeInfo({
8
+ workspace,
9
+ displayPath,
10
+ policy,
11
+ toolNames,
12
+ capabilityObserver,
13
+ observability,
14
+ callRegistry,
15
+ lifecycle,
16
+ relayStatus,
17
+ runtimeDir,
18
+ processTracker,
19
+ processSessionManager,
20
+ managedJobManager,
21
+ }) {
22
+ return {
23
+ name: SERVER_NAME,
24
+ protocol_version: MCP_PROTOCOL_VERSION,
25
+ supported_protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
26
+ workspace: displayPath(workspace),
27
+ workspace_name: policy.exposeAbsolutePaths ? basename(workspace) : "workspace",
28
+ policy,
29
+ policy_contract: {
30
+ named_profile_is_canonical: policy.profile === "custom" || policyMatchesNamedProfile(policy),
31
+ full_catalog_complete: policy.profile === "full"
32
+ ? isCanonicalFullPolicy(policy) && toolNames.length + 1 === allToolNames().length
33
+ : null,
34
+ machine_bridge_internal_denials_under_full: policy.profile === "full" && isCanonicalFullPolicy(policy) ? false : null,
35
+ },
36
+ enforcement: {
37
+ filesystem_scope: policy.unrestrictedPaths ? "local-user-accessible" : "workspace",
38
+ sensitive_filename_filter: false,
39
+ operating_system_permissions_apply: true,
40
+ host_policy_is_independent: true,
41
+ },
42
+ tool_delivery: {
43
+ full_profile_scope: "local-daemon-and-relay-advertisement",
44
+ daemon_advertised_tool_count: toolNames.length + 1,
45
+ host_exposed_tools_known_to_server: false,
46
+ host_may_expose_subset: true,
47
+ },
48
+ tools: ["server_info", ...toolNames],
49
+ observability: {
50
+ relay_readiness: "authenticated-hello-acknowledged",
51
+ brief_relay_interruptions: "debug-only",
52
+ raw_transport_details: "debug-only",
53
+ per_tool_events: "structured-debug-events",
54
+ default_logs_include_tool_failures: false,
55
+ tool_arguments_or_results_logged: false,
56
+ capability_routing: capabilityObserver.snapshot(),
57
+ tool_calls: observability.snapshot(),
58
+ in_flight_calls: callRegistry.snapshot(),
59
+ },
60
+ runtime: {
61
+ environment: policy.minimalEnv ? "isolated-minimal" : "full-parent",
62
+ lifecycle: lifecycle.snapshot(),
63
+ relay: relayStatus(),
64
+ runtime_dir: policy.exposeAbsolutePaths ? runtimeDir : "<private-runtime-dir>",
65
+ processes: processTracker.snapshot(),
66
+ process_sessions: processSessionManager.status(),
67
+ managed_jobs: managedJobManager.status(),
68
+ local_resources: managedJobManager.resourceInfo(),
69
+ },
70
+ };
71
+ }
72
+
73
+ export async function buildProjectOverview({
74
+ workspace,
75
+ displayPath,
76
+ policy,
77
+ toolNames,
78
+ capabilityObserver,
79
+ listTopLevel,
80
+ runProcess,
81
+ safeErrorMessage,
82
+ throwIfCancelled,
83
+ }, context = {}) {
84
+ throwIfCancelled(context);
85
+ const top = await listTopLevel(context).catch((error) => ({ error: safeErrorMessage(error), entries: [] }));
86
+ const git = await runProcess(
87
+ "git",
88
+ ["-c", "core.fsmonitor=false", "-C", workspace, "rev-parse", "--show-toplevel"],
89
+ 10_000,
90
+ true,
91
+ 512 * 1024,
92
+ context,
93
+ );
94
+ return {
95
+ workspace: displayPath(workspace),
96
+ workspaceName: policy.exposeAbsolutePaths ? basename(workspace) : "workspace",
97
+ gitRoot: git.code === 0 ? displayPath(git.stdout.trim()) : "",
98
+ policy,
99
+ tools: ["server_info", ...toolNames],
100
+ capabilityRouting: capabilityObserver.snapshot(),
101
+ topLevel: top.entries || [],
102
+ };
103
+ }
104
+
105
+ function policyMatchesNamedProfile(policy) {
106
+ if (!Object.hasOwn(POLICY_PROFILES, policy.profile)) return false;
107
+ const named = POLICY_PROFILES[policy.profile];
108
+ return policy.allowWrite === named.allowWrite
109
+ && policy.execMode === named.execMode
110
+ && policy.unrestrictedPaths === named.unrestrictedPaths
111
+ && policy.minimalEnv === named.minimalEnv
112
+ && policy.exposeAbsolutePaths === named.exposeAbsolutePaths;
113
+ }