@botbuddy/cli 1.59.6 → 1.59.8

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.
@@ -0,0 +1,478 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { lstat, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { registerContainerHost } from "../api.mjs";
8
+ import { awaitHostApproval } from "./approval-wait.mjs";
9
+ import { bootstrapFreshDaemon } from "./bootstrap.mjs";
10
+ import { createBootstrapResumeStore } from "./bootstrap-resume.mjs";
11
+ import { validateDaemonRelease, validateDaemonVersion } from "./release.mjs";
12
+ import {
13
+ installedBinaryPath,
14
+ installRemoteService,
15
+ installService,
16
+ stageRemoteDaemonRelease,
17
+ serviceStatus,
18
+ serviceUserUid,
19
+ assertSshHost,
20
+ } from "./installer.mjs";
21
+ import { renderLinuxUnit, renderMacDaemon } from "./service.mjs";
22
+ import { buildRuntimeConfig } from "./runtime-config.mjs";
23
+ import {
24
+ DAEMON_PRODUCTION_MCP_SERVER,
25
+ enrollDaemonDeviceProfileFromHelperIdentity,
26
+ enrollDaemonDeviceProfileFromBootstrapIdentity,
27
+ } from "./enrollment.mjs";
28
+ import {
29
+ createRemoteBootstrapIdentity,
30
+ deliverDaemonProfileFromBootstrapIdentityOverSSH,
31
+ deliverDaemonProfileFromHelperIdentityOverSSH,
32
+ inspectRemoteBootstrapIdentity,
33
+ verifyRemoteHelperIdentity as verifyRemoteHelperIdentityOnTarget,
34
+ verifyRemoteServiceIdentity,
35
+ } from "./profile-delivery.mjs";
36
+
37
+ const execFile = promisify(execFileCallback);
38
+ const fail = (code) => {
39
+ throw new Error(code);
40
+ };
41
+ const platformName = (value) => value === "macos" ? "darwin" : value;
42
+
43
+ export const daemonHelp = `Usage:
44
+ bb daemon render <darwin|linux> '<json service spec>'
45
+ bb daemon install <darwin|linux> '<json service spec>'
46
+ bb daemon status <darwin|linux> ['<json ssh spec>']
47
+ bb daemon config-render '<json host policy>'
48
+ bb daemon enroll '<json host-bound enrollment spec>'
49
+ bb daemon bootstrap '<json fresh-host bootstrap spec>'
50
+
51
+ The Go payload is verified from the extracted universal package before sudo.
52
+ Install runs the Go runtime's --check-config receipt before loading a native,
53
+ root-owned service. Add "sshHost" and target "arch" to deploy from this CLI
54
+ over SSH; the target receives only the Go payload and native service commands.
55
+
56
+ Enrollment keeps the existing Mini P-256 identity on the target. After a
57
+ read-only target custody check, it writes a distinct, host-bound Daemon OAuth
58
+ profile through the public Go stdin installer. It does not start or activate
59
+ the service.
60
+
61
+ Bootstrap creates (or, with mode "resume", inspects) a target-private identity,
62
+ registers only its public SPKI through the authenticated MCP control-plane route,
63
+ and prints the exact /docker approval URL when policy approval is pending. It
64
+ then uses one bounded pushed bb wait on that exact host (never a local poll),
65
+ re-registers the same SPKI/consent tuple, and proceeds only when the server
66
+ again reports the host registered. Resume never regenerates the target identity.
67
+ It then performs a distinct daemon device grant, schema-v3 profile delivery,
68
+ and the existing remote native install/check-config/service activation path.`;
69
+
70
+ const DAEMON_HOST_KEY_RE = /^bbh-[0-9a-f]{24}$/;
71
+ const SAFE_ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
72
+ const SAFE_SERVICE_USER = /^[a-z_][a-z0-9_-]*$/i;
73
+
74
+ function releaseCanBeStaged(release, platform) {
75
+ const target = platform === "darwin" ? "darwin-arm64" : "linux-x64";
76
+ return release &&
77
+ release.target === target &&
78
+ /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(release.releaseVersion) &&
79
+ /^[0-9a-f]{40}$/.test(release.sourceCommit) &&
80
+ /^[0-9a-f]{64}$/.test(release.sha256) &&
81
+ Buffer.isBuffer(release.payload) &&
82
+ createHash("sha256").update(release.payload).digest("hex") === release.sha256;
83
+ }
84
+
85
+ async function safeRootAncestry(path, inspect = lstat) {
86
+ const segments = path.split("/").filter(Boolean);
87
+ let current = "";
88
+ for (const segment of segments) {
89
+ current += `/${segment}`;
90
+ const stat = await inspect(current).catch((error) => error?.code === "ENOENT" ? null : Promise.reject(error));
91
+ if (!stat) return;
92
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 0o022) !== 0) {
93
+ fail("daemon_stage_unsafe_destination");
94
+ }
95
+ }
96
+ }
97
+
98
+ function checksumCommand(platform) {
99
+ return platform === "darwin" ? ["shasum", ["-a", "256"]] : ["sha256sum", []];
100
+ }
101
+
102
+ function checksumMatches(output, expected) {
103
+ return String(output ?? "").trim().split(/\s+/)[0] === expected;
104
+ }
105
+
106
+ async function assertInstalledDaemonBinary(binary, release, platform, exec) {
107
+ const [checksumCommandName, checksumArgs] = checksumCommand(platform);
108
+ const statArgs = platform === "darwin"
109
+ ? ["stat", "-f", "%u:%OLp", binary]
110
+ : ["stat", "-c", "%u:%a", binary];
111
+ try {
112
+ await exec("sudo", ["test", "-f", binary]);
113
+ await exec("sudo", ["test", "!", "-L", binary]);
114
+ const [ownership, checksum] = await Promise.all([
115
+ exec("sudo", statArgs),
116
+ exec("sudo", [checksumCommandName, ...checksumArgs, binary]),
117
+ ]);
118
+ if (!/^0:(?:755|100755)$/.test(String(ownership?.stdout ?? "").trim()) || !checksumMatches(checksum?.stdout, release.sha256)) {
119
+ fail("daemon_stage_installed_payload_invalid");
120
+ }
121
+ } catch (error) {
122
+ if (error?.message?.startsWith("daemon_stage_")) throw error;
123
+ fail("daemon_stage_installed_payload_invalid");
124
+ }
125
+ }
126
+
127
+ // Install a snapshot of the already hash-verified payload before asking the
128
+ // unprivileged service identity to execute it. The extracted package tree is
129
+ // deliberately never made traversable for that identity.
130
+ export async function stageVerifiedDaemonBinary(release, {
131
+ platform,
132
+ exec = execFile,
133
+ makeTemp = mkdtemp,
134
+ write = writeFile,
135
+ remove = rm,
136
+ inspect = lstat,
137
+ random = randomUUID,
138
+ } = {}) {
139
+ if (!new Set(["darwin", "linux"]).has(platform) || !releaseCanBeStaged(release, platform)) {
140
+ fail("daemon_stage_release_invalid");
141
+ }
142
+ const binary = installedBinaryPath(release);
143
+ const binaryDirectory = binary.slice(0, binary.lastIndexOf("/"));
144
+ await safeRootAncestry(binaryDirectory, inspect);
145
+ const localDirectory = await makeTemp(join(tmpdir(), "botbuddy-daemon-"));
146
+ const snapshot = join(localDirectory, "botbuddy-daemon");
147
+ const binaryStage = join(binaryDirectory, `.botbuddy-daemon.${release.sha256}.${random()}`);
148
+ let staged = false;
149
+ try {
150
+ await write(snapshot, release.payload, { mode: 0o500 });
151
+ let existing = false;
152
+ try {
153
+ await exec("sudo", ["test", "-e", binary]);
154
+ existing = true;
155
+ } catch (error) {
156
+ if (error?.code !== 1) throw error;
157
+ }
158
+ if (!existing) {
159
+ const group = platform === "darwin" ? "wheel" : "root";
160
+ const [checksumCommandName, checksumArgs] = checksumCommand(platform);
161
+ await exec("sudo", ["install", "-d", "-o", "root", "-g", group, "-m", "0755", binaryDirectory]);
162
+ await exec("sudo", ["install", "-o", "root", "-g", group, "-m", "0755", snapshot, binaryStage]);
163
+ staged = true;
164
+ const checksum = await exec("sudo", [checksumCommandName, ...checksumArgs, binaryStage]);
165
+ if (!checksumMatches(checksum?.stdout, release.sha256)) fail("daemon_stage_digest_invalid");
166
+ await exec("sudo", ["mv", "-n", binaryStage, binary]);
167
+ staged = false;
168
+ }
169
+ await assertInstalledDaemonBinary(binary, release, platform, exec);
170
+ return { ...release, binary };
171
+ } finally {
172
+ if (staged) await exec("sudo", ["rm", "-f", binaryStage]).catch(() => {});
173
+ await remove(localDirectory, { recursive: true, force: true });
174
+ }
175
+ }
176
+
177
+ function enrollmentSpec(args) {
178
+ if (args.length !== 2) fail("Usage: bb daemon enroll '<json host-bound enrollment spec>'");
179
+ let spec;
180
+ try { spec = JSON.parse(args[1]); } catch { fail("daemon_enrollment_spec_invalid"); }
181
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) fail("daemon_enrollment_spec_invalid");
182
+ const allowed = new Set([
183
+ "serverUrl", "sshHost", "serviceUser", "daemonBinary", "profilePath", "expectedHostKey", "requestingHost",
184
+ "caller", "callerHarness", "callerTicket",
185
+ ]);
186
+ if (Object.keys(spec).some((key) => !allowed.has(key))) fail("daemon_enrollment_spec_invalid");
187
+ if (
188
+ spec.serverUrl !== DAEMON_PRODUCTION_MCP_SERVER ||
189
+ typeof spec.sshHost !== "string" ||
190
+ typeof spec.serviceUser !== "string" || !SAFE_SERVICE_USER.test(spec.serviceUser) ||
191
+ typeof spec.daemonBinary !== "string" || !SAFE_ABSOLUTE_PATH.test(spec.daemonBinary) ||
192
+ typeof spec.profilePath !== "string" || !SAFE_ABSOLUTE_PATH.test(spec.profilePath) || !spec.profilePath.endsWith("/profile.json") ||
193
+ typeof spec.expectedHostKey !== "string" || !DAEMON_HOST_KEY_RE.test(spec.expectedHostKey) ||
194
+ typeof spec.requestingHost !== "string" || spec.requestingHost.length === 0 || Buffer.byteLength(spec.requestingHost, "utf8") > 80
195
+ ) fail("daemon_enrollment_spec_invalid");
196
+ try { assertSshHost(spec.sshHost); } catch { fail("daemon_enrollment_spec_invalid"); }
197
+ return spec;
198
+ }
199
+
200
+ export function parseDaemonArgs(args) {
201
+ const [subcommand, requestedPlatform, ...rest] = args;
202
+ const platform = platformName(requestedPlatform);
203
+ if (
204
+ !new Set(["render", "install"]).has(subcommand) ||
205
+ !new Set(["darwin", "linux"]).has(platform) || rest.length !== 1
206
+ ) {
207
+ fail(
208
+ "Usage: bb daemon <render|install> <darwin|linux> '<json service spec>'",
209
+ );
210
+ }
211
+ let spec;
212
+ try {
213
+ spec = JSON.parse(rest[0]);
214
+ } catch {
215
+ fail("daemon service spec must be JSON");
216
+ }
217
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
218
+ fail("daemon service spec must be JSON");
219
+ }
220
+ return { subcommand, platform, spec };
221
+ }
222
+
223
+ export async function checkRuntimeConfig(
224
+ release,
225
+ config,
226
+ { exec = execFile, serviceUser } = {},
227
+ ) {
228
+ if (
229
+ typeof serviceUser !== "string" ||
230
+ !SAFE_SERVICE_USER.test(serviceUser)
231
+ ) fail("daemon_runtime_user_mismatch");
232
+ let result;
233
+ try {
234
+ result = await exec(
235
+ "sudo",
236
+ ["-u", serviceUser, release.binary, "--check-config", "--config", config],
237
+ { timeout: 5_000, maxBuffer: 16 * 1024 },
238
+ );
239
+ } catch {
240
+ fail("daemon_runtime_config_invalid");
241
+ }
242
+ let receipt;
243
+ try {
244
+ receipt = JSON.parse(String(result?.stdout ?? ""));
245
+ } catch {
246
+ fail("daemon_runtime_config_invalid");
247
+ }
248
+ if (!receipt || Object.keys(receipt).length !== 1 || receipt.valid !== true) {
249
+ fail("daemon_runtime_config_invalid");
250
+ }
251
+ return receipt;
252
+ }
253
+
254
+ export async function checkRuntimeVersion(release, exec = execFile) {
255
+ let result;
256
+ try {
257
+ result = await exec(release.binary, ["--version"], {
258
+ timeout: 5_000,
259
+ maxBuffer: 16 * 1024,
260
+ });
261
+ } catch {
262
+ fail("daemon_release_version_invalid");
263
+ }
264
+ const runtimeTarget = release.target === "linux-x64"
265
+ ? "linux-amd64"
266
+ : release.target;
267
+ return validateDaemonVersion(String(result?.stdout ?? ""), {
268
+ releaseVersion: release.releaseVersion,
269
+ sourceCommit: release.sourceCommit,
270
+ target: runtimeTarget,
271
+ });
272
+ }
273
+
274
+ export async function cmdDaemon(args, {
275
+ log = console.log,
276
+ notice = (line) => process.stderr.write(`${line}\n`),
277
+ install = installService,
278
+ stageLocal = stageVerifiedDaemonBinary,
279
+ status = serviceStatus,
280
+ validateRelease = validateDaemonRelease,
281
+ installRemote = installRemoteService,
282
+ stageRemote = stageRemoteDaemonRelease,
283
+ resolveUser = serviceUserUid,
284
+ verifyVersion = checkRuntimeVersion,
285
+ verifyRuntime = checkRuntimeConfig,
286
+ enroll = enrollDaemonDeviceProfileFromHelperIdentity,
287
+ deliver = deliverDaemonProfileFromHelperIdentityOverSSH,
288
+ verifyRemoteIdentity = verifyRemoteServiceIdentity,
289
+ verifyRemoteHelperIdentity = verifyRemoteHelperIdentityOnTarget,
290
+ bootstrap = bootstrapFreshDaemon,
291
+ registerHost = registerContainerHost,
292
+ createBootstrapIdentity = createRemoteBootstrapIdentity,
293
+ inspectBootstrapIdentity = inspectRemoteBootstrapIdentity,
294
+ enrollBootstrap = enrollDaemonDeviceProfileFromBootstrapIdentity,
295
+ deliverBootstrap = deliverDaemonProfileFromBootstrapIdentityOverSSH,
296
+ awaitApproval = awaitHostApproval,
297
+ resumeStore = createBootstrapResumeStore(),
298
+ } = {}) {
299
+ if (["--help", "-h", "help", undefined].includes(args[0])) {
300
+ log(daemonHelp);
301
+ return 0;
302
+ }
303
+ if (args[0] === "config-render") {
304
+ if (args.length !== 2) {
305
+ fail("Usage: bb daemon config-render '<json host policy>'");
306
+ }
307
+ let input;
308
+ try {
309
+ input = JSON.parse(args[1]);
310
+ } catch {
311
+ fail("daemon runtime config must be JSON");
312
+ }
313
+ log(JSON.stringify(buildRuntimeConfig(input)));
314
+ return 0;
315
+ }
316
+ if (args[0] === "bootstrap") {
317
+ if (args.length !== 2) fail("Usage: bb daemon bootstrap '<json fresh-host bootstrap spec>'");
318
+ let spec;
319
+ try { spec = JSON.parse(args[1]); } catch { fail("daemon_bootstrap_spec_invalid"); }
320
+ let receipt;
321
+ try {
322
+ receipt = await bootstrap(spec, {
323
+ verifyRemoteIdentity,
324
+ createIdentity: createBootstrapIdentity,
325
+ inspectIdentity: inspectBootstrapIdentity,
326
+ validateRelease,
327
+ stageRemote,
328
+ registerHost: async (payload) => {
329
+ const result = await registerHost(payload, { serverUrl: spec?.serverUrl });
330
+ if (!result?.ok) throw new Error("daemon_bootstrap_registration_failed");
331
+ return result.data;
332
+ },
333
+ enroll: enrollBootstrap,
334
+ deliver: deliverBootstrap,
335
+ installRemote,
336
+ onApprovalRequired: ({ approval_url: approvalUrl, host_key: hostKey }) => {
337
+ notice(`bb daemon bootstrap: owner approval required for ${hostKey}: ${approvalUrl}`);
338
+ },
339
+ resumeStore,
340
+ ...(typeof awaitApproval === "function" ? { awaitApproval } : {}),
341
+ });
342
+ } catch (error) {
343
+ if (error?.message?.startsWith("daemon_bootstrap_")) throw error;
344
+ fail("daemon_bootstrap_failed");
345
+ }
346
+ log(JSON.stringify(receipt));
347
+ return receipt.outcome === "installed" ? 0 : 4;
348
+ }
349
+ if (args[0] === "enroll") {
350
+ const spec = enrollmentSpec(args);
351
+ let remoteIdentity;
352
+ try {
353
+ remoteIdentity = await verifyRemoteIdentity({
354
+ sshHost: spec.sshHost,
355
+ serviceUser: spec.serviceUser,
356
+ });
357
+ } catch {
358
+ fail("daemon_enrollment_remote_identity_invalid");
359
+ }
360
+ if (!remoteIdentity || typeof remoteIdentity.directInstall !== "boolean") {
361
+ fail("daemon_enrollment_remote_identity_invalid");
362
+ }
363
+ try {
364
+ await verifyRemoteHelperIdentity({
365
+ sshHost: spec.sshHost,
366
+ serviceUser: spec.serviceUser,
367
+ daemonBinary: spec.daemonBinary,
368
+ expectedHostKey: spec.expectedHostKey,
369
+ directInstall: remoteIdentity.directInstall,
370
+ });
371
+ } catch {
372
+ fail("daemon_enrollment_remote_helper_identity_invalid");
373
+ }
374
+ const receipt = await enroll({
375
+ serverUrl: spec.serverUrl,
376
+ expectedHostKey: spec.expectedHostKey,
377
+ requestingHost: spec.requestingHost,
378
+ ...(typeof spec.caller === "string" ? { caller: spec.caller } : {}),
379
+ ...(typeof spec.callerHarness === "string" ? { callerHarness: spec.callerHarness } : {}),
380
+ ...(typeof spec.callerTicket === "string" ? { callerTicket: spec.callerTicket } : {}),
381
+ log,
382
+ deliver: (artifact) => deliver({
383
+ artifact,
384
+ sshHost: spec.sshHost,
385
+ serviceUser: spec.serviceUser,
386
+ daemonBinary: spec.daemonBinary,
387
+ profilePath: spec.profilePath,
388
+ expectedHostKey: spec.expectedHostKey,
389
+ directInstall: remoteIdentity.directInstall,
390
+ }),
391
+ });
392
+ log(JSON.stringify(receipt));
393
+ return 0;
394
+ }
395
+ if (args[0] === "status") {
396
+ const platform = platformName(args[1]);
397
+ if (
398
+ !new Set(["darwin", "linux"]).has(platform) ||
399
+ ![2, 3].includes(args.length)
400
+ ) fail("Usage: bb daemon status <darwin|linux> ['<json ssh spec>']");
401
+ let statusSpec = {};
402
+ if (args.length === 3) {
403
+ try {
404
+ statusSpec = JSON.parse(args[2]);
405
+ } catch {
406
+ fail("daemon service spec must be JSON");
407
+ }
408
+ if (
409
+ !statusSpec || typeof statusSpec !== "object" ||
410
+ Array.isArray(statusSpec) || typeof statusSpec.sshHost !== "string"
411
+ ) fail("daemon service spec must include sshHost");
412
+ }
413
+ log(JSON.stringify(await status({ platform, host: statusSpec.sshHost })));
414
+ return 0;
415
+ }
416
+ const { subcommand, platform, spec } = parseDaemonArgs(args);
417
+ const remote = typeof spec.sshHost === "string";
418
+ if (remote && typeof spec.arch !== "string") {
419
+ fail("daemon_remote_arch_required");
420
+ }
421
+ const release = await validateRelease({ releaseRoot: spec.releaseRoot }, {
422
+ platform,
423
+ arch: spec.arch,
424
+ });
425
+ const serviceSpec = {
426
+ user: spec.user,
427
+ home: spec.home,
428
+ config: spec.config,
429
+ binary: installedBinaryPath(release),
430
+ };
431
+ const definition = platform === "darwin"
432
+ ? renderMacDaemon(serviceSpec)
433
+ : renderLinuxUnit(serviceSpec);
434
+ if (subcommand === "render") {
435
+ log(definition);
436
+ return 0;
437
+ }
438
+ if (remote) {
439
+ const receipt = await installRemote({
440
+ platform,
441
+ host: spec.sshHost,
442
+ user: spec.user,
443
+ definition,
444
+ release,
445
+ config: spec.config,
446
+ });
447
+ log(
448
+ JSON.stringify({
449
+ schema_version: 1,
450
+ ...receipt,
451
+ release_version: release.releaseVersion,
452
+ source_commit: release.sourceCommit,
453
+ }),
454
+ );
455
+ return 0;
456
+ }
457
+ await resolveUser(spec.user);
458
+ if (!releaseCanBeStaged(release, platform)) fail("daemon_stage_release_invalid");
459
+ const stagedRelease = await stageLocal(release, { platform });
460
+ await verifyVersion(stagedRelease);
461
+ await verifyRuntime(stagedRelease, spec.config, { serviceUser: spec.user });
462
+ const receipt = await install({
463
+ platform,
464
+ user: spec.user,
465
+ definition,
466
+ release: stagedRelease,
467
+ config: spec.config,
468
+ });
469
+ log(
470
+ JSON.stringify({
471
+ schema_version: 1,
472
+ ...receipt,
473
+ release_version: release.releaseVersion,
474
+ source_commit: release.sourceCommit,
475
+ }),
476
+ );
477
+ return 0;
478
+ }