borgmcp-server 0.1.8 → 0.1.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/README.md +57 -43
- package/SECURITY.md +12 -1
- package/dist/bootstrap.d.ts +8 -1
- package/dist/bootstrap.js +39 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +176 -1
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.js +7 -0
- package/dist/credentials.js.map +1 -1
- package/dist/debug-log.d.ts +1 -1
- package/dist/debug-log.js +1 -1
- package/dist/debug-log.js.map +1 -1
- package/dist/https-server.d.ts +3 -0
- package/dist/https-server.js +25 -7
- package/dist/https-server.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/managed-service.d.ts +20 -0
- package/dist/managed-service.js +80 -0
- package/dist/managed-service.js.map +1 -0
- package/dist/portable-credential-store.d.ts +16 -0
- package/dist/portable-credential-store.js +305 -0
- package/dist/portable-credential-store.js.map +1 -0
- package/dist/registry-artifact.d.ts +11 -0
- package/dist/registry-artifact.js +99 -0
- package/dist/registry-artifact.js.map +1 -0
- package/dist/runtime-identity.d.ts +19 -0
- package/dist/runtime-identity.js +61 -0
- package/dist/runtime-identity.js.map +1 -0
- package/dist/runtime-lifecycle.d.ts +56 -0
- package/dist/runtime-lifecycle.js +486 -0
- package/dist/runtime-lifecycle.js.map +1 -0
- package/dist/runtime-operator.d.ts +24 -0
- package/dist/runtime-operator.js +95 -0
- package/dist/runtime-operator.js.map +1 -0
- package/dist/service.d.ts +53 -5
- package/dist/service.js +326 -15
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +8 -0
- package/dist/store.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/bootstrap.ts +46 -5
- package/src/cli.ts +171 -1
- package/src/credentials.ts +7 -0
- package/src/debug-log.ts +2 -1
- package/src/https-server.ts +27 -7
- package/src/index.ts +46 -1
- package/src/main.ts +1 -0
- package/src/managed-service.ts +107 -0
- package/src/portable-credential-store.ts +306 -0
- package/src/registry-artifact.ts +111 -0
- package/src/runtime-identity.ts +82 -0
- package/src/runtime-lifecycle.ts +567 -0
- package/src/runtime-operator.ts +124 -0
- package/src/service.ts +401 -20
- package/src/store.ts +10 -0
package/dist/service.d.ts
CHANGED
|
@@ -3,19 +3,49 @@ import { type CubeInvitationResult } from "./credentials.js";
|
|
|
3
3
|
import { type HttpsServerOptions, type RunningServer } from "./https-server.js";
|
|
4
4
|
import { openStore, type LivenessStore, type StorageLimits } from "./store.js";
|
|
5
5
|
import type { CubeAccess } from "./store.js";
|
|
6
|
+
import { type RuntimeBuildIdentity } from "./runtime-identity.js";
|
|
7
|
+
import { type RuntimeUpdateResult } from "./runtime-operator.js";
|
|
6
8
|
export interface ServerService {
|
|
7
9
|
readonly start: (args: readonly string[]) => Promise<void>;
|
|
8
|
-
readonly setup?: (options: SetupOptions) => Promise<
|
|
10
|
+
readonly setup?: (options: SetupOptions) => Promise<ServerSetupResult>;
|
|
11
|
+
readonly status?: () => Promise<ServerRuntimeStatus>;
|
|
12
|
+
readonly update?: () => Promise<RuntimeUpdateResult>;
|
|
9
13
|
readonly rotateClient?: (clientId: string) => Promise<string>;
|
|
10
14
|
readonly revokeClient?: (clientId: string) => Promise<void>;
|
|
11
15
|
readonly grantClient?: (clientId: string, cubeId: string, access: CubeAccess) => Promise<void>;
|
|
12
16
|
readonly ungrantClient?: (clientId: string, cubeId: string) => Promise<void>;
|
|
13
17
|
readonly createClientInvitation?: (recoveryCredential: string, cubeSelector?: string, access?: CubeAccess) => Promise<string | CubeInvitationResult>;
|
|
14
18
|
readonly replaceOwnerInvitation?: (recoveryCredential: string) => Promise<string>;
|
|
19
|
+
readonly invite?: () => Promise<string>;
|
|
15
20
|
}
|
|
16
21
|
export interface SetupOptions {
|
|
17
22
|
readonly reinitialize: boolean;
|
|
18
23
|
}
|
|
24
|
+
export type ServerSetupResult = (Omit<BootstrapResult, "recoveryCredential" | "initialInvitation"> & {
|
|
25
|
+
readonly artifact?: {
|
|
26
|
+
readonly version: string;
|
|
27
|
+
readonly integrity: string;
|
|
28
|
+
readonly sourceSha: string | null;
|
|
29
|
+
};
|
|
30
|
+
}) | {
|
|
31
|
+
readonly existing: true;
|
|
32
|
+
readonly artifact?: {
|
|
33
|
+
readonly version: string;
|
|
34
|
+
readonly integrity: string;
|
|
35
|
+
readonly sourceSha: string | null;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export interface ServerRuntimeStatus {
|
|
39
|
+
readonly status: "running" | "stopped";
|
|
40
|
+
readonly artifact: {
|
|
41
|
+
readonly version: string;
|
|
42
|
+
readonly integrity: string;
|
|
43
|
+
} | null;
|
|
44
|
+
readonly buildIdentity: string | null;
|
|
45
|
+
readonly endpoint: string | null;
|
|
46
|
+
readonly mode: "foreground" | "managed" | "stopped";
|
|
47
|
+
readonly dataIdentity: "available" | "unavailable";
|
|
48
|
+
}
|
|
19
49
|
export interface ServerEnvironment {
|
|
20
50
|
readonly BORG_SERVER_TLS_KEY_FILE?: string;
|
|
21
51
|
readonly BORG_SERVER_TLS_CERT_FILE?: string;
|
|
@@ -25,13 +55,17 @@ export interface ServerEnvironment {
|
|
|
25
55
|
readonly BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE?: string;
|
|
26
56
|
readonly BORG_SERVER_MAX_DATABASE_BYTES?: string;
|
|
27
57
|
readonly BORG_SERVER_MIN_FREE_DISK_BYTES?: string;
|
|
58
|
+
readonly BORG_SERVER_SOURCE_SHA?: string;
|
|
59
|
+
readonly BORG_SERVER_ARTIFACT_INTEGRITY?: string;
|
|
60
|
+
readonly BORG_SERVER_PROCESS_MODE?: "foreground" | "managed";
|
|
61
|
+
readonly BORG_SERVER_RUNTIME_DIR?: string;
|
|
28
62
|
}
|
|
29
63
|
interface ServiceDependencies {
|
|
30
64
|
readonly environment: ServerEnvironment;
|
|
31
65
|
readonly readFile: (path: string) => Promise<Buffer>;
|
|
32
66
|
readonly readPrivateKey: (path: string) => Promise<Buffer>;
|
|
33
67
|
readonly startServer: (options: HttpsServerOptions) => Promise<RunningServer>;
|
|
34
|
-
readonly onStarted: (origin: string) => void;
|
|
68
|
+
readonly onStarted: (origin: string, identity: RuntimeBuildIdentity) => void;
|
|
35
69
|
readonly waitForShutdown: (server: RunningServer, signal?: AbortSignal) => Promise<void>;
|
|
36
70
|
readonly debugOutput?: (line: string) => void;
|
|
37
71
|
readonly installShutdownHandlers?: () => {
|
|
@@ -44,6 +78,15 @@ interface ServiceDependencies {
|
|
|
44
78
|
};
|
|
45
79
|
readonly onStartupPhase?: (phase: "pre-lock" | "post-lock" | "pre-listen") => Promise<void>;
|
|
46
80
|
}
|
|
81
|
+
export type RuntimeLockStatus = {
|
|
82
|
+
readonly running: false;
|
|
83
|
+
} | {
|
|
84
|
+
readonly running: true;
|
|
85
|
+
readonly pid: number;
|
|
86
|
+
readonly identity: RuntimeBuildIdentity | null;
|
|
87
|
+
readonly endpoint: string | null;
|
|
88
|
+
readonly mode: "foreground" | "managed";
|
|
89
|
+
};
|
|
47
90
|
export interface NodeServerTestHooks {
|
|
48
91
|
readonly onStartupPhase?: (phase: "pre-lock" | "post-lock" | "pre-listen") => Promise<void>;
|
|
49
92
|
readonly onSignalObserved?: () => void;
|
|
@@ -56,13 +99,18 @@ export declare function assertLanCaKeyOffline(runtimeDataDirectory: string): Pro
|
|
|
56
99
|
export declare function selectServerEnvironment(environment: NodeJS.ProcessEnv): ServerEnvironment;
|
|
57
100
|
export declare function resolveStorageLimits(environment: ServerEnvironment): StorageLimits;
|
|
58
101
|
export declare const nodeServerService: ServerService;
|
|
59
|
-
export declare function
|
|
60
|
-
export declare function
|
|
102
|
+
export declare function inspectNodeRuntime(runtimeDataDirectory: string, managedRuntimeDirectory: string): Promise<ServerRuntimeStatus>;
|
|
103
|
+
export declare function setupNodeServerInstallation(setupDataDirectory: string, bindHost: string, options: SetupOptions, credentialRoot?: string): Promise<BootstrapResult | {
|
|
104
|
+
readonly existing: true;
|
|
105
|
+
}>;
|
|
106
|
+
export declare function createOfflineCredentialService(offlineDataDirectory: string, credentialRoot?: string): Pick<Required<ServerService>, "rotateClient" | "revokeClient" | "grantClient" | "ungrantClient" | "createClientInvitation" | "replaceOwnerInvitation" | "invite">;
|
|
61
107
|
interface RuntimeLock {
|
|
62
108
|
readonly release: () => Promise<void>;
|
|
109
|
+
readonly updateOrigin?: (origin: string) => Promise<void>;
|
|
63
110
|
}
|
|
64
111
|
export declare function isFatalTeardownError(error: unknown): boolean;
|
|
65
|
-
export declare function acquireRuntimeLock(runtimeDataDirectory: string, purpose?: "server" | "exclusive-admin"): Promise<RuntimeLock>;
|
|
112
|
+
export declare function acquireRuntimeLock(runtimeDataDirectory: string, purpose?: "server" | "exclusive-admin", identity?: RuntimeBuildIdentity, mode?: "foreground" | "managed"): Promise<RuntimeLock>;
|
|
113
|
+
export declare function inspectRuntimeLock(runtimeDataDirectory: string): Promise<RuntimeLockStatus>;
|
|
66
114
|
export declare function acquireInvitationMintLock(runtimeDataDirectory: string): Promise<RuntimeLock>;
|
|
67
115
|
export declare function installProcessShutdownHandlers(): {
|
|
68
116
|
readonly signal: AbortSignal;
|
package/dist/service.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { lstat, open, readFile, unlink } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
5
7
|
import { bootstrapServer, loadDigestKey, loadTlsPrivateKey, } from "./bootstrap.js";
|
|
6
8
|
import { CredentialAuthority, CredentialDigester, } from "./credentials.js";
|
|
7
9
|
import { CoordinationApi } from "./coordination-api.js";
|
|
@@ -10,9 +12,17 @@ import { MigrationCompatibilityError } from "./migrations.js";
|
|
|
10
12
|
import { createEnrollmentExchange } from "./enrollment.js";
|
|
11
13
|
import { DEFAULT_SERVICE_LIMITS, startHttpsServer, } from "./https-server.js";
|
|
12
14
|
import { resolveBindOptions } from "./network-policy.js";
|
|
15
|
+
import { DEFAULT_PORT } from "./network-policy.js";
|
|
16
|
+
import { readPortableServerCredential, writePortableServerCredential, } from "./portable-credential-store.js";
|
|
13
17
|
import { invitationCubeAmbiguousError, operatorErrors, } from "./operator-error.js";
|
|
14
18
|
import { parseStartOptions } from "./start-options.js";
|
|
15
19
|
import { DEFAULT_STORAGE_LIMITS, openStore, preparePrivateDataDirectory, InvitationCubeAmbiguousError, InvitationCubeNotFoundError, } from "./store.js";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { loadRuntimeBuildIdentity } from "./runtime-identity.js";
|
|
22
|
+
import { createRuntimeLifecycle, createUnixNpmArtifactUnpacker, inspectActiveRuntimeArtifact, } from "./runtime-lifecycle.js";
|
|
23
|
+
import { createRegistryArtifactSource } from "./registry-artifact.js";
|
|
24
|
+
import { createRuntimeOperator } from "./runtime-operator.js";
|
|
25
|
+
import { createManagedServiceDefinition } from "./managed-service.js";
|
|
16
26
|
const guardedRuntimeFailures = new Set();
|
|
17
27
|
let nodeServerTestHooks;
|
|
18
28
|
export function installNodeServerTestHooks(hooks) {
|
|
@@ -70,8 +80,20 @@ export function createNodeServerService(dependencies) {
|
|
|
70
80
|
throw operatorErrors.SERVER_FILES_MISSING;
|
|
71
81
|
}
|
|
72
82
|
let runtimeLock;
|
|
83
|
+
let runtimeIdentity;
|
|
73
84
|
try {
|
|
74
|
-
|
|
85
|
+
runtimeIdentity = await loadRuntimeBuildIdentity({
|
|
86
|
+
...(dependencies.environment.BORG_SERVER_SOURCE_SHA === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { sourceSha: dependencies.environment.BORG_SERVER_SOURCE_SHA }),
|
|
89
|
+
...(dependencies.environment.BORG_SERVER_ARTIFACT_INTEGRITY === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { artifactIntegrity: dependencies.environment.BORG_SERVER_ARTIFACT_INTEGRITY }),
|
|
92
|
+
artifactDescriptorPath: fileURLToPath(new URL("../../artifact.json", import.meta.url)),
|
|
93
|
+
});
|
|
94
|
+
runtimeLock = dataDirectory === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: await acquireRuntimeLock(dataDirectory, "server", runtimeIdentity, dependencies.environment.BORG_SERVER_PROCESS_MODE ?? "foreground");
|
|
75
97
|
await dependencies.onStartupPhase?.("post-lock");
|
|
76
98
|
throwIfShutdown(shutdown?.signal);
|
|
77
99
|
}
|
|
@@ -146,6 +168,7 @@ export function createNodeServerService(dependencies) {
|
|
|
146
168
|
? {}
|
|
147
169
|
: { handleCoordination: (request) => coordinationApi.handle(request) }),
|
|
148
170
|
debugLogger,
|
|
171
|
+
runtimeIdentity,
|
|
149
172
|
});
|
|
150
173
|
throwIfShutdown(shutdown?.signal);
|
|
151
174
|
if (authRuntime !== undefined) {
|
|
@@ -172,7 +195,8 @@ export function createNodeServerService(dependencies) {
|
|
|
172
195
|
let failure;
|
|
173
196
|
try {
|
|
174
197
|
throwIfShutdown(shutdown?.signal);
|
|
175
|
-
|
|
198
|
+
await runtimeLock?.updateOrigin?.(running.origin);
|
|
199
|
+
dependencies.onStarted(running.origin, runtimeIdentity);
|
|
176
200
|
debugLogger.emit({ event: "lifecycle", action: "listening" });
|
|
177
201
|
await dependencies.waitForShutdown(running, shutdown?.signal);
|
|
178
202
|
}
|
|
@@ -216,6 +240,13 @@ export function selectServerEnvironment(environment) {
|
|
|
216
240
|
const maxActivityEntries = environment["BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE"];
|
|
217
241
|
const maxDatabaseBytes = environment["BORG_SERVER_MAX_DATABASE_BYTES"];
|
|
218
242
|
const minFreeDiskBytes = environment["BORG_SERVER_MIN_FREE_DISK_BYTES"];
|
|
243
|
+
const sourceSha = environment["BORG_SERVER_SOURCE_SHA"];
|
|
244
|
+
const artifactIntegrity = environment["BORG_SERVER_ARTIFACT_INTEGRITY"];
|
|
245
|
+
const processMode = environment["BORG_SERVER_PROCESS_MODE"];
|
|
246
|
+
const runtimeDirectory = environment["BORG_SERVER_RUNTIME_DIR"];
|
|
247
|
+
if (processMode !== undefined && processMode !== "foreground" && processMode !== "managed") {
|
|
248
|
+
throw new Error("BORG_SERVER_PROCESS_MODE is invalid.");
|
|
249
|
+
}
|
|
219
250
|
return {
|
|
220
251
|
...(keyFile === undefined ? {} : { BORG_SERVER_TLS_KEY_FILE: keyFile }),
|
|
221
252
|
...(certificateFile === undefined
|
|
@@ -229,6 +260,10 @@ export function selectServerEnvironment(environment) {
|
|
|
229
260
|
: { BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE: maxActivityEntries }),
|
|
230
261
|
...(maxDatabaseBytes === undefined ? {} : { BORG_SERVER_MAX_DATABASE_BYTES: maxDatabaseBytes }),
|
|
231
262
|
...(minFreeDiskBytes === undefined ? {} : { BORG_SERVER_MIN_FREE_DISK_BYTES: minFreeDiskBytes }),
|
|
263
|
+
...(sourceSha === undefined ? {} : { BORG_SERVER_SOURCE_SHA: sourceSha }),
|
|
264
|
+
...(artifactIntegrity === undefined ? {} : { BORG_SERVER_ARTIFACT_INTEGRITY: artifactIntegrity }),
|
|
265
|
+
...(processMode === undefined ? {} : { BORG_SERVER_PROCESS_MODE: processMode }),
|
|
266
|
+
...(runtimeDirectory === undefined ? {} : { BORG_SERVER_RUNTIME_DIR: runtimeDirectory }),
|
|
232
267
|
};
|
|
233
268
|
}
|
|
234
269
|
export function resolveStorageLimits(environment) {
|
|
@@ -260,6 +295,9 @@ function storageOperatorErrorCode(name) {
|
|
|
260
295
|
}
|
|
261
296
|
const serverEnvironment = selectServerEnvironment(process.env);
|
|
262
297
|
const dataDirectory = serverEnvironment.BORG_SERVER_DATA_DIR ?? join(homedir(), ".borg", "server");
|
|
298
|
+
const runtimeDirectory = serverEnvironment.BORG_SERVER_RUNTIME_DIR ?? join(homedir(), ".borg", "server-runtime");
|
|
299
|
+
const credentialFile = join(homedir(), ".borg", "credentials");
|
|
300
|
+
const nodeRuntimeOperator = createNodeRuntimeOperator(runtimeDirectory, dataDirectory);
|
|
263
301
|
const startOnlyService = createNodeServerService({
|
|
264
302
|
environment: { ...serverEnvironment, BORG_SERVER_DATA_DIR: dataDirectory },
|
|
265
303
|
readFile,
|
|
@@ -268,8 +306,29 @@ const startOnlyService = createNodeServerService({
|
|
|
268
306
|
const running = await startHttpsServer(options);
|
|
269
307
|
return nodeServerTestHooks?.wrapRunningServer?.(running) ?? running;
|
|
270
308
|
},
|
|
271
|
-
onStarted: (origin) => {
|
|
272
|
-
|
|
309
|
+
onStarted: (origin, identity) => {
|
|
310
|
+
if (process.stderr.isTTY !== true || serverEnvironment.BORG_SERVER_PROCESS_MODE === "managed") {
|
|
311
|
+
console.error(JSON.stringify({
|
|
312
|
+
status: "running",
|
|
313
|
+
artifact: `borgmcp-server@${identity.package_version}`,
|
|
314
|
+
artifact_integrity: identity.artifact_integrity,
|
|
315
|
+
build_identity: identity.source_sha,
|
|
316
|
+
endpoint: origin,
|
|
317
|
+
mode: serverEnvironment.BORG_SERVER_PROCESS_MODE ?? "foreground",
|
|
318
|
+
data_identity: "available",
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.error([
|
|
323
|
+
"Starting verified local server in the foreground.",
|
|
324
|
+
`Artifact: borgmcp-server@${identity.package_version} (${identity.artifact_integrity ?? "unavailable"})`,
|
|
325
|
+
`Build identity: ${identity.source_sha ?? "unavailable"}`,
|
|
326
|
+
`Endpoint: ${origin}`,
|
|
327
|
+
"Data and identity: preserved",
|
|
328
|
+
"Ctrl-C stops the foreground process.",
|
|
329
|
+
"Foreground mode does not manage persistence.",
|
|
330
|
+
].join("\n"));
|
|
331
|
+
}
|
|
273
332
|
nodeServerTestHooks?.onListening?.(origin);
|
|
274
333
|
},
|
|
275
334
|
onStartupPhase: (phase) => nodeServerTestHooks?.onStartupPhase?.(phase) ?? Promise.resolve(),
|
|
@@ -285,9 +344,127 @@ const startOnlyService = createNodeServerService({
|
|
|
285
344
|
});
|
|
286
345
|
export const nodeServerService = {
|
|
287
346
|
start: startOnlyService.start,
|
|
288
|
-
setup: (options) =>
|
|
289
|
-
|
|
347
|
+
setup: async (options) => {
|
|
348
|
+
const bindHost = resolveSetupBindHost(serverEnvironment);
|
|
349
|
+
if ((await inspectRuntimeLock(dataDirectory)).running)
|
|
350
|
+
throw operatorErrors.RUNTIME_ACTIVE;
|
|
351
|
+
const artifact = await nodeRuntimeOperator.prepareLatest(30_000);
|
|
352
|
+
const result = await setupNodeServerInstallation(dataDirectory, bindHost, options, credentialFile);
|
|
353
|
+
if (!("existing" in result)) {
|
|
354
|
+
const { recoveryCredential: _recovery, initialInvitation: _invitation, ...publicResult } = result;
|
|
355
|
+
return {
|
|
356
|
+
...publicResult,
|
|
357
|
+
artifact: {
|
|
358
|
+
version: artifact.version,
|
|
359
|
+
integrity: artifact.integrity,
|
|
360
|
+
sourceSha: artifact.sourceSha,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
...result,
|
|
366
|
+
artifact: {
|
|
367
|
+
version: artifact.version,
|
|
368
|
+
integrity: artifact.integrity,
|
|
369
|
+
sourceSha: artifact.sourceSha,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
status: () => inspectNodeRuntime(dataDirectory, runtimeDirectory),
|
|
374
|
+
update: () => nodeRuntimeOperator.updateLatest(30_000),
|
|
375
|
+
...createOfflineCredentialService(dataDirectory, credentialFile),
|
|
290
376
|
};
|
|
377
|
+
function createNodeRuntimeOperator(managedRuntimeDirectory, runtimeDataDirectory) {
|
|
378
|
+
const platform = process.platform === "darwin" ? "launchd" : "systemd";
|
|
379
|
+
const definition = createManagedServiceDefinition({
|
|
380
|
+
platform,
|
|
381
|
+
nodeExecutable: process.execPath,
|
|
382
|
+
runtimeRoot: managedRuntimeDirectory,
|
|
383
|
+
dataDirectory: runtimeDataDirectory,
|
|
384
|
+
definitionPath: platform === "launchd"
|
|
385
|
+
? join(homedir(), "Library", "LaunchAgents", "ai.borgmcp.server.plist")
|
|
386
|
+
: join(homedir(), ".config", "systemd", "user", "ai.borgmcp.server.service"),
|
|
387
|
+
...(platform === "launchd" ? { launchdDomain: `gui/${process.getuid?.() ?? 0}` } : {}),
|
|
388
|
+
});
|
|
389
|
+
const run = async (command, signal) => {
|
|
390
|
+
const [executable, ...args] = command;
|
|
391
|
+
await promisify(execFile)(executable, args, {
|
|
392
|
+
signal,
|
|
393
|
+
timeout: 20_000,
|
|
394
|
+
maxBuffer: 64 * 1024,
|
|
395
|
+
encoding: "utf8",
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
const lifecycle = createRuntimeLifecycle({
|
|
399
|
+
unpack: createUnixNpmArtifactUnpacker(),
|
|
400
|
+
restart: (signal) => run(definition.restart, signal),
|
|
401
|
+
stop: (signal) => run(definition.stop, signal),
|
|
402
|
+
probe: (signal) => waitForRuntimeIdentity(runtimeDataDirectory, signal),
|
|
403
|
+
});
|
|
404
|
+
return createRuntimeOperator({
|
|
405
|
+
runtimeRoot: managedRuntimeDirectory,
|
|
406
|
+
artifacts: createRegistryArtifactSource(),
|
|
407
|
+
lifecycle,
|
|
408
|
+
isRunning: async () => {
|
|
409
|
+
const status = await inspectRuntimeLock(runtimeDataDirectory);
|
|
410
|
+
if (status.running && status.mode !== "managed") {
|
|
411
|
+
throw new Error("Foreground runtime must be stopped before artifact activation.");
|
|
412
|
+
}
|
|
413
|
+
return status.running;
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function waitForRuntimeIdentity(runtimeDataDirectory, signal) {
|
|
418
|
+
while (!signal.aborted) {
|
|
419
|
+
try {
|
|
420
|
+
const status = await inspectRuntimeLock(runtimeDataDirectory);
|
|
421
|
+
if (status.running && status.identity !== null)
|
|
422
|
+
return status.identity;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (error !== operatorErrors.RUNTIME_LOCK_STALE)
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
await new Promise((resolve) => {
|
|
429
|
+
const timer = setTimeout(resolve, 50);
|
|
430
|
+
signal.addEventListener("abort", () => {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
resolve();
|
|
433
|
+
}, { once: true });
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
throw new Error("Managed runtime identity probe was cancelled.");
|
|
437
|
+
}
|
|
438
|
+
export async function inspectNodeRuntime(runtimeDataDirectory, managedRuntimeDirectory) {
|
|
439
|
+
const [lock, activeArtifact, dataIdentity] = await Promise.all([
|
|
440
|
+
inspectRuntimeLock(runtimeDataDirectory),
|
|
441
|
+
inspectActiveRuntimeArtifact(managedRuntimeDirectory),
|
|
442
|
+
hasDataIdentity(runtimeDataDirectory),
|
|
443
|
+
]);
|
|
444
|
+
const identity = lock.running ? lock.identity : null;
|
|
445
|
+
const artifact = identity?.artifact_integrity === null || identity === null
|
|
446
|
+
? activeArtifact === null ? null : { version: activeArtifact.version, integrity: activeArtifact.integrity }
|
|
447
|
+
: { version: identity.package_version, integrity: identity.artifact_integrity };
|
|
448
|
+
return Object.freeze({
|
|
449
|
+
status: lock.running ? "running" : "stopped",
|
|
450
|
+
artifact,
|
|
451
|
+
buildIdentity: identity?.source_sha ?? null,
|
|
452
|
+
endpoint: lock.running ? lock.endpoint : null,
|
|
453
|
+
mode: lock.running ? lock.mode : "stopped",
|
|
454
|
+
dataIdentity,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async function hasDataIdentity(directory) {
|
|
458
|
+
try {
|
|
459
|
+
const metadata = await lstat(join(directory, "server.json"));
|
|
460
|
+
return metadata.isFile() && !metadata.isSymbolicLink() ? "available" : "unavailable";
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
if (error.code === "ENOENT")
|
|
464
|
+
return "unavailable";
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
291
468
|
const managedInstallationFiles = Object.freeze([
|
|
292
469
|
"borg.db",
|
|
293
470
|
"borg.db-wal",
|
|
@@ -300,20 +477,34 @@ const managedInstallationFiles = Object.freeze([
|
|
|
300
477
|
"server.crt",
|
|
301
478
|
"server.json",
|
|
302
479
|
]);
|
|
303
|
-
export async function setupNodeServerInstallation(setupDataDirectory, bindHost, options) {
|
|
480
|
+
export async function setupNodeServerInstallation(setupDataDirectory, bindHost, options, credentialRoot) {
|
|
304
481
|
const directory = await preparePrivateDataDirectory(setupDataDirectory);
|
|
305
482
|
const runtimeLock = await acquireRuntimeLock(directory);
|
|
306
483
|
let invitationLock;
|
|
307
484
|
try {
|
|
308
485
|
invitationLock = await acquireInvitationMintLock(directory);
|
|
309
486
|
const existing = await inspectManagedInstallation(directory);
|
|
310
|
-
if (existing.length !== 0 && !options.reinitialize)
|
|
311
|
-
|
|
487
|
+
if (existing.length !== 0 && !options.reinitialize) {
|
|
488
|
+
const names = new Set(existing.map((path) => basename(path)));
|
|
489
|
+
const complete = [
|
|
490
|
+
"borg.db",
|
|
491
|
+
"credential-digest.key",
|
|
492
|
+
"ca.crt",
|
|
493
|
+
"server.key",
|
|
494
|
+
"server.crt",
|
|
495
|
+
"server.json",
|
|
496
|
+
].every((name) => names.has(name));
|
|
497
|
+
if (!complete)
|
|
498
|
+
throw operatorErrors.INSTALLATION_EXISTS;
|
|
499
|
+
return Object.freeze({ existing: true });
|
|
500
|
+
}
|
|
312
501
|
if (options.reinitialize) {
|
|
313
502
|
for (const path of existing)
|
|
314
503
|
await unlink(path);
|
|
315
504
|
}
|
|
316
|
-
return await bootstrapServer(directory, bindHost)
|
|
505
|
+
return await bootstrapServer(directory, bindHost, () => new Date(), credentialRoot === undefined
|
|
506
|
+
? async () => undefined
|
|
507
|
+
: (record) => writePortableServerCredential(credentialRoot, record));
|
|
317
508
|
}
|
|
318
509
|
finally {
|
|
319
510
|
if (invitationLock === undefined)
|
|
@@ -349,7 +540,7 @@ function resolveSetupBindHost(environment) {
|
|
|
349
540
|
lanConsent: true,
|
|
350
541
|
}).host;
|
|
351
542
|
}
|
|
352
|
-
export function createOfflineCredentialService(offlineDataDirectory) {
|
|
543
|
+
export function createOfflineCredentialService(offlineDataDirectory, credentialRoot) {
|
|
353
544
|
const withAuthority = async (operation) => {
|
|
354
545
|
const runtimeLock = await acquireRuntimeLock(offlineDataDirectory);
|
|
355
546
|
let invitationLock;
|
|
@@ -388,7 +579,7 @@ export function createOfflineCredentialService(offlineDataDirectory) {
|
|
|
388
579
|
const digestKey = await loadDigestKey(join(offlineDataDirectory, "credential-digest.key"));
|
|
389
580
|
digester = new CredentialDigester(digestKey);
|
|
390
581
|
digestKey.fill(0);
|
|
391
|
-
return operation(new CredentialAuthority(runtime.credentials, digester));
|
|
582
|
+
return await operation(new CredentialAuthority(runtime.credentials, digester));
|
|
392
583
|
}
|
|
393
584
|
catch (error) {
|
|
394
585
|
if (error instanceof MigrationCompatibilityError)
|
|
@@ -447,6 +638,21 @@ export function createOfflineCredentialService(offlineDataDirectory) {
|
|
|
447
638
|
throw operatorErrors.RECOVERY_INVALID;
|
|
448
639
|
return invitation;
|
|
449
640
|
}),
|
|
641
|
+
invite: () => withInvitationAuthority(async (authority) => {
|
|
642
|
+
if (credentialRoot === undefined)
|
|
643
|
+
throw new Error("Local owner credential store is unavailable.");
|
|
644
|
+
const config = JSON.parse((await readFile(join(offlineDataDirectory, "server.json"))).toString("utf8"));
|
|
645
|
+
if (typeof config.bind_host !== "string" || typeof config.ca_spki_sha256 !== "string") {
|
|
646
|
+
throw new Error("Server identity is invalid.");
|
|
647
|
+
}
|
|
648
|
+
const origin = `https://${config.bind_host === "::1" ? "[::1]" : config.bind_host}:${DEFAULT_PORT}`;
|
|
649
|
+
const trustIdentity = `spki-sha256:${config.ca_spki_sha256}`;
|
|
650
|
+
const record = await readPortableServerCredential(credentialRoot, origin, trustIdentity);
|
|
651
|
+
const invitation = authority.createInvitationForOwnerCredential(record.credential, 15 * 60_000);
|
|
652
|
+
if (invitation === null)
|
|
653
|
+
throw new Error("Local owner credential is invalid.");
|
|
654
|
+
return invitation;
|
|
655
|
+
}),
|
|
450
656
|
};
|
|
451
657
|
}
|
|
452
658
|
const canonicalCubeUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -515,13 +721,20 @@ export function isFatalTeardownError(error) {
|
|
|
515
721
|
function fatalTeardownError(primary, cleanup) {
|
|
516
722
|
return new FatalTeardownError(fatalTeardownCapability, primary, cleanup);
|
|
517
723
|
}
|
|
518
|
-
export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclusive-admin") {
|
|
724
|
+
export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclusive-admin", identity, mode = "foreground") {
|
|
519
725
|
const path = join(runtimeDataDirectory, "runtime.lock");
|
|
520
726
|
const nonce = randomUUID();
|
|
521
727
|
try {
|
|
522
728
|
const handle = await open(path, "wx", 0o600);
|
|
729
|
+
const record = {
|
|
730
|
+
pid: process.pid,
|
|
731
|
+
nonce,
|
|
732
|
+
purpose,
|
|
733
|
+
mode,
|
|
734
|
+
...(identity === undefined ? {} : { runtime_identity: identity }),
|
|
735
|
+
};
|
|
523
736
|
try {
|
|
524
|
-
await handle.writeFile(JSON.stringify(
|
|
737
|
+
await handle.writeFile(JSON.stringify(record));
|
|
525
738
|
}
|
|
526
739
|
catch (error) {
|
|
527
740
|
await handle.close();
|
|
@@ -529,6 +742,15 @@ export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclus
|
|
|
529
742
|
throw error;
|
|
530
743
|
}
|
|
531
744
|
return {
|
|
745
|
+
updateOrigin: async (origin) => {
|
|
746
|
+
if (!/^https:\/\/(?:\[[0-9a-f:]+\]|[0-9.]+):[0-9]{1,5}$/u.test(origin)) {
|
|
747
|
+
throw new Error("Runtime endpoint is invalid.");
|
|
748
|
+
}
|
|
749
|
+
record.endpoint = origin;
|
|
750
|
+
await handle.truncate(0);
|
|
751
|
+
await handle.write(JSON.stringify(record), 0, "utf8");
|
|
752
|
+
await handle.sync();
|
|
753
|
+
},
|
|
532
754
|
release: async () => {
|
|
533
755
|
await handle.close();
|
|
534
756
|
try {
|
|
@@ -565,6 +787,95 @@ export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclus
|
|
|
565
787
|
throw operatorErrors.RUNTIME_LOCK_STALE;
|
|
566
788
|
}
|
|
567
789
|
}
|
|
790
|
+
export async function inspectRuntimeLock(runtimeDataDirectory) {
|
|
791
|
+
const path = join(runtimeDataDirectory, "runtime.lock");
|
|
792
|
+
let metadata;
|
|
793
|
+
try {
|
|
794
|
+
metadata = await lstat(path);
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
if (error.code === "ENOENT")
|
|
798
|
+
return Object.freeze({ running: false });
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0 ||
|
|
802
|
+
metadata.size > 8 * 1024) {
|
|
803
|
+
throw operatorErrors.RUNTIME_LOCK_UNSAFE;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
807
|
+
if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || value.purpose !== "server" ||
|
|
808
|
+
(value.endpoint !== undefined &&
|
|
809
|
+
(typeof value.endpoint !== "string" || !isRuntimeEndpoint(value.endpoint))) ||
|
|
810
|
+
(value.mode !== "foreground" && value.mode !== "managed")) {
|
|
811
|
+
throw new Error();
|
|
812
|
+
}
|
|
813
|
+
const pid = value.pid;
|
|
814
|
+
if (!processIsAlive(pid))
|
|
815
|
+
throw operatorErrors.RUNTIME_LOCK_STALE;
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
running: true,
|
|
818
|
+
pid,
|
|
819
|
+
identity: decodeRuntimeLockIdentity(value.runtime_identity),
|
|
820
|
+
endpoint: value.endpoint ?? null,
|
|
821
|
+
mode: value.mode,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
if (error === operatorErrors.RUNTIME_LOCK_STALE)
|
|
826
|
+
throw error;
|
|
827
|
+
throw operatorErrors.RUNTIME_LOCK_INVALID;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function isRuntimeEndpoint(value) {
|
|
831
|
+
if (value.length > 512)
|
|
832
|
+
return false;
|
|
833
|
+
try {
|
|
834
|
+
const endpoint = new URL(value);
|
|
835
|
+
if (endpoint.protocol !== "https:" || endpoint.username !== "" || endpoint.password !== "" ||
|
|
836
|
+
endpoint.pathname !== "/" || endpoint.search !== "" || endpoint.hash !== "")
|
|
837
|
+
return false;
|
|
838
|
+
const host = endpoint.hostname.startsWith("[") && endpoint.hostname.endsWith("]")
|
|
839
|
+
? endpoint.hostname.slice(1, -1)
|
|
840
|
+
: endpoint.hostname;
|
|
841
|
+
resolveBindOptions({ host, port: Number(endpoint.port || "443"), lanConsent: true });
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function decodeRuntimeLockIdentity(value) {
|
|
849
|
+
if (value === undefined)
|
|
850
|
+
return null;
|
|
851
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
852
|
+
throw new Error();
|
|
853
|
+
const identity = value;
|
|
854
|
+
const packageVersion = identity["package_version"];
|
|
855
|
+
const sourceSha = identity["source_sha"];
|
|
856
|
+
const artifactIntegrity = identity["artifact_integrity"];
|
|
857
|
+
const protocolVersion = identity["protocol_version"];
|
|
858
|
+
const startedAt = identity["started_at"];
|
|
859
|
+
if (typeof packageVersion !== "string" ||
|
|
860
|
+
!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(packageVersion) ||
|
|
861
|
+
(sourceSha !== null &&
|
|
862
|
+
(typeof sourceSha !== "string" || !/^[0-9a-f]{40}$/u.test(sourceSha))) ||
|
|
863
|
+
(artifactIntegrity !== null &&
|
|
864
|
+
(typeof artifactIntegrity !== "string" ||
|
|
865
|
+
!/^sha512-[A-Za-z0-9+/]{86}==$/u.test(artifactIntegrity))) ||
|
|
866
|
+
typeof protocolVersion !== "string" || protocolVersion.length < 1 || protocolVersion.length > 32 ||
|
|
867
|
+
typeof startedAt !== "string" || startedAt.length > 64 ||
|
|
868
|
+
!Number.isFinite(Date.parse(startedAt)) || new Date(startedAt).toISOString() !== startedAt) {
|
|
869
|
+
throw new Error();
|
|
870
|
+
}
|
|
871
|
+
return Object.freeze({
|
|
872
|
+
package_version: packageVersion,
|
|
873
|
+
source_sha: sourceSha,
|
|
874
|
+
artifact_integrity: artifactIntegrity,
|
|
875
|
+
protocol_version: protocolVersion,
|
|
876
|
+
started_at: startedAt,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
568
879
|
export async function acquireInvitationMintLock(runtimeDataDirectory) {
|
|
569
880
|
const path = join(runtimeDataDirectory, "invitation-mint.lock");
|
|
570
881
|
const nonce = randomUUID();
|