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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const APPLICATION_PROXY_ROUTE_SCOPE = "application-proxy-selection-only";
|
|
2
|
+
|
|
3
|
+
export function relayStatusSnapshot(state, now = Date.now()) {
|
|
4
|
+
const current = Number(now) || 0;
|
|
5
|
+
return {
|
|
6
|
+
authenticated: state.authenticated === true,
|
|
7
|
+
ready: state.ready === true,
|
|
8
|
+
readiness_probe_delivered: state.readinessProbeDelivered === true,
|
|
9
|
+
closed: state.closed === true,
|
|
10
|
+
network_route: state.networkRoute,
|
|
11
|
+
network_route_scope: state.networkRouteScope,
|
|
12
|
+
reconnect_attempt: state.reconnectAttempt,
|
|
13
|
+
outage_active: state.outageStartedAt > 0,
|
|
14
|
+
outage_count: state.outageCount,
|
|
15
|
+
outage_started_at: isoTimestamp(state.outageStartedAt),
|
|
16
|
+
outage_duration_ms: state.outageStartedAt > 0 ? Math.max(0, current - state.outageStartedAt) : 0,
|
|
17
|
+
last_close_category: state.outageCount > 0 ? state.lastCloseCategory : null,
|
|
18
|
+
last_close_code: state.outageCount > 0 ? state.lastCloseCode : null,
|
|
19
|
+
last_transport_error_class: state.outageCount > 0 ? (state.lastTransportErrorClass || null) : null,
|
|
20
|
+
last_disconnected_at: isoTimestamp(state.lastDisconnectedAt),
|
|
21
|
+
last_ready_at: isoTimestamp(state.lastReadyAt),
|
|
22
|
+
last_ready_duration_ms: state.lastReadyDurationMs,
|
|
23
|
+
last_reconnect_delay_ms: state.lastReconnectDelayMs,
|
|
24
|
+
next_reconnect_at: isoTimestamp(state.nextReconnectAt),
|
|
25
|
+
next_reconnect_in_ms: state.nextReconnectAt > 0 ? Math.max(0, state.nextReconnectAt - current) : 0,
|
|
26
|
+
session_generation: state.sessionGeneration,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function relayOutageFields(state, now, cause) {
|
|
31
|
+
const current = Number(now) || 0;
|
|
32
|
+
return {
|
|
33
|
+
event: "relay.outage.active",
|
|
34
|
+
outage_seconds: roundSeconds(Math.max(0, current - state.outageStartedAt)),
|
|
35
|
+
attempts: state.outageAttempts,
|
|
36
|
+
cause,
|
|
37
|
+
close_category: state.lastCloseCategory,
|
|
38
|
+
close_code: state.lastCloseCode,
|
|
39
|
+
network_route: state.networkRoute,
|
|
40
|
+
network_route_scope: state.networkRouteScope,
|
|
41
|
+
next_reconnect_in_ms: state.nextReconnectAt > 0 ? Math.max(0, state.nextReconnectAt - current) : 0,
|
|
42
|
+
...(state.lastTransportErrorClass ? { error_class: state.lastTransportErrorClass } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function relayRecoveryFields(state, outageMs) {
|
|
47
|
+
return {
|
|
48
|
+
event: "relay.outage.recovered",
|
|
49
|
+
outage_seconds: roundSeconds(outageMs),
|
|
50
|
+
attempts: state.outageAttempts,
|
|
51
|
+
close_category: state.lastCloseCategory,
|
|
52
|
+
close_code: state.lastCloseCode,
|
|
53
|
+
network_route: state.networkRoute,
|
|
54
|
+
network_route_scope: state.networkRouteScope,
|
|
55
|
+
...(state.lastTransportErrorClass ? { error_class: state.lastTransportErrorClass } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isoTimestamp(value) {
|
|
60
|
+
return Number(value) > 0 ? new Date(Number(value)).toISOString() : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function roundSeconds(milliseconds) {
|
|
64
|
+
return Math.max(1, Math.round(Number(milliseconds || 0) / 1000));
|
|
65
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { effectiveLogFormat, effectiveLogLevel } from "./cli-options.mjs";
|
|
2
|
+
import { resolvePolicy } from "./cli-policy.mjs";
|
|
3
|
+
import { ensurePreferredDeviceRoot } from "./device-root-provider.mjs";
|
|
4
|
+
import { createLogger } from "./log.mjs";
|
|
5
|
+
import {
|
|
6
|
+
ensureWorkerSecrets,
|
|
7
|
+
promotePendingDeviceIdentity,
|
|
8
|
+
saveState,
|
|
9
|
+
} from "./state.mjs";
|
|
10
|
+
import { ensureWorkerDeployment } from "./worker-deployment.mjs";
|
|
11
|
+
|
|
12
|
+
export async function convergeRemoteConfiguration({ args, state }) {
|
|
13
|
+
const workerName = validateWorkerName(args.workerName);
|
|
14
|
+
ensureWorkerSecrets(state, {
|
|
15
|
+
rotateSecrets: Boolean(args.rotateSecrets),
|
|
16
|
+
deferDeviceRotation: true,
|
|
17
|
+
workerName,
|
|
18
|
+
allowWorkerRename: Boolean(args.forceWorker),
|
|
19
|
+
});
|
|
20
|
+
if (!state.worker.pendingDeviceIdentity) {
|
|
21
|
+
const candidate = await ensurePreferredDeviceRoot({
|
|
22
|
+
profileDir: state.paths.profileDir,
|
|
23
|
+
workspaceHash: state.workspace.hash,
|
|
24
|
+
existing: args.rotateSecrets ? null : state.worker.deviceIdentity,
|
|
25
|
+
rotate: Boolean(args.rotateSecrets),
|
|
26
|
+
});
|
|
27
|
+
if (candidate.keyId !== state.worker.deviceIdentity.keyId) state.worker.pendingDeviceIdentity = candidate;
|
|
28
|
+
}
|
|
29
|
+
state.policy = resolvePolicy(args, state.policy);
|
|
30
|
+
state.policy.updatedAt = new Date().toISOString();
|
|
31
|
+
saveState(state);
|
|
32
|
+
const logger = createLogger({
|
|
33
|
+
level: args.json ? "error" : effectiveLogLevel(args),
|
|
34
|
+
format: effectiveLogFormat(args),
|
|
35
|
+
component: "worker",
|
|
36
|
+
});
|
|
37
|
+
await ensureWorkerDeployment(state, args, { logger });
|
|
38
|
+
if (promotePendingDeviceIdentity(state)) saveState(state);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateWorkerName(value) {
|
|
42
|
+
if (value === undefined || value === null || value === false) return undefined;
|
|
43
|
+
const name = String(value).trim();
|
|
44
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) {
|
|
45
|
+
throw new Error("--worker-name must be 1-63 lowercase letters, digits, or hyphens, and cannot start or end with a hyphen");
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export async function activatePersistentRuntime(options = {}) {
|
|
2
|
+
const required = [
|
|
3
|
+
"acquireStartupLock",
|
|
4
|
+
"stopAutostart",
|
|
5
|
+
"acquireDaemonLock",
|
|
6
|
+
"prepareRemoteState",
|
|
7
|
+
"createRuntime",
|
|
8
|
+
"installAutostart",
|
|
9
|
+
"startAutostart",
|
|
10
|
+
"inspectDaemon",
|
|
11
|
+
"checkWorker",
|
|
12
|
+
];
|
|
13
|
+
for (const name of required) {
|
|
14
|
+
if (typeof options[name] !== "function") throw new TypeError(`runtime activation requires ${name}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const startupLock = await options.acquireStartupLock();
|
|
18
|
+
let startupReleased = false;
|
|
19
|
+
let daemonLock = null;
|
|
20
|
+
let candidateRuntime = null;
|
|
21
|
+
let candidateRelayVerified = false;
|
|
22
|
+
try {
|
|
23
|
+
const providerStop = await options.stopAutostart();
|
|
24
|
+
if (providerStop?.ok === false && providerStop?.active === true) {
|
|
25
|
+
throw new Error("the existing autostart service could not be stopped before activation");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
daemonLock = await options.acquireDaemonLock();
|
|
29
|
+
if (!daemonLock?.acquired) {
|
|
30
|
+
const owner = daemonLock?.owner || {};
|
|
31
|
+
if (owner.mode === "foreground") {
|
|
32
|
+
throw new Error(`a foreground daemon is active (pid ${owner.pid || "unknown"}); stop it explicitly before activation`);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`the existing daemon could not be taken over (pid ${owner.pid || "unknown"})`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const readiness = await options.prepareRemoteState();
|
|
38
|
+
candidateRuntime = options.createRuntime({ daemonLock, readiness });
|
|
39
|
+
if (!candidateRuntime || typeof candidateRuntime.start !== "function" || typeof candidateRuntime.stop !== "function") {
|
|
40
|
+
throw new TypeError("runtime activation candidate must expose start and stop");
|
|
41
|
+
}
|
|
42
|
+
await candidateRuntime.start();
|
|
43
|
+
candidateRelayVerified = true;
|
|
44
|
+
|
|
45
|
+
const installed = await options.installAutostart();
|
|
46
|
+
const terminalError = candidateRuntime.terminalError?.();
|
|
47
|
+
if (terminalError) throw terminalError;
|
|
48
|
+
if (installed?.ok === false) {
|
|
49
|
+
throw new Error(`autostart installation failed (${installed.provider || "unknown"})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
candidateRuntime.stop();
|
|
53
|
+
candidateRuntime = null;
|
|
54
|
+
daemonLock.release();
|
|
55
|
+
daemonLock = null;
|
|
56
|
+
startupLock.release();
|
|
57
|
+
startupReleased = true;
|
|
58
|
+
|
|
59
|
+
const serviceStart = await options.startAutostart();
|
|
60
|
+
if (serviceStart?.ok === false) {
|
|
61
|
+
throw new Error(`autostart start failed (${serviceStart.provider || "unknown"})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const convergence = await waitForActivatedRuntime({
|
|
65
|
+
expectedVersion: options.expectedVersion,
|
|
66
|
+
inspectDaemon: options.inspectDaemon,
|
|
67
|
+
checkWorker: options.checkWorker,
|
|
68
|
+
maximumAttempts: options.maximumAttempts,
|
|
69
|
+
wait: options.wait,
|
|
70
|
+
});
|
|
71
|
+
if (!convergence.ok) {
|
|
72
|
+
throw new Error(`persistent runtime activation did not converge (${convergence.reason})`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
serviceStart,
|
|
77
|
+
convergence,
|
|
78
|
+
candidateRelayVerified,
|
|
79
|
+
};
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const cleanupErrors = [];
|
|
82
|
+
try { candidateRuntime?.stop?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
83
|
+
try { daemonLock?.release?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
84
|
+
if (!startupReleased) {
|
|
85
|
+
try { startupLock.release(); }
|
|
86
|
+
catch (failure) { cleanupErrors.push(failure); }
|
|
87
|
+
}
|
|
88
|
+
if (cleanupErrors.length) {
|
|
89
|
+
throw new AggregateError(
|
|
90
|
+
[error, ...cleanupErrors],
|
|
91
|
+
"persistent runtime activation failed and local cleanup was incomplete; inspect the daemon and service state before retrying",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function waitForActivatedRuntime({
|
|
99
|
+
inspectDaemon,
|
|
100
|
+
checkWorker,
|
|
101
|
+
expectedVersion,
|
|
102
|
+
maximumAttempts = 240,
|
|
103
|
+
wait = defaultWait,
|
|
104
|
+
} = {}) {
|
|
105
|
+
if (typeof inspectDaemon !== "function" || typeof checkWorker !== "function") {
|
|
106
|
+
throw new TypeError("runtime activation requires daemon and Worker inspectors");
|
|
107
|
+
}
|
|
108
|
+
const attempts = boundedAttempts(maximumAttempts);
|
|
109
|
+
let daemon = null;
|
|
110
|
+
let worker = null;
|
|
111
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
112
|
+
daemon = await inspectDaemon();
|
|
113
|
+
if (daemon?.alive && daemon?.verified_service_daemon && daemon?.version === expectedVersion) {
|
|
114
|
+
worker = await checkWorker();
|
|
115
|
+
if (worker?.ok && worker?.version === expectedVersion) {
|
|
116
|
+
return { ok: true, attempts: attempt, daemon, worker };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (attempt < attempts) await wait(attempt);
|
|
120
|
+
}
|
|
121
|
+
const reasons = [];
|
|
122
|
+
if (!daemon?.alive) reasons.push("daemon_not_running");
|
|
123
|
+
else if (!daemon?.verified_service_daemon) reasons.push(`daemon_identity_${daemon?.identity_reason || "unverified"}`);
|
|
124
|
+
else if (daemon?.version !== expectedVersion) reasons.push(`daemon_version_${daemon?.version || "unknown"}`);
|
|
125
|
+
if (!worker?.ok) reasons.push(`worker_${worker?.error || "not_ready"}`);
|
|
126
|
+
else if (worker?.version !== expectedVersion) reasons.push(`worker_version_${worker?.version || "unknown"}`);
|
|
127
|
+
return { ok: false, attempts, daemon, worker, reason: reasons.join(",") || "activation_not_converged" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function boundedAttempts(value) {
|
|
131
|
+
const parsed = Number(value);
|
|
132
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1200) {
|
|
133
|
+
throw new Error("runtime activation attempts must be between 1 and 1200");
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function defaultWait(attempt) {
|
|
139
|
+
const delay = Math.min(1_000, 100 + attempt * 25);
|
|
140
|
+
return new Promise((resolvePromise) => { setTimeout(resolvePromise, delay); });
|
|
141
|
+
}
|
|
@@ -11,6 +11,7 @@ export async function diagnoseRuntime({
|
|
|
11
11
|
runProcess,
|
|
12
12
|
probeShell,
|
|
13
13
|
managedJobManager,
|
|
14
|
+
relayStatus = () => null,
|
|
14
15
|
throwIfCancelled,
|
|
15
16
|
}, context = {}) {
|
|
16
17
|
throwIfCancelled(context);
|
|
@@ -24,6 +25,25 @@ export async function diagnoseRuntime({
|
|
|
24
25
|
detail: `profile=${policy.profile}; exec_mode=${policy.execMode}; unrestricted_paths=${policy.unrestrictedPaths}`,
|
|
25
26
|
}];
|
|
26
27
|
|
|
28
|
+
const relay = typeof relayStatus === "function" ? relayStatus() : null;
|
|
29
|
+
checks.push(relay ? {
|
|
30
|
+
layer: "remote-relay",
|
|
31
|
+
ok: relay.ready === true,
|
|
32
|
+
network_route: relay.network_route || "unknown",
|
|
33
|
+
network_route_scope: relay.network_route_scope || "unknown",
|
|
34
|
+
outage_active: relay.outage_active === true,
|
|
35
|
+
outage_count: Number(relay.outage_count) || 0,
|
|
36
|
+
last_close_category: relay.last_close_category || null,
|
|
37
|
+
last_close_code: Number.isFinite(Number(relay.last_close_code)) ? Number(relay.last_close_code) : null,
|
|
38
|
+
last_transport_error_class: relay.last_transport_error_class || null,
|
|
39
|
+
last_disconnected_at: relay.last_disconnected_at || null,
|
|
40
|
+
last_ready_at: relay.last_ready_at || null,
|
|
41
|
+
last_ready_duration_ms: Number(relay.last_ready_duration_ms) || 0,
|
|
42
|
+
next_reconnect_in_ms: Number(relay.next_reconnect_in_ms) || 0,
|
|
43
|
+
} : {
|
|
44
|
+
layer: "remote-relay", ok: false, skipped: true, transport: "stdio-or-local",
|
|
45
|
+
});
|
|
46
|
+
|
|
27
47
|
const probe = join(runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
28
48
|
try {
|
|
29
49
|
await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
|
|
@@ -82,6 +102,7 @@ export async function diagnoseRuntime({
|
|
|
82
102
|
interpretation: {
|
|
83
103
|
tool_call_blocked_before_response: "host/platform or connector gateway",
|
|
84
104
|
diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
|
|
105
|
+
system_network_stack_scope: "application proxy selection only; an operating-system VPN or TUN may still intercept the relay connection",
|
|
85
106
|
managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
|
|
86
107
|
},
|
|
87
108
|
policy,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { RelayConnection } from "./relay-connection.mjs";
|
|
3
|
-
import { createDaemonAuthentication, createDaemonPreflightHeaders } from "./device-identity.mjs";
|
|
3
|
+
import { createDaemonAuthentication, createDaemonPreflightHeaders, createDeviceSessionIdentity, validateDeviceSessionIdentity } from "./device-identity.mjs";
|
|
4
4
|
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, SERVER_NAME } from "./tools.mjs";
|
|
5
5
|
import { normalizeAccountRole } from "./account-access.mjs";
|
|
6
6
|
import { clampInteger } from "./numbers.mjs";
|
|
7
7
|
import { isPlainRecord } from "./records.mjs";
|
|
8
|
-
|
|
9
8
|
export const MAX_RELAY_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
10
|
-
|
|
11
9
|
export function createRuntimeRelayConnection(runtime, { workerUrl, deviceIdentity, expectedVersion, onFatal }) {
|
|
12
|
-
if (!workerUrl) return null;
|
|
10
|
+
if (!workerUrl || !deviceIdentity) return null;
|
|
11
|
+
const sessionIdentity = deviceIdentity?.certificate ? validateDeviceSessionIdentity(deviceIdentity)
|
|
12
|
+
: createDeviceSessionIdentity(deviceIdentity, workerUrl, SERVER_NAME, String(expectedVersion || ""));
|
|
13
13
|
return new RelayConnection({
|
|
14
14
|
workerUrl,
|
|
15
15
|
logger: runtime.logger,
|
|
@@ -17,7 +17,7 @@ export function createRuntimeRelayConnection(runtime, { workerUrl, deviceIdentit
|
|
|
17
17
|
expectedServer: SERVER_NAME,
|
|
18
18
|
expectedVersion: String(expectedVersion || ""),
|
|
19
19
|
connectionHeaders: () => createDaemonPreflightHeaders(
|
|
20
|
-
|
|
20
|
+
sessionIdentity,
|
|
21
21
|
workerUrl,
|
|
22
22
|
SERVER_NAME,
|
|
23
23
|
String(expectedVersion || ""),
|
|
@@ -28,7 +28,7 @@ export function createRuntimeRelayConnection(runtime, { workerUrl, deviceIdentit
|
|
|
28
28
|
tools: runtime.tools(),
|
|
29
29
|
policy: runtime.policy,
|
|
30
30
|
protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
31
|
-
authentication: await createDaemonAuthentication(
|
|
31
|
+
authentication: await createDaemonAuthentication(sessionIdentity, welcome, runtime.relayInstanceId),
|
|
32
32
|
}),
|
|
33
33
|
onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
|
|
34
34
|
onDisconnect: () => runtime.handleRelayDisconnect(),
|
|
@@ -90,8 +90,9 @@ function normalizeRelayAuthorization(value) {
|
|
|
90
90
|
const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
|
|
91
91
|
const accountVersion = Number(value.account_version);
|
|
92
92
|
const clientId = typeof value.client_id === "string" && /^mcp_client_[A-Za-z0-9_-]{43}$/.test(value.client_id) ? value.client_id : "";
|
|
93
|
+
const familyId = typeof value.family_id === "string" && /^mcp_family_[A-Za-z0-9_-]{43}$/.test(value.family_id) ? value.family_id : "";
|
|
93
94
|
let role;
|
|
94
95
|
try { role = normalizeAccountRole(value.role); } catch { return null; }
|
|
95
|
-
if (!accountId || !clientId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
96
|
-
return Object.freeze({ account_id: accountId, account_version: accountVersion, client_id: clientId, role });
|
|
96
|
+
if (!accountId || !clientId || !familyId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
97
|
+
return Object.freeze({ account_id: accountId, account_version: accountVersion, client_id: clientId, family_id: familyId, role });
|
|
97
98
|
}
|
|
@@ -77,16 +77,19 @@ export async function buildProjectOverview({
|
|
|
77
77
|
displayPath,
|
|
78
78
|
policy,
|
|
79
79
|
toolNames,
|
|
80
|
+
daemonPolicy = policy,
|
|
81
|
+
daemonToolNames = toolNames,
|
|
80
82
|
capabilityObserver,
|
|
81
83
|
listTopLevel,
|
|
82
|
-
|
|
84
|
+
runInternalProcess,
|
|
85
|
+
gitExecutable,
|
|
83
86
|
safeErrorMessage,
|
|
84
87
|
throwIfCancelled,
|
|
85
88
|
}, context = {}) {
|
|
86
89
|
throwIfCancelled(context);
|
|
87
90
|
const top = await listTopLevel(context).catch((error) => ({ error: safeErrorMessage(error), entries: [] }));
|
|
88
|
-
const git = await
|
|
89
|
-
|
|
91
|
+
const git = await runInternalProcess(
|
|
92
|
+
gitExecutable(),
|
|
90
93
|
["-c", "core.fsmonitor=false", "-C", workspace, "rev-parse", "--show-toplevel"],
|
|
91
94
|
10_000,
|
|
92
95
|
true,
|
|
@@ -99,6 +102,8 @@ export async function buildProjectOverview({
|
|
|
99
102
|
gitRoot: git.code === 0 ? displayPath(git.stdout.trim()) : "",
|
|
100
103
|
policy,
|
|
101
104
|
tools: ["server_info", ...toolNames],
|
|
105
|
+
daemonPolicy,
|
|
106
|
+
daemonTools: ["server_info", ...daemonToolNames],
|
|
102
107
|
capabilityRouting: capabilityObserver.snapshot(),
|
|
103
108
|
topLevel: top.entries || [],
|
|
104
109
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clampInteger } from "./numbers.mjs";
|
|
2
2
|
|
|
3
3
|
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
4
|
-
server_info: (runtime) => runtime.runtimeInfo(),
|
|
4
|
+
server_info: (runtime, _args, context) => runtime.runtimeInfo(context),
|
|
5
5
|
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
6
6
|
session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
|
|
7
7
|
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
@@ -25,7 +25,7 @@ const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
|
25
25
|
browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
|
|
26
26
|
browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
|
|
27
27
|
browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
|
|
28
|
-
list_roots: (runtime) => runtime.listRoots(),
|
|
28
|
+
list_roots: (runtime, _args, context) => runtime.listRoots(context),
|
|
29
29
|
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
30
30
|
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInteger(args.max_files, 1000, 1, 10000), context),
|
|
31
31
|
read_file: (runtime, args, context) => runtime.readFile(args, context),
|
|
@@ -39,13 +39,13 @@ const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
|
39
39
|
git_log: (runtime, args, context) => runtime.gitLog(args, context),
|
|
40
40
|
git_show: (runtime, args, context) => runtime.gitShow(args, context),
|
|
41
41
|
diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
|
|
42
|
-
list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
|
|
42
|
+
list_local_resources: (runtime, _args, context) => runtime.managedJobManager.listResources(context),
|
|
43
43
|
generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
|
|
44
|
-
stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
|
|
45
|
-
start_job: (runtime, args) => runtime.managedJobManager.start(args),
|
|
46
|
-
list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
|
|
47
|
-
read_job: (runtime, args) => runtime.managedJobManager.read(args),
|
|
48
|
-
cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
|
|
44
|
+
stage_job: (runtime, args, context) => runtime.managedJobManager.stage(args, context),
|
|
45
|
+
start_job: (runtime, args, context) => runtime.managedJobManager.start(args, context),
|
|
46
|
+
list_jobs: (runtime, args, context) => runtime.managedJobManager.list(args, context),
|
|
47
|
+
read_job: (runtime, args, context) => runtime.managedJobManager.read(args, context),
|
|
48
|
+
cancel_job: (runtime, args, context) => runtime.managedJobManager.cancel(args, context),
|
|
49
49
|
run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
|
|
50
50
|
start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
|
|
51
51
|
read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
|