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.
- package/CHANGELOG.md +21 -0
- package/CODE_OF_CONDUCT.md +24 -0
- package/CONTRIBUTING.md +3 -1
- package/GOVERNANCE.md +50 -0
- package/README.md +30 -2
- package/SECURITY.md +1 -1
- package/SUPPORT.md +31 -0
- package/browser-extension/browser-operations.js +1 -1
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +1 -1
- package/docs/ARCHITECTURE.md +17 -18
- package/docs/AUDIT.md +28 -0
- package/docs/ENGINEERING.md +5 -2
- package/docs/LOGGING.md +2 -0
- package/docs/OPERATIONS.md +7 -3
- package/docs/PROJECT_STANDARDS.md +3 -2
- package/docs/TESTING.md +7 -4
- package/docs/UPGRADING.md +30 -0
- package/package.json +15 -7
- package/scripts/coverage-check.mjs +24 -9
- package/src/local/agent-context.mjs +7 -148
- package/src/local/agent-contract.mjs +206 -0
- package/src/local/browser-bridge.mjs +30 -281
- package/src/local/browser-extension-protocol.mjs +19 -4
- package/src/local/browser-operation-service.mjs +325 -0
- package/src/local/call-registry.mjs +44 -1
- package/src/local/capability-ranking.mjs +13 -0
- package/src/local/cli-account-admin.mjs +1 -1
- package/src/local/cli.mjs +16 -7
- package/src/local/daemon-process.mjs +1 -1
- package/src/local/full-access-test.mjs +1 -1
- package/src/local/job-runner.mjs +9 -3
- package/src/local/monotonic-deadline.mjs +7 -0
- package/src/local/numbers.mjs +8 -0
- package/src/local/policy.mjs +88 -12
- package/src/local/project-metadata.mjs +7 -1
- package/src/local/records.mjs +6 -0
- package/src/local/runtime-capabilities.mjs +66 -0
- package/src/local/runtime-diagnostics.mjs +91 -0
- package/src/local/runtime-reporting.mjs +113 -0
- package/src/local/runtime.mjs +53 -190
- package/src/local/service-convergence.mjs +1 -1
- package/src/local/service-environment.mjs +129 -0
- package/src/local/service.mjs +22 -53
- package/src/local/state.mjs +2 -2
- package/src/local/windows-service.mjs +248 -0
- package/src/local/worker-health.mjs +1 -1
- package/src/worker/access.ts +4 -4
- package/src/worker/account-admin.ts +2 -2
- package/src/worker/authority.ts +3 -3
- package/src/worker/index.ts +29 -337
- package/src/worker/oauth-controller.ts +334 -0
- package/src/worker/oauth-state.ts +1 -1
- package/src/worker/oauth-tokens.ts +2 -2
- package/src/worker/tool-catalog.ts +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.local.json +27 -0
package/src/local/numbers.mjs
CHANGED
|
@@ -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
|
package/src/local/policy.mjs
CHANGED
|
@@ -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]) => [
|
|
59
|
+
Object.entries(contract.availability).map(([name, value]) => [
|
|
60
|
+
name,
|
|
61
|
+
Object.freeze(/** @type {AvailabilityRequirements} */ ({ ...value })),
|
|
62
|
+
]),
|
|
13
63
|
));
|
|
14
64
|
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
31
|
-
const
|
|
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));
|
package/src/local/records.mjs
CHANGED
|
@@ -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
|
+
}
|