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.
Files changed (63) hide show
  1. package/README.md +57 -43
  2. package/SECURITY.md +12 -1
  3. package/dist/bootstrap.d.ts +8 -1
  4. package/dist/bootstrap.js +39 -6
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.js +176 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/credentials.d.ts +1 -0
  10. package/dist/credentials.js +7 -0
  11. package/dist/credentials.js.map +1 -1
  12. package/dist/debug-log.d.ts +1 -1
  13. package/dist/debug-log.js +1 -1
  14. package/dist/debug-log.js.map +1 -1
  15. package/dist/https-server.d.ts +3 -0
  16. package/dist/https-server.js +25 -7
  17. package/dist/https-server.js.map +1 -1
  18. package/dist/index.d.ts +14 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/main.js +1 -0
  22. package/dist/main.js.map +1 -1
  23. package/dist/managed-service.d.ts +20 -0
  24. package/dist/managed-service.js +80 -0
  25. package/dist/managed-service.js.map +1 -0
  26. package/dist/portable-credential-store.d.ts +16 -0
  27. package/dist/portable-credential-store.js +305 -0
  28. package/dist/portable-credential-store.js.map +1 -0
  29. package/dist/registry-artifact.d.ts +11 -0
  30. package/dist/registry-artifact.js +99 -0
  31. package/dist/registry-artifact.js.map +1 -0
  32. package/dist/runtime-identity.d.ts +19 -0
  33. package/dist/runtime-identity.js +61 -0
  34. package/dist/runtime-identity.js.map +1 -0
  35. package/dist/runtime-lifecycle.d.ts +56 -0
  36. package/dist/runtime-lifecycle.js +486 -0
  37. package/dist/runtime-lifecycle.js.map +1 -0
  38. package/dist/runtime-operator.d.ts +24 -0
  39. package/dist/runtime-operator.js +95 -0
  40. package/dist/runtime-operator.js.map +1 -0
  41. package/dist/service.d.ts +53 -5
  42. package/dist/service.js +326 -15
  43. package/dist/service.js.map +1 -1
  44. package/dist/store.d.ts +1 -0
  45. package/dist/store.js +8 -0
  46. package/dist/store.js.map +1 -1
  47. package/npm-shrinkwrap.json +2 -2
  48. package/package.json +1 -1
  49. package/src/bootstrap.ts +46 -5
  50. package/src/cli.ts +171 -1
  51. package/src/credentials.ts +7 -0
  52. package/src/debug-log.ts +2 -1
  53. package/src/https-server.ts +27 -7
  54. package/src/index.ts +46 -1
  55. package/src/main.ts +1 -0
  56. package/src/managed-service.ts +107 -0
  57. package/src/portable-credential-store.ts +306 -0
  58. package/src/registry-artifact.ts +111 -0
  59. package/src/runtime-identity.ts +82 -0
  60. package/src/runtime-lifecycle.ts +567 -0
  61. package/src/runtime-operator.ts +124 -0
  62. package/src/service.ts +401 -20
  63. package/src/store.ts +10 -0
package/src/bootstrap.ts CHANGED
@@ -1,16 +1,23 @@
1
1
  import { createHash, randomBytes, randomUUID, X509Certificate } from "node:crypto";
2
- import { lstat, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { lstat, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
4
  import { generate } from "selfsigned";
5
5
 
6
- import { CredentialAuthority, CredentialDigester } from "./credentials.js";
6
+ import { CredentialAuthority, CredentialDigester, generateSecret } from "./credentials.js";
7
+ import type { PortableServerCredential } from "./portable-credential-store.js";
7
8
  import { openStore } from "./store.js";
8
9
 
9
10
  export interface BootstrapResult {
10
11
  readonly serverId: string;
11
12
  readonly caFingerprint: string;
13
+ /** @deprecated Retained for offline administrative compatibility; never rendered by setup. */
12
14
  readonly recoveryCredential: string;
15
+ /** @deprecated The already-consumed bootstrap invitation; never rendered by setup. */
13
16
  readonly initialInvitation: string;
17
+ readonly ownerAccess: {
18
+ readonly clientId: string;
19
+ readonly serverCapabilities: readonly ["create_cube"];
20
+ };
14
21
  readonly paths: {
15
22
  readonly database: string;
16
23
  readonly digestKey: string;
@@ -26,6 +33,7 @@ export async function bootstrapServer(
26
33
  dataDirectory: string,
27
34
  bindHost = "127.0.0.1",
28
35
  clock: () => Date = () => new Date(),
36
+ persistOwnerCredential: (record: PortableServerCredential) => Promise<void> = async () => undefined,
29
37
  ): Promise<BootstrapResult> {
30
38
  const directory = resolve(dataDirectory);
31
39
  const paths = {
@@ -63,6 +71,7 @@ export async function bootstrapServer(
63
71
  .digest("hex");
64
72
  const digestKey = randomBytes(32);
65
73
  const runtime = await openStore({ path: paths.database, clock });
74
+ let completed = false;
66
75
  try {
67
76
  await Promise.all([
68
77
  writePrivate(paths.digestKey, digestKey),
@@ -81,20 +90,52 @@ export async function bootstrapServer(
81
90
  try {
82
91
  const authority = new CredentialAuthority(runtime.credentials, digester, clock);
83
92
  const recoveryCredential = authority.createRecoveryCredential();
84
- const initialInvitation = authority.createBootstrapInvitation(15 * 60_000);
85
- return {
93
+ const invitation = authority.createBootstrapInvitation(15 * 60_000);
94
+ const credential = generateSecret();
95
+ const enrollment = authority.exchangeInvitation({
96
+ invitation,
97
+ retryKey: randomUUID(),
98
+ clientCredential: credential,
99
+ clientName: "Local owner",
100
+ });
101
+ if (enrollment?.purpose !== "owner" || enrollment.serverCapabilities[0] !== "create_cube") {
102
+ throw new Error("Local owner provisioning failed.");
103
+ }
104
+ await persistOwnerCredential(Object.freeze({
105
+ version: 2,
106
+ origin: `https://${bindHost === "::1" ? "[::1]" : bindHost}:7091`,
107
+ trustIdentity: `spki-sha256:${caFingerprint}`,
108
+ credential,
109
+ clientId: enrollment.clientId,
110
+ serverCapabilities: ["create_cube"] as const,
111
+ }));
112
+ const result = {
86
113
  serverId,
87
114
  caFingerprint,
88
115
  recoveryCredential,
89
- initialInvitation,
116
+ initialInvitation: invitation,
117
+ ownerAccess: {
118
+ clientId: enrollment.clientId,
119
+ serverCapabilities: ["create_cube"] as const,
120
+ },
90
121
  paths,
91
122
  };
123
+ completed = true;
124
+ return result;
92
125
  } finally {
93
126
  digester.destroy();
94
127
  }
95
128
  } finally {
96
129
  digestKey.fill(0);
97
130
  runtime.close();
131
+ if (!completed) {
132
+ await Promise.all([
133
+ ...Object.values(paths),
134
+ `${paths.database}-wal`,
135
+ `${paths.database}-shm`,
136
+ `${paths.database}-journal`,
137
+ ].map((path) => unlink(path).catch(() => undefined)));
138
+ }
98
139
  }
99
140
  }
100
141
 
package/src/cli.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { ServerService } from "./service.js";
2
+ import { RuntimeUpdateFailure } from "./runtime-operator.js";
2
3
 
3
4
  export interface CliIo {
4
5
  readonly stdout: (message: string) => void;
5
6
  readonly stderr: (message: string) => void;
6
7
  readonly readSecret?: (prompt: string) => Promise<string>;
8
+ readonly isTTY?: boolean;
7
9
  }
8
10
 
9
11
  const usage = `Usage: borg-mcp-server <command> [options]
@@ -11,6 +13,9 @@ const usage = `Usage: borg-mcp-server <command> [options]
11
13
  Commands:
12
14
  setup [--reinitialize] Prepare an offline server installation
13
15
  start Start the server process
16
+ status [--json] Report exact local runtime evidence
17
+ update [--json] Verify and activate the latest server artifact
18
+ invite Create a single-use invitation in an interactive terminal.
14
19
  client-rotate <client-id> Rotate one client credential offline
15
20
  client-revoke <client-id> Revoke one client and its credentials offline
16
21
  client-grant <client-id> <cube-id> <read|write|manage> Set one offline cube grant
@@ -58,11 +63,109 @@ export async function runCli(
58
63
  return 1;
59
64
  }
60
65
  const result = await service.setup({ reinitialize: extraArgs[0] === "--reinitialize" });
61
- io.stdout(`Server setup complete.\nRecovery credential (shown once; keep offline): ${result.recoveryCredential}\nOwner enrollment invitation (single-use, shown once; enroll the owner client): ${result.initialInvitation}\nSetup created no cube.\nNext: start the server with \`borg-mcp-server start\`.`);
66
+ const artifactIdentity = result.artifact === undefined
67
+ ? "borgmcp-server@unavailable"
68
+ : `borgmcp-server@${result.artifact.version}`;
69
+ const artifact = result.artifact === undefined
70
+ ? "Artifact: unavailable"
71
+ : `Artifact: ${artifactIdentity} (${result.artifact.integrity})`;
72
+ if (io.isTTY === false) {
73
+ io.stdout(JSON.stringify({
74
+ status: "prepared",
75
+ artifact: artifactIdentity,
76
+ build_identity: result.artifact?.sourceSha ?? null,
77
+ owner_access: "prepared",
78
+ process: "stopped",
79
+ }));
80
+ return 0;
81
+ }
82
+ if ("existing" in result) {
83
+ io.stdout([
84
+ "Local server is already prepared.",
85
+ artifact,
86
+ `Build identity: ${result.artifact?.sourceSha ?? "unavailable"}`,
87
+ "Data and identity: unchanged",
88
+ "No server process started.",
89
+ "Next: borg-mcp-server start",
90
+ ].join("\n"));
91
+ return 0;
92
+ }
93
+ io.stdout([
94
+ "Local server setup completed.",
95
+ artifact,
96
+ "Local owner access: prepared.",
97
+ "No server process started.",
98
+ "Next: start the server, then run borg assimilate.",
99
+ ].join("\n"));
62
100
  return 0;
63
101
  case "start":
64
102
  await service.start(extraArgs);
65
103
  return 0;
104
+ case "status": {
105
+ if (extraArgs.length > 1 || (extraArgs.length === 1 && extraArgs[0] !== "--json") ||
106
+ service.status === undefined) return invalidArguments(io);
107
+ const status = await service.status();
108
+ if (extraArgs[0] === "--json" || io.isTTY === false) {
109
+ io.stdout(JSON.stringify({
110
+ status: status.status,
111
+ artifact: status.artifact === null
112
+ ? null
113
+ : `borgmcp-server@${status.artifact.version}`,
114
+ artifact_integrity: status.artifact?.integrity ?? null,
115
+ build_identity: status.buildIdentity,
116
+ endpoint: status.endpoint,
117
+ mode: status.mode,
118
+ data_identity: status.dataIdentity,
119
+ }));
120
+ } else {
121
+ io.stdout(renderRuntimeStatus(status));
122
+ }
123
+ return 0;
124
+ }
125
+ case "update": {
126
+ if (extraArgs.length > 1 || (extraArgs.length === 1 && extraArgs[0] !== "--json") ||
127
+ service.update === undefined) return invalidArguments(io);
128
+ let result: Awaited<ReturnType<NonNullable<ServerService["update"]>>>;
129
+ try {
130
+ result = await service.update();
131
+ } catch (error) {
132
+ if (!(error instanceof RuntimeUpdateFailure)) throw error;
133
+ renderUpdateFailure(error, io, extraArgs[0] === "--json" || io.isTTY === false);
134
+ return 1;
135
+ }
136
+ if (extraArgs[0] === "--json" || io.isTTY === false) {
137
+ io.stdout(JSON.stringify({
138
+ status: result.outcome,
139
+ artifact: `borgmcp-server@${result.artifact.version}`,
140
+ artifact_integrity: result.artifact.integrity,
141
+ build_identity: result.runningIdentity?.source_sha ?? result.artifact.sourceSha,
142
+ mode: result.outcome === "updated" ? "managed" : "stopped",
143
+ data_identity: result.dataIdentity,
144
+ }));
145
+ } else if (result.outcome === "updated") {
146
+ io.stdout([
147
+ `Verifying borgmcp-server@${result.artifact.version}...`,
148
+ "Artifact verified and activated.",
149
+ "Restarting the verified local server...",
150
+ `Local server is running.`,
151
+ `Artifact: borgmcp-server@${result.artifact.version} (${result.artifact.integrity})`,
152
+ `Build identity: ${result.runningIdentity?.source_sha ?? "unavailable"}`,
153
+ "Data and identity: preserved",
154
+ "Next: borg-mcp-server status",
155
+ ].join("\n"));
156
+ } else {
157
+ io.stdout([
158
+ `Verifying borgmcp-server@${result.artifact.version}...`,
159
+ "Artifact verified and activated.",
160
+ "No server process started.",
161
+ `Artifact: borgmcp-server@${result.artifact.version} (${result.artifact.integrity})`,
162
+ `Build identity: ${result.artifact.sourceSha ?? "unavailable"}`,
163
+ "Data and identity: preserved",
164
+ "Next: borg-mcp-server start",
165
+ ].join("\n"));
166
+ }
167
+ return 0;
168
+ }
66
169
  case "client-rotate":
67
170
  if (extraArgs.length !== 1 || service.rotateClient === undefined) return invalidArguments(io);
68
171
  io.stdout(`Client credential rotated (shown once): ${await service.rotateClient(extraArgs[0]!)}`);
@@ -115,6 +218,16 @@ export async function runCli(
115
218
  }
116
219
  return 0;
117
220
  }
221
+ case "invite": {
222
+ if (extraArgs.length !== 0 || service.invite === undefined) return invalidArguments(io);
223
+ if (io.isTTY !== true) {
224
+ io.stderr("Invitation creation requires an interactive terminal.");
225
+ return 1;
226
+ }
227
+ const invitation = await service.invite();
228
+ io.stdout(`Invitation (single-use; shown once): ${invitation}\nShare it only with the intended recipient.`);
229
+ return 0;
230
+ }
118
231
  case "help":
119
232
  case "--help":
120
233
  case "-h":
@@ -127,6 +240,63 @@ export async function runCli(
127
240
  }
128
241
  }
129
242
 
243
+ function renderUpdateFailure(failure: RuntimeUpdateFailure, io: CliIo, machine: boolean): void {
244
+ const state = failure.code === "ARTIFACT_VERIFICATION_FAILED"
245
+ ? "verification_failed"
246
+ : failure.recovery === "restored"
247
+ ? "restored"
248
+ : failure.recovery === "stopped"
249
+ ? "stopped"
250
+ : "recovery_failed";
251
+ if (machine) {
252
+ io.stdout(JSON.stringify({
253
+ status: "failed",
254
+ error_code: failure.code,
255
+ recovery: state,
256
+ data_identity: "preserved",
257
+ }));
258
+ return;
259
+ }
260
+ if (failure.code === "ARTIFACT_VERIFICATION_FAILED") {
261
+ io.stderr([
262
+ "Update stopped: artifact verification failed.",
263
+ "No activation occurred.",
264
+ "The last verified runtime remains available.",
265
+ "Next: borg-mcp-server status",
266
+ ].join("\n"));
267
+ return;
268
+ }
269
+ io.stderr([
270
+ "Update stopped: activation did not complete.",
271
+ failure.recovery === "restored"
272
+ ? "The last verified runtime was restored."
273
+ : failure.recovery === "stopped"
274
+ ? "The server stopped safely."
275
+ : "Recovery did not complete; inspect server status.",
276
+ "Data and identity: preserved",
277
+ "Next: borg-mcp-server status",
278
+ ].join("\n"));
279
+ }
280
+
281
+ function renderRuntimeStatus(status: Awaited<ReturnType<NonNullable<ServerService["status"]>>>): string {
282
+ const heading = status.status === "running"
283
+ ? status.buildIdentity === null
284
+ ? "Local server is reachable, but its running build identity is unavailable."
285
+ : "Local server is running."
286
+ : "Local server is stopped.";
287
+ const artifact = status.artifact === null
288
+ ? "Artifact: unavailable"
289
+ : `Artifact: borgmcp-server@${status.artifact.version} (${status.artifact.integrity})`;
290
+ return [
291
+ heading,
292
+ artifact,
293
+ `Build identity: ${status.buildIdentity ?? "unavailable"}`,
294
+ `Endpoint: ${status.endpoint ?? "unavailable"}`,
295
+ `Mode: ${status.mode}`,
296
+ `Data and identity: ${status.dataIdentity}`,
297
+ ].join("\n");
298
+ }
299
+
130
300
  function parseClientInviteArguments(
131
301
  args: readonly string[],
132
302
  ): { readonly cubeSelector?: string; readonly access?: "read" | "write" | "manage" } | undefined {
@@ -210,6 +210,13 @@ export class CredentialAuthority {
210
210
  return this.#createInvitation("client", ttlMs);
211
211
  }
212
212
 
213
+ createInvitationForOwnerCredential(clientCredential: string, ttlMs: number): string | null {
214
+ const principal = this.authenticate(`Bearer ${clientCredential}`);
215
+ if (principal?.kind !== "client" ||
216
+ !this.#store.clientHasServerCapability(principal.id, "create_cube")) return null;
217
+ return this.#createInvitation("client", ttlMs);
218
+ }
219
+
213
220
  createCubeInvitation(
214
221
  recoveryCredential: string,
215
222
  cubeSelector: { readonly kind: "id" | "name"; readonly value: string },
package/src/debug-log.ts CHANGED
@@ -3,6 +3,7 @@ import type { Principal } from "./principal.js";
3
3
  export type DebugRoute =
4
4
  | "health"
5
5
  | "protocol"
6
+ | "runtime"
6
7
  | "enrollment_exchange"
7
8
  | "client_attach"
8
9
  | "cubes"
@@ -158,7 +159,7 @@ function enumValue<const T extends string>(value: unknown, allowed: readonly T[]
158
159
  }
159
160
 
160
161
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
161
- const DEBUG_ROUTES: readonly DebugRoute[] = ["health", "protocol", "enrollment_exchange", "client_attach", "cubes", "cube", "cube_roles", "cube_role", "cube_role_section_patch", "cube_taxonomy_patch", "cube_drones", "cube_logs", "cube_acks", "cube_decisions", "cube_stream", "unknown"];
162
+ const DEBUG_ROUTES: readonly DebugRoute[] = ["health", "protocol", "runtime", "enrollment_exchange", "client_attach", "cubes", "cube", "cube_roles", "cube_role", "cube_role_section_patch", "cube_taxonomy_patch", "cube_drones", "cube_logs", "cube_acks", "cube_decisions", "cube_stream", "unknown"];
162
163
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OTHER"] as const;
163
164
  const AUTH_RESULTS = ["not_required", "missing", "invalid", "expired", "revoked", "evicted", "rejected", "accepted"] as const;
164
165
  const AUTHZ_RESULTS = ["not_checked", "accepted", "denied_or_not_found"] as const;
@@ -17,6 +17,7 @@ import { resolveBindOptions, type BindOptionsInput } from "./network-policy.js";
17
17
  import type { CoordinationRequest, CoordinationResponse } from "./coordination-api.js";
18
18
  import type { Principal } from "./principal.js";
19
19
  import { disabledDebugLogger, type DebugLogger, type DebugRoute } from "./debug-log.js";
20
+ import { RUNTIME_INFO_PATH, type RuntimeBuildIdentity } from "./runtime-identity.js";
20
21
 
21
22
  export interface ServiceLimits {
22
23
  readonly maxConnections: number;
@@ -66,6 +67,7 @@ export interface RequestHandlerContext {
66
67
  ) => Promise<Principal | "missing" | "invalid" | "expired" | "revoked" | "evicted" | "rejected">;
67
68
  readonly handleCoordination?: (request: CoordinationRequest) => Promise<CoordinationResponse>;
68
69
  readonly debugLogger: DebugLogger;
70
+ readonly runtimeIdentity?: RuntimeBuildIdentity;
69
71
  }
70
72
 
71
73
  export interface HttpsServerOptions {
@@ -80,6 +82,7 @@ export interface HttpsServerOptions {
80
82
  readonly handleCoordination?: RequestHandlerContext["handleCoordination"];
81
83
  readonly limits?: ServiceLimits;
82
84
  readonly debugLogger?: DebugLogger;
85
+ readonly runtimeIdentity?: RuntimeBuildIdentity;
83
86
  readonly testHooks?: {
84
87
  readonly identifyRemoteAddress?: (socket: Socket) => string;
85
88
  readonly identifyConnectionAddress?: (socket: Socket) => string;
@@ -183,6 +186,7 @@ export function createRequestHandlerContext(
183
186
  ...(options.handleCoordination === undefined
184
187
  ? {}
185
188
  : { handleCoordination: options.handleCoordination }),
189
+ ...(options.runtimeIdentity === undefined ? {} : { runtimeIdentity: options.runtimeIdentity }),
186
190
  debugLogger: options.debugLogger ?? disabledDebugLogger,
187
191
  });
188
192
  }
@@ -342,7 +346,7 @@ async function handleRequest(
342
346
  return;
343
347
  }
344
348
 
345
- if (isCoordinationPath(path) && context.handleCoordination !== undefined) {
349
+ if (isAuthenticatedPath(path) && context.authorizeCoordination !== undefined) {
346
350
  let decoded: unknown;
347
351
  if (requestBody.length === 0) {
348
352
  decoded = undefined;
@@ -354,12 +358,7 @@ async function handleRequest(
354
358
  }
355
359
  }
356
360
  const authorization = request.headers.authorization;
357
- const authorizeCoordination = context.authorizeCoordination;
358
- if (authorizeCoordination === undefined) {
359
- sendJson(response, 500, protocolError("INTERNAL_ERROR", "Coordination authentication is unavailable."), true);
360
- return;
361
- }
362
- const authentication = await authorizeCoordination(authorization, signal);
361
+ const authentication = await context.authorizeCoordination(authorization, signal);
363
362
  trace.authentication = typeof authentication === "string" ? authentication : "accepted";
364
363
  if (signal.aborted) return;
365
364
  if (typeof authentication === "string") {
@@ -376,6 +375,22 @@ async function handleRequest(
376
375
  return;
377
376
  }
378
377
  trace.principal = authentication;
378
+ if (path === RUNTIME_INFO_PATH) {
379
+ if (request.method !== "GET" || requestBody.length !== 0) {
380
+ sendEmpty(response, request.method === "GET" ? 400 : 405, true);
381
+ return;
382
+ }
383
+ if (context.runtimeIdentity === undefined) {
384
+ sendEmpty(response, 404);
385
+ return;
386
+ }
387
+ sendJson(response, 200, context.runtimeIdentity);
388
+ return;
389
+ }
390
+ if (context.handleCoordination === undefined) {
391
+ sendJson(response, 500, protocolError("INTERNAL_ERROR", "Coordination handling is unavailable."), true);
392
+ return;
393
+ }
379
394
  const clientIdentity = credentialRateLimitIdentity(authentication, request.method, path);
380
395
  const credentialRetry = credentialRateLimiter.consume(clientIdentity);
381
396
  if (credentialRetry !== null) return sendRateLimited(response, credentialRetry);
@@ -431,6 +446,7 @@ function debugRoute(rawUrl: string | undefined): DebugRoute {
431
446
  if (path === null) return "unknown";
432
447
  if (path === HEALTH_PATH) return "health";
433
448
  if (path === PROTOCOL_INFO_PATH) return "protocol";
449
+ if (path === RUNTIME_INFO_PATH) return "runtime";
434
450
  if (path === ENROLLMENT_EXCHANGE_PATH) return "enrollment_exchange";
435
451
  if (path === ATTACH_PATH) return "client_attach";
436
452
  if (path === "/api/cubes") return "cubes";
@@ -616,6 +632,10 @@ function isCoordinationPath(path: string | null): path is string {
616
632
  path?.startsWith("/api/cubes/") === true;
617
633
  }
618
634
 
635
+ function isAuthenticatedPath(path: string | null): path is string {
636
+ return path === RUNTIME_INFO_PATH || isCoordinationPath(path);
637
+ }
638
+
619
639
  function credentialRateLimitIdentity(
620
640
  principal: Principal,
621
641
  method: string | undefined,
package/src/index.ts CHANGED
@@ -1,3 +1,48 @@
1
1
  export { runCli } from "./cli.js";
2
2
  export type { CliIo } from "./cli.js";
3
- export type { ServerService, SetupOptions } from "./service.js";
3
+ export { inspectRuntimeLock } from "./service.js";
4
+ export { inspectNodeRuntime } from "./service.js";
5
+ export type {
6
+ RuntimeLockStatus,
7
+ ServerRuntimeStatus,
8
+ ServerSetupResult,
9
+ ServerService,
10
+ SetupOptions,
11
+ } from "./service.js";
12
+ export {
13
+ createRuntimeBuildIdentity,
14
+ loadRuntimeBuildIdentity,
15
+ RUNTIME_INFO_PATH,
16
+ SERVER_PACKAGE_VERSION,
17
+ } from "./runtime-identity.js";
18
+ export type {
19
+ LoadRuntimeBuildIdentityInput,
20
+ RuntimeBuildIdentity,
21
+ RuntimeBuildIdentityInput,
22
+ } from "./runtime-identity.js";
23
+ export { createManagedServiceDefinition } from "./managed-service.js";
24
+ export type {
25
+ ManagedServiceDefinition,
26
+ ManagedServiceInput,
27
+ ManagedServicePlatform,
28
+ } from "./managed-service.js";
29
+ export {
30
+ createRuntimeLifecycle,
31
+ RuntimeActivationError,
32
+ createUnixNpmArtifactUnpacker,
33
+ inspectActiveRuntimeArtifact,
34
+ } from "./runtime-lifecycle.js";
35
+ export { createRegistryArtifactSource } from "./registry-artifact.js";
36
+ export type { RegistryArtifactSource, RegistryRuntimeArtifact } from "./registry-artifact.js";
37
+ export { createRuntimeOperator } from "./runtime-operator.js";
38
+ export { RuntimeUpdateFailure } from "./runtime-operator.js";
39
+ export type { RuntimeOperator, RuntimeUpdateResult } from "./runtime-operator.js";
40
+ export type {
41
+ ActivateRuntimeArtifactInput,
42
+ RuntimeLifecycle,
43
+ RuntimeLifecycleDependencies,
44
+ RuntimeCommandResult,
45
+ RuntimeCommandRunner,
46
+ StageRuntimeArtifactInput,
47
+ VerifiedRuntimeArtifact,
48
+ } from "./runtime-lifecycle.js";
package/src/main.ts CHANGED
@@ -10,6 +10,7 @@ const io = {
10
10
  stdout: (message: string): void => console.log(message),
11
11
  stderr: (message: string): void => console.error(message),
12
12
  readSecret: readHiddenSecret,
13
+ isTTY: process.stdout.isTTY === true,
13
14
  };
14
15
 
15
16
  async function readHiddenSecret(prompt: string): Promise<string> {
@@ -0,0 +1,107 @@
1
+ import { isAbsolute, join } from "node:path";
2
+
3
+ export type ManagedServicePlatform = "launchd" | "systemd";
4
+
5
+ export interface ManagedServiceInput {
6
+ readonly platform: ManagedServicePlatform;
7
+ readonly nodeExecutable: string;
8
+ readonly runtimeRoot: string;
9
+ readonly dataDirectory: string;
10
+ readonly definitionPath: string;
11
+ readonly launchdDomain?: string;
12
+ }
13
+
14
+ export interface ManagedServiceDefinition {
15
+ readonly platform: ManagedServicePlatform;
16
+ readonly label: "ai.borgmcp.server";
17
+ readonly definitionPath: string;
18
+ readonly content: string;
19
+ readonly install: readonly [string, ...string[]];
20
+ readonly restart: readonly [string, ...string[]];
21
+ readonly stop: readonly [string, ...string[]];
22
+ readonly status: readonly [string, ...string[]];
23
+ }
24
+
25
+ const serviceLabel = "ai.borgmcp.server" as const;
26
+
27
+ export function createManagedServiceDefinition(input: ManagedServiceInput): ManagedServiceDefinition {
28
+ for (const path of [
29
+ input.nodeExecutable,
30
+ input.runtimeRoot,
31
+ input.dataDirectory,
32
+ input.definitionPath,
33
+ ]) {
34
+ if (!isAbsolute(path) || /[\r\n\0]/u.test(path)) {
35
+ throw new Error("Managed service paths must be absolute and single-line.");
36
+ }
37
+ }
38
+ const entrypoint = join(input.runtimeRoot, "current", "package", "dist", "main.js");
39
+ if (input.platform === "launchd") {
40
+ const domain = input.launchdDomain;
41
+ if (domain === undefined || !/^gui\/[1-9][0-9]*$/u.test(domain)) {
42
+ throw new Error("Managed launchd domain is invalid.");
43
+ }
44
+ const service = `${domain}/${serviceLabel}`;
45
+ return Object.freeze({
46
+ platform: "launchd",
47
+ label: serviceLabel,
48
+ definitionPath: input.definitionPath,
49
+ content: launchdDefinition(input.nodeExecutable, entrypoint, input.dataDirectory),
50
+ install: ["launchctl", "bootstrap", domain, input.definitionPath] as const,
51
+ restart: ["launchctl", "kickstart", "-k", service] as const,
52
+ stop: ["launchctl", "kill", "SIGTERM", service] as const,
53
+ status: ["launchctl", "print", service] as const,
54
+ });
55
+ }
56
+ return Object.freeze({
57
+ platform: "systemd",
58
+ label: serviceLabel,
59
+ definitionPath: input.definitionPath,
60
+ content: systemdDefinition(input.nodeExecutable, entrypoint, input.dataDirectory),
61
+ install: ["systemctl", "--user", "enable", "--now", serviceLabel] as const,
62
+ restart: ["systemctl", "--user", "restart", serviceLabel] as const,
63
+ stop: ["systemctl", "--user", "stop", serviceLabel] as const,
64
+ status: ["systemctl", "--user", "show", serviceLabel, "--property=ActiveState,SubState,MainPID"] as const,
65
+ });
66
+ }
67
+
68
+ function launchdDefinition(nodeExecutable: string, entrypoint: string, dataDirectory: string): string {
69
+ return `<?xml version="1.0" encoding="UTF-8"?>
70
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
71
+ <plist version="1.0"><dict>
72
+ <key>Label</key><string>${serviceLabel}</string>
73
+ <key>ProgramArguments</key><array><string>${xml(nodeExecutable)}</string><string>${xml(entrypoint)}</string><string>start</string></array>
74
+ <key>EnvironmentVariables</key><dict><key>BORG_SERVER_DATA_DIR</key><string>${xml(dataDirectory)}</string><key>BORG_SERVER_PROCESS_MODE</key><string>managed</string></dict>
75
+ <key>RunAtLoad</key><true/>
76
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
77
+ <key>ProcessType</key><string>Background</string>
78
+ </dict></plist>
79
+ `;
80
+ }
81
+
82
+ function systemdDefinition(nodeExecutable: string, entrypoint: string, dataDirectory: string): string {
83
+ return `[Unit]
84
+ Description=Borg MCP server
85
+
86
+ [Service]
87
+ Type=simple
88
+ ExecStart=${systemdQuote(nodeExecutable)} ${systemdQuote(entrypoint)} start
89
+ Environment=${systemdQuote(`BORG_SERVER_DATA_DIR=${dataDirectory}`)}
90
+ Environment="BORG_SERVER_PROCESS_MODE=managed"
91
+ Restart=on-failure
92
+ RestartSec=2
93
+ TimeoutStopSec=15
94
+
95
+ [Install]
96
+ WantedBy=default.target
97
+ `;
98
+ }
99
+
100
+ function xml(value: string): string {
101
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
102
+ .replaceAll('"', "&quot;").replaceAll("'", "&apos;");
103
+ }
104
+
105
+ function systemdQuote(value: string): string {
106
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
107
+ }