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.
- 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 +26 -0
- package/SUPPORT.md +31 -0
- package/browser-extension/browser-operations.js +10 -4
- package/browser-extension/devtools-input.js +2 -2
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +1 -1
- package/docs/ARCHITECTURE.md +14 -17
- package/docs/AUDIT.md +28 -0
- package/docs/ENGINEERING.md +8 -2
- package/docs/PROJECT_STANDARDS.md +4 -2
- package/docs/TESTING.md +9 -3
- package/docs/UPGRADING.md +32 -0
- package/package.json +12 -4
- package/scripts/coverage-check.mjs +27 -10
- package/src/local/account-access.mjs +7 -5
- 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-local-admin.mjs +74 -38
- package/src/local/cli-options.mjs +18 -18
- package/src/local/cli-policy.mjs +1 -1
- package/src/local/cli-service.mjs +132 -0
- package/src/local/cli.mjs +27 -109
- 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/managed-job-plan.mjs +3 -3
- package/src/local/monotonic-deadline.mjs +7 -0
- package/src/local/numbers.mjs +8 -0
- package/src/local/policy.mjs +97 -16
- package/src/local/project-metadata.mjs +7 -1
- package/src/local/project-package.mjs +1 -1
- package/src/local/records.mjs +6 -0
- package/src/local/resource-operations.mjs +1 -1
- 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 +54 -191
- package/src/local/service-convergence.mjs +1 -1
- package/src/local/service.mjs +1 -1
- package/src/local/state.mjs +1 -1
- package/src/local/windows-service.mjs +1 -1
- package/src/local/worker-deployment.mjs +15 -10
- package/src/local/worker-health.mjs +8 -4
- package/src/worker/access.ts +8 -7
- package/src/worker/account-admin.ts +2 -2
- package/src/worker/authority.ts +3 -3
- package/src/worker/http.ts +2 -2
- package/src/worker/index.ts +29 -337
- package/src/worker/oauth-controller.ts +343 -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/policy.mjs
CHANGED
|
@@ -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]) => [
|
|
60
|
+
Object.entries(contract.availability).map(([name, value]) => [
|
|
61
|
+
name,
|
|
62
|
+
Object.freeze(/** @type {AvailabilityRequirements} */ ({ ...value })),
|
|
63
|
+
]),
|
|
13
64
|
));
|
|
14
65
|
|
|
15
|
-
|
|
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 (!
|
|
21
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
75
|
-
if (!
|
|
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) {
|
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);
|
|
@@ -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
|
+
}
|