machine-bridge-mcp 1.1.4 → 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 (57) 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 +30 -2
  6. package/SECURITY.md +1 -1
  7. package/SUPPORT.md +31 -0
  8. package/browser-extension/browser-operations.js +1 -1
  9. package/browser-extension/manifest.json +2 -2
  10. package/browser-extension/page-automation.js +1 -1
  11. package/docs/ARCHITECTURE.md +17 -18
  12. package/docs/AUDIT.md +28 -0
  13. package/docs/ENGINEERING.md +5 -2
  14. package/docs/LOGGING.md +2 -0
  15. package/docs/OPERATIONS.md +7 -3
  16. package/docs/PROJECT_STANDARDS.md +3 -2
  17. package/docs/TESTING.md +7 -4
  18. package/docs/UPGRADING.md +30 -0
  19. package/package.json +15 -7
  20. package/scripts/coverage-check.mjs +24 -9
  21. package/src/local/agent-context.mjs +7 -148
  22. package/src/local/agent-contract.mjs +206 -0
  23. package/src/local/browser-bridge.mjs +30 -281
  24. package/src/local/browser-extension-protocol.mjs +19 -4
  25. package/src/local/browser-operation-service.mjs +325 -0
  26. package/src/local/call-registry.mjs +44 -1
  27. package/src/local/capability-ranking.mjs +13 -0
  28. package/src/local/cli-account-admin.mjs +1 -1
  29. package/src/local/cli.mjs +16 -7
  30. package/src/local/daemon-process.mjs +1 -1
  31. package/src/local/full-access-test.mjs +1 -1
  32. package/src/local/job-runner.mjs +9 -3
  33. package/src/local/monotonic-deadline.mjs +7 -0
  34. package/src/local/numbers.mjs +8 -0
  35. package/src/local/policy.mjs +88 -12
  36. package/src/local/project-metadata.mjs +7 -1
  37. package/src/local/records.mjs +6 -0
  38. package/src/local/runtime-capabilities.mjs +66 -0
  39. package/src/local/runtime-diagnostics.mjs +91 -0
  40. package/src/local/runtime-reporting.mjs +113 -0
  41. package/src/local/runtime.mjs +53 -190
  42. package/src/local/service-convergence.mjs +1 -1
  43. package/src/local/service-environment.mjs +129 -0
  44. package/src/local/service.mjs +22 -53
  45. package/src/local/state.mjs +2 -2
  46. package/src/local/windows-service.mjs +248 -0
  47. package/src/local/worker-health.mjs +1 -1
  48. package/src/worker/access.ts +4 -4
  49. package/src/worker/account-admin.ts +2 -2
  50. package/src/worker/authority.ts +3 -3
  51. package/src/worker/index.ts +29 -337
  52. package/src/worker/oauth-controller.ts +334 -0
  53. package/src/worker/oauth-state.ts +1 -1
  54. package/src/worker/oauth-tokens.ts +2 -2
  55. package/src/worker/tool-catalog.ts +1 -1
  56. package/tsconfig.json +2 -1
  57. package/tsconfig.local.json +27 -0
@@ -1,3 +1,11 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {unknown} value
5
+ * @param {number} fallback
6
+ * @param {number} minimum
7
+ * @param {number} maximum
8
+ */
1
9
  export function clampInteger(value, fallback, minimum, maximum) {
2
10
  const parsed = typeof value === "number"
3
11
  ? value
@@ -1,35 +1,96 @@
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
  ));
10
56
  export const POLICY_ORIGINS = Object.freeze(new Set(contract.origins.map(String)));
57
+ /** @type {Readonly<Record<string, Readonly<AvailabilityRequirements>>>} */
11
58
  export const POLICY_AVAILABILITY = Object.freeze(Object.fromEntries(
12
- Object.entries(contract.availability).map(([name, value]) => [name, Object.freeze({ ...value })]),
59
+ Object.entries(contract.availability).map(([name, value]) => [
60
+ name,
61
+ Object.freeze(/** @type {AvailabilityRequirements} */ ({ ...value })),
62
+ ]),
13
63
  ));
14
64
 
15
- const TOOLS = Object.freeze(catalog.map((tool) => Object.freeze({ ...tool })));
65
+ /** @type {ReadonlyArray<Readonly<ToolDefinition>>} */
66
+ const TOOLS = Object.freeze(catalog.map((tool) => Object.freeze(/** @type {ToolDefinition} */ ({ ...tool }))));
16
67
  const TOOL_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
17
68
 
69
+ /** @param {unknown} name @param {unknown} [origin] */
18
70
  export function policyProfile(name, origin = "explicit") {
19
71
  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 });
72
+ const canonical = POLICY_PROFILES[profile];
73
+ if (!canonical) throw new BridgeError("invalid_request", `unknown policy profile: ${profile}`);
74
+ return normalizePolicy({ ...canonical, origin, revision: DEFAULT_POLICY_REVISION });
22
75
  }
23
76
 
77
+ /** @param {PolicyInput} [policy] @returns {Readonly<NormalizedPolicy>} */
24
78
  export function normalizePolicy(policy = {}) {
25
- const execMode = ["off", "direct", "shell"].includes(policy.execMode)
26
- ? policy.execMode
79
+ const requestedExecMode = policy.execMode;
80
+ /** @type {ExecMode} */
81
+ const execMode = requestedExecMode === "off" || requestedExecMode === "direct" || requestedExecMode === "shell"
82
+ ? requestedExecMode
27
83
  : policy.allowExec === true
28
84
  ? "shell"
29
85
  : "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;
86
+ const requestedOrigin = typeof policy.origin === "string" ? policy.origin : "";
87
+ const origin = POLICY_ORIGINS.has(requestedOrigin) ? requestedOrigin : "custom";
88
+ const requestedRevision = typeof policy.revision === "number" ? policy.revision : Number.NaN;
89
+ const revision = Number.isInteger(requestedRevision) && requestedRevision > 0
90
+ ? requestedRevision
91
+ : DEFAULT_POLICY_REVISION;
32
92
  const requestedProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
93
+ /** @type {NormalizedPolicy} */
33
94
  const normalized = {
34
95
  profile: requestedProfile,
35
96
  origin,
@@ -46,6 +107,7 @@ export function normalizePolicy(policy = {}) {
46
107
  return Object.freeze(normalized);
47
108
  }
48
109
 
110
+ /** @param {PolicyCapabilities} left @param {PolicyCapabilities} right */
49
111
  export function policyCapabilitiesEqual(left, right) {
50
112
  return left.allowWrite === right.allowWrite
51
113
  && left.execMode === right.execMode
@@ -54,10 +116,12 @@ export function policyCapabilitiesEqual(left, right) {
54
116
  && left.exposeAbsolutePaths === right.exposeAbsolutePaths;
55
117
  }
56
118
 
119
+ /** @param {PolicyInput} [policy] */
57
120
  export function isCanonicalFullPolicy(policy = {}) {
58
121
  return policyCapabilitiesEqual(normalizePolicy(policy), POLICY_PROFILES.full);
59
122
  }
60
123
 
124
+ /** @param {PolicyInput} [policy] */
61
125
  export function assertCanonicalFullPolicy(policy = {}) {
62
126
  const normalized = normalizePolicy(policy);
63
127
  if (normalized.profile !== "full" || !isCanonicalFullPolicy(normalized)) {
@@ -69,9 +133,10 @@ export function assertCanonicalFullPolicy(policy = {}) {
69
133
  return normalized;
70
134
  }
71
135
 
136
+ /** @param {PolicyInput} policy @param {unknown} availability */
72
137
  export function policyAllowsAvailability(policy, availability) {
73
138
  const normalized = normalizePolicy(policy);
74
- const requirements = POLICY_AVAILABILITY[availability];
139
+ const requirements = POLICY_AVAILABILITY[String(availability || "")];
75
140
  if (!requirements) return false;
76
141
  if (requirements.profile && normalized.profile !== requirements.profile) return false;
77
142
  if (requirements.allowWrite === true && normalized.allowWrite !== true) return false;
@@ -82,15 +147,18 @@ export function policyAllowsAvailability(policy, availability) {
82
147
  return true;
83
148
  }
84
149
 
150
+ /** @param {unknown} name */
85
151
  export function toolDefinition(name) {
86
152
  return TOOL_BY_NAME.get(String(name || "")) || null;
87
153
  }
88
154
 
155
+ /** @param {PolicyInput} policy @param {unknown} name */
89
156
  export function policyAllowsTool(policy, name) {
90
157
  const tool = toolDefinition(name);
91
158
  return Boolean(tool && policyAllowsAvailability(policy, tool.availability));
92
159
  }
93
160
 
161
+ /** @param {PolicyInput} policy @param {unknown} name */
94
162
  export function assertToolAllowed(policy, name) {
95
163
  const tool = toolDefinition(name);
96
164
  if (!tool) throw new BridgeError("not_found", `unknown tool: ${String(name || "")}`);
@@ -102,12 +170,14 @@ export function assertToolAllowed(policy, name) {
102
170
  return tool;
103
171
  }
104
172
 
173
+ /** @param {PolicyInput} [policy] */
105
174
  export function toolsForPolicy(policy = {}) {
106
175
  const normalized = normalizePolicy(policy);
107
176
  return TOOLS.filter((tool) => policyAllowsAvailability(normalized, tool.availability))
108
177
  .map(({ availability, ...tool }) => structuredClone(tool));
109
178
  }
110
179
 
180
+ /** @param {PolicyInput} [policy] */
111
181
  export function toolNamesForPolicy(policy = {}) {
112
182
  const normalized = normalizePolicy(policy);
113
183
  return TOOLS.filter((tool) => policyAllowsAvailability(normalized, tool.availability)).map((tool) => tool.name);
@@ -117,23 +187,29 @@ export function allToolNames() {
117
187
  return TOOLS.map((tool) => tool.name);
118
188
  }
119
189
 
120
-
190
+ /**
191
+ * @param {PolicyInput} policy
192
+ * @param {((tool: string) => unknown) | null | undefined} provided
193
+ */
121
194
  export function createToolAuthorizer(policy, provided) {
122
195
  if (typeof provided === "function") return provided;
123
196
  const gate = new PolicyGate(policy);
124
- return (tool) => gate.assert(tool);
197
+ return (/** @type {string} */ tool) => gate.assert(tool);
125
198
  }
126
199
 
127
200
  export class PolicyGate {
201
+ /** @param {PolicyInput} policy */
128
202
  constructor(policy) {
129
203
  this.policy = normalizePolicy(policy);
130
204
  this.allowedNames = new Set(toolNamesForPolicy(this.policy));
131
205
  }
132
206
 
207
+ /** @param {unknown} name */
133
208
  allows(name) {
134
209
  return this.allowedNames.has(String(name || ""));
135
210
  }
136
211
 
212
+ /** @param {unknown} name */
137
213
  assert(name) {
138
214
  return assertToolAllowed(this.policy, name);
139
215
  }
@@ -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));
@@ -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);
@@ -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
+ const named = POLICY_PROFILES[policy.profile];
107
+ if (!named) return false;
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
+ }