machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26
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 +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
6
|
+
|
|
7
|
+
const MAX_SBOM_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const SBOM_TIMEOUT_MS = 30_000;
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
|
|
11
|
+
export function generateAndValidateSbom(options = {}) {
|
|
12
|
+
const npmCli = String(options.npmCli || process.env.npm_execpath || "").trim();
|
|
13
|
+
if (!npmCli) throw new Error("sbom check must run through an npm lifecycle so npm_execpath is available");
|
|
14
|
+
const cwd = resolve(options.cwd || root);
|
|
15
|
+
const result = spawnSync(process.execPath, [npmCli, "sbom", "--sbom-format", "cyclonedx"], {
|
|
16
|
+
cwd,
|
|
17
|
+
env: options.env || process.env,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
maxBuffer: MAX_SBOM_BYTES,
|
|
20
|
+
timeout: SBOM_TIMEOUT_MS,
|
|
21
|
+
killSignal: "SIGKILL",
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
});
|
|
24
|
+
if (result.error) throw result.error;
|
|
25
|
+
if (result.status !== 0) throw new Error(`npm sbom failed: ${boundedText(result.stderr || result.stdout)}`);
|
|
26
|
+
if (Buffer.byteLength(result.stdout) > MAX_SBOM_BYTES) throw new Error("npm sbom output exceeds the fixed byte budget");
|
|
27
|
+
let document;
|
|
28
|
+
try { document = JSON.parse(result.stdout); }
|
|
29
|
+
catch { throw new Error("npm sbom did not return valid JSON"); }
|
|
30
|
+
return validateCycloneDxSbom(document, {
|
|
31
|
+
packageName: options.packageName || packageJson.name,
|
|
32
|
+
packageVersion: options.packageVersion || packageJson.version,
|
|
33
|
+
forbiddenPaths: options.forbiddenPaths || [cwd, homedir()],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateCycloneDxSbom(document, options = {}) {
|
|
38
|
+
if (!isRecord(document)) throw new Error("SBOM root must be an object");
|
|
39
|
+
if (document.bomFormat !== "CycloneDX" || document.specVersion !== "1.5") {
|
|
40
|
+
throw new Error("SBOM must be CycloneDX 1.5");
|
|
41
|
+
}
|
|
42
|
+
const component = isRecord(document.metadata) && isRecord(document.metadata.component)
|
|
43
|
+
? document.metadata.component
|
|
44
|
+
: null;
|
|
45
|
+
const packageName = String(options.packageName || "");
|
|
46
|
+
const packageVersion = String(options.packageVersion || "");
|
|
47
|
+
if (!component || component.name !== packageName || component.version !== packageVersion) {
|
|
48
|
+
throw new Error("SBOM metadata component does not match the current package identity");
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(document.components) || document.components.length < 1 || document.components.length > 10_000) {
|
|
51
|
+
throw new Error("SBOM components must be a non-empty bounded array");
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(document.dependencies) || document.dependencies.length < 1 || document.dependencies.length > 20_000) {
|
|
54
|
+
throw new Error("SBOM dependencies must be a non-empty bounded array");
|
|
55
|
+
}
|
|
56
|
+
const references = new Set();
|
|
57
|
+
for (const item of document.components) {
|
|
58
|
+
if (!isRecord(item) || typeof item["bom-ref"] !== "string" || !item["bom-ref"]
|
|
59
|
+
|| typeof item.name !== "string" || !item.name || typeof item.version !== "string" || !item.version) {
|
|
60
|
+
throw new Error("SBOM contains an invalid component record");
|
|
61
|
+
}
|
|
62
|
+
if (references.has(item["bom-ref"])) throw new Error("SBOM contains duplicate component references");
|
|
63
|
+
references.add(item["bom-ref"]);
|
|
64
|
+
}
|
|
65
|
+
const rootReference = String(component["bom-ref"] || "");
|
|
66
|
+
if (!rootReference || !document.dependencies.some((entry) => isRecord(entry)
|
|
67
|
+
&& entry.ref === rootReference && Array.isArray(entry.dependsOn))) {
|
|
68
|
+
throw new Error("SBOM dependency graph omits the root package");
|
|
69
|
+
}
|
|
70
|
+
const serialized = JSON.stringify(document);
|
|
71
|
+
for (const path of options.forbiddenPaths || []) {
|
|
72
|
+
const candidate = String(path || "");
|
|
73
|
+
if (candidate && candidate.length >= 2 && serialized.includes(candidate)) {
|
|
74
|
+
throw new Error("SBOM contains a local filesystem path");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
bom_format: document.bomFormat,
|
|
79
|
+
spec_version: document.specVersion,
|
|
80
|
+
package: `${packageName}@${packageVersion}`,
|
|
81
|
+
components: document.components.length,
|
|
82
|
+
dependencies: document.dependencies.length,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function boundedText(value) {
|
|
91
|
+
return String(value || "").replace(/[\r\n]+/g, " ").slice(0, 1000);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const summary = generateAndValidateSbom();
|
|
96
|
+
process.stdout.write(`CycloneDX SBOM validated (${summary.components} components, ${summary.dependencies} dependencies)\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) await main();
|
|
@@ -5,13 +5,16 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
|
|
8
|
+
import { defaultStateRoot, expandHome, selectedWorkspace } from "../src/local/state.mjs";
|
|
9
9
|
import { ensureOwnerOnlyDirectorySync } from "../src/local/secure-file.mjs";
|
|
10
10
|
import { createCandidateRuntimePrefix, pruneInactiveCandidateRuntimes } from "./candidate-runtime-store.mjs";
|
|
11
11
|
import { writePrereleaseActivation } from "./prerelease-activation.mjs";
|
|
12
12
|
import { verifyTarball } from "./release-acceptance.mjs";
|
|
13
13
|
import { parseReleaseVersion } from "./release-channel.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { discoverForegroundDaemonRecovery } from "./foreground-daemon-recovery.mjs";
|
|
15
|
+
import { persistentActivationSpawnOptions, persistentCandidateFailureMessage } from "./persistent-activation-process.mjs";
|
|
16
|
+
import { assertCandidateMatchesCurrentSource, validateCandidateManifest } from "./release-candidate-manifest.mjs";
|
|
17
|
+
import { computePromotionContentDigest } from "./promotion-digest.mjs";
|
|
15
18
|
|
|
16
19
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
20
|
const candidateDirectory = join(root, ".release-candidate");
|
|
@@ -22,9 +25,17 @@ const npmCli = process.env.npm_execpath;
|
|
|
22
25
|
if (!npmCli) fail("candidate startup must run through npm so npm_execpath is available");
|
|
23
26
|
|
|
24
27
|
try {
|
|
25
|
-
const
|
|
26
|
-
validateCandidateManifest(
|
|
27
|
-
|
|
28
|
+
const currentPackage = readJson(join(root, "package.json"), "current package");
|
|
29
|
+
const manifest = validateCandidateManifest(
|
|
30
|
+
readJson(manifestPath, "release candidate manifest"),
|
|
31
|
+
{ packageName: currentPackage.name, packageVersion: currentPackage.version },
|
|
32
|
+
);
|
|
33
|
+
assertCandidateMatchesCurrentSource(manifest, {
|
|
34
|
+
packageName: currentPackage.name,
|
|
35
|
+
packageVersion: currentPackage.version,
|
|
36
|
+
promotionDigest: computePromotionContentDigest(root, { npmCli }),
|
|
37
|
+
});
|
|
38
|
+
const tarball = join(candidateDirectory, manifest.filename);
|
|
28
39
|
verifyTarball(tarball, manifest);
|
|
29
40
|
|
|
30
41
|
const npmVersion = runNpm(["--version"], root).stdout.trim();
|
|
@@ -111,15 +122,21 @@ function activatePersistentCandidate({ manifest, installedPackage, installPrefix
|
|
|
111
122
|
const args = ["activate", ...withoutManagedFlags(forwardedArgs), "--state-dir", stateRoot, "--json"];
|
|
112
123
|
const previous = currentGlobalInstallation(manifest.package_name);
|
|
113
124
|
console.log("Activating the exact prerelease as the persistent login daemon. Portable-root startup does not prompt; a separately provisioned Secure Enclave broker may request one user-presence operation.");
|
|
114
|
-
const result = spawnSync(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
windowsHide: true,
|
|
120
|
-
});
|
|
125
|
+
const result = spawnSync(
|
|
126
|
+
process.execPath,
|
|
127
|
+
[cli, ...args],
|
|
128
|
+
persistentActivationSpawnOptions({ cwd: root, env: process.env }),
|
|
129
|
+
);
|
|
121
130
|
if (result.error) throw result.error;
|
|
122
|
-
if (result.status !== 0)
|
|
131
|
+
if (result.status !== 0) {
|
|
132
|
+
const output = result.stderr || result.stdout;
|
|
133
|
+
const requestedWorkspace = argumentListValue("--workspace", withoutManagedFlags(forwardedArgs))
|
|
134
|
+
|| selectedWorkspace(stateRoot);
|
|
135
|
+
const previousRuntime = discoverForegroundDaemonRecovery({
|
|
136
|
+
output, stateRoot, workspace: requestedWorkspace,
|
|
137
|
+
});
|
|
138
|
+
throw new Error(persistentCandidateFailureMessage(output, { cli, stateRoot, previousRuntime }));
|
|
139
|
+
}
|
|
123
140
|
let activation;
|
|
124
141
|
try { activation = JSON.parse(result.stdout); } catch { throw new Error("persistent candidate activation did not return valid JSON"); }
|
|
125
142
|
if (
|
|
@@ -155,6 +172,7 @@ function activatePersistentCandidate({ manifest, installedPackage, installPrefix
|
|
|
155
172
|
if (previous?.version) console.log(`Rollback baseline retained: globally installed ${previous.version}.`);
|
|
156
173
|
}
|
|
157
174
|
|
|
175
|
+
|
|
158
176
|
function currentGlobalInstallation(packageName) {
|
|
159
177
|
try {
|
|
160
178
|
const globalRoot = runNpm(["root", "--global"], root).stdout.trim();
|
|
@@ -180,6 +198,13 @@ function withoutManagedFlags(args) {
|
|
|
180
198
|
return out;
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
function argumentListValue(name, args) {
|
|
202
|
+
const exact = args.find(value => value.startsWith(`${name}=`));
|
|
203
|
+
if (exact) return exact.slice(name.length + 1);
|
|
204
|
+
const index = args.indexOf(name);
|
|
205
|
+
return index >= 0 ? args[index + 1] || "" : "";
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
function argumentValue(name) {
|
|
184
209
|
const exact = process.argv.find((value) => value.startsWith(`${name}=`));
|
|
185
210
|
if (exact) return exact.slice(name.length + 1);
|
|
@@ -198,6 +223,7 @@ function runNpm(args, cwd) {
|
|
|
198
223
|
encoding: "utf8",
|
|
199
224
|
env: process.env,
|
|
200
225
|
timeout: 300_000,
|
|
226
|
+
killSignal: "SIGKILL",
|
|
201
227
|
windowsHide: true,
|
|
202
228
|
});
|
|
203
229
|
if (result.error) throw result.error;
|
|
@@ -36,6 +36,9 @@ import { createHash } from "node:crypto";
|
|
|
36
36
|
*/
|
|
37
37
|
/**
|
|
38
38
|
* @typedef {object} ProjectionState
|
|
39
|
+
* @property {string} target
|
|
40
|
+
* @property {string} targetDir
|
|
41
|
+
* @property {string} scopeRoot
|
|
39
42
|
* @property {string[]} configFiles
|
|
40
43
|
* @property {InstructionItem | null} builtinInstructions
|
|
41
44
|
* @property {InstructionItem | null} automaticProjectContext
|
|
@@ -47,18 +50,34 @@ import { createHash } from "node:crypto";
|
|
|
47
50
|
/** @param {ProjectionState} state @param {SkillSummary[]} skills */
|
|
48
51
|
export function capabilityFingerprint(state, skills) {
|
|
49
52
|
return sha256(JSON.stringify({
|
|
50
|
-
|
|
53
|
+
target: state.target,
|
|
54
|
+
target_dir: state.targetDir,
|
|
55
|
+
scope_root: state.scopeRoot,
|
|
56
|
+
configs: [...state.configFiles],
|
|
51
57
|
instructions: [
|
|
52
|
-
state.builtinInstructions
|
|
53
|
-
state.automaticProjectContext
|
|
54
|
-
state.modelInstructions
|
|
55
|
-
...state.instructions.map(
|
|
58
|
+
instructionFingerprintItem(state.builtinInstructions),
|
|
59
|
+
instructionFingerprintItem(state.automaticProjectContext),
|
|
60
|
+
instructionFingerprintItem(state.modelInstructions),
|
|
61
|
+
...state.instructions.map(instructionFingerprintItem),
|
|
56
62
|
],
|
|
57
|
-
skills: skills
|
|
58
|
-
|
|
63
|
+
skills: skills
|
|
64
|
+
.map((skill) => [skill.id, skill.name, skill.entrypoint, skill.sourceRoot, skill.sha256])
|
|
65
|
+
.sort((left, right) => String(left[0]).localeCompare(String(right[0]))),
|
|
66
|
+
commands: [...state.commands.values()]
|
|
67
|
+
.map((command) => [
|
|
68
|
+
command.name, command.description, command.argv, command.cwd, command.timeoutSeconds,
|
|
69
|
+
command.allowExtraArgs, command.source, command.sourceType || "", command.script || "",
|
|
70
|
+
])
|
|
71
|
+
.sort((left, right) => String(left[0]).localeCompare(String(right[0]))),
|
|
59
72
|
}));
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
/** @param {InstructionItem | null | undefined} item */
|
|
76
|
+
function instructionFingerprintItem(item) {
|
|
77
|
+
if (!item) return null;
|
|
78
|
+
return [item.source || "", item.path || "", item.scope, item.sha256, item.precedence, item.bytes];
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/** @param {SkillSummary} skill @param {DisplayPath} displayPath */
|
|
63
82
|
export function publicSkill(skill, displayPath) {
|
|
64
83
|
return {
|
|
@@ -134,16 +134,37 @@ export class AgentContextManager {
|
|
|
134
134
|
skillRelevant: Boolean(selected),
|
|
135
135
|
});
|
|
136
136
|
const refresh = capabilityFingerprint(state, discovered.skills);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
const knownRefreshFingerprint = args.known_refresh_fingerprint === undefined
|
|
138
|
+
? ""
|
|
139
|
+
: String(args.known_refresh_fingerprint || "");
|
|
140
|
+
if (knownRefreshFingerprint && !/^[a-f0-9]{64}$/.test(knownRefreshFingerprint)) {
|
|
141
|
+
throw new Error("known_refresh_fingerprint must be a lowercase SHA-256 hex digest");
|
|
142
|
+
}
|
|
143
|
+
const contextUnchanged = Boolean(knownRefreshFingerprint && knownRefreshFingerprint === refresh);
|
|
144
|
+
const staticContext = contextUnchanged ? {} : {
|
|
140
145
|
effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context)),
|
|
141
146
|
builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
|
|
142
147
|
automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
|
|
143
148
|
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path, context) : null,
|
|
144
149
|
instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path, context), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
task,
|
|
153
|
+
target: this.displayPath(state.target, context),
|
|
154
|
+
...staticContext,
|
|
145
155
|
instructions_truncated: state.instructionsTruncated,
|
|
146
|
-
refresh: {
|
|
156
|
+
refresh: {
|
|
157
|
+
strategy: "rescan-on-every-call",
|
|
158
|
+
fingerprint: refresh,
|
|
159
|
+
context_unchanged: contextUnchanged,
|
|
160
|
+
generated_at: new Date().toISOString(),
|
|
161
|
+
},
|
|
162
|
+
context_reuse: {
|
|
163
|
+
static_context_omitted: contextUnchanged,
|
|
164
|
+
omitted_fields: contextUnchanged
|
|
165
|
+
? ["effective_instructions", "builtin_instructions", "automatic_project_context", "model_instructions_file", "instruction_files"]
|
|
166
|
+
: [],
|
|
167
|
+
},
|
|
147
168
|
selected_skill: selectedSkill,
|
|
148
169
|
skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, (value) => this.displayPath(value, context)), score })),
|
|
149
170
|
command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), (value) => this.displayPath(value, context))[0], score })),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { trimAutostartLogs } from "./service.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAINTENANCE_INTERVAL_MS = 15 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keep long-lived background-service logs within the same bounds enforced at
|
|
9
|
+
* startup. The injected scheduler/trim hooks keep the lifecycle deterministic
|
|
10
|
+
* in tests without weakening the production file-safety checks.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} stateRoot
|
|
13
|
+
* @param {{
|
|
14
|
+
* intervalMs?: number,
|
|
15
|
+
* trim?: (stateRoot: string) => void,
|
|
16
|
+
* onError?: (error: unknown) => void,
|
|
17
|
+
* scheduler?: {setInterval: (callback: () => void, delay: number) => any},
|
|
18
|
+
* }} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function startAutostartLogMaintenance(stateRoot, options = {}) {
|
|
21
|
+
const intervalMs = positiveInteger(options.intervalMs, DEFAULT_MAINTENANCE_INTERVAL_MS);
|
|
22
|
+
const trim = typeof options.trim === "function" ? options.trim : trimAutostartLogs;
|
|
23
|
+
const onError = typeof options.onError === "function" ? options.onError : () => {};
|
|
24
|
+
const scheduler = options.scheduler || { setInterval };
|
|
25
|
+
const maintain = () => {
|
|
26
|
+
try { trim(stateRoot); } catch (error) { onError(error); }
|
|
27
|
+
};
|
|
28
|
+
const timer = scheduler.setInterval(maintain, intervalMs);
|
|
29
|
+
timer?.unref?.();
|
|
30
|
+
return { intervalMs, maintain };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveInteger(value, fallback) {
|
|
34
|
+
const number = Number(value);
|
|
35
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
36
|
+
}
|
|
@@ -34,6 +34,11 @@ export class CapabilityObserver {
|
|
|
34
34
|
matched_commands: Array.isArray(result?.command_matches) ? result.command_matches.length : 0,
|
|
35
35
|
matched_applications: Array.isArray(result?.application_matches) ? result.application_matches.length : 0,
|
|
36
36
|
recommended_tools: Array.isArray(result?.recommended_tools) ? [...result.recommended_tools] : [],
|
|
37
|
+
primary_route: result?.execution_routing?.primary_route?.id || null,
|
|
38
|
+
routing_ambiguity: result?.execution_routing?.ambiguity?.level || "none",
|
|
39
|
+
routing_score_gap: Number.isFinite(Number(result?.execution_routing?.ambiguity?.score_gap))
|
|
40
|
+
? Number(result.execution_routing.ambiguity.score_gap)
|
|
41
|
+
: 0,
|
|
37
42
|
refresh_fingerprint: String(result?.refresh?.fingerprint || ""),
|
|
38
43
|
};
|
|
39
44
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* onSettle?: (code: number | null, signal: string | null, source: "close" | "exit_fallback") => void,
|
|
6
|
+
* onFallback?: () => void,
|
|
7
|
+
* schedule?: (callback: () => void, delay: number) => unknown,
|
|
8
|
+
* clearSchedule?: (timer: unknown) => void,
|
|
9
|
+
* fallbackMs?: unknown,
|
|
10
|
+
* readExitState?: () => { code?: number | null, signal?: string | null },
|
|
11
|
+
* }} ChildProcessSettlementOptions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @param {ChildProcessSettlementOptions} [options] */
|
|
15
|
+
export function createChildProcessSettlement(options = {}) {
|
|
16
|
+
if (typeof options.onSettle !== "function") throw new TypeError("child settlement requires onSettle");
|
|
17
|
+
const onSettle = options.onSettle;
|
|
18
|
+
/** @type {(callback: () => void, delay: number) => unknown} */
|
|
19
|
+
const defaultSchedule = (callback, delay) => setTimeout(callback, delay);
|
|
20
|
+
/** @type {(timer: unknown) => void} */
|
|
21
|
+
const defaultClearSchedule = timer => clearTimeout(/** @type {ReturnType<typeof setTimeout>} */ (timer));
|
|
22
|
+
const schedule = typeof options.schedule === "function" ? options.schedule : defaultSchedule;
|
|
23
|
+
const clearSchedule = typeof options.clearSchedule === "function" ? options.clearSchedule : defaultClearSchedule;
|
|
24
|
+
const delay = boundedDelay(options.fallbackMs);
|
|
25
|
+
let settled = false;
|
|
26
|
+
/** @type {unknown} */
|
|
27
|
+
let timer = null;
|
|
28
|
+
let timerSet = false;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {number | null} code
|
|
32
|
+
* @param {string | null} signal
|
|
33
|
+
* @param {"close" | "exit_fallback"} source
|
|
34
|
+
*/
|
|
35
|
+
function settle(code, signal, source) {
|
|
36
|
+
if (settled) return false;
|
|
37
|
+
settled = true;
|
|
38
|
+
if (timerSet) clearSchedule(timer);
|
|
39
|
+
timer = null;
|
|
40
|
+
timerSet = false;
|
|
41
|
+
onSettle(code, signal, source);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {number | null} code @param {string | null} signal */
|
|
46
|
+
function onClose(code, signal) { return settle(code, signal, "close"); }
|
|
47
|
+
|
|
48
|
+
/** @param {number | null} code @param {string | null} signal */
|
|
49
|
+
function onExit(code, signal) {
|
|
50
|
+
if (settled || timerSet) return false;
|
|
51
|
+
timerSet = true;
|
|
52
|
+
timer = schedule(() => {
|
|
53
|
+
timer = null;
|
|
54
|
+
timerSet = false;
|
|
55
|
+
options.onFallback?.();
|
|
56
|
+
const observed = safeExitState(options.readExitState);
|
|
57
|
+
const settledCode = typeof observed.code === "number" && Number.isInteger(observed.code) ? observed.code : code;
|
|
58
|
+
const settledSignal = observed.signal || signal;
|
|
59
|
+
settle(settledCode, settledSignal, "exit_fallback");
|
|
60
|
+
}, delay);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cancel() {
|
|
65
|
+
if (settled) return false;
|
|
66
|
+
settled = true;
|
|
67
|
+
if (timerSet) clearSchedule(timer);
|
|
68
|
+
timer = null;
|
|
69
|
+
timerSet = false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Object.freeze({ onClose, onExit, cancel });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {unknown} value */
|
|
77
|
+
function boundedDelay(value) {
|
|
78
|
+
if (value === undefined) return 1000;
|
|
79
|
+
const parsed = Number(value);
|
|
80
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 10_000) {
|
|
81
|
+
throw new TypeError("child settlement fallback must be between 0 and 10000 milliseconds");
|
|
82
|
+
}
|
|
83
|
+
return Math.floor(parsed);
|
|
84
|
+
}
|
|
85
|
+
export function childExitedBeforeTimeout({ exitCode = null, signalCode = null, processState = "unknown" } = {}) {
|
|
86
|
+
return Number.isInteger(exitCode) || Boolean(signalCode) || processState === "zombie";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {ChildProcessSettlementOptions["readExitState"]} reader @returns {{ code: number | null, signal: string | null }} */
|
|
90
|
+
function safeExitState(reader) {
|
|
91
|
+
if (typeof reader !== "function") return { code: null, signal: null };
|
|
92
|
+
try {
|
|
93
|
+
const value = reader();
|
|
94
|
+
const code = value?.code;
|
|
95
|
+
const signal = value?.signal;
|
|
96
|
+
return {
|
|
97
|
+
code: typeof code === "number" && Number.isInteger(code) ? code : null,
|
|
98
|
+
signal: typeof signal === "string" && signal ? signal : null,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return { code: null, signal: null };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -2,8 +2,9 @@ import { acquireDaemonLockWithTakeover, inspectWorkspaceDaemon } from "./daemon-
|
|
|
2
2
|
import { effectiveLogFormat, effectiveLogLevel } from "./cli-options.mjs";
|
|
3
3
|
import { createLogger } from "./log.mjs";
|
|
4
4
|
import { activatePersistentRuntime } from "./runtime-activation.mjs";
|
|
5
|
-
import { installAutostart, startAutostart, stopAutostart } from "./service.mjs";
|
|
6
|
-
import {
|
|
5
|
+
import { autostartStatus, installAutostart, startAutostart, stopAutostart } from "./service.mjs";
|
|
6
|
+
import { startOwnedServiceRuntime } from "./service-runtime.mjs";
|
|
7
|
+
import { acquireMachineServiceLockWithWait, acquireStartupLockWithWait, daemonLockPathForState, loadState, readDaemonLockOwner } from "./state.mjs";
|
|
7
8
|
import { workerHealth } from "./worker-health.mjs";
|
|
8
9
|
|
|
9
10
|
export function createActivateCommand({
|
|
@@ -42,13 +43,41 @@ export function createActivateCommand({
|
|
|
42
43
|
const result = await activatePersistentRuntime({
|
|
43
44
|
expectedVersion,
|
|
44
45
|
acquireStartupLock: () => acquireStartupLockWithWait(state, { operation: "activate", logger }),
|
|
46
|
+
acquireServiceLock: () => acquireMachineServiceLockWithWait({ operation: "activate", logger }),
|
|
47
|
+
inspectActivationOwnership: async () => {
|
|
48
|
+
const daemon = inspectWorkspaceDaemon(state);
|
|
49
|
+
const owner = daemon.alive && daemon.verified_service_daemon
|
|
50
|
+
? readDaemonLockOwner(daemonLockPathForState(state))
|
|
51
|
+
: null;
|
|
52
|
+
return {
|
|
53
|
+
daemon,
|
|
54
|
+
provider: await autostartStatus(),
|
|
55
|
+
previousRuntime: owner ? { version: owner.version, entryScript: owner.entryScript } : null,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
45
58
|
stopAutostart: () => stopAutostart({ logger: structuredLogger(true) }),
|
|
46
59
|
acquireDaemonLock: () => acquireDaemonLockWithTakeover(state, {
|
|
47
60
|
takeOverServiceOwner: true,
|
|
48
61
|
ownerMetadata: { mode: "foreground", version: expectedVersion },
|
|
49
62
|
logger,
|
|
50
63
|
}),
|
|
51
|
-
prepareRemoteState: () => prepareRemoteState({
|
|
64
|
+
prepareRemoteState: ({ onRemotePrepared } = {}) => prepareRemoteState({
|
|
65
|
+
args: activationArgs,
|
|
66
|
+
workspace,
|
|
67
|
+
state,
|
|
68
|
+
logger,
|
|
69
|
+
onRemotePrepared,
|
|
70
|
+
}),
|
|
71
|
+
repairRemoteState: async ({ onRemotePrepared } = {}) => {
|
|
72
|
+
logger.warn("candidate device authentication was rejected; redeploying the same Worker once with the current device identity");
|
|
73
|
+
return prepareRemoteState({
|
|
74
|
+
args: { ...activationArgs, forceWorker: true, rotateSecrets: false },
|
|
75
|
+
workspace,
|
|
76
|
+
state,
|
|
77
|
+
logger,
|
|
78
|
+
onRemotePrepared,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
52
81
|
createRuntime: ({ daemonLock, readiness }) => createRemoteRuntime({
|
|
53
82
|
args: activationArgs,
|
|
54
83
|
workspace,
|
|
@@ -61,10 +90,17 @@ export function createActivateCommand({
|
|
|
61
90
|
workspace,
|
|
62
91
|
stateRoot: state.paths.stateRoot,
|
|
63
92
|
entryScript: process.argv[1],
|
|
93
|
+
version: expectedVersion,
|
|
64
94
|
logger: structuredLogger(true),
|
|
65
95
|
}),
|
|
66
|
-
startAutostart: () =>
|
|
67
|
-
|
|
96
|
+
startAutostart: () => startOwnedServiceRuntime({ logger: structuredLogger(true) }),
|
|
97
|
+
restorePreviousAutostart: () => startAutostart({ logger: structuredLogger(true) }),
|
|
98
|
+
inspectPreviousAutostart: (identity) => inspectWorkspaceDaemon(state, {
|
|
99
|
+
expectedVersion: identity.version, expectedEntryScript: identity.entryScript,
|
|
100
|
+
}),
|
|
101
|
+
inspectDaemon: async () => inspectWorkspaceDaemon(state, {
|
|
102
|
+
expectedVersion, expectedEntryScript: process.argv[1],
|
|
103
|
+
}),
|
|
68
104
|
checkWorker: async () => workerHealth(state.worker.url, expectedVersion, { expectedWorkerName: state.worker.name }),
|
|
69
105
|
});
|
|
70
106
|
const convergence = result.convergence;
|
|
@@ -76,6 +112,7 @@ export function createActivateCommand({
|
|
|
76
112
|
daemon: convergence.daemon,
|
|
77
113
|
service: result.serviceStart,
|
|
78
114
|
candidate_relay_verified_before_handoff: result.candidateRelayVerified,
|
|
115
|
+
candidate_auth_recovery_redeployed: result.candidateRecoveryRedeployed,
|
|
79
116
|
};
|
|
80
117
|
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
81
118
|
else {
|
|
@@ -4,6 +4,8 @@ import { inspectWorkspaceDaemon, stopWorkspaceServiceDaemon } from "./daemon-pro
|
|
|
4
4
|
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
5
5
|
import { serviceEnvironmentSummary } from "./service-environment.mjs";
|
|
6
6
|
import { scheduleServiceRestart } from "./service-restart-scheduler.mjs";
|
|
7
|
+
import { startOwnedServiceRuntime } from "./service-runtime.mjs";
|
|
8
|
+
import { loadServiceOwner } from "./service-owner.mjs";
|
|
7
9
|
import { loadState, resolveWorkspace, selectedWorkspace } from "./state.mjs";
|
|
8
10
|
|
|
9
11
|
const SERVICE_ACTION_HANDLERS = new Map([
|
|
@@ -21,12 +23,18 @@ export function createServiceCommand(dependencies) {
|
|
|
21
23
|
chooseWorkspace: requiredFunction(dependencies.chooseWorkspace, "chooseWorkspace"),
|
|
22
24
|
stateRootFromArgs: requiredFunction(dependencies.stateRootFromArgs, "stateRootFromArgs"),
|
|
23
25
|
structuredLogger: requiredFunction(dependencies.structuredLogger, "structuredLogger"),
|
|
26
|
+
currentPackageVersion: requiredFunction(dependencies.currentPackageVersion, "currentPackageVersion"),
|
|
24
27
|
service: dependencies.service || defaultService,
|
|
25
28
|
inspectWorkspaceDaemon: dependencies.inspectWorkspaceDaemon || inspectWorkspaceDaemon,
|
|
26
29
|
stopWorkspaceServiceDaemon: dependencies.stopWorkspaceServiceDaemon || stopWorkspaceServiceDaemon,
|
|
27
30
|
stopAndRemoveAutostart: dependencies.stopAndRemoveAutostart || stopAndRemoveAutostart,
|
|
28
31
|
serviceEnvironmentSummary: dependencies.serviceEnvironmentSummary || serviceEnvironmentSummary,
|
|
32
|
+
loadServiceOwner: dependencies.loadServiceOwner || loadServiceOwner,
|
|
29
33
|
scheduleServiceRestart: dependencies.scheduleServiceRestart || scheduleServiceRestart,
|
|
34
|
+
startOwnedServiceRuntime: dependencies.startOwnedServiceRuntime || startOwnedServiceRuntime,
|
|
35
|
+
acquireMachineServiceLockWithWait: requiredFunction(
|
|
36
|
+
dependencies.acquireMachineServiceLockWithWait, "acquireMachineServiceLockWithWait",
|
|
37
|
+
),
|
|
30
38
|
loadState: dependencies.loadState || loadState,
|
|
31
39
|
resolveWorkspace: dependencies.resolveWorkspace || resolveWorkspace,
|
|
32
40
|
selectedWorkspace: dependencies.selectedWorkspace || selectedWorkspace,
|
|
@@ -42,18 +50,45 @@ async function serviceCommand(args, context) {
|
|
|
42
50
|
const handler = SERVICE_ACTION_HANDLERS.get(action);
|
|
43
51
|
if (!handler) throw new Error(`Unknown service action: ${action}`);
|
|
44
52
|
const stateRoot = context.stateRootFromArgs(args);
|
|
45
|
-
|
|
53
|
+
if (!new Set(["install", "start", "stop", "uninstall", "remove"]).has(action)) {
|
|
54
|
+
return handler({ args, stateRoot, service: context.service, context });
|
|
55
|
+
}
|
|
56
|
+
const lock = await context.acquireMachineServiceLockWithWait({ operation: `service-${action}` });
|
|
57
|
+
if (!lock?.acquired || typeof lock.release !== "function") {
|
|
58
|
+
throw new Error("machine-service operation lock could not be acquired");
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await handler({ args, stateRoot, service: context.service, context });
|
|
62
|
+
} finally {
|
|
63
|
+
lock.release();
|
|
64
|
+
}
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
async function serviceStatusAction({ args, stateRoot, service, context }) {
|
|
49
68
|
const status = await service.autostartStatus();
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
let owner = null;
|
|
70
|
+
let ownerProjection = { status: "missing", version: null };
|
|
71
|
+
try {
|
|
72
|
+
owner = context.loadServiceOwner();
|
|
73
|
+
if (owner) ownerProjection = { status: owner.status, version: owner.version };
|
|
74
|
+
} catch {
|
|
75
|
+
ownerProjection = { status: "invalid", version: null, error_class: "invalid_state" };
|
|
76
|
+
}
|
|
77
|
+
const explicitState = hasExplicitServiceTarget(args) ? optionalServiceState(args, stateRoot, context) : null;
|
|
78
|
+
const ownerState = !explicitState && owner?.status === "committed"
|
|
79
|
+
? context.loadState(owner.workspace, { stateDir: owner.stateRoot })
|
|
80
|
+
: null;
|
|
81
|
+
const state = explicitState || ownerState || optionalServiceState(args, stateRoot, context);
|
|
82
|
+
const effectiveStateRoot = ownerState ? owner.stateRoot : stateRoot;
|
|
83
|
+
const workspaceDaemon = state ? context.inspectWorkspaceDaemon(state, ownerState ? {
|
|
84
|
+
expectedVersion: owner.version, expectedEntryScript: owner.entryScript,
|
|
85
|
+
} : {}) : null;
|
|
52
86
|
printServiceResult({
|
|
53
87
|
...status,
|
|
54
88
|
workspace: state?.workspace?.path || null,
|
|
55
89
|
workspace_daemon: workspaceDaemon,
|
|
56
|
-
|
|
90
|
+
service_owner: ownerProjection,
|
|
91
|
+
service_environment: context.serviceEnvironmentSummary(effectiveStateRoot),
|
|
57
92
|
effective_active: Boolean(status.active || workspaceDaemon?.alive),
|
|
58
93
|
orphaned_workspace_daemon: Boolean(status.active === false && workspaceDaemon?.alive && workspaceDaemon?.verified_service_daemon),
|
|
59
94
|
}, context, false);
|
|
@@ -66,10 +101,19 @@ async function serviceInstallAction({ args, stateRoot, service, context }) {
|
|
|
66
101
|
if (!state.worker?.url) {
|
|
67
102
|
throw new Error("No deployed Worker is recorded for this workspace. Run `machine-mcp` once before `machine-mcp service install`.");
|
|
68
103
|
}
|
|
104
|
+
const provider = await service.autostartStatus();
|
|
105
|
+
const daemon = context.inspectWorkspaceDaemon(state);
|
|
106
|
+
if (typeof provider?.active !== "boolean") {
|
|
107
|
+
throw new Error("machine service activity could not be verified before installation");
|
|
108
|
+
}
|
|
109
|
+
if (provider.active || daemon?.alive) {
|
|
110
|
+
throw new Error("refusing to replace the machine service definition while a provider or workspace daemon is active");
|
|
111
|
+
}
|
|
69
112
|
const result = await service.installAutostart({
|
|
70
113
|
workspace,
|
|
71
114
|
stateRoot,
|
|
72
115
|
entryScript: context.entryScript,
|
|
116
|
+
version: context.currentPackageVersion(),
|
|
73
117
|
logger: context.structuredLogger(Boolean(args.quiet)),
|
|
74
118
|
});
|
|
75
119
|
printServiceResult(result, context);
|
|
@@ -77,7 +121,13 @@ async function serviceInstallAction({ args, stateRoot, service, context }) {
|
|
|
77
121
|
|
|
78
122
|
async function serviceStartAction({ args, service, context }) {
|
|
79
123
|
assertGlobalServiceAction(args, "start");
|
|
80
|
-
const
|
|
124
|
+
const logger = context.structuredLogger(Boolean(args.quiet));
|
|
125
|
+
const result = await context.startOwnedServiceRuntime({
|
|
126
|
+
logger,
|
|
127
|
+
readProvider: service.autostartStatus,
|
|
128
|
+
mutateProvider: service.startAutostart,
|
|
129
|
+
stopProvider: service.stopAutostart,
|
|
130
|
+
});
|
|
81
131
|
printServiceResult(result, context);
|
|
82
132
|
}
|
|
83
133
|
|