machine-bridge-mcp 2.0.0 → 3.0.0-beta.11
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 +120 -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 +28 -13
- package/docs/AUDIT.md +101 -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 +14 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +34 -23
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +21 -9
- 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 +37 -6
- 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 +87 -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 +119 -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 +18 -44
- 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-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +85 -95
- 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/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.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 +23 -4
- 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/observability.ts +4 -2
- package/src/worker/websocket-protocol.ts +8 -2
package/src/local/state.mjs
CHANGED
|
@@ -7,9 +7,11 @@ import serverMetadata from "../shared/server-metadata.json" with { type: "json"
|
|
|
7
7
|
import { replaceFileSync } from "./atomic-fs.mjs";
|
|
8
8
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
9
9
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
10
|
-
import { createDeviceIdentity
|
|
10
|
+
import { createDeviceIdentity } from "./device-identity.mjs";
|
|
11
|
+
import { validateDeviceRootIdentity } from "./device-root-provider.mjs";
|
|
11
12
|
import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
|
|
12
13
|
import { chmodRegularFileSync, ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
14
|
+
import { isPlainRecord } from "./records.mjs";
|
|
13
15
|
|
|
14
16
|
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
15
17
|
export const appName = String(serverMetadata.name);
|
|
@@ -17,6 +19,7 @@ const STATE_MARKER = ".machine-bridge-mcp-state";
|
|
|
17
19
|
const STATE_MARKER_SCHEMA = 2;
|
|
18
20
|
export const STATE_SCHEMA_VERSION = 6;
|
|
19
21
|
const GLOBAL_CONFIG_SCHEMA = 1;
|
|
22
|
+
const RECOVERY_MARKER_SCHEMA = 1;
|
|
20
23
|
const CORRUPT_RECOVERY = Symbol("corrupt-recovery");
|
|
21
24
|
const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
|
|
22
25
|
const MAX_LOCK_BYTES = 64 * 1024;
|
|
@@ -66,7 +69,7 @@ export function loadGlobalConfig(stateRoot = defaultStateRoot()) {
|
|
|
66
69
|
const file = configPath(root);
|
|
67
70
|
if (!existsSync(file)) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
|
|
68
71
|
ownerOnlyFile(file);
|
|
69
|
-
const config = readJsonObjectOrBackup(file);
|
|
72
|
+
const config = readJsonObjectOrBackup(file, { allowEmptyRecovery: true });
|
|
70
73
|
if (config[CORRUPT_RECOVERY]) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
|
|
71
74
|
if (config.schemaVersion !== GLOBAL_CONFIG_SCHEMA) {
|
|
72
75
|
throw new Error("global configuration schema is obsolete; remove the state root and initialize the current version");
|
|
@@ -149,6 +152,16 @@ function assertStateRootSeparatedFromWorkspace(stateRoot, workspace) {
|
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
|
|
155
|
+
function samePotentialPathIdentity(left, right) {
|
|
156
|
+
try {
|
|
157
|
+
const a = canonicalizePotentialPath(String(left));
|
|
158
|
+
const b = canonicalizePotentialPath(String(right));
|
|
159
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
function sameWorkspaceIdentity(left, right) {
|
|
153
166
|
try {
|
|
154
167
|
const a = resolveWorkspace(left);
|
|
@@ -169,13 +182,20 @@ export function loadState(workspace, options = {}) {
|
|
|
169
182
|
const profileDir = profileDirForWorkspace(canonicalWorkspace, stateRoot);
|
|
170
183
|
const statePath = path.join(profileDir, "state.json");
|
|
171
184
|
ensureOwnerOnlyDir(profileDir);
|
|
185
|
+
const recoveryPath = recoveryMarkerPath(statePath);
|
|
186
|
+
const recoveryPending = existsSync(recoveryPath);
|
|
187
|
+
const recoveryMarker = recoveryPending ? readRecoveryMarker(recoveryPath, statePath) : null;
|
|
172
188
|
let state = {};
|
|
173
189
|
if (existsSync(statePath)) {
|
|
174
190
|
ownerOnlyFile(statePath);
|
|
175
|
-
state = readJsonObjectOrBackup(statePath);
|
|
176
|
-
if (
|
|
191
|
+
state = readJsonObjectOrBackup(statePath, { recoveryPath });
|
|
192
|
+
if (state.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
177
193
|
throw new Error("workspace state schema is obsolete; remove the state root and initialize the current version");
|
|
178
194
|
}
|
|
195
|
+
assertWorkspaceStateEnvelope(state, { canonicalWorkspace, stateRoot, profileDir, statePath });
|
|
196
|
+
if (recoveryPending) unlinkSync(recoveryPath);
|
|
197
|
+
} else if (recoveryPending) {
|
|
198
|
+
throw recoveryRequiredError(recoveryMarker);
|
|
179
199
|
}
|
|
180
200
|
state.schemaVersion = STATE_SCHEMA_VERSION;
|
|
181
201
|
state.workspace = {
|
|
@@ -185,17 +205,57 @@ export function loadState(workspace, options = {}) {
|
|
|
185
205
|
};
|
|
186
206
|
state.paths = { stateRoot, profileDir, statePath };
|
|
187
207
|
state.worker ||= {};
|
|
208
|
+
delete state.worker.accountAdminSecret;
|
|
188
209
|
state.policy ||= {};
|
|
189
210
|
state.resources ||= {};
|
|
190
211
|
return state;
|
|
191
212
|
}
|
|
192
213
|
|
|
193
214
|
export function saveState(state) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
ensureOwnerOnlyDir(
|
|
198
|
-
atomicWriteJson(statePath, { ...state });
|
|
215
|
+
const envelope = assertWorkspaceStateEnvelope(state);
|
|
216
|
+
assertNoForeignMaintenance(envelope.stateRoot);
|
|
217
|
+
assertValidStateMarker(path.join(envelope.stateRoot, STATE_MARKER));
|
|
218
|
+
ensureOwnerOnlyDir(envelope.profileDir);
|
|
219
|
+
atomicWriteJson(envelope.statePath, { ...state });
|
|
220
|
+
const recoveryPath = recoveryMarkerPath(envelope.statePath);
|
|
221
|
+
if (existsSync(recoveryPath)) unlinkSync(recoveryPath);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function assertWorkspaceStateEnvelope(state, expected = {}) {
|
|
225
|
+
if (!isPlainRecord(state) || state.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
226
|
+
throw new Error("workspace state envelope is incomplete or uses the wrong schema");
|
|
227
|
+
}
|
|
228
|
+
if (!isPlainRecord(state.workspace) || typeof state.workspace.path !== "string" || typeof state.workspace.hash !== "string") {
|
|
229
|
+
throw new Error("workspace state envelope is missing workspace identity");
|
|
230
|
+
}
|
|
231
|
+
if (!isPlainRecord(state.paths) || !isPlainRecord(state.worker) || !isPlainRecord(state.policy) || !isPlainRecord(state.resources)) {
|
|
232
|
+
throw new Error("workspace state envelope is missing required object fields");
|
|
233
|
+
}
|
|
234
|
+
const canonicalWorkspace = resolveWorkspace(expected.canonicalWorkspace || state.workspace.path);
|
|
235
|
+
const expectedHash = workspaceHash(canonicalWorkspace);
|
|
236
|
+
if (!sameWorkspaceIdentity(state.workspace.path, canonicalWorkspace) || state.workspace.hash !== expectedHash) {
|
|
237
|
+
throw new Error("workspace state envelope does not match the selected workspace");
|
|
238
|
+
}
|
|
239
|
+
if (typeof state.paths.stateRoot !== "string" || !state.paths.stateRoot
|
|
240
|
+
|| typeof state.paths.profileDir !== "string" || !state.paths.profileDir
|
|
241
|
+
|| typeof state.paths.statePath !== "string" || !state.paths.statePath) {
|
|
242
|
+
throw new Error("workspace state envelope is missing canonical state paths");
|
|
243
|
+
}
|
|
244
|
+
const stateRoot = canonicalizePotentialPath(expected.stateRoot || state.paths.stateRoot);
|
|
245
|
+
const profileDir = canonicalizePotentialPath(expected.profileDir || state.paths.profileDir);
|
|
246
|
+
const statePath = canonicalizePotentialPath(expected.statePath || state.paths.statePath);
|
|
247
|
+
const expectedProfileDir = canonicalizePotentialPath(profileDirForWorkspace(canonicalWorkspace, stateRoot));
|
|
248
|
+
const expectedStatePath = canonicalizePotentialPath(path.join(expectedProfileDir, "state.json"));
|
|
249
|
+
if (!samePotentialPathIdentity(profileDir, expectedProfileDir) || !samePotentialPathIdentity(statePath, expectedStatePath)) {
|
|
250
|
+
throw new Error("workspace state envelope contains inconsistent state paths");
|
|
251
|
+
}
|
|
252
|
+
if (!samePotentialPathIdentity(state.paths.stateRoot, stateRoot)
|
|
253
|
+
|| !samePotentialPathIdentity(state.paths.profileDir, profileDir)
|
|
254
|
+
|| !samePotentialPathIdentity(state.paths.statePath, statePath)) {
|
|
255
|
+
throw new Error("workspace state envelope does not match the active state location");
|
|
256
|
+
}
|
|
257
|
+
assertStateRootSeparatedFromWorkspace(stateRoot, canonicalWorkspace);
|
|
258
|
+
return { canonicalWorkspace, stateRoot, profileDir, statePath };
|
|
199
259
|
}
|
|
200
260
|
|
|
201
261
|
export function daemonLockPathForState(state) {
|
|
@@ -422,7 +482,7 @@ function readBoundedUtf8(filePath, maxBytes, label) {
|
|
|
422
482
|
}
|
|
423
483
|
}
|
|
424
484
|
|
|
425
|
-
function readJsonObjectOrBackup(filePath) {
|
|
485
|
+
function readJsonObjectOrBackup(filePath, options = {}) {
|
|
426
486
|
const text = readBoundedUtf8(filePath, MAX_STATE_JSON_BYTES, "state JSON");
|
|
427
487
|
let parsed;
|
|
428
488
|
try {
|
|
@@ -434,10 +494,39 @@ function readJsonObjectOrBackup(filePath) {
|
|
|
434
494
|
replaceFileSync(filePath, backupPath);
|
|
435
495
|
ownerOnlyFile(backupPath);
|
|
436
496
|
pruneBackups(filePath, 3);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
497
|
+
if (options.allowEmptyRecovery === true) {
|
|
498
|
+
const recovered = {};
|
|
499
|
+
Object.defineProperty(recovered, CORRUPT_RECOVERY, { value: true });
|
|
500
|
+
return recovered;
|
|
501
|
+
}
|
|
502
|
+
const recoveryPath = options.recoveryPath || recoveryMarkerPath(filePath);
|
|
503
|
+
const marker = { schemaVersion: RECOVERY_MARKER_SCHEMA, backup: path.basename(backupPath), detectedAt: new Date().toISOString() };
|
|
504
|
+
replaceFileAtomicallySync(recoveryPath, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
|
|
505
|
+
ownerOnlyFile(recoveryPath);
|
|
506
|
+
throw recoveryRequiredError(marker);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function recoveryMarkerPath(statePath) { return `${statePath}.recovery-required`; }
|
|
511
|
+
|
|
512
|
+
function readRecoveryMarker(markerPath, statePath) {
|
|
513
|
+
ownerOnlyFile(markerPath);
|
|
514
|
+
let marker;
|
|
515
|
+
try { marker = JSON.parse(readBoundedUtf8(markerPath, MAX_MARKER_BYTES, "state recovery marker")); }
|
|
516
|
+
catch { throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing"); }
|
|
517
|
+
if (marker?.schemaVersion !== RECOVERY_MARKER_SCHEMA || typeof marker.backup !== "string"
|
|
518
|
+
|| !marker.backup.startsWith(`${path.basename(statePath)}.corrupt-`) || path.basename(marker.backup) !== marker.backup
|
|
519
|
+
|| !Number.isFinite(Date.parse(String(marker.detectedAt || "")))) {
|
|
520
|
+
throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing");
|
|
440
521
|
}
|
|
522
|
+
return Object.freeze({ schemaVersion: marker.schemaVersion, backup: marker.backup, detectedAt: marker.detectedAt });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function recoveryRequiredError(marker) {
|
|
526
|
+
const backup = marker?.backup || "the bounded corrupt backup";
|
|
527
|
+
const error = new Error(`workspace state recovery is required; invalid JSON was preserved as ${backup}. Restore a valid current-schema state.json or intentionally remove this profile before restarting`);
|
|
528
|
+
error.code = "state_recovery_required";
|
|
529
|
+
return error;
|
|
441
530
|
}
|
|
442
531
|
|
|
443
532
|
function ensureStateRoot(inputRoot) {
|
|
@@ -592,10 +681,11 @@ function pruneBackups(filePath, keep) {
|
|
|
592
681
|
|
|
593
682
|
export function ensureWorkerSecrets(state, options = {}) {
|
|
594
683
|
state.worker ||= {};
|
|
684
|
+
delete state.worker.accountAdminSecret;
|
|
595
685
|
const enrollingDeviceIdentity = !state.worker.deviceIdentity;
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
686
|
+
if (enrollingDeviceIdentity || (options.rotateSecrets && !options.deferDeviceRotation)) state.worker.deviceIdentity = createDeviceIdentity();
|
|
687
|
+
else validateDeviceRootIdentity(state.worker.deviceIdentity);
|
|
688
|
+
if (state.worker.pendingDeviceIdentity) validateDeviceRootIdentity(state.worker.pendingDeviceIdentity);
|
|
599
689
|
delete state.worker.daemonSecret;
|
|
600
690
|
if (!state.worker.oauthTokenVersion || enrollingDeviceIdentity || options.rotateSecrets) {
|
|
601
691
|
state.worker.oauthTokenVersion = randomToken("token_version");
|
|
@@ -622,6 +712,42 @@ export function ensureWorkerSecrets(state, options = {}) {
|
|
|
622
712
|
delete state.worker.updatedAt;
|
|
623
713
|
}
|
|
624
714
|
|
|
715
|
+
export function deploymentDeviceIdentity(state) {
|
|
716
|
+
const identity = state?.worker?.pendingDeviceIdentity || state?.worker?.deviceIdentity;
|
|
717
|
+
return validateDeviceRootIdentity(identity);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function promotePendingDeviceIdentity(state) {
|
|
721
|
+
const pending = state?.worker?.pendingDeviceIdentity;
|
|
722
|
+
if (!pending) return false;
|
|
723
|
+
validateDeviceRootIdentity(pending);
|
|
724
|
+
const current = state.worker.deviceIdentity;
|
|
725
|
+
if (current) {
|
|
726
|
+
const previous = Array.isArray(state.worker.previousDeviceIdentities) ? state.worker.previousDeviceIdentities : [];
|
|
727
|
+
state.worker.previousDeviceIdentities = [...previous, publicDeviceRootRecord(current)].slice(-2);
|
|
728
|
+
}
|
|
729
|
+
state.worker.deviceIdentity = pending;
|
|
730
|
+
delete state.worker.pendingDeviceIdentity;
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function publicDeviceRootRecord(identity) {
|
|
735
|
+
validateDeviceRootIdentity(identity);
|
|
736
|
+
return {
|
|
737
|
+
scheme: identity.scheme,
|
|
738
|
+
provider: identity.provider || "portable-jwk-v1",
|
|
739
|
+
brokerProtocol: identity.brokerProtocol || undefined,
|
|
740
|
+
brokerPath: identity.brokerPath || undefined,
|
|
741
|
+
brokerIdentifier: identity.brokerIdentifier || undefined,
|
|
742
|
+
brokerTeamIdentifier: identity.brokerTeamIdentifier || undefined,
|
|
743
|
+
keyTag: identity.keyTag || undefined,
|
|
744
|
+
publicJwk: identity.publicJwk,
|
|
745
|
+
keyId: identity.keyId,
|
|
746
|
+
createdAt: identity.createdAt,
|
|
747
|
+
retiredAt: new Date().toISOString(),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
625
751
|
function defaultWorkerName(hash) {
|
|
626
752
|
return `mbm-${String(hash || "default").slice(0, 12)}`;
|
|
627
753
|
}
|
|
@@ -640,8 +766,8 @@ export function ownerOnlyFile(filePath) {
|
|
|
640
766
|
|
|
641
767
|
export function redactState(state) {
|
|
642
768
|
const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
|
|
643
|
-
if (clone.worker?.accountAdminSecret) clone.worker.accountAdminSecret = "<redacted>";
|
|
644
769
|
if (clone.worker?.deviceIdentity?.privateJwk?.d) clone.worker.deviceIdentity.privateJwk.d = "<redacted>";
|
|
770
|
+
redactBrokerPaths(clone.worker);
|
|
645
771
|
if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
|
|
646
772
|
if (clone.resources && typeof clone.resources === "object") {
|
|
647
773
|
for (const value of Object.values(clone.resources)) {
|
|
@@ -653,6 +779,18 @@ export function redactState(state) {
|
|
|
653
779
|
return clone;
|
|
654
780
|
}
|
|
655
781
|
|
|
782
|
+
|
|
783
|
+
function redactBrokerPaths(worker) {
|
|
784
|
+
if (!worker || typeof worker !== "object") return;
|
|
785
|
+
for (const identity of [
|
|
786
|
+
worker.deviceIdentity,
|
|
787
|
+
worker.pendingDeviceIdentity,
|
|
788
|
+
...(Array.isArray(worker.previousDeviceIdentities) ? worker.previousDeviceIdentities : []),
|
|
789
|
+
]) {
|
|
790
|
+
if (identity?.brokerPath) identity.brokerPath = "<local-broker-path>";
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
656
794
|
function redactHomeInValue(value) {
|
|
657
795
|
const home = os.homedir();
|
|
658
796
|
if (typeof value === "string") {
|
package/src/local/stdio.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./tools.mjs";
|
|
16
16
|
|
|
17
17
|
const MAX_LINE_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const MAX_PENDING_TOOL_CALLS = 32;
|
|
18
19
|
const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
|
|
19
20
|
|
|
20
21
|
export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "" }) {
|
|
@@ -108,6 +109,10 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
const args = asObject(params.arguments);
|
|
112
|
+
if (pending.size >= MAX_PENDING_TOOL_CALLS) {
|
|
113
|
+
send(rpcError(message.id, -32000, "Too many concurrent tool calls"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
111
116
|
const callId = `stdio_${randomBytes(16).toString("hex")}`;
|
|
112
117
|
const key = jsonRpcIdKey(message.id);
|
|
113
118
|
if (key && pending.has(key)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { errorCode, normalizeBridgeError } from "./errors.mjs";
|
|
3
|
+
import { normalizeToolResult } from "./tool-result-boundary.mjs";
|
|
3
4
|
|
|
4
5
|
export class ToolExecutor {
|
|
5
6
|
constructor(options = {}) {
|
|
@@ -9,12 +10,13 @@ export class ToolExecutor {
|
|
|
9
10
|
this.accountAccessGate = options.accountAccessGate;
|
|
10
11
|
this.operationAuthorizer = options.operationAuthorizer;
|
|
11
12
|
this.observability = options.observability;
|
|
13
|
+
this.securityAudit = options.securityAudit || null;
|
|
12
14
|
this.logger = options.logger || console;
|
|
13
15
|
this.safeMessage = typeof options.safeMessage === "function" ? options.safeMessage : (error) => String(error?.message || error || "operation failed");
|
|
14
16
|
this.slowMs = Number.isFinite(Number(options.slowMs)) ? Math.max(1, Number(options.slowMs)) : 30_000;
|
|
15
17
|
this.pipeline = composeMiddleware([
|
|
16
18
|
lifecycleMiddleware(this.callRegistry),
|
|
17
|
-
observabilityMiddleware(this.observability, this.logger, this.safeMessage, this.slowMs),
|
|
19
|
+
observabilityMiddleware(this.observability, this.securityAudit, this.logger, this.safeMessage, this.slowMs),
|
|
18
20
|
authorizeMiddleware(this.policyGate, this.accountAccessGate, this.operationAuthorizer),
|
|
19
21
|
], invokeHandler(this.handlers));
|
|
20
22
|
}
|
|
@@ -32,10 +34,15 @@ function authorizeMiddleware(policyGate, accountAccessGate, operationAuthorizer)
|
|
|
32
34
|
return async (operation, next) => {
|
|
33
35
|
policyGate.assert(operation.tool);
|
|
34
36
|
if (operation.context.origin === "relay") {
|
|
35
|
-
const
|
|
37
|
+
const authorization = operation.request.authorization;
|
|
38
|
+
const role = authorization?.role;
|
|
36
39
|
if (!role) throw new Error("relay tool call is missing an account role");
|
|
37
40
|
accountAccessGate.assert(role, operation.tool);
|
|
38
|
-
|
|
41
|
+
operation.context.authority = accountAccessGate.authority(authorization, policyGate.policy, "relay");
|
|
42
|
+
const decision = await operationAuthorizer?.authorize(operation);
|
|
43
|
+
if (decision) operation.context.operationAuthorization = decision;
|
|
44
|
+
} else {
|
|
45
|
+
operation.context.authority = accountAccessGate.authority({}, policyGate.policy, "local");
|
|
39
46
|
}
|
|
40
47
|
return next(operation);
|
|
41
48
|
};
|
|
@@ -61,7 +68,7 @@ function lifecycleMiddleware(callRegistry) {
|
|
|
61
68
|
};
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
function observabilityMiddleware(observability, logger, safeMessage, slowMs) {
|
|
71
|
+
function observabilityMiddleware(observability, securityAudit, logger, safeMessage, slowMs) {
|
|
65
72
|
return async (operation, next) => {
|
|
66
73
|
const started = performance.now();
|
|
67
74
|
observability.start(operation.tool);
|
|
@@ -71,23 +78,30 @@ function observabilityMiddleware(observability, logger, safeMessage, slowMs) {
|
|
|
71
78
|
origin: operation.context.origin,
|
|
72
79
|
}, "Tool call started");
|
|
73
80
|
try {
|
|
74
|
-
const
|
|
81
|
+
const normalizedResult = normalizeToolResult(await next(operation));
|
|
82
|
+
const result = normalizedResult.value;
|
|
75
83
|
const durationMs = performance.now() - started;
|
|
76
84
|
const slow = durationMs >= slowMs;
|
|
77
85
|
observability.finish(operation.tool, { status: "completed", durationMs, slow });
|
|
86
|
+
await recordSecurityAudit(securityAudit, operation, { outcome: "completed", durationMs, outputBytes: normalizedResult.bytes });
|
|
78
87
|
logger.event?.("debug", slow ? "tool.call.slow" : "tool.call.completed", {
|
|
79
88
|
call_id: shortCallId(operation.context.callId), tool: operation.tool, origin: operation.context.origin, duration_ms: durationMs,
|
|
89
|
+
authority_role: operation.context.authority?.principal?.role || "local",
|
|
90
|
+
risk_category: operation.context.operationAuthorization?.category || "ordinary",
|
|
80
91
|
}, slow ? "Tool call completed slowly" : "Tool call completed");
|
|
81
92
|
return result;
|
|
82
93
|
} catch (error) {
|
|
83
|
-
const normalized = normalizeBridgeError(error, { safeMessage: () => safeMessage(error, operation.args) });
|
|
94
|
+
const normalized = normalizeBridgeError(error, { safeMessage: () => safeMessage(error, operation.args, operation.context) });
|
|
84
95
|
const durationMs = performance.now() - started;
|
|
85
96
|
const code = errorCode(normalized);
|
|
86
97
|
const status = code === "cancelled" ? "cancelled" : code === "timeout" ? "timeout" : "failed";
|
|
87
98
|
observability.finish(operation.tool, { status, durationMs, errorCode: code, slow: durationMs >= slowMs });
|
|
99
|
+
await recordSecurityAudit(securityAudit, operation, { outcome: status, durationMs, errorCode: code });
|
|
88
100
|
logger.event?.("debug", "tool.call.failed", {
|
|
89
101
|
call_id: shortCallId(operation.context.callId), tool: operation.tool, origin: operation.context.origin,
|
|
90
102
|
duration_ms: durationMs, error_code: code, retryable: normalized.retryable,
|
|
103
|
+
authority_role: operation.context.authority?.principal?.role || "local",
|
|
104
|
+
risk_category: operation.context.operationAuthorization?.category || "ordinary",
|
|
91
105
|
}, "Tool call failed");
|
|
92
106
|
throw normalized;
|
|
93
107
|
}
|
|
@@ -102,6 +116,25 @@ function invokeHandler(handlers) {
|
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
|
|
120
|
+
async function recordSecurityAudit(securityAudit, operation, outcome) {
|
|
121
|
+
if (!securityAudit?.record || operation.context.origin !== "relay") return;
|
|
122
|
+
const decision = operation.context.operationAuthorization || {};
|
|
123
|
+
const recorded = await securityAudit.record({
|
|
124
|
+
...outcome,
|
|
125
|
+
tool: operation.tool,
|
|
126
|
+
riskCategory: decision.category || "ordinary operation",
|
|
127
|
+
targetHash: decision.targetHash || "",
|
|
128
|
+
principal: operation.context.authority?.principal || {},
|
|
129
|
+
inputBytes: safeByteLength(operation.args),
|
|
130
|
+
});
|
|
131
|
+
if (!recorded) operation.context.auditWarning = "security_audit_unavailable";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function safeByteLength(value) {
|
|
135
|
+
try { return Buffer.byteLength(JSON.stringify(value)); } catch { return 0; }
|
|
136
|
+
}
|
|
137
|
+
|
|
105
138
|
function shortCallId(value) {
|
|
106
139
|
return String(value || "").slice(0, 20);
|
|
107
140
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { BridgeError } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
export const MAX_TOOL_RESULT_BYTES = 7 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
export function normalizeToolResult(value, options = {}) {
|
|
7
|
+
const maximumBytes = positiveInteger(options.maximumBytes, MAX_TOOL_RESULT_BYTES);
|
|
8
|
+
let serialized;
|
|
9
|
+
try {
|
|
10
|
+
serialized = JSON.stringify(value, rejectUnsupportedJsonValue);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof BridgeError) throw error;
|
|
13
|
+
throw new BridgeError("internal_error", "tool returned a result that could not be serialized", {
|
|
14
|
+
cause: error instanceof Error ? error : undefined,
|
|
15
|
+
expose: false,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (typeof serialized !== "string") {
|
|
19
|
+
throw new BridgeError("internal_error", "tool returned an unsupported result", { expose: false });
|
|
20
|
+
}
|
|
21
|
+
const bytes = Buffer.byteLength(serialized);
|
|
22
|
+
if (bytes > maximumBytes) {
|
|
23
|
+
throw new BridgeError("limit_exceeded", "tool result exceeded the relay response limit", {
|
|
24
|
+
retryable: false,
|
|
25
|
+
details: { maximum_bytes: maximumBytes, actual_bytes: bytes },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return Object.freeze({ value: JSON.parse(serialized), bytes });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new BridgeError("internal_error", "tool result normalization failed", {
|
|
32
|
+
cause: error instanceof Error ? error : undefined,
|
|
33
|
+
expose: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rejectUnsupportedJsonValue(_key, current) {
|
|
39
|
+
if (typeof current === "bigint" || typeof current === "function" || typeof current === "symbol" || typeof current === "undefined") {
|
|
40
|
+
throw new BridgeError("internal_error", "tool returned a non-JSON value", { expose: false });
|
|
41
|
+
}
|
|
42
|
+
if (typeof current === "number" && !Number.isFinite(current)) {
|
|
43
|
+
throw new BridgeError("internal_error", "tool returned a non-finite number", { expose: false });
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function positiveInteger(value, fallback) {
|
|
49
|
+
const parsed = Number(value);
|
|
50
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
51
|
+
}
|
package/src/local/tools.mjs
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { accessSync, constants as fsConstants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { BridgeError } from "./errors.mjs";
|
|
5
|
+
|
|
6
|
+
export function resolveTrustedExecutable(options = {}) {
|
|
7
|
+
const platform = String(options.platform || process.platform);
|
|
8
|
+
const candidates = Array.isArray(options.candidates) ? options.candidates : [];
|
|
9
|
+
const deniedRoots = [options.workspace, options.stateRoot, options.runtimeDir, options.home || safeHome()]
|
|
10
|
+
.filter(Boolean).map((value) => canonicalOrResolved(String(value), options.realpath || realpathSync));
|
|
11
|
+
const realpath = options.realpath || realpathSync;
|
|
12
|
+
const stat = options.stat || statSync;
|
|
13
|
+
const access = options.access || accessSync;
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if (!path.isAbsolute(String(candidate || ""))) continue;
|
|
16
|
+
let canonical;
|
|
17
|
+
let info;
|
|
18
|
+
try {
|
|
19
|
+
canonical = realpath(String(candidate));
|
|
20
|
+
info = stat(canonical);
|
|
21
|
+
if (!info.isFile()) continue;
|
|
22
|
+
if (platform !== "win32") access(canonical, fsConstants.X_OK);
|
|
23
|
+
} catch { continue; }
|
|
24
|
+
if (platform !== "win32" && (Number(info.mode) & 0o022) !== 0) continue;
|
|
25
|
+
if (deniedRoots.some((root) => isWithin(canonical, root, platform))) continue;
|
|
26
|
+
return canonical;
|
|
27
|
+
}
|
|
28
|
+
const label = String(options.label || "required executable").replace(/[^A-Za-z0-9._ -]/g, "").trim() || "required executable";
|
|
29
|
+
const reason = String(options.reason || "trusted_executable_unavailable").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
30
|
+
throw new BridgeError("unavailable", `trusted ${label} is unavailable`, { details: { reason, platform } });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createTrustedExecutableResolver(options = {}) {
|
|
34
|
+
const injected = typeof options.resolve === "function" ? options.resolve : null;
|
|
35
|
+
let cached = "";
|
|
36
|
+
return () => {
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
cached = String(injected ? injected() : resolveTrustedExecutable(options));
|
|
39
|
+
if (!path.isAbsolute(cached)) throw new BridgeError("unavailable", `trusted ${options.label || "executable"} is unavailable`);
|
|
40
|
+
return cached;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalOrResolved(value, realpath) {
|
|
45
|
+
try { return realpath(path.resolve(value)); } catch { return path.resolve(value); }
|
|
46
|
+
}
|
|
47
|
+
function isWithin(candidate, root, platform) {
|
|
48
|
+
const left = platform === "win32" ? candidate.toLowerCase() : candidate;
|
|
49
|
+
const right = platform === "win32" ? root.toLowerCase() : root;
|
|
50
|
+
const relative = path.relative(right, left);
|
|
51
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
52
|
+
}
|
|
53
|
+
function safeHome() { try { return os.homedir(); } catch { return ""; } }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createTrustedExecutableResolver, resolveTrustedExecutable } from "./trusted-executable.mjs";
|
|
3
|
+
|
|
4
|
+
export function resolveTrustedGitExecutable(options = {}) {
|
|
5
|
+
const platform = String(options.platform || process.platform);
|
|
6
|
+
return resolveTrustedExecutable({
|
|
7
|
+
...options,
|
|
8
|
+
platform,
|
|
9
|
+
label: "Git executable",
|
|
10
|
+
reason: "trusted_git_executable_unavailable",
|
|
11
|
+
candidates: Array.isArray(options.candidates) ? options.candidates : defaultGitCandidates(platform, options.env || process.env),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createTrustedGitResolver(options = {}) {
|
|
16
|
+
const platform = String(options.platform || process.platform);
|
|
17
|
+
return createTrustedExecutableResolver({
|
|
18
|
+
...options,
|
|
19
|
+
platform,
|
|
20
|
+
label: "Git executable",
|
|
21
|
+
reason: "trusted_git_executable_unavailable",
|
|
22
|
+
candidates: Array.isArray(options.candidates) ? options.candidates : defaultGitCandidates(platform, options.env || process.env),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defaultGitCandidates(platform, env) {
|
|
27
|
+
if (platform === "win32") {
|
|
28
|
+
return [env.ProgramFiles, env["ProgramFiles(x86)"]]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.flatMap((root) => [path.join(root, "Git", "cmd", "git.exe"), path.join(root, "Git", "bin", "git.exe")]);
|
|
31
|
+
}
|
|
32
|
+
if (platform === "darwin") return ["/usr/bin/git", "/opt/homebrew/bin/git", "/usr/local/bin/git"];
|
|
33
|
+
return ["/usr/bin/git", "/bin/git", "/usr/local/bin/git"];
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveTrustedExecutable } from "./trusted-executable.mjs";
|
|
3
|
+
|
|
4
|
+
export function resolveTrustedGithubCli(options = {}) {
|
|
5
|
+
const platform = String(options.platform || process.platform);
|
|
6
|
+
return resolveTrustedExecutable({
|
|
7
|
+
...options,
|
|
8
|
+
platform,
|
|
9
|
+
label: "GitHub CLI executable",
|
|
10
|
+
reason: "trusted_github_cli_unavailable",
|
|
11
|
+
candidates: Array.isArray(options.candidates) ? options.candidates : githubCliCandidates(platform, options.env || process.env),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function githubCliCandidates(platform, env) {
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
return [
|
|
18
|
+
env.ProgramFiles && path.join(env.ProgramFiles, "GitHub CLI", "gh.exe"),
|
|
19
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs", "GitHub CLI", "gh.exe"),
|
|
20
|
+
].filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
if (platform === "darwin") return ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"];
|
|
23
|
+
return ["/usr/bin/gh", "/usr/local/bin/gh", "/snap/bin/gh"];
|
|
24
|
+
}
|
|
@@ -50,6 +50,20 @@ export async function startWindowsTask(logger = console, options = {}) {
|
|
|
50
50
|
const before = await statusWindowsTask({ ...options, run });
|
|
51
51
|
if (before.installed === null) return { ok: false, provider: "schtasks", task: WINDOWS_TASK, reason: "task_status_unavailable", status: before };
|
|
52
52
|
if (before.installed === false) return { ok: false, provider: "schtasks", task: WINDOWS_TASK, reason: "not_installed", status: before };
|
|
53
|
+
if (before.active === true) {
|
|
54
|
+
logger.info?.("Windows Scheduled Task is already running");
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
provider: "schtasks",
|
|
58
|
+
task: WINDOWS_TASK,
|
|
59
|
+
installed: true,
|
|
60
|
+
active_before: true,
|
|
61
|
+
active: true,
|
|
62
|
+
already_running: true,
|
|
63
|
+
reason: "already_running",
|
|
64
|
+
status: before,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
53
67
|
const command = await run("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
54
68
|
const after = await waitForWindowsStatus(
|
|
55
69
|
status => status.active === true || completedSince(before, status),
|
|
@@ -68,6 +82,12 @@ export async function startWindowsTask(logger = console, options = {}) {
|
|
|
68
82
|
reason: after.active === true ? "started" : completed ? "completed" : "start_not_observed",
|
|
69
83
|
};
|
|
70
84
|
}
|
|
85
|
+
export async function restartWindowsTask(logger = console, options = {}) {
|
|
86
|
+
const stopped = await stopWindowsTask(logger, options);
|
|
87
|
+
if (!stopped.ok) return { ok: false, provider: "schtasks", task: WINDOWS_TASK, reason: "stop_failed", stop: stopped };
|
|
88
|
+
const started = await startWindowsTask(logger, options);
|
|
89
|
+
return { ...started, restarted: started.ok === true, stop: stopped };
|
|
90
|
+
}
|
|
71
91
|
|
|
72
92
|
export async function stopWindowsTask(logger = console, options = {}) {
|
|
73
93
|
const run = requiredRun(options.run);
|
|
@@ -2,7 +2,7 @@ import { createHmac } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import path, { resolve } from "node:path";
|
|
4
4
|
import { runWrangler } from "./shell.mjs";
|
|
5
|
-
import { packageRoot, saveState } from "./state.mjs";
|
|
5
|
+
import { deploymentDeviceIdentity, packageRoot, saveState } from "./state.mjs";
|
|
6
6
|
import { withWorkerSecretsFile } from "./worker-secret-file.mjs";
|
|
7
7
|
import { publicDeviceJwkJson } from "./device-identity.mjs";
|
|
8
8
|
import {
|
|
@@ -81,8 +81,7 @@ export async function ensureWorkerDeployment(state, args = {}, options = {}) {
|
|
|
81
81
|
export function workerDeploymentFingerprint(state, options = {}) {
|
|
82
82
|
const root = resolve(options.packageRoot || packageRoot);
|
|
83
83
|
const keyMaterial = [
|
|
84
|
-
|
|
85
|
-
publicDeviceJwkJson(state.worker.deviceIdentity),
|
|
84
|
+
publicDeviceJwkJson(deploymentDeviceIdentity(state)),
|
|
86
85
|
String(state.worker.oauthTokenVersion || ""),
|
|
87
86
|
].join("\0");
|
|
88
87
|
const fingerprint = createHmac("sha256", keyMaterial);
|
|
@@ -119,7 +118,7 @@ export function workerUrlMatchesName(workerUrl, workerName) {
|
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
function hasCompleteWorkerState(worker = {}) {
|
|
122
|
-
return Boolean(worker.url && worker.mcpServerUrl && worker.
|
|
121
|
+
return Boolean(worker.url && worker.mcpServerUrl && worker.deviceIdentity && worker.oauthTokenVersion && worker.name);
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
function workerDeployHashFiles(root) {
|
|
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { createExclusiveFileSync } from "./exclusive-file.mjs";
|
|
5
5
|
import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
|
|
6
6
|
import { publicDeviceJwkJson } from "./device-identity.mjs";
|
|
7
|
+
import { deploymentDeviceIdentity } from "./state.mjs";
|
|
7
8
|
import { chmodRegularFileSync, ensureOwnerOnlyDirectorySync } from "./secure-file.mjs";
|
|
8
9
|
|
|
9
10
|
const SECRET_FILE_PATTERN = /^worker-secrets-(\d+)-(\d+)(?:-p(\d+))?(?:-([a-f0-9]+))?\.json$/;
|
|
@@ -17,8 +18,7 @@ export async function withWorkerSecretsFile(state, callback, options = {}) {
|
|
|
17
18
|
const random = (options.randomBytes || randomBytes)(6).toString("hex");
|
|
18
19
|
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${createdAt}-p${processStartedAt}-${random}.json`);
|
|
19
20
|
const payload = {
|
|
20
|
-
|
|
21
|
-
DAEMON_DEVICE_PUBLIC_KEY: publicDeviceJwkJson(state.worker.deviceIdentity),
|
|
21
|
+
DAEMON_DEVICE_PUBLIC_KEY: publicDeviceJwkJson(deploymentDeviceIdentity(state)),
|
|
22
22
|
OAUTH_TOKEN_VERSION: state.worker.oauthTokenVersion,
|
|
23
23
|
};
|
|
24
24
|
|