machine-bridge-mcp 2.0.0 → 3.0.0-beta.9
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 +101 -0
- package/CONTRIBUTING.md +20 -25
- package/README.md +21 -14
- package/SECURITY.md +147 -123
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/service-worker.js +31 -12
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/AUDIT.md +85 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +9 -3
- package/docs/GETTING_STARTED.md +10 -9
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOGGING.md +12 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +33 -22
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +19 -7
- package/docs/THREAT_MODEL.md +138 -67
- package/docs/TOOL_REFERENCE.md +2 -2
- package/docs/UPGRADING.md +102 -18
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +36 -5
- package/scripts/candidate-runtime-store.mjs +38 -0
- package/scripts/check-plan.mjs +19 -0
- package/scripts/check-runner.mjs +4 -1
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/coverage-check.mjs +30 -2
- package/scripts/github-push.mjs +11 -0
- package/scripts/github-release.mjs +38 -7
- package/scripts/install-published-prerelease.mjs +167 -0
- package/scripts/local-release-acceptance.mjs +23 -12
- package/scripts/npm-publication-policy.mjs +16 -0
- package/scripts/prerelease-activation.mjs +79 -0
- package/scripts/privacy-check.mjs +7 -5
- package/scripts/promotion-digest.mjs +121 -0
- package/scripts/publish-npm.mjs +51 -0
- package/scripts/published-release.mjs +80 -0
- package/scripts/release-acceptance.mjs +30 -1
- package/scripts/release-candidate-manifest.mjs +40 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-soak.mjs +237 -0
- package/scripts/start-release-candidate.mjs +113 -7
- package/src/local/account-access.mjs +6 -0
- package/src/local/account-admin.mjs +34 -16
- package/src/local/agent-context.mjs +45 -42
- package/src/local/app-automation.mjs +9 -4
- package/src/local/authority-context.mjs +106 -0
- package/src/local/browser-bridge-http.mjs +4 -1
- package/src/local/browser-bridge.mjs +14 -9
- package/src/local/browser-broker-routes.mjs +4 -0
- package/src/local/browser-broker-server.mjs +5 -4
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-protocol.mjs +9 -4
- package/src/local/browser-operation-service.mjs +7 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-store.mjs +56 -47
- package/src/local/cli-account-admin.mjs +41 -7
- package/src/local/cli-activate.mjs +90 -0
- package/src/local/cli-approval.mjs +14 -58
- package/src/local/cli-local-admin.mjs +5 -15
- package/src/local/cli-options.mjs +11 -7
- package/src/local/cli-service.mjs +35 -5
- package/src/local/cli.mjs +112 -58
- package/src/local/daemon-process.mjs +6 -0
- package/src/local/delegated-process-sandbox.mjs +147 -0
- package/src/local/device-identity.mjs +157 -48
- package/src/local/device-root-provider.mjs +87 -0
- package/src/local/errors.mjs +25 -3
- package/src/local/git-service.mjs +16 -15
- package/src/local/job-runner.mjs +35 -27
- package/src/local/log.mjs +14 -1
- package/src/local/macos-trust-broker.mjs +320 -0
- package/src/local/managed-job-lock.mjs +18 -0
- package/src/local/managed-job-projection.mjs +9 -1
- package/src/local/managed-job-runner.mjs +13 -1
- package/src/local/managed-job-storage.mjs +2 -1
- package/src/local/managed-job-terminal.mjs +138 -0
- package/src/local/managed-jobs.mjs +88 -32
- package/src/local/operation-authorization.mjs +75 -239
- package/src/local/operation-risk.mjs +28 -0
- package/src/local/operation-state-lock.mjs +7 -86
- package/src/local/owner-state-lock.mjs +104 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/process-execution.mjs +21 -9
- package/src/local/process-sessions.mjs +27 -17
- package/src/local/process-tracker.mjs +30 -6
- package/src/local/process-tree-ownership.mjs +59 -0
- package/src/local/process-tree.mjs +22 -18
- package/src/local/relay-connection.mjs +47 -35
- package/src/local/relay-diagnostics.mjs +65 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/runtime-activation.mjs +141 -0
- package/src/local/runtime-diagnostics.mjs +21 -0
- package/src/local/runtime-relay.mjs +9 -8
- package/src/local/runtime-reporting.mjs +8 -3
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +73 -39
- package/src/local/secure-file.mjs +5 -1
- package/src/local/security-audit-log.mjs +204 -0
- package/src/local/service-convergence.mjs +17 -1
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +50 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-status.mjs +47 -0
- package/src/local/service.mjs +142 -43
- package/src/local/state-inventory.mjs +21 -3
- package/src/local/state.mjs +155 -17
- package/src/local/stdio.mjs +5 -0
- package/src/local/tool-executor.mjs +39 -6
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/tools.mjs +1 -0
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service.mjs +20 -0
- package/src/local/worker-deployment.mjs +3 -4
- package/src/local/worker-secret-file.mjs +2 -2
- package/src/local/workspace-file-service.mjs +34 -31
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/server-metadata.json +1 -1
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/account-admin.ts +23 -22
- package/src/worker/authority.ts +9 -2
- package/src/worker/daemon-auth.ts +45 -67
- package/src/worker/daemon-sockets.ts +12 -2
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +151 -0
- package/src/worker/http.ts +47 -16
- package/src/worker/index.ts +53 -16
- package/src/worker/nonce-store.ts +1 -5
- package/src/worker/oauth-client-admin.ts +45 -0
- package/src/worker/oauth-controller.ts +55 -6
- package/src/worker/oauth-refresh-families.ts +21 -7
- package/src/worker/oauth-state.ts +6 -0
- package/src/worker/oauth-tokens.ts +28 -8
- package/src/worker/websocket-protocol.ts +8 -2
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
5
6
|
import { dirname, join, resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { defaultStateRoot, expandHome } from "../src/local/state.mjs";
|
|
9
|
+
import { ensureOwnerOnlyDirectorySync } from "../src/local/secure-file.mjs";
|
|
10
|
+
import { createCandidateRuntimePrefix, pruneInactiveCandidateRuntimes } from "./candidate-runtime-store.mjs";
|
|
11
|
+
import { writePrereleaseActivation } from "./prerelease-activation.mjs";
|
|
7
12
|
import { verifyTarball } from "./release-acceptance.mjs";
|
|
13
|
+
import { parseReleaseVersion } from "./release-channel.mjs";
|
|
14
|
+
import { validateCandidateManifest } from "./release-candidate-manifest.mjs";
|
|
8
15
|
|
|
9
16
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
17
|
const candidateDirectory = join(root, ".release-candidate");
|
|
11
18
|
const manifestPath = join(candidateDirectory, "manifest.json");
|
|
12
|
-
const
|
|
19
|
+
const foregroundInstallPrefix = join(candidateDirectory, "runtime");
|
|
13
20
|
const npmCli = process.env.npm_execpath;
|
|
14
21
|
|
|
15
22
|
if (!npmCli) fail("candidate startup must run through npm so npm_execpath is available");
|
|
16
23
|
|
|
17
24
|
try {
|
|
18
25
|
const manifest = readJson(manifestPath, "release candidate manifest");
|
|
19
|
-
|
|
26
|
+
validateCandidateManifest(manifest);
|
|
20
27
|
const tarball = join(candidateDirectory, String(manifest.filename || ""));
|
|
21
28
|
verifyTarball(tarball, manifest);
|
|
22
29
|
|
|
@@ -25,8 +32,13 @@ try {
|
|
|
25
32
|
throw new Error(`candidate startup requires npm 12 or newer; current ${npmVersion}`);
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const activateService = process.argv.includes("--activate-service");
|
|
36
|
+
const stateRoot = resolve(expandHome(argumentValue("--state-dir") || defaultStateRoot()));
|
|
37
|
+
const installPrefix = activateService
|
|
38
|
+
? createCandidateRuntimePrefix({ stateRoot, version: manifest.package_version, shasum: manifest.shasum })
|
|
39
|
+
: foregroundInstallPrefix;
|
|
40
|
+
if (!activateService) rmSync(installPrefix, { recursive: true, force: true });
|
|
41
|
+
ensureOwnerOnlyDirectorySync(installPrefix);
|
|
30
42
|
runNpm([
|
|
31
43
|
"install",
|
|
32
44
|
"--global",
|
|
@@ -53,10 +65,17 @@ try {
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
if (!allowWorkerDeploy) {
|
|
56
|
-
throw new Error("
|
|
68
|
+
throw new Error("candidate activation may update the configured same-name Worker; rerun with --allow-worker-deploy to authorize that live candidate deployment");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const forwardedArgs = process.argv.slice(2).filter((value) => ![
|
|
72
|
+
"--install-only", "--allow-worker-deploy", "--activate-service",
|
|
73
|
+
].includes(value));
|
|
74
|
+
if (activateService) {
|
|
75
|
+
activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs });
|
|
76
|
+
process.exit(0);
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
const forwardedArgs = process.argv.slice(2).filter((value) => value !== "--install-only" && value !== "--allow-worker-deploy");
|
|
60
79
|
const cli = join(installedPackage, "bin", "machine-mcp.mjs");
|
|
61
80
|
console.log("Authorized live candidate deployment: startup may update the configured same-name Worker when its version or deployment hash differs.");
|
|
62
81
|
console.log("Starting the exact candidate in the foreground. Leave this process running while the coding agent verifies the Worker, relay, and local runtime end to end.");
|
|
@@ -86,6 +105,93 @@ try {
|
|
|
86
105
|
fail(error?.message || error);
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
function activatePersistentCandidate({ manifest, installedPackage, installPrefix, stateRoot, forwardedArgs }) {
|
|
109
|
+
const releaseVersion = parseReleaseVersion(manifest.package_version);
|
|
110
|
+
const cli = join(installedPackage, "bin", "machine-mcp.mjs");
|
|
111
|
+
const args = ["activate", ...withoutManagedFlags(forwardedArgs), "--state-dir", stateRoot, "--json"];
|
|
112
|
+
const previous = currentGlobalInstallation(manifest.package_name);
|
|
113
|
+
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(process.execPath, [cli, ...args], {
|
|
115
|
+
cwd: root,
|
|
116
|
+
env: process.env,
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
timeout: 300_000,
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
if (result.error) throw result.error;
|
|
122
|
+
if (result.status !== 0) throw new Error(`persistent candidate activation failed: ${result.stderr || result.stdout}`);
|
|
123
|
+
let activation;
|
|
124
|
+
try { activation = JSON.parse(result.stdout); } catch { throw new Error("persistent candidate activation did not return valid JSON"); }
|
|
125
|
+
if (
|
|
126
|
+
activation.ok !== true
|
|
127
|
+
|| activation.version !== manifest.package_version
|
|
128
|
+
|| activation.daemon?.version !== manifest.package_version
|
|
129
|
+
|| activation.worker?.health?.version !== manifest.package_version
|
|
130
|
+
) {
|
|
131
|
+
throw new Error("persistent candidate activation did not converge on the exact candidate version");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let recordPath = "";
|
|
135
|
+
if (releaseVersion.prerelease) {
|
|
136
|
+
recordPath = writePrereleaseActivation({
|
|
137
|
+
schema_version: 1,
|
|
138
|
+
package_name: manifest.package_name,
|
|
139
|
+
package_version: manifest.package_version,
|
|
140
|
+
source: "local-candidate",
|
|
141
|
+
shasum: manifest.shasum,
|
|
142
|
+
integrity: manifest.integrity,
|
|
143
|
+
promotion_content_sha256: manifest.promotion_content_sha256,
|
|
144
|
+
activated_at: new Date().toISOString(),
|
|
145
|
+
workspace_hash: workspaceHash(activation.workspace),
|
|
146
|
+
runtime_entry: cli,
|
|
147
|
+
...(previous ? { previous } : {}),
|
|
148
|
+
}, stateRoot);
|
|
149
|
+
}
|
|
150
|
+
const removedRuntimes = pruneInactiveCandidateRuntimes({ stateRoot, activePrefix: installPrefix });
|
|
151
|
+
console.log(`Persistent release candidate activated: ${manifest.package_version}`);
|
|
152
|
+
if (recordPath) console.log(`Activation record: ${recordPath}`);
|
|
153
|
+
if (removedRuntimes.length) console.log(`Removed ${removedRuntimes.length} inactive candidate runtime(s).`);
|
|
154
|
+
console.log("The Worker and login daemon now run the exact candidate. The terminal may close; the coding agent should verify the live deployment through Machine Bridge.");
|
|
155
|
+
if (previous?.version) console.log(`Rollback baseline retained: globally installed ${previous.version}.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function currentGlobalInstallation(packageName) {
|
|
159
|
+
try {
|
|
160
|
+
const globalRoot = runNpm(["root", "--global"], root).stdout.trim();
|
|
161
|
+
const packageRoot = join(globalRoot, packageName);
|
|
162
|
+
const packagePath = join(packageRoot, "package.json");
|
|
163
|
+
if (!existsSync(packagePath)) return null;
|
|
164
|
+
const pkg = readJson(packagePath, "globally installed package");
|
|
165
|
+
return { version: String(pkg.version || ""), entry: join(packageRoot, "bin", "machine-mcp.mjs") };
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function withoutManagedFlags(args) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
174
|
+
const value = args[index];
|
|
175
|
+
if (value === "--json" || value.startsWith("--json=")) continue;
|
|
176
|
+
if (value === "--state-dir") { index += 1; continue; }
|
|
177
|
+
if (value.startsWith("--state-dir=")) continue;
|
|
178
|
+
out.push(value);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function argumentValue(name) {
|
|
184
|
+
const exact = process.argv.find((value) => value.startsWith(`${name}=`));
|
|
185
|
+
if (exact) return exact.slice(name.length + 1);
|
|
186
|
+
const index = process.argv.indexOf(name);
|
|
187
|
+
return index >= 0 ? process.argv[index + 1] : "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function workspaceHash(workspace) {
|
|
191
|
+
const value = process.platform === "win32" ? String(workspace).toLowerCase() : String(workspace);
|
|
192
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
193
|
+
}
|
|
194
|
+
|
|
89
195
|
function runNpm(args, cwd) {
|
|
90
196
|
const result = spawnSync(process.execPath, [npmCli, ...args], {
|
|
91
197
|
cwd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import accessContract from "../shared/access-contract.json" with { type: "json" };
|
|
2
2
|
import { BridgeError } from "./errors.mjs";
|
|
3
|
+
import { buildAuthorityContext } from "./authority-context.mjs";
|
|
3
4
|
import { policyProfile, toolNamesForPolicy, assertToolAllowed } from "./policy.mjs";
|
|
4
5
|
|
|
5
6
|
export const ACCOUNT_ACCESS_REVISION = Number(accessContract.revision);
|
|
@@ -32,6 +33,11 @@ export class AccountAccessGate {
|
|
|
32
33
|
return normalized;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
authority(authorization, daemonPolicy, origin = "relay") {
|
|
37
|
+
const role = origin === "relay" ? normalizeAccountRole(authorization?.role) : "owner";
|
|
38
|
+
return buildAuthorityContext({ authorization: { ...authorization, role }, daemonPolicy, origin });
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
names(role) {
|
|
36
42
|
return accountRoleToolNames(role);
|
|
37
43
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createHash,
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { ADMIN_AUTH_SCHEME, adminAuthTranscript } from "../shared/admin-auth.mjs";
|
|
3
3
|
import { ACCOUNT_ROLES, normalizeAccountRole } from "./account-access.mjs";
|
|
4
|
+
import { encodeDeviceSessionCertificate, signWithDeviceSessionIdentity, validateDeviceSessionIdentity } from "./device-identity.mjs";
|
|
4
5
|
import { BridgeError } from "./errors.mjs";
|
|
5
6
|
|
|
6
7
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -10,15 +11,18 @@ export function generateAccountPassword() {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export class AccountAdminClient {
|
|
13
|
-
constructor({ workerUrl,
|
|
14
|
+
constructor({ workerUrl, sessionIdentity, fetchImpl = fetch }) {
|
|
14
15
|
this.workerUrl = normalizeWorkerUrl(workerUrl);
|
|
15
|
-
|
|
16
|
-
this.adminSecret = adminSecret;
|
|
16
|
+
this.sessionIdentity = validateDeviceSessionIdentity(sessionIdentity);
|
|
17
17
|
this.fetchImpl = fetchImpl;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
list() {
|
|
21
|
-
|
|
20
|
+
list() { return this.request("GET", "/admin/accounts"); }
|
|
21
|
+
listClients() { return this.request("GET", "/admin/clients"); }
|
|
22
|
+
|
|
23
|
+
removeClient({ clientId }) {
|
|
24
|
+
if (!/^mcp_client_[A-Za-z0-9_-]{43}$/.test(String(clientId || ""))) throw new BridgeError("invalid_request", "OAuth client id is invalid");
|
|
25
|
+
return this.request("DELETE", "/admin/clients", { client_id: clientId });
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
create({ name, role, password, displayName = "" }) {
|
|
@@ -61,7 +65,7 @@ export class AccountAdminClient {
|
|
|
61
65
|
async request(method, pathname, body) {
|
|
62
66
|
const serializedBody = body === undefined ? "" : JSON.stringify(body);
|
|
63
67
|
const headers = accountAdminRequestHeaders({
|
|
64
|
-
|
|
68
|
+
sessionIdentity: this.sessionIdentity,
|
|
65
69
|
origin: this.workerUrl,
|
|
66
70
|
method,
|
|
67
71
|
pathname,
|
|
@@ -89,25 +93,39 @@ export class AccountAdminClient {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
export function accountAdminRequestHeaders({
|
|
97
|
+
sessionIdentity,
|
|
98
|
+
origin,
|
|
99
|
+
method,
|
|
100
|
+
pathname,
|
|
101
|
+
body = "",
|
|
102
|
+
now = Date.now(),
|
|
103
|
+
nonce = randomBytes(24).toString("base64url"),
|
|
104
|
+
}) {
|
|
105
|
+
validateDeviceSessionIdentity(sessionIdentity, now);
|
|
95
106
|
const issuedAt = Math.floor(Number(now) / 1000);
|
|
96
107
|
const bodyHash = createHash("sha256").update(String(body)).digest("hex");
|
|
97
|
-
const transcript = adminAuthTranscript({
|
|
98
|
-
|
|
108
|
+
const transcript = adminAuthTranscript({
|
|
109
|
+
origin,
|
|
110
|
+
method: String(method).toUpperCase(),
|
|
111
|
+
pathname,
|
|
112
|
+
bodyHash,
|
|
113
|
+
keyId: sessionIdentity.keyId,
|
|
114
|
+
issuedAt,
|
|
115
|
+
nonce,
|
|
116
|
+
});
|
|
99
117
|
return {
|
|
100
118
|
"X-Bridge-Admin-Scheme": ADMIN_AUTH_SCHEME,
|
|
101
119
|
"X-Bridge-Admin-Time": String(issuedAt),
|
|
102
120
|
"X-Bridge-Admin-Nonce": nonce,
|
|
103
121
|
"X-Bridge-Admin-Body-SHA256": bodyHash,
|
|
104
|
-
"X-Bridge-Admin-
|
|
122
|
+
"X-Bridge-Admin-Key": sessionIdentity.keyId,
|
|
123
|
+
"X-Bridge-Admin-Signature": signWithDeviceSessionIdentity(sessionIdentity, transcript),
|
|
124
|
+
"X-Bridge-Device-Certificate": encodeDeviceSessionCertificate(sessionIdentity),
|
|
105
125
|
};
|
|
106
126
|
}
|
|
107
127
|
|
|
108
|
-
export function accountRoleNames() {
|
|
109
|
-
return Object.keys(ACCOUNT_ROLES);
|
|
110
|
-
}
|
|
128
|
+
export function accountRoleNames() { return Object.keys(ACCOUNT_ROLES); }
|
|
111
129
|
|
|
112
130
|
function normalizeWorkerUrl(value) {
|
|
113
131
|
const url = new URL(String(value || ""));
|
|
@@ -29,9 +29,10 @@ const MAX_INSTRUCTION_FILE_BYTES = 512 * 1024;
|
|
|
29
29
|
const MAX_INSTRUCTION_FILES = 64;
|
|
30
30
|
|
|
31
31
|
export class AgentContextManager {
|
|
32
|
-
constructor({ workspace, policy, displayPath, resolveExistingPath, throwIfCancelled = () => {}, home = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "" }) {
|
|
32
|
+
constructor({ workspace, policy, policyForContext = null, displayPath, resolveExistingPath, throwIfCancelled = () => {}, home = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "" }) {
|
|
33
33
|
this.workspace = resolve(workspace);
|
|
34
34
|
this.policy = policy || {};
|
|
35
|
+
this.policyForContext = typeof policyForContext === "function" ? policyForContext : () => this.policy;
|
|
35
36
|
this.displayPath = displayPath;
|
|
36
37
|
this.resolveExistingPath = resolveExistingPath;
|
|
37
38
|
this.throwIfCancelled = throwIfCancelled;
|
|
@@ -44,23 +45,23 @@ export class AgentContextManager {
|
|
|
44
45
|
const includeContent = args.include_instruction_content !== false;
|
|
45
46
|
const skillLimit = clampInteger(args.max_skills, 100, 1, MAX_SKILL_RESULTS);
|
|
46
47
|
const discoveredSkills = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
47
|
-
const contextSkills = contextSkillSummaries(discoveredSkills.skills, this.displayPath, skillLimit, MAX_CONTEXT_SKILL_SUMMARY_CHARS);
|
|
48
|
+
const contextSkills = contextSkillSummaries(discoveredSkills.skills, (value) => this.displayPath(value, context), skillLimit, MAX_CONTEXT_SKILL_SUMMARY_CHARS);
|
|
48
49
|
const result = {
|
|
49
|
-
target: this.displayPath(state.target),
|
|
50
|
-
scope_root: this.displayPath(state.scopeRoot),
|
|
50
|
+
target: this.displayPath(state.target, context),
|
|
51
|
+
scope_root: this.displayPath(state.scopeRoot, context),
|
|
51
52
|
precedence: "built-in defaults, automatic project facts, user-global guidance, then scope root to target directory; explicit files loaded later have higher precedence",
|
|
52
|
-
config_files: state.configFiles.map((file) => this.displayPath(file)),
|
|
53
|
+
config_files: state.configFiles.map((file) => this.displayPath(file, context)),
|
|
53
54
|
builtin_instructions: publicVirtualInstruction(state.builtinInstructions, includeContent),
|
|
54
55
|
automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, includeContent),
|
|
55
56
|
model_instructions_file: state.modelInstructions ? {
|
|
56
|
-
path: this.displayPath(state.modelInstructions.path),
|
|
57
|
+
path: this.displayPath(state.modelInstructions.path, context),
|
|
57
58
|
bytes: state.modelInstructions.bytes,
|
|
58
59
|
sha256: state.modelInstructions.sha256,
|
|
59
60
|
...(includeContent ? { content: state.modelInstructions.content } : {}),
|
|
60
61
|
} : null,
|
|
61
62
|
instruction_files: state.instructions.map((item) => ({
|
|
62
63
|
scope: item.scope,
|
|
63
|
-
path: this.displayPath(item.path),
|
|
64
|
+
path: this.displayPath(item.path, context),
|
|
64
65
|
bytes: item.bytes,
|
|
65
66
|
sha256: item.sha256,
|
|
66
67
|
precedence: item.precedence,
|
|
@@ -68,9 +69,9 @@ export class AgentContextManager {
|
|
|
68
69
|
})),
|
|
69
70
|
skills: contextSkills.skills,
|
|
70
71
|
skills_truncated: discoveredSkills.truncated || contextSkills.truncated,
|
|
71
|
-
skill_warnings: publicSkillWarnings(discoveredSkills.warnings, this.displayPath),
|
|
72
|
+
skill_warnings: publicSkillWarnings(discoveredSkills.warnings, (value) => this.displayPath(value, context)),
|
|
72
73
|
instructions_truncated: state.instructionsTruncated,
|
|
73
|
-
commands: publicCommands(state.commands, this.displayPath),
|
|
74
|
+
commands: publicCommands(state.commands, (value) => this.displayPath(value, context)),
|
|
74
75
|
guidance: [
|
|
75
76
|
"Apply built-in instructions and automatic project context as lower-precedence defaults; explicit global/project instruction files loaded later take precedence.",
|
|
76
77
|
"Treat instruction_files as authoritative workspace guidance in the returned precedence order.",
|
|
@@ -78,7 +79,7 @@ export class AgentContextManager {
|
|
|
78
79
|
"Prefer run_local_command for registered repeatable commands. Use run_process or exec_command only when no registered command fits and policy permits it.",
|
|
79
80
|
],
|
|
80
81
|
};
|
|
81
|
-
if (includeContent) result.effective_instructions = renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath);
|
|
82
|
+
if (includeContent) result.effective_instructions = renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context));
|
|
82
83
|
return result;
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -86,11 +87,11 @@ export class AgentContextManager {
|
|
|
86
87
|
const state = await this.discoverState(args.path || ".", context);
|
|
87
88
|
const fingerprint = capabilityFingerprint(state, []);
|
|
88
89
|
return {
|
|
89
|
-
target: this.displayPath(state.target),
|
|
90
|
-
instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
|
|
90
|
+
target: this.displayPath(state.target, context),
|
|
91
|
+
instructions: renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context)),
|
|
91
92
|
builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
|
|
92
93
|
automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
|
|
93
|
-
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
|
|
94
|
+
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path, context) : null,
|
|
94
95
|
capability_refresh: {
|
|
95
96
|
strategy: "resolve_task_capabilities-rescans-on-every-call",
|
|
96
97
|
instruction_and_command_fingerprint: fingerprint,
|
|
@@ -125,7 +126,7 @@ export class AgentContextManager {
|
|
|
125
126
|
let selectedSkill = null;
|
|
126
127
|
if (selected && args.include_selected_skill !== false) {
|
|
127
128
|
const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
|
|
128
|
-
selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
|
|
129
|
+
selectedSkill = { ...publicSkill(selected, (value) => this.displayPath(value, context)), instructions: content.text };
|
|
129
130
|
}
|
|
130
131
|
const recommendedTools = recommendTools(task, {
|
|
131
132
|
commandsAvailable: state.commands.size > 0,
|
|
@@ -135,20 +136,20 @@ export class AgentContextManager {
|
|
|
135
136
|
const refresh = capabilityFingerprint(state, discovered.skills);
|
|
136
137
|
return {
|
|
137
138
|
task,
|
|
138
|
-
target: this.displayPath(state.target),
|
|
139
|
-
effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
|
|
139
|
+
target: this.displayPath(state.target, context),
|
|
140
|
+
effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), (value) => this.displayPath(value, context)),
|
|
140
141
|
builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
|
|
141
142
|
automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
|
|
142
|
-
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
|
|
143
|
-
instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
|
|
143
|
+
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path, context) : null,
|
|
144
|
+
instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path, context), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
|
|
144
145
|
instructions_truncated: state.instructionsTruncated,
|
|
145
146
|
refresh: { strategy: "rescan-on-every-call", fingerprint: refresh, generated_at: new Date().toISOString() },
|
|
146
147
|
selected_skill: selectedSkill,
|
|
147
|
-
skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, this.displayPath), score })),
|
|
148
|
-
command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), this.displayPath)[0], score })),
|
|
148
|
+
skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, (value) => this.displayPath(value, context)), score })),
|
|
149
|
+
command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), (value) => this.displayPath(value, context))[0], score })),
|
|
149
150
|
recommended_tools: recommendedTools,
|
|
150
151
|
host_semantics: "Machine Bridge can discover, rank, and load capabilities automatically. The MCP host remains responsible for deciding whether to call the recommended tools.",
|
|
151
|
-
warnings: publicSkillWarnings(discovered.warnings, this.displayPath),
|
|
152
|
+
warnings: publicSkillWarnings(discovered.warnings, (value) => this.displayPath(value, context)),
|
|
152
153
|
truncated: discovered.truncated,
|
|
153
154
|
};
|
|
154
155
|
}
|
|
@@ -171,11 +172,11 @@ export class AgentContextManager {
|
|
|
171
172
|
maxResults: clampInteger(args.max_results, 100, 1, MAX_SKILL_RESULTS),
|
|
172
173
|
}, context);
|
|
173
174
|
return {
|
|
174
|
-
target: this.displayPath(state.target),
|
|
175
|
-
scope_root: this.displayPath(state.scopeRoot),
|
|
176
|
-
skill_roots: state.skillRoots.map((root) => this.displayPath(root)),
|
|
177
|
-
skills: result.skills.map((skill) => publicSkill(skill, this.displayPath)),
|
|
178
|
-
warnings: publicSkillWarnings(result.warnings, this.displayPath),
|
|
175
|
+
target: this.displayPath(state.target, context),
|
|
176
|
+
scope_root: this.displayPath(state.scopeRoot, context),
|
|
177
|
+
skill_roots: state.skillRoots.map((root) => this.displayPath(root, context)),
|
|
178
|
+
skills: result.skills.map((skill) => publicSkill(skill, (value) => this.displayPath(value, context))),
|
|
179
|
+
warnings: publicSkillWarnings(result.warnings, (value) => this.displayPath(value, context)),
|
|
179
180
|
truncated: result.truncated,
|
|
180
181
|
};
|
|
181
182
|
}
|
|
@@ -184,14 +185,14 @@ export class AgentContextManager {
|
|
|
184
185
|
const requested = requiredString(args.skill, "skill");
|
|
185
186
|
const state = await this.discoverState(args.path || ".", context);
|
|
186
187
|
const result = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
187
|
-
const matches = result.skills.filter((skill) => skill.id === requested || skill.name === requested || this.displayPath(skill.entrypoint) === requested);
|
|
188
|
+
const matches = result.skills.filter((skill) => skill.id === requested || skill.name === requested || this.displayPath(skill.entrypoint, context) === requested);
|
|
188
189
|
if (!matches.length) throw new Error(`local skill not found: ${requested}`);
|
|
189
190
|
if (matches.length > 1) throw new Error(`local skill name is ambiguous; use its id: ${requested}`);
|
|
190
191
|
const skill = matches[0];
|
|
191
192
|
const content = await readRegularUtf8(skill.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
|
|
192
193
|
const inventory = await listSkillFiles(skill.directory, clampInteger(args.max_files, 200, 1, MAX_SKILL_FILES), context, this.throwIfCancelled);
|
|
193
194
|
return {
|
|
194
|
-
skill: publicSkill(skill, this.displayPath),
|
|
195
|
+
skill: publicSkill(skill, (value) => this.displayPath(value, context)),
|
|
195
196
|
instructions: content.text,
|
|
196
197
|
files: inventory.files,
|
|
197
198
|
files_truncated: inventory.truncated,
|
|
@@ -202,9 +203,9 @@ export class AgentContextManager {
|
|
|
202
203
|
async listLocalCommands(args = {}, context = {}) {
|
|
203
204
|
const state = await this.discoverState(args.path || ".", context);
|
|
204
205
|
return {
|
|
205
|
-
target: this.displayPath(state.target),
|
|
206
|
-
scope_root: this.displayPath(state.scopeRoot),
|
|
207
|
-
commands: publicCommands(state.commands, this.displayPath),
|
|
206
|
+
target: this.displayPath(state.target, context),
|
|
207
|
+
scope_root: this.displayPath(state.scopeRoot, context),
|
|
208
|
+
commands: publicCommands(state.commands, (value) => this.displayPath(value, context)),
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
@@ -227,14 +228,15 @@ export class AgentContextManager {
|
|
|
227
228
|
|
|
228
229
|
async discoverState(inputPath, context = {}) {
|
|
229
230
|
this.throwIfCancelled(context);
|
|
231
|
+
const effectivePolicy = this.policyForContext(context);
|
|
230
232
|
this.workspace = await realpath(this.workspace);
|
|
231
|
-
const target = await realpath(await this.resolveExistingPath(inputPath));
|
|
233
|
+
const target = await realpath(await this.resolveExistingPath(inputPath, context));
|
|
232
234
|
const targetInfo = await stat(target);
|
|
233
235
|
const targetDir = targetInfo.isDirectory() ? target : dirname(target);
|
|
234
236
|
const scopeRoot = await findScopeRoot({
|
|
235
237
|
targetDir,
|
|
236
238
|
workspace: this.workspace,
|
|
237
|
-
unrestricted:
|
|
239
|
+
unrestricted: effectivePolicy.unrestrictedPaths === true,
|
|
238
240
|
});
|
|
239
241
|
const directories = directoriesBetween(scopeRoot, targetDir);
|
|
240
242
|
const state = {
|
|
@@ -243,7 +245,7 @@ export class AgentContextManager {
|
|
|
243
245
|
scopeRoot,
|
|
244
246
|
instructionFiles: [...DEFAULT_INSTRUCTION_FILES],
|
|
245
247
|
instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
|
|
246
|
-
skillRoots: defaultSkillRoots(directories, this.home, this.codexHome,
|
|
248
|
+
skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, effectivePolicy.unrestrictedPaths === true),
|
|
247
249
|
commands: new Map(),
|
|
248
250
|
builtinInstructionsEnabled: true,
|
|
249
251
|
automaticProjectContextEnabled: true,
|
|
@@ -264,7 +266,7 @@ export class AgentContextManager {
|
|
|
264
266
|
const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
|
|
265
267
|
const config = await readOptionalConfig(globalConfig, this.home, false);
|
|
266
268
|
if (config) {
|
|
267
|
-
if (
|
|
269
|
+
if (effectivePolicy.unrestrictedPaths === true) this.applyConfig(state, config, globalConfig, this.home, { global: true, unrestricted: true });
|
|
268
270
|
else {
|
|
269
271
|
state.configFiles.push(globalConfig);
|
|
270
272
|
if (config.builtinInstructions !== null) state.builtinInstructionsEnabled = config.builtinInstructions;
|
|
@@ -284,20 +286,20 @@ export class AgentContextManager {
|
|
|
284
286
|
|
|
285
287
|
if (this.home) {
|
|
286
288
|
if (state.modelInstructionsFile) await this.collectModelInstructions(state, context);
|
|
287
|
-
if (
|
|
289
|
+
if (effectivePolicy.unrestrictedPaths === true && this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
for (const directory of directories) {
|
|
291
293
|
this.throwIfCancelled(context);
|
|
292
294
|
const configPath = join(directory, CONFIG_RELATIVE_PATH);
|
|
293
295
|
const config = await readOptionalConfig(configPath, directory, true);
|
|
294
|
-
if (config) this.applyConfig(state, config, configPath, directory, { global: false });
|
|
296
|
+
if (config) this.applyConfig(state, config, configPath, directory, { global: false, unrestricted: effectivePolicy.unrestrictedPaths === true });
|
|
295
297
|
await this.collectDirectoryInstruction(state, directory, context, "project");
|
|
296
298
|
}
|
|
297
299
|
return state;
|
|
298
300
|
}
|
|
299
301
|
|
|
300
|
-
applyConfig(state, config, configPath, baseDir, { global = false } = {}) {
|
|
302
|
+
applyConfig(state, config, configPath, baseDir, { global = false, unrestricted = false } = {}) {
|
|
301
303
|
state.configFiles.push(configPath);
|
|
302
304
|
if (config.builtinInstructions !== null) {
|
|
303
305
|
if (!global) throw new Error(`builtin_instructions is only allowed in the global agent config: ${configPath}`);
|
|
@@ -317,14 +319,14 @@ export class AgentContextManager {
|
|
|
317
319
|
if (state.instructionBytes >= state.instructionMaxBytes) state.instructionsTruncated = true;
|
|
318
320
|
}
|
|
319
321
|
if (config.skillRoots) {
|
|
320
|
-
state.skillRoots = config.skillRoots.map((value) => resolveConfiguredPath(value, baseDir, this.home, this.workspace,
|
|
322
|
+
state.skillRoots = config.skillRoots.map((value) => resolveConfiguredPath(value, baseDir, this.home, this.workspace, unrestricted));
|
|
321
323
|
}
|
|
322
324
|
for (const [name, definition] of config.commands) {
|
|
323
325
|
if (definition === null) {
|
|
324
326
|
state.commands.delete(name);
|
|
325
327
|
continue;
|
|
326
328
|
}
|
|
327
|
-
const cwd = resolveConfiguredPath(definition.cwd, baseDir, this.home, this.workspace,
|
|
329
|
+
const cwd = resolveConfiguredPath(definition.cwd, baseDir, this.home, this.workspace, unrestricted);
|
|
328
330
|
state.commands.set(name, {
|
|
329
331
|
name,
|
|
330
332
|
description: definition.description,
|
|
@@ -379,13 +381,14 @@ export class AgentContextManager {
|
|
|
379
381
|
}
|
|
380
382
|
|
|
381
383
|
async discoverSkills(state, options = {}, context = {}) {
|
|
384
|
+
const effectivePolicy = this.policyForContext(context);
|
|
382
385
|
return discoverLocalSkills({
|
|
383
386
|
skillRoots: state.skillRoots,
|
|
384
387
|
query: String(options.query || ""),
|
|
385
388
|
maxResults: clampInteger(options.maxResults, 100, 1, MAX_SKILL_RESULTS),
|
|
386
389
|
workspace: this.workspace,
|
|
387
|
-
unrestricted:
|
|
388
|
-
displayPath: this.displayPath,
|
|
390
|
+
unrestricted: effectivePolicy.unrestrictedPaths === true,
|
|
391
|
+
displayPath: (value) => this.displayPath(value, context),
|
|
389
392
|
context,
|
|
390
393
|
throwIfCancelled: this.throwIfCancelled,
|
|
391
394
|
});
|
|
@@ -102,9 +102,9 @@ export class AppAutomationManager {
|
|
|
102
102
|
if (target) argv.push(target);
|
|
103
103
|
command = { cmd: "open", argv };
|
|
104
104
|
} else if (this.platform === "win32") {
|
|
105
|
-
const
|
|
106
|
-
const targetFragment = target ? ` -ArgumentList
|
|
107
|
-
command = { cmd: "powershell.exe", argv: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath
|
|
105
|
+
const fileLiteral = powershellSingleQuotedLiteral(resolvedApplication);
|
|
106
|
+
const targetFragment = target ? ` -ArgumentList ${powershellSingleQuotedLiteral(target)}` : "";
|
|
107
|
+
command = { cmd: "powershell.exe", argv: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath ${fileLiteral}${targetFragment}`] };
|
|
108
108
|
} else if (resolvedApplication.toLowerCase().endsWith(".desktop")) {
|
|
109
109
|
command = { cmd: "gio", argv: ["launch", resolvedApplication, ...(target ? [target] : [])] };
|
|
110
110
|
} else {
|
|
@@ -113,7 +113,8 @@ export class AppAutomationManager {
|
|
|
113
113
|
: { cmd: resolvedApplication, argv: [] };
|
|
114
114
|
}
|
|
115
115
|
const result = await this.runProcess(command.cmd, command.argv, clampInt(args.timeout_seconds, 30, 1, 120) * 1000, false, 256 * 1024, context, undefined, null);
|
|
116
|
-
|
|
116
|
+
const publicResolvedApplication = /[\\/]/.test(resolvedApplication) ? this.displayPath(resolvedApplication) : resolvedApplication;
|
|
117
|
+
return { application, resolved_application: publicResolvedApplication, target, platform: this.platform, ...result };
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async inspectApplication(args = {}, context = {}) {
|
|
@@ -417,3 +418,7 @@ function main() {
|
|
|
417
418
|
}
|
|
418
419
|
(() => { try { return JSON.stringify(main()); } catch (error) { return JSON.stringify({ error: String(error.message || error) }); } })()
|
|
419
420
|
`;
|
|
421
|
+
|
|
422
|
+
export function powershellSingleQuotedLiteral(value) {
|
|
423
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
424
|
+
}
|