bazilion 0.10.0 → 0.11.0

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/dist/cli.js +1249 -136
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +3027 -396
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/migrations/0001_init.sql +20 -0
  6. package/dist/web/client/assets/{ProductionTeamPolicyEditor-CiQ-VVDo.js → ProductionTeamPolicyEditor-B6dbpuYy.js} +1 -1
  7. package/dist/web/client/assets/{ProfileCommunicationEditor-D84F5yfr.js → ProfileCommunicationEditor-Bf8UvmZT.js} +1 -1
  8. package/dist/web/client/assets/{TeamTabs-Btoe_FAE.js → TeamTabs-sQBHChtd.js} +1 -1
  9. package/dist/web/client/assets/{TemplatesTabs-CAYY03F5.js → TemplatesTabs-DPsuwsWB.js} +1 -1
  10. package/dist/web/client/assets/{_id-BqU2BodK.js → _id-C9rkCclz.js} +1 -1
  11. package/dist/web/client/assets/{_id-Q98Of6k9.js → _id-D65IzC8H.js} +1 -1
  12. package/dist/web/client/assets/{_id-BeauECmk.js → _id-DE4IY5bL.js} +1 -1
  13. package/dist/web/client/assets/{_id-CKtoio3t.js → _id-DYASsGQ8.js} +1 -1
  14. package/dist/web/client/assets/{activity-DXN_N85w.js → activity-C7o6-xHg.js} +1 -1
  15. package/dist/web/client/assets/{agents-C5Xsdge1.js → agents-UroO4d25.js} +1 -1
  16. package/dist/web/client/assets/{agents-Cgxo4H5G.js → agents-h8yWXQ2H.js} +1 -1
  17. package/dist/web/client/assets/{approvals-BnG7fbw-.js → approvals-DKhF0YLC.js} +1 -1
  18. package/dist/web/client/assets/chevron-down-BK6NPMMP.js +1 -0
  19. package/dist/web/client/assets/{config-xw4NyrXF.js → config-q8JDresK.js} +1 -1
  20. package/dist/web/client/assets/{context-D2V1Oi4z.js → context-CBucAoi6.js} +1 -1
  21. package/dist/web/client/assets/{inbox-DK6hWCMp.js → inbox-BwoRikj1.js} +1 -1
  22. package/dist/web/client/assets/{index-VdURSoM4.js → index-DUXAevpZ.js} +2 -2
  23. package/dist/web/client/assets/{login-CvptUoMR.js → login-CNAV9Wer.js} +1 -1
  24. package/dist/web/client/assets/{mcp-BDFM_sW-.js → mcp-BKdEFwHO.js} +1 -1
  25. package/dist/web/client/assets/{members-BJvph_TR.js → members-BDqZQh26.js} +1 -1
  26. package/dist/web/client/assets/{memory-DRytqZCD.js → memory-DVdEVr6L.js} +1 -1
  27. package/dist/web/client/assets/plus-b4yFXP0O.js +1 -0
  28. package/dist/web/client/assets/{policy-CGP50OtQ.js → policy-DdDctqvg.js} +1 -1
  29. package/dist/web/client/assets/{routes-DJnWYvC9.js → routes-BEfALSZv.js} +1 -1
  30. package/dist/web/client/assets/{services-DHheiv7L.js → services-DZP2ybzb.js} +1 -1
  31. package/dist/web/client/assets/{skills-DmsitluU.js → skills-DoeIjITs.js} +1 -1
  32. package/dist/web/client/assets/styles-BYM0UGdF.css +2 -0
  33. package/dist/web/client/assets/{teams-CbxSaorm.js → teams-DlHRnrQw.js} +1 -1
  34. package/dist/web/client/assets/{teams-BdC4gthq.js → teams-W3j-UoJ9.js} +1 -1
  35. package/dist/web/client/assets/telegram-2OdeGv4E.js +1 -0
  36. package/dist/web/client/assets/{tokens-CjRplDrJ.js → tokens-MuBfmgue.js} +1 -1
  37. package/dist/web/client/assets/triggers-CX7RMQFQ.js +1 -0
  38. package/dist/web/client/assets/{welcome-wBfX9dAd.js → welcome-CvNr5IMi.js} +1 -1
  39. package/dist/web/client/assets/wire-constants-BhVKOCQa.js +74 -0
  40. package/dist/web/server/assets/{ChatPane-eUY_dga3.js → ChatPane-Ba5wTsVk.js} +325 -36
  41. package/dist/web/server/assets/{_id-ClN9hY2x.js → _id-Cg7x6q3g.js} +2 -2
  42. package/dist/web/server/assets/{_id-DkYodSkG.js → _id-DqON-59C.js} +1 -1
  43. package/dist/web/server/assets/{_tanstack-start-manifest_v-D09Xu_6u.js → _tanstack-start-manifest_v-BXauUXg6.js} +51 -51
  44. package/dist/web/server/assets/{router-DIdp8ZNt.js → router-C5_NjTsP.js} +5 -5
  45. package/dist/web/server/assets/{routes-Jb0zjKav.js → routes-B6cMLIPk.js} +3 -3
  46. package/dist/web/server/assets/{routes-CXz4hTAr.js → routes-BrA-U_Hm.js} +1 -1
  47. package/dist/web/server/assets/{routes-lIclH9Br.js → routes-C19CMLZt.js} +1 -1
  48. package/dist/web/server/assets/{telegram-7ub--6xz.js → telegram-CBF8SJzB.js} +1 -24
  49. package/dist/web/server/assets/{telegram-Bazhmpr4.js → telegram-XwyKSEtj.js} +1 -1
  50. package/dist/web/server/assets/{triggers-Ca57_qym.js → triggers-CBl4yMyw.js} +1 -1
  51. package/dist/web/server/assets/{triggers-C_0y6QV9.js → triggers-D79NoSXk.js} +3 -2
  52. package/dist/web/server/assets/{triggers-BMpHsGIr.js → triggers-_3rdhwSN.js} +27 -3
  53. package/dist/web/server/server.js +4 -4
  54. package/dist/worker.js +1609 -135
  55. package/dist/worker.js.map +1 -1
  56. package/package.json +4 -3
  57. package/dist/web/client/assets/chevron-down-BhcBspw3.js +0 -1
  58. package/dist/web/client/assets/plus-C9ee3SIO.js +0 -1
  59. package/dist/web/client/assets/styles-B5eQAwLP.css +0 -2
  60. package/dist/web/client/assets/telegram-D61G_alO.js +0 -1
  61. package/dist/web/client/assets/triggers-MLzTm_DM.js +0 -1
  62. package/dist/web/client/assets/wire-constants-BxfyUMXB.js +0 -74
  63. package/dist/web/server/assets/{Sidebar-O_CH1-JD.js → Sidebar-DsRVCszF.js} +1 -1
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { defineCommand as defineCommand25, runCommand, showUsage } from "citty";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bazilion",
9
- version: "0.10.0",
9
+ version: "0.11.0",
10
10
  description: "Multi-agent runtime CLI \u2014 spawn LLM agents, manage profiles/teams/skills, and run the local daemon.",
11
11
  license: "MIT",
12
12
  repository: {
@@ -68,6 +68,7 @@ var package_default = {
68
68
  "qrcode-terminal": "^0.12.0",
69
69
  react: "^19.2.7",
70
70
  "react-dom": "^19.2.7",
71
+ tar: "^7.5.16",
71
72
  typebox: "^1.3.0",
72
73
  undici: "^8.5.0",
73
74
  yaml: "^2.9.0"
@@ -372,12 +373,12 @@ var listCmd = defineCommand({
372
373
  async run({ args }) {
373
374
  const client = createClient2();
374
375
  const q = args.all ? "?includeArchived=true" : "";
375
- const list3 = await client.get(`/api/agents${q}`);
376
- if (list3.length === 0) {
376
+ const list4 = await client.get(`/api/agents${q}`);
377
+ if (list4.length === 0) {
377
378
  console.log("(no agents)");
378
379
  return;
379
380
  }
380
- const rows = list3.map(
381
+ const rows = list4.map(
381
382
  (a) => args.long ? [a.id, a.status, a.name, a.profileId] : (
382
383
  // Short UUID prefix — all CLI commands accept 4+ char prefixes now.
383
384
  [a.id.slice(0, 8), a.status, a.name]
@@ -448,6 +449,74 @@ var deleteCmd = defineCommand({
448
449
  console.log(`deleted agent ${args.id}`);
449
450
  }
450
451
  });
452
+ function bashApprovalModeForTty(inputIsTty, outputIsTty) {
453
+ return inputIsTty && outputIsTty ? "interactive" : "auto_deny";
454
+ }
455
+ function buildChatRequest(message, attachments, bashApprovalMode) {
456
+ return {
457
+ message,
458
+ bashApprovalMode,
459
+ ...attachments && attachments.length > 0 ? { attachments } : {}
460
+ };
461
+ }
462
+ function terminalSafe(value) {
463
+ let safe = "";
464
+ for (const character of value) {
465
+ const code = character.charCodeAt(0);
466
+ const isControl = code <= 9 || code >= 11 && code <= 31 || code >= 127 && code <= 159;
467
+ safe += isControl ? `\\x${code.toString(16).padStart(2, "0")}` : character;
468
+ }
469
+ return safe;
470
+ }
471
+ async function promptForCommandApproval(approval, prompt) {
472
+ prompt.write(" [shell command approval required]");
473
+ prompt.write(" command:");
474
+ for (const line of approval.command.split("\n")) prompt.write(` ${terminalSafe(line)}`);
475
+ prompt.write(" risks:");
476
+ for (const risk of approval.risks) {
477
+ prompt.write(
478
+ ` - ${terminalSafe(risk.code)} (${terminalSafe(risk.severity)}): ${terminalSafe(risk.message)}`
479
+ );
480
+ }
481
+ try {
482
+ const answer = (await prompt.question(" Allow this command once? [y/N] ")).trim().toLowerCase();
483
+ return answer === "y" || answer === "yes" ? "allow" : "deny";
484
+ } catch {
485
+ prompt.write(" [approval input unavailable; denying]");
486
+ return "deny";
487
+ }
488
+ }
489
+ async function respondToCommandApproval(client, approval, prompt) {
490
+ const decision2 = prompt ? await promptForCommandApproval(approval, prompt) : "deny";
491
+ await client.post(`/api/shell-approvals/${encodeURIComponent(approval.id)}`, { decision: decision2 });
492
+ return decision2;
493
+ }
494
+ function closeDeltaLine(state) {
495
+ if (!state.inDeltaStream) return;
496
+ process.stdout.write("\n");
497
+ state.inDeltaStream = false;
498
+ }
499
+ function printCommandApprovalStatus(approval) {
500
+ switch (approval.status) {
501
+ case "allowed":
502
+ console.log(" [shell command allowed]");
503
+ break;
504
+ case "denied":
505
+ console.log(" [shell command denied]");
506
+ break;
507
+ case "auto_denied":
508
+ console.log(" [shell command auto-denied: non-interactive turn]");
509
+ break;
510
+ case "expired":
511
+ console.log(" [shell command approval expired]");
512
+ break;
513
+ case "cancelled":
514
+ console.log(" [shell command approval cancelled]");
515
+ break;
516
+ case "pending":
517
+ break;
518
+ }
519
+ }
451
520
  function printEvent(e, state) {
452
521
  switch (e.type) {
453
522
  case "assistant_delta":
@@ -494,16 +563,32 @@ function printEvent(e, state) {
494
563
  break;
495
564
  }
496
565
  }
497
- async function streamTurn(client, agentId, message, attachments) {
566
+ async function streamTurn(client, agentId, message, attachments, options = { bashApprovalMode: "auto_deny" }) {
498
567
  const state = { inDeltaStream: false };
499
- for await (const frame of client.stream("POST", `/api/agents/${agentId}/chat`, {
500
- message,
501
- ...attachments && attachments.length > 0 ? { attachments } : {}
502
- })) {
568
+ const respondedApprovalIds = /* @__PURE__ */ new Set();
569
+ for await (const frame of client.stream(
570
+ "POST",
571
+ `/api/agents/${agentId}/chat`,
572
+ buildChatRequest(message, attachments, options.bashApprovalMode)
573
+ )) {
503
574
  if (frame.kind === "event") {
504
- if (frame.event.type !== "user_message") printEvent(frame.event, state);
575
+ const event = frame.event;
576
+ if (event.type === "command_approval") {
577
+ closeDeltaLine(state);
578
+ const approval = event.approval;
579
+ if (approval.status === "pending") {
580
+ if (!respondedApprovalIds.has(approval.id)) {
581
+ respondedApprovalIds.add(approval.id);
582
+ await respondToCommandApproval(client, approval, options.approvalPrompt);
583
+ }
584
+ } else {
585
+ printCommandApprovalStatus(approval);
586
+ }
587
+ } else if (event.type !== "user_message") {
588
+ printEvent(event, state);
589
+ }
505
590
  } else if (frame.kind === "fatal") {
506
- if (state.inDeltaStream) process.stdout.write("\n");
591
+ closeDeltaLine(state);
507
592
  throw new Error(frame.error);
508
593
  }
509
594
  }
@@ -528,34 +613,78 @@ var chatCmd = defineCommand({
528
613
  async run({ args }) {
529
614
  const client = createClient2();
530
615
  const resolved = await client.get(`/api/agents/${args.id}`);
616
+ const bashApprovalMode = bashApprovalModeForTty(stdin.isTTY, stdout.isTTY);
531
617
  const attachments = [
532
618
  ...loadImages(asPaths(args.image)),
533
619
  ...loadFiles(asPaths(args.file))
534
620
  ];
535
621
  if (args.message || attachments.length > 0) {
536
- await streamTurn(client, resolved.agent.id, args.message ?? "", attachments);
622
+ const rl2 = bashApprovalMode === "interactive" ? createInterface({ input: stdin, output: stdout }) : null;
623
+ try {
624
+ await streamTurn(client, resolved.agent.id, args.message ?? "", attachments, {
625
+ bashApprovalMode,
626
+ ...rl2 ? {
627
+ approvalPrompt: {
628
+ question: (question) => rl2.question(question),
629
+ write: (line) => console.log(line)
630
+ }
631
+ } : {}
632
+ });
633
+ } finally {
634
+ rl2?.close();
635
+ }
537
636
  return;
538
637
  }
539
638
  console.log(`chatting with ${resolved.agent.name} (${resolved.model})`);
540
639
  console.log("(type /exit to quit)");
541
640
  const rl = createInterface({ input: stdin, output: stdout });
542
- rl.setPrompt("> ");
543
- rl.prompt();
544
- for await (const line of rl) {
545
- const trimmed = line.trim();
546
- if (trimmed === "/exit" || trimmed === "/quit") break;
547
- if (!trimmed) {
641
+ try {
642
+ if (bashApprovalMode === "interactive") {
643
+ while (true) {
644
+ let line;
645
+ try {
646
+ line = await rl.question("> ");
647
+ } catch {
648
+ break;
649
+ }
650
+ const trimmed = line.trim();
651
+ if (trimmed === "/exit" || trimmed === "/quit") break;
652
+ if (!trimmed) continue;
653
+ try {
654
+ await streamTurn(client, resolved.agent.id, trimmed, void 0, {
655
+ bashApprovalMode,
656
+ approvalPrompt: {
657
+ question: (question) => rl.question(question),
658
+ write: (output) => console.log(output)
659
+ }
660
+ });
661
+ } catch (err) {
662
+ console.error(`error: ${err.message}`);
663
+ }
664
+ }
665
+ } else {
666
+ rl.setPrompt("> ");
548
667
  rl.prompt();
549
- continue;
550
- }
551
- try {
552
- await streamTurn(client, resolved.agent.id, trimmed);
553
- } catch (err) {
554
- console.error(`error: ${err.message}`);
668
+ for await (const line of rl) {
669
+ const trimmed = line.trim();
670
+ if (trimmed === "/exit" || trimmed === "/quit") break;
671
+ if (!trimmed) {
672
+ rl.prompt();
673
+ continue;
674
+ }
675
+ try {
676
+ await streamTurn(client, resolved.agent.id, trimmed, void 0, {
677
+ bashApprovalMode
678
+ });
679
+ } catch (err) {
680
+ console.error(`error: ${err.message}`);
681
+ }
682
+ rl.prompt();
683
+ }
555
684
  }
556
- rl.prompt();
685
+ } finally {
686
+ rl.close();
557
687
  }
558
- rl.close();
559
688
  }
560
689
  });
561
690
  var chatResetCmd = defineCommand({
@@ -567,8 +696,8 @@ var chatResetCmd = defineCommand({
567
696
  async run({ args }) {
568
697
  if (!args.force) {
569
698
  process.stdout.write(`reset all chat history for ${args.id}? [y/N] `);
570
- const answer = await new Promise((resolve3) => {
571
- process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
699
+ const answer = await new Promise((resolve4) => {
700
+ process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
572
701
  });
573
702
  if (answer !== "y" && answer !== "yes") {
574
703
  console.log("aborted");
@@ -716,8 +845,8 @@ var chatCompactCmd = defineCommand({
716
845
  process.stdout.write(
717
846
  `compact chat history for ${args.id}? head turns become a summary; tail is preserved. [y/N] `
718
847
  );
719
- const answer = await new Promise((resolve3) => {
720
- process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
848
+ const answer = await new Promise((resolve4) => {
849
+ process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
721
850
  });
722
851
  if (answer !== "y" && answer !== "yes") {
723
852
  console.log("aborted");
@@ -990,8 +1119,8 @@ var openaiLoginCmd = defineCommand3({
990
1119
  },
991
1120
  onPrompt: async (prompt) => {
992
1121
  process.stdout.write(`${prompt.message}: `);
993
- return new Promise((resolve3) => {
994
- process.stdin.once("data", (d) => resolve3(String(d).trim()));
1122
+ return new Promise((resolve4) => {
1123
+ process.stdin.once("data", (d) => resolve4(String(d).trim()));
995
1124
  });
996
1125
  },
997
1126
  onProgress: (msg) => {
@@ -1065,12 +1194,435 @@ var authCommand = defineCommand3({
1065
1194
  });
1066
1195
 
1067
1196
  // src/commands/backup.ts
1068
- import { spawn } from "child_process";
1069
- import { createWriteStream, existsSync as existsSync2, mkdirSync, readdirSync, rmSync } from "fs";
1070
- import { resolve } from "path";
1197
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
1198
+ import {
1199
+ chmodSync as chmodSync2,
1200
+ closeSync as closeSync2,
1201
+ copyFileSync,
1202
+ createWriteStream,
1203
+ existsSync as existsSync2,
1204
+ fsyncSync as fsyncSync2,
1205
+ lstatSync as lstatSync2,
1206
+ mkdirSync as mkdirSync2,
1207
+ mkdtempSync,
1208
+ openSync as openSync2,
1209
+ readdirSync,
1210
+ readFileSync as readFileSync4,
1211
+ renameSync as renameSync2,
1212
+ rmSync as rmSync2
1213
+ } from "fs";
1214
+ import { tmpdir } from "os";
1215
+ import { basename as basename3, dirname as dirname2, isAbsolute, parse, posix, relative, resolve as resolve2 } from "path";
1216
+ import { DatabaseSync } from "node:sqlite";
1071
1217
  import { Readable } from "stream";
1072
1218
  import { pipeline } from "stream/promises";
1073
1219
  import { defineCommand as defineCommand4 } from "citty";
1220
+ import { extract, list as list2 } from "tar";
1221
+
1222
+ // src/backup-schema.ts
1223
+ import { createHash } from "crypto";
1224
+ var CANONICAL_MIGRATION = "0001_init";
1225
+ var CANONICAL_SCHEMA_HASH = "f2da105d21bdea7bedff3ba601243c22d0a7b2d023b954c714940642152b227f";
1226
+ var CANONICAL_OBJECTS = [
1227
+ ["index", "agent_triggers_agent"],
1228
+ ["index", "agent_triggers_enabled"],
1229
+ ["index", "communication_approval_events_attempt"],
1230
+ ["index", "communication_approvals_queue"],
1231
+ ["index", "communication_approvals_teams"],
1232
+ ["index", "idx_agents_telegram_topic_id"],
1233
+ ["index", "idx_provider_models_provider"],
1234
+ ["index", "messages_policy_delivery_queue"],
1235
+ ["index", "messages_to_unread"],
1236
+ ["index", "team_policy_baseline_owner"],
1237
+ ["index", "team_policy_blocks_team_time"],
1238
+ ["index", "team_template_active_position"],
1239
+ ["index", "trigger_dispatches_claimable"],
1240
+ ["index", "trigger_dispatches_trigger_time"],
1241
+ ["index", "web_tokens_active"],
1242
+ ["table", "agent_skills"],
1243
+ ["table", "agent_triggers"],
1244
+ ["table", "agents"],
1245
+ ["table", "communication_approval_events"],
1246
+ ["table", "communication_approval_message_grants"],
1247
+ ["table", "communication_approvals"],
1248
+ ["table", "config"],
1249
+ ["table", "mcp_servers"],
1250
+ ["table", "messages"],
1251
+ ["table", "profile_communication_defaults"],
1252
+ ["table", "profile_default_skills"],
1253
+ ["table", "profiles"],
1254
+ ["table", "provider_models"],
1255
+ ["table", "provider_state"],
1256
+ ["table", "schema_migrations"],
1257
+ ["table", "secrets"],
1258
+ ["table", "skill_meta"],
1259
+ ["table", "source_slot_bindings"],
1260
+ ["table", "team_agent_state"],
1261
+ ["table", "team_policies"],
1262
+ ["table", "team_policy_block_events"],
1263
+ ["table", "team_policy_edges"],
1264
+ ["table", "team_template_edges"],
1265
+ ["table", "team_template_revision_edges"],
1266
+ ["table", "team_template_revision_slots"],
1267
+ ["table", "team_template_revisions"],
1268
+ ["table", "team_template_slots"],
1269
+ ["table", "team_templates"],
1270
+ ["table", "teams"],
1271
+ ["table", "telegram_allowed_users"],
1272
+ ["table", "template_instantiations"],
1273
+ ["table", "trigger_dispatches"],
1274
+ ["table", "web_tokens"],
1275
+ ["trigger", "create_team_policy"],
1276
+ ["trigger", "prevent_detached_team_policy_delete"],
1277
+ ["trigger", "validate_source_binding_insert"],
1278
+ ["trigger", "validate_team_agent_state_insert"],
1279
+ ["trigger", "validate_team_policy_baseline_update"],
1280
+ ["trigger", "validate_team_policy_edge_insert"],
1281
+ ["trigger", "validate_team_template_edge_insert"]
1282
+ ];
1283
+ function normalizeSql(sql) {
1284
+ return sql.replace(/\s+/g, " ").trim();
1285
+ }
1286
+ function assertCanonicalBackupSchema(db) {
1287
+ const migrations = db.prepare("SELECT version FROM schema_migrations ORDER BY version").all();
1288
+ if (migrations.length !== 1 || migrations[0]?.version !== CANONICAL_MIGRATION) {
1289
+ throw new Error(
1290
+ `schema_migrations must contain exactly ${CANONICAL_MIGRATION}; found ` + (migrations.map((row) => row.version).join(", ") || "none")
1291
+ );
1292
+ }
1293
+ const rows = db.prepare(
1294
+ `SELECT type, name, tbl_name, sql
1295
+ FROM sqlite_schema
1296
+ WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`
1297
+ ).all();
1298
+ const byKey = new Map(rows.map((row) => [`${row.type}\0${row.name}`, row]));
1299
+ const canonicalKeys = new Set(CANONICAL_OBJECTS.map(([type, name]) => `${type}\0${name}`));
1300
+ for (const row of rows) {
1301
+ if (!canonicalKeys.has(`${row.type}\0${row.name}`)) {
1302
+ throw new Error(`unexpected schema ${row.type} is not canonical: ${row.name}`);
1303
+ }
1304
+ }
1305
+ const canonicalRows = [];
1306
+ for (const [type, name] of CANONICAL_OBJECTS) {
1307
+ const row = byKey.get(`${type}\0${name}`);
1308
+ if (!row) throw new Error(`required canonical schema ${type} is missing: ${name}`);
1309
+ canonicalRows.push(row);
1310
+ }
1311
+ const payload = canonicalRows.map((row) => [row.type, row.name, row.tbl_name, normalizeSql(row.sql)].join("\0")).join("\n");
1312
+ const actualHash = createHash("sha256").update(payload).digest("hex");
1313
+ if (actualHash !== CANONICAL_SCHEMA_HASH) {
1314
+ throw new Error(
1315
+ "canonical schema fingerprint does not match this Bazilion release; restore a backup created from the current clean-install schema"
1316
+ );
1317
+ }
1318
+ }
1319
+
1320
+ // src/daemon-liveness.ts
1321
+ import { createHash as createHash2, randomUUID } from "crypto";
1322
+ import {
1323
+ chmodSync,
1324
+ closeSync,
1325
+ fstatSync,
1326
+ fsyncSync,
1327
+ ftruncateSync,
1328
+ futimesSync,
1329
+ lstatSync,
1330
+ mkdirSync,
1331
+ openSync,
1332
+ readFileSync as readFileSync3,
1333
+ realpathSync,
1334
+ renameSync,
1335
+ rmSync,
1336
+ writeSync
1337
+ } from "fs";
1338
+ import { basename as basename2, dirname, join as join2, resolve } from "path";
1339
+ var DAEMON_LIVENESS_FILENAME = "daemon-runtime.json";
1340
+ var STALE_AFTER_MS = 6e4;
1341
+ var PROBE_TIMEOUT_MS = 750;
1342
+ function canonicalHomeIdentity(home) {
1343
+ const absolute = resolve(home);
1344
+ try {
1345
+ return realpathSync.native(absolute);
1346
+ } catch (error) {
1347
+ if (errorCode(error) !== "ENOENT") throw error;
1348
+ const parent = dirname(absolute);
1349
+ if (parent === absolute) throw error;
1350
+ return join2(canonicalHomeIdentity(parent), basename2(absolute));
1351
+ }
1352
+ }
1353
+ function daemonLivenessPath(home) {
1354
+ const canonicalHome = canonicalHomeIdentity(home);
1355
+ const identity = createHash2("sha256").update(canonicalHome).digest("hex").slice(0, 24);
1356
+ return join2(dirname(canonicalHome), `.bazilion-runtime-${identity}.json`);
1357
+ }
1358
+ function errorCode(error) {
1359
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
1360
+ }
1361
+ function parseRecord(raw) {
1362
+ try {
1363
+ const value = JSON.parse(raw);
1364
+ if (value.version !== 1 || typeof value.instanceId !== "string" || !value.instanceId || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0 || !["daemon", "restore"].includes(value.owner ?? "") || ![
1365
+ "starting",
1366
+ "listening",
1367
+ "restoring",
1368
+ "swapping",
1369
+ "installed",
1370
+ "recovery-required"
1371
+ ].includes(value.state ?? "") || !Number.isFinite(value.startedAt)) {
1372
+ return null;
1373
+ }
1374
+ if (value.owner === "daemon" && !["starting", "listening"].includes(value.state ?? "") || value.owner === "restore" && !["restoring", "swapping", "installed", "recovery-required"].includes(value.state ?? "")) {
1375
+ return null;
1376
+ }
1377
+ if (value.owner === "restore" && ["swapping", "recovery-required"].includes(value.state ?? "") && (typeof value.recoveryPath !== "string" || !value.recoveryPath || typeof value.hadPrevious !== "boolean")) {
1378
+ return null;
1379
+ }
1380
+ if (value.owner === "daemon" && value.state === "listening" && (typeof value.host !== "string" || !value.host || !Number.isSafeInteger(value.port) || (value.port ?? 0) < 1 || (value.port ?? 0) > 65535)) {
1381
+ return null;
1382
+ }
1383
+ return value;
1384
+ } catch {
1385
+ return null;
1386
+ }
1387
+ }
1388
+ function readRecord(path) {
1389
+ try {
1390
+ const stat = lstatSync(path);
1391
+ const raw = stat.isFile() ? readFileSync3(path, "utf8") : "";
1392
+ return { raw, stat, record: stat.isFile() ? parseRecord(raw) : null };
1393
+ } catch (error) {
1394
+ if (errorCode(error) === "ENOENT") return null;
1395
+ throw error;
1396
+ }
1397
+ }
1398
+ function pidIsAlive(pid) {
1399
+ try {
1400
+ process.kill(pid, 0);
1401
+ return true;
1402
+ } catch (error) {
1403
+ return errorCode(error) !== "ESRCH";
1404
+ }
1405
+ }
1406
+ function endpointFor(record) {
1407
+ if (record.state !== "listening" || !record.host || !record.port) return null;
1408
+ const normalized = record.host.toLowerCase();
1409
+ if (normalized === "0.0.0.0") return `http://127.0.0.1:${record.port}`;
1410
+ if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") {
1411
+ return `http://[::1]:${record.port}`;
1412
+ }
1413
+ if (normalized === "localhost" || normalized === "127.0.0.1") {
1414
+ return `http://${normalized}:${record.port}`;
1415
+ }
1416
+ if (normalized === "::1" || normalized === "[::1]") {
1417
+ return `http://[::1]:${record.port}`;
1418
+ }
1419
+ return null;
1420
+ }
1421
+ async function endpointHasOwner(endpoint3, instanceId) {
1422
+ const controller = new AbortController();
1423
+ const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
1424
+ try {
1425
+ const response = await fetch(new URL("/api/health", endpoint3), {
1426
+ signal: controller.signal
1427
+ });
1428
+ return response.headers.get("x-bazilion-daemon-instance") === instanceId;
1429
+ } catch {
1430
+ return false;
1431
+ } finally {
1432
+ clearTimeout(timer);
1433
+ }
1434
+ }
1435
+ function sameFile(left, right) {
1436
+ return left.dev === right.dev && left.ino === right.ino;
1437
+ }
1438
+ function writeRecord(fd, record) {
1439
+ const body = `${JSON.stringify(record, null, 2)}
1440
+ `;
1441
+ ftruncateSync(fd, 0);
1442
+ writeSync(fd, body, 0, "utf8");
1443
+ fsyncSync(fd);
1444
+ }
1445
+ function fsyncParentDirectory(path) {
1446
+ let directoryFd = null;
1447
+ try {
1448
+ directoryFd = openSync(dirname(path), "r");
1449
+ fsyncSync(directoryFd);
1450
+ } catch (error) {
1451
+ if (process.platform !== "win32") throw error;
1452
+ } finally {
1453
+ if (directoryFd !== null) closeSync(directoryFd);
1454
+ }
1455
+ }
1456
+ function reclaimRecord(path, existing) {
1457
+ const identity = createHash2("sha256").update(existing.record?.instanceId ?? existing.raw).update(`\0${existing.stat.dev}\0${existing.stat.ino}`).digest("hex").slice(0, 24);
1458
+ const claim = `${path}.reclaim-${identity}`;
1459
+ try {
1460
+ mkdirSync(claim, { mode: 448 });
1461
+ } catch (error) {
1462
+ if (errorCode(error) !== "EEXIST") throw error;
1463
+ return null;
1464
+ }
1465
+ const current = readRecord(path);
1466
+ if (!current || !sameFile(current.stat, existing.stat) || current.raw !== existing.raw) {
1467
+ return null;
1468
+ }
1469
+ try {
1470
+ renameSync(path, join2(claim, "stale-record.json"));
1471
+ return claim;
1472
+ } catch (error) {
1473
+ if (errorCode(error) === "ENOENT") return null;
1474
+ throw error;
1475
+ }
1476
+ }
1477
+ async function acquireHomeRestoreLock(home) {
1478
+ const targetHome = resolve(home);
1479
+ mkdirSync(dirname(targetHome), { recursive: true });
1480
+ const path = daemonLivenessPath(targetHome);
1481
+ mkdirSync(dirname(path), { recursive: true });
1482
+ const instanceId = randomUUID();
1483
+ let fd = null;
1484
+ let reclaimClaim = null;
1485
+ for (let attempt = 0; attempt < 8 && fd === null; attempt++) {
1486
+ try {
1487
+ fd = openSync(path, "wx", 384);
1488
+ break;
1489
+ } catch (error) {
1490
+ if (errorCode(error) !== "EEXIST") throw error;
1491
+ }
1492
+ const existing = readRecord(path);
1493
+ if (!existing) continue;
1494
+ const endpoint3 = existing.record ? endpointFor(existing.record) : null;
1495
+ if (endpoint3 && existing.record && await endpointHasOwner(endpoint3, existing.record.instanceId)) {
1496
+ throw new Error(
1497
+ `bazilion daemon is running at ${endpoint3} for ${targetHome} \u2014 stop it before restoring`
1498
+ );
1499
+ }
1500
+ if (!existing.record) {
1501
+ throw new Error(
1502
+ `the daemon ownership record for ${targetHome} is invalid; verify no daemon is running before removing it manually`
1503
+ );
1504
+ }
1505
+ const livePid = pidIsAlive(existing.record.pid);
1506
+ if (existing.record.owner === "restore" && ["swapping", "recovery-required"].includes(existing.record.state)) {
1507
+ if (livePid) {
1508
+ throw new Error(
1509
+ `bazilion restore process ${existing.record.pid} is ${existing.record.state} ${targetHome} \u2014 wait for it to finish`
1510
+ );
1511
+ }
1512
+ const phase = existing.record.state.replace("-", " ");
1513
+ throw new Error(
1514
+ `Bazilion restore ${phase} for ${targetHome}; recovery data is at ${existing.record.recoveryPath}. Recover the previous home, then remove ${path} manually.`
1515
+ );
1516
+ }
1517
+ if (livePid) {
1518
+ const ageMs = Math.max(0, Date.now() - existing.stat.mtimeMs);
1519
+ const staleDetail = ageMs > STALE_AFTER_MS ? " (its heartbeat is stale)" : "";
1520
+ const owner = existing.record.owner === "daemon" ? "daemon" : "restore";
1521
+ throw new Error(
1522
+ `bazilion ${owner} process ${existing.record.pid} owns ${targetHome}${staleDetail} \u2014 stop it before restoring`
1523
+ );
1524
+ }
1525
+ const claim = reclaimRecord(path, existing);
1526
+ if (claim) {
1527
+ reclaimClaim = claim;
1528
+ console.warn(`removed stale Bazilion daemon ownership record for ${targetHome}`);
1529
+ }
1530
+ }
1531
+ if (fd === null) {
1532
+ throw new Error(
1533
+ `could not safely acquire exclusive restore ownership for ${targetHome}; try again`
1534
+ );
1535
+ }
1536
+ const ownedStat = fstatSync(fd);
1537
+ const record = {
1538
+ version: 1,
1539
+ instanceId,
1540
+ pid: process.pid,
1541
+ owner: "restore",
1542
+ state: "restoring",
1543
+ startedAt: Date.now()
1544
+ };
1545
+ try {
1546
+ writeRecord(fd, record);
1547
+ fsyncParentDirectory(path);
1548
+ if (reclaimClaim) rmSync(reclaimClaim, { recursive: true, force: true });
1549
+ try {
1550
+ chmodSync(path, 384);
1551
+ } catch {
1552
+ }
1553
+ } catch (error) {
1554
+ closeSync(fd);
1555
+ fd = null;
1556
+ try {
1557
+ if (sameFile(lstatSync(path), ownedStat)) rmSync(path, { force: true });
1558
+ } catch {
1559
+ }
1560
+ throw error;
1561
+ }
1562
+ let released = false;
1563
+ const stillOwnsPath = () => {
1564
+ try {
1565
+ return sameFile(lstatSync(path), ownedStat);
1566
+ } catch {
1567
+ return false;
1568
+ }
1569
+ };
1570
+ const heartbeat = setInterval(() => {
1571
+ if (released) return;
1572
+ if (!stillOwnsPath()) {
1573
+ console.error("restore ownership was lost; exiting to protect the Bazilion database");
1574
+ process.exit(1);
1575
+ }
1576
+ try {
1577
+ const now = /* @__PURE__ */ new Date();
1578
+ futimesSync(fd, now, now);
1579
+ } catch (error) {
1580
+ console.error(
1581
+ "restore ownership heartbeat failed:",
1582
+ error instanceof Error ? error.message : error
1583
+ );
1584
+ process.exit(1);
1585
+ }
1586
+ }, 2e3);
1587
+ heartbeat.unref();
1588
+ return {
1589
+ path,
1590
+ markSwapping(recoveryPath, hadPrevious) {
1591
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1592
+ record.state = "swapping";
1593
+ record.recoveryPath = resolve(recoveryPath);
1594
+ record.hadPrevious = hadPrevious;
1595
+ writeRecord(fd, record);
1596
+ fsyncParentDirectory(path);
1597
+ },
1598
+ markInstalled() {
1599
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1600
+ record.state = "installed";
1601
+ delete record.recoveryPath;
1602
+ delete record.hadPrevious;
1603
+ writeRecord(fd, record);
1604
+ },
1605
+ markRecoveryRequired(recoveryPath, hadPrevious) {
1606
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1607
+ record.state = "recovery-required";
1608
+ record.recoveryPath = resolve(recoveryPath);
1609
+ record.hadPrevious = hadPrevious;
1610
+ writeRecord(fd, record);
1611
+ },
1612
+ release() {
1613
+ if (released) return;
1614
+ released = true;
1615
+ clearInterval(heartbeat);
1616
+ const ownsPath = stillOwnsPath();
1617
+ const retainForRecovery = ["swapping", "recovery-required"].includes(record.state);
1618
+ closeSync(fd);
1619
+ fd = null;
1620
+ if (ownsPath && !retainForRecovery) rmSync(path, { force: true });
1621
+ }
1622
+ };
1623
+ }
1624
+
1625
+ // src/commands/backup.ts
1074
1626
  var createCmd = defineCommand4({
1075
1627
  meta: { name: "create", description: "Download a tar.gz backup of ~/.bazilion from the server" },
1076
1628
  args: {
@@ -1083,7 +1635,14 @@ var createCmd = defineCommand4({
1083
1635
  async run({ args }) {
1084
1636
  const cfg = loadClientConfig();
1085
1637
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1086
- const outAbs = resolve(args.output ?? `bazilion-backup-${date}.tar.gz`);
1638
+ const outAbs = resolve2(args.output ?? `bazilion-backup-${date}.tar.gz`);
1639
+ const configuredHome = resolve2(resolveCliPaths().home);
1640
+ const outputFromHome = relative(configuredHome, outAbs);
1641
+ if (!outputFromHome || !outputFromHome.startsWith("..") && !isAbsolute(outputFromHome)) {
1642
+ throw new Error(
1643
+ `backup output must be outside BAZILION_HOME (${configuredHome}) to avoid nesting backups`
1644
+ );
1645
+ }
1087
1646
  console.log(`downloading backup \u2192 ${outAbs}`);
1088
1647
  const res = await fetch(`${cfg.serverUrl}/api/backup`, {
1089
1648
  headers: { authorization: `Bearer ${cfg.token}`, origin: cfg.serverUrl }
@@ -1097,21 +1656,462 @@ var createCmd = defineCommand4({
1097
1656
  }
1098
1657
  throw new Error(`backup failed: ${err}`);
1099
1658
  }
1100
- await pipeline(Readable.fromWeb(res.body), createWriteStream(outAbs));
1659
+ const downloadDir = mkdtempSync(resolve2(tmpdir(), "bazilion-download-"));
1660
+ const partial = resolve2(downloadDir, "backup.tar.gz");
1661
+ try {
1662
+ await pipeline(
1663
+ Readable.fromWeb(res.body),
1664
+ createWriteStream(partial, { flags: "wx", mode: 384 })
1665
+ );
1666
+ await validateArchive(partial);
1667
+ try {
1668
+ renameSync2(partial, outAbs);
1669
+ } catch (error) {
1670
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") throw error;
1671
+ const installPartial = resolve2(
1672
+ dirname2(outAbs),
1673
+ `.${basename3(outAbs)}.${randomUUID2()}.installing`
1674
+ );
1675
+ try {
1676
+ copyFileSync(partial, installPartial);
1677
+ chmodSync2(installPartial, 384);
1678
+ renameSync2(installPartial, outAbs);
1679
+ } finally {
1680
+ rmSync2(installPartial, { force: true });
1681
+ }
1682
+ }
1683
+ } finally {
1684
+ rmSync2(downloadDir, { recursive: true, force: true });
1685
+ }
1101
1686
  console.log("done");
1102
1687
  }
1103
1688
  });
1104
- async function probeServerRunning(port) {
1689
+ async function probeServerRunning(serverUrl) {
1105
1690
  try {
1106
1691
  const ctrl = new AbortController();
1107
1692
  const timer = setTimeout(() => ctrl.abort(), 500);
1108
- const res = await fetch(`http://127.0.0.1:${port}/login`, { signal: ctrl.signal });
1109
- clearTimeout(timer);
1110
- return res.status >= 200 && res.status < 600;
1693
+ try {
1694
+ const res = await fetch(new URL("/api/health", serverUrl), { signal: ctrl.signal });
1695
+ return res.status >= 200 && res.status < 600;
1696
+ } finally {
1697
+ clearTimeout(timer);
1698
+ }
1111
1699
  } catch {
1112
1700
  return false;
1113
1701
  }
1114
1702
  }
1703
+ function loopbackServerUrl(raw) {
1704
+ if (!raw) return null;
1705
+ try {
1706
+ const url = new URL(raw);
1707
+ if (!["http:", "https:"].includes(url.protocol)) return null;
1708
+ const hostname = url.hostname.toLowerCase();
1709
+ if (!["127.0.0.1", "localhost", "[::1]", "::1"].includes(hostname)) return null;
1710
+ return url.origin;
1711
+ } catch {
1712
+ return null;
1713
+ }
1714
+ }
1715
+ function configuredLoopbackUrls() {
1716
+ const urls = /* @__PURE__ */ new Set(["http://127.0.0.1:4321"]);
1717
+ const envUrl = loopbackServerUrl(process.env.BAZILION_SERVER);
1718
+ if (envUrl) urls.add(envUrl);
1719
+ try {
1720
+ const clientUrl = loopbackServerUrl(loadClientConfig().serverUrl);
1721
+ if (clientUrl) urls.add(clientUrl);
1722
+ } catch {
1723
+ }
1724
+ return [...urls];
1725
+ }
1726
+ var EXTRACTABLE_ENTRY_TYPES = /* @__PURE__ */ new Set([
1727
+ "File",
1728
+ "OldFile",
1729
+ "Directory",
1730
+ "SymbolicLink"
1731
+ ]);
1732
+ function normalizeArchivePath(rawPath) {
1733
+ if (!rawPath || rawPath.includes("\0")) throw new Error("backup contains an invalid empty path");
1734
+ if (rawPath.includes("\\")) {
1735
+ throw new Error(`backup contains a non-portable path: ${JSON.stringify(rawPath)}`);
1736
+ }
1737
+ if (rawPath.startsWith("/") || /^[A-Za-z]:/.test(rawPath)) {
1738
+ throw new Error(`backup contains an absolute path: ${JSON.stringify(rawPath)}`);
1739
+ }
1740
+ const segments = [];
1741
+ for (const segment of rawPath.split("/")) {
1742
+ if (!segment || segment === ".") continue;
1743
+ if (segment === "..") {
1744
+ throw new Error(`backup path escapes the restore directory: ${JSON.stringify(rawPath)}`);
1745
+ }
1746
+ segments.push(segment);
1747
+ }
1748
+ return segments.join("/");
1749
+ }
1750
+ function validateArchiveSymbolicLink(path, rawTarget) {
1751
+ if (!rawTarget || rawTarget.includes("\0")) {
1752
+ throw new Error(`backup contains an invalid symbolic link target: ${path}`);
1753
+ }
1754
+ const segments = path.split("/");
1755
+ const isTeamSlot = segments.length === 2 && segments[0] === "teams" && /^[a-z0-9][a-z0-9-]*$/.test(segments[1] ?? "");
1756
+ const portableTarget = rawTarget.replaceAll("\\", "/");
1757
+ const absoluteOnAnyPlatform = isAbsolute(rawTarget) || posix.isAbsolute(portableTarget) || /^[A-Za-z]:\//.test(portableTarget);
1758
+ if (absoluteOnAnyPlatform) {
1759
+ if (!isTeamSlot) {
1760
+ throw new Error(`backup absolute symbolic links are only allowed for Team slots: ${path}`);
1761
+ }
1762
+ if (!isAbsolute(rawTarget)) {
1763
+ throw new Error(`backup contains an invalid Team link target: ${path}`);
1764
+ }
1765
+ return;
1766
+ }
1767
+ if (isTeamSlot) {
1768
+ throw new Error(`backup contains an invalid Team link target: ${path}`);
1769
+ }
1770
+ if (/^[A-Za-z]:/.test(portableTarget)) {
1771
+ throw new Error(`backup contains a non-portable symbolic link target: ${path}`);
1772
+ }
1773
+ const target = posix.normalize(posix.join(posix.dirname(path), portableTarget));
1774
+ if (target === ".." || target.startsWith("../") || posix.isAbsolute(target)) {
1775
+ throw new Error(`backup symbolic link target escapes the restore directory: ${path}`);
1776
+ }
1777
+ }
1778
+ async function validateArchive(file) {
1779
+ const entries = [];
1780
+ await list2({
1781
+ file,
1782
+ strict: true,
1783
+ onentry(entry2) {
1784
+ if (entry2.meta) return;
1785
+ const path = normalizeArchivePath(entry2.path);
1786
+ if (!path) return;
1787
+ if (!EXTRACTABLE_ENTRY_TYPES.has(entry2.type)) {
1788
+ throw new Error(`backup contains unsupported ${entry2.type} entry: ${path}`);
1789
+ }
1790
+ if (entry2.type === "SymbolicLink") {
1791
+ validateArchiveSymbolicLink(path, entry2.linkpath);
1792
+ }
1793
+ entries.push({ path, type: entry2.type, linkpath: entry2.linkpath });
1794
+ }
1795
+ });
1796
+ const byPath = /* @__PURE__ */ new Map();
1797
+ for (const entry2 of entries) {
1798
+ if (byPath.has(entry2.path)) throw new Error(`backup contains a duplicate path: ${entry2.path}`);
1799
+ byPath.set(entry2.path, entry2);
1800
+ }
1801
+ const symlinks = new Set(
1802
+ entries.filter((entry2) => entry2.type === "SymbolicLink").map((entry2) => entry2.path)
1803
+ );
1804
+ for (const entry2 of entries) {
1805
+ const segments = entry2.path.split("/");
1806
+ for (let i = 1; i < segments.length; i++) {
1807
+ const ancestor = segments.slice(0, i).join("/");
1808
+ if (symlinks.has(ancestor)) {
1809
+ throw new Error(`backup entry traverses symbolic link ${ancestor}: ${entry2.path}`);
1810
+ }
1811
+ const ancestorEntry = byPath.get(ancestor);
1812
+ if (ancestorEntry && ancestorEntry.type !== "Directory") {
1813
+ throw new Error(`backup entry traverses non-directory ${ancestor}: ${entry2.path}`);
1814
+ }
1815
+ }
1816
+ }
1817
+ for (const required of ["bazilion.db", "auth.json"]) {
1818
+ const entry2 = byPath.get(required);
1819
+ if (!entry2 || entry2.type !== "File" && entry2.type !== "OldFile") {
1820
+ throw new Error(`backup is missing required regular file: ${required}`);
1821
+ }
1822
+ }
1823
+ for (const transient of [
1824
+ "bazilion.db-wal",
1825
+ "bazilion.db-shm",
1826
+ "bazilion.db-journal",
1827
+ DAEMON_LIVENESS_FILENAME
1828
+ ]) {
1829
+ if (byPath.has(transient)) {
1830
+ throw new Error(`backup contains unsafe transient SQLite file: ${transient}`);
1831
+ }
1832
+ }
1833
+ return entries;
1834
+ }
1835
+ function validateAuthFile(path) {
1836
+ let parsed;
1837
+ try {
1838
+ parsed = JSON.parse(readFileSync4(path, "utf8"));
1839
+ } catch (error) {
1840
+ const detail = error instanceof Error ? error.message : String(error);
1841
+ throw new Error(`backup auth.json is invalid: ${detail}`);
1842
+ }
1843
+ if (typeof parsed !== "object" || parsed === null || !("token" in parsed) || typeof parsed.token !== "string" || !parsed.token.trim()) {
1844
+ throw new Error("backup auth.json is invalid: token is missing");
1845
+ }
1846
+ return parsed.token;
1847
+ }
1848
+ function validateDatabase(path, authToken) {
1849
+ let db = null;
1850
+ try {
1851
+ db = new DatabaseSync(path, { readOnly: true });
1852
+ const integrity = db.prepare("PRAGMA integrity_check").all();
1853
+ if (integrity.length !== 1 || integrity[0]?.integrity_check !== "ok") {
1854
+ const detail = integrity.map((row) => row.integrity_check).join("; ") || "no result";
1855
+ throw new Error(`integrity_check failed: ${detail}`);
1856
+ }
1857
+ const foreignKeyErrors = db.prepare("PRAGMA foreign_key_check").all();
1858
+ if (foreignKeyErrors.length > 0) {
1859
+ throw new Error(`foreign_key_check failed with ${foreignKeyErrors.length} violation(s)`);
1860
+ }
1861
+ assertCanonicalBackupSchema(db);
1862
+ const tokenHash = createHash3("sha256").update(authToken).digest("hex");
1863
+ const activeBootstrap = db.prepare(
1864
+ `SELECT 1 FROM web_tokens
1865
+ WHERE token_hash = ? AND label = 'bootstrap' AND revoked_at IS NULL
1866
+ LIMIT 1`
1867
+ ).get(tokenHash);
1868
+ if (!activeBootstrap) {
1869
+ throw new Error("auth.json token does not match an active web_tokens row");
1870
+ }
1871
+ } catch (error) {
1872
+ if (error instanceof Error && error.message.startsWith("backup database is invalid:")) {
1873
+ throw error;
1874
+ }
1875
+ const detail = error instanceof Error ? error.message : String(error);
1876
+ throw new Error(`backup database is invalid: ${detail}`);
1877
+ } finally {
1878
+ db?.close();
1879
+ }
1880
+ }
1881
+ var SLUG_ID = /^[a-z0-9][a-z0-9-]*$/;
1882
+ var AGENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
1883
+ function validateEntityId(id, pattern, entity) {
1884
+ if (typeof id !== "string" || !pattern.test(id)) {
1885
+ throw new Error(`${entity} has an invalid canonical id: ${JSON.stringify(id)}`);
1886
+ }
1887
+ return id;
1888
+ }
1889
+ function containedChildDirectory(root, id, rowLabel) {
1890
+ const child = resolve2(root, id);
1891
+ const fromRoot = relative(root, child);
1892
+ if (!fromRoot || fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
1893
+ throw new Error(`${rowLabel} directory escapes its canonical root: ${JSON.stringify(id)}`);
1894
+ }
1895
+ return child;
1896
+ }
1897
+ function requireExtractedDirectory(path, rowLabel) {
1898
+ try {
1899
+ if (lstatSync2(path).isDirectory()) return;
1900
+ } catch {
1901
+ }
1902
+ throw new Error(`${rowLabel} is missing its canonical directory in the backup`);
1903
+ }
1904
+ function requireExtractedTeamSlot(path, rowLabel) {
1905
+ try {
1906
+ const entry2 = lstatSync2(path);
1907
+ if (entry2.isDirectory() || entry2.isSymbolicLink()) return;
1908
+ } catch {
1909
+ }
1910
+ throw new Error(`${rowLabel} is missing its canonical Team slot in the backup`);
1911
+ }
1912
+ function rebaseRestoredHomeDirectories(database, payload, targetHome) {
1913
+ let db = null;
1914
+ try {
1915
+ db = new DatabaseSync(database);
1916
+ db.exec("PRAGMA foreign_keys = ON");
1917
+ const profiles = db.prepare("SELECT id FROM profiles ORDER BY id").all();
1918
+ const agents = db.prepare("SELECT id FROM agents ORDER BY id").all();
1919
+ const teams = db.prepare("SELECT id FROM teams ORDER BY id").all();
1920
+ const rewrites = [];
1921
+ for (const [table, rows, idPattern] of [
1922
+ ["profiles", profiles, SLUG_ID],
1923
+ ["agents", agents, AGENT_ID]
1924
+ ]) {
1925
+ const collection = table;
1926
+ const stagedRoot = resolve2(payload, collection);
1927
+ const targetRoot = resolve2(targetHome, collection);
1928
+ for (const row of rows) {
1929
+ const id = validateEntityId(row.id, idPattern, `${table} row`);
1930
+ const rowLabel = `${table} row ${JSON.stringify(id)}`;
1931
+ const stagedDir = containedChildDirectory(stagedRoot, id, rowLabel);
1932
+ requireExtractedDirectory(stagedDir, rowLabel);
1933
+ rewrites.push({
1934
+ table,
1935
+ id,
1936
+ dir: containedChildDirectory(targetRoot, id, rowLabel)
1937
+ });
1938
+ }
1939
+ }
1940
+ const stagedTeamsRoot = resolve2(payload, "teams");
1941
+ for (const row of teams) {
1942
+ const id = validateEntityId(row.id, SLUG_ID, "teams row");
1943
+ const rowLabel = `teams row ${JSON.stringify(id)}`;
1944
+ requireExtractedTeamSlot(containedChildDirectory(stagedTeamsRoot, id, rowLabel), rowLabel);
1945
+ }
1946
+ db.exec("PRAGMA journal_mode = DELETE");
1947
+ db.exec("PRAGMA synchronous = FULL");
1948
+ let transactionOpen = false;
1949
+ try {
1950
+ db.exec("BEGIN IMMEDIATE");
1951
+ transactionOpen = true;
1952
+ const updateProfile = db.prepare("UPDATE profiles SET dir = ? WHERE id = ?");
1953
+ const updateAgent = db.prepare("UPDATE agents SET dir = ? WHERE id = ?");
1954
+ for (const rewrite of rewrites) {
1955
+ const result = rewrite.table === "profiles" ? updateProfile.run(rewrite.dir, rewrite.id) : updateAgent.run(rewrite.dir, rewrite.id);
1956
+ if (result.changes !== 1) {
1957
+ throw new Error(`could not rebase ${rewrite.table} row ${JSON.stringify(rewrite.id)}`);
1958
+ }
1959
+ }
1960
+ const readProfile = db.prepare("SELECT dir FROM profiles WHERE id = ?");
1961
+ const readAgent = db.prepare("SELECT dir FROM agents WHERE id = ?");
1962
+ for (const rewrite of rewrites) {
1963
+ const row = rewrite.table === "profiles" ? readProfile.get(rewrite.id) : readAgent.get(rewrite.id);
1964
+ if (row?.dir !== rewrite.dir) {
1965
+ throw new Error(
1966
+ `could not verify rebased ${rewrite.table} row ${JSON.stringify(rewrite.id)}`
1967
+ );
1968
+ }
1969
+ }
1970
+ db.exec("COMMIT");
1971
+ transactionOpen = false;
1972
+ } catch (error) {
1973
+ if (transactionOpen) db.exec("ROLLBACK");
1974
+ throw error;
1975
+ }
1976
+ } catch (error) {
1977
+ const detail = error instanceof Error ? error.message : String(error);
1978
+ throw new Error(`backup database is invalid: ${detail}`);
1979
+ } finally {
1980
+ db?.close();
1981
+ }
1982
+ for (const suffix of ["-wal", "-shm", "-journal"]) {
1983
+ if (existsSync2(`${database}${suffix}`)) {
1984
+ throw new Error(`backup database is invalid: path rebasing left transient ${suffix} state`);
1985
+ }
1986
+ }
1987
+ }
1988
+ function removeRebuildableQmdIndexes(payload) {
1989
+ const teamsDir = resolve2(payload, "teams");
1990
+ if (!existsSync2(teamsDir) || !lstatSync2(teamsDir).isDirectory()) return;
1991
+ for (const team of readdirSync(teamsDir, { withFileTypes: true })) {
1992
+ if (!team.isDirectory() || team.isSymbolicLink()) continue;
1993
+ const memoryDir = resolve2(teamsDir, team.name, "memory");
1994
+ if (!existsSync2(memoryDir) || !lstatSync2(memoryDir).isDirectory()) continue;
1995
+ for (const entry2 of readdirSync(memoryDir, { withFileTypes: true })) {
1996
+ if (entry2.isFile() && entry2.name.startsWith(".qmd-index.sqlite")) {
1997
+ rmSync2(resolve2(memoryDir, entry2.name), { force: true });
1998
+ }
1999
+ }
2000
+ }
2001
+ }
2002
+ function installValidatedPayload(payload, targetHome, staging, lifecycle, operations = { rename: renameSync2 }) {
2003
+ const previous = resolve2(staging, "previous-home");
2004
+ const hadPrevious = existsSync2(targetHome);
2005
+ const targetParent = dirname2(targetHome);
2006
+ const syncDirectory = operations.fsyncDirectory ?? fsyncDirectory;
2007
+ const recoveryError = (message, recoveryPath, cause) => {
2008
+ let markerError;
2009
+ try {
2010
+ lifecycle?.markRecoveryRequired(recoveryPath, hadPrevious);
2011
+ } catch (error) {
2012
+ markerError = error;
2013
+ }
2014
+ const causeDetail = cause instanceof Error ? cause.message : String(cause);
2015
+ const markerDetail = markerError ? `; recovery marker update failed: ${markerError instanceof Error ? markerError.message : markerError}` : "";
2016
+ return new RestoreRecoveryRequiredError(
2017
+ `${message} (${causeDetail}); recovery data is at ${recoveryPath}${markerDetail}`,
2018
+ recoveryPath
2019
+ );
2020
+ };
2021
+ lifecycle?.markSwapping(previous, hadPrevious);
2022
+ if (hadPrevious) {
2023
+ try {
2024
+ operations.rename(targetHome, previous);
2025
+ } catch (moveError) {
2026
+ lifecycle?.markInstalled();
2027
+ throw moveError;
2028
+ }
2029
+ try {
2030
+ syncDirectory(targetParent);
2031
+ operations.afterPreviousMoved?.();
2032
+ } catch (postMoveError) {
2033
+ throw recoveryError(
2034
+ "the previous home moved but the swap could not continue safely",
2035
+ previous,
2036
+ postMoveError
2037
+ );
2038
+ }
2039
+ }
2040
+ try {
2041
+ operations.rename(payload, targetHome);
2042
+ } catch (installError) {
2043
+ if (hadPrevious) {
2044
+ try {
2045
+ operations.rename(previous, targetHome);
2046
+ } catch (rollbackError) {
2047
+ const installDetail = installError instanceof Error ? installError.message : String(installError);
2048
+ throw recoveryError(
2049
+ `restore install failed (${installDetail}) and rollback failed`,
2050
+ previous,
2051
+ rollbackError
2052
+ );
2053
+ }
2054
+ try {
2055
+ syncDirectory(targetParent);
2056
+ } catch (rollbackSyncError) {
2057
+ throw recoveryError(
2058
+ "the previous home was restored but its directory entry could not be made durable",
2059
+ targetHome,
2060
+ rollbackSyncError
2061
+ );
2062
+ }
2063
+ try {
2064
+ lifecycle?.markInstalled();
2065
+ } catch (phaseError) {
2066
+ throw recoveryError(
2067
+ "the previous home was restored but could not be marked safe",
2068
+ targetHome,
2069
+ phaseError
2070
+ );
2071
+ }
2072
+ } else {
2073
+ lifecycle?.markInstalled();
2074
+ }
2075
+ throw installError;
2076
+ }
2077
+ try {
2078
+ syncDirectory(targetParent);
2079
+ } catch (installSyncError) {
2080
+ throw recoveryError(
2081
+ "the restored home was installed but its directory entry could not be made durable",
2082
+ hadPrevious ? previous : targetHome,
2083
+ installSyncError
2084
+ );
2085
+ }
2086
+ try {
2087
+ lifecycle?.markInstalled();
2088
+ } catch (phaseError) {
2089
+ throw recoveryError(
2090
+ "the restored home was installed but could not be marked safe",
2091
+ hadPrevious ? previous : targetHome,
2092
+ phaseError
2093
+ );
2094
+ }
2095
+ }
2096
+ var RestoreRecoveryRequiredError = class extends Error {
2097
+ recoveryPath;
2098
+ constructor(message, recoveryPath) {
2099
+ super(message);
2100
+ this.name = "RestoreRecoveryRequiredError";
2101
+ this.recoveryPath = recoveryPath;
2102
+ }
2103
+ };
2104
+ function fsyncDirectory(path) {
2105
+ let fd = null;
2106
+ try {
2107
+ fd = openSync2(path, "r");
2108
+ fsyncSync2(fd);
2109
+ } catch (error) {
2110
+ if (process.platform !== "win32") throw error;
2111
+ } finally {
2112
+ if (fd !== null) closeSync2(fd);
2113
+ }
2114
+ }
1115
2115
  var restoreCmd = defineCommand4({
1116
2116
  meta: {
1117
2117
  name: "restore",
@@ -1130,37 +2130,91 @@ var restoreCmd = defineCommand4({
1130
2130
  }
1131
2131
  },
1132
2132
  async run({ args }) {
1133
- const file = resolve(args.file);
2133
+ const file = resolve2(args.file);
1134
2134
  if (!existsSync2(file)) throw new Error(`backup file not found: ${file}`);
1135
2135
  const paths = resolveCliPaths(args.home);
1136
- const targetHome = paths.home;
1137
- const defaultHome = !args.home && !process.env.BAZILION_HOME;
1138
- if (defaultHome && !args.force && await probeServerRunning(4321)) {
1139
- throw new Error(
1140
- "bazilion server appears to be running on :4321 \u2014 stop it first (DB corruption risk), or pass --force to override."
1141
- );
2136
+ const targetHome = resolve2(paths.home);
2137
+ if (targetHome === parse(targetHome).root) {
2138
+ throw new Error(`refusing to restore over filesystem root: ${targetHome}`);
1142
2139
  }
1143
- if (existsSync2(targetHome)) {
1144
- const entries = readdirSync(targetHome);
1145
- if (entries.length > 0) {
1146
- if (!args.force) {
2140
+ const targetParent = dirname2(targetHome);
2141
+ mkdirSync2(targetParent, { recursive: true });
2142
+ const restoreLock = await acquireHomeRestoreLock(targetHome);
2143
+ try {
2144
+ const targetsConfiguredHome = targetHome === resolve2(resolveCliPaths().home);
2145
+ if (targetsConfiguredHome) {
2146
+ for (const serverUrl of configuredLoopbackUrls()) {
2147
+ if (await probeServerRunning(serverUrl)) {
2148
+ throw new Error(
2149
+ `bazilion server appears to be running at ${serverUrl} \u2014 stop it before restoring.`
2150
+ );
2151
+ }
2152
+ }
2153
+ }
2154
+ if (existsSync2(targetHome)) {
2155
+ if (!lstatSync2(targetHome).isDirectory()) {
2156
+ throw new Error(`${targetHome} exists and is not a directory`);
2157
+ }
2158
+ const entries = readdirSync(targetHome);
2159
+ if (entries.length > 0 && !args.force) {
1147
2160
  throw new Error(
1148
2161
  `${targetHome} is not empty. Pass --force to overwrite (destroys existing data).`
1149
2162
  );
1150
2163
  }
1151
- rmSync(targetHome, { recursive: true, force: true });
1152
2164
  }
2165
+ const staging = mkdtempSync(resolve2(targetParent, ".bazilion-restore-"));
2166
+ const stagedArchive = resolve2(staging, "backup.tar.gz");
2167
+ const payload = resolve2(staging, "payload");
2168
+ let preserveStaging = false;
2169
+ try {
2170
+ copyFileSync(file, stagedArchive);
2171
+ await validateArchive(stagedArchive);
2172
+ mkdirSync2(payload);
2173
+ await extract({
2174
+ file: stagedArchive,
2175
+ cwd: payload,
2176
+ strict: true,
2177
+ preserveOwner: false,
2178
+ // The private copy has already passed complete entry-path and link
2179
+ // validation above. Preserve paths here so an intentionally absolute
2180
+ // external linked-Team target is not stripped into a different,
2181
+ // payload-relative path by npm-tar.
2182
+ preservePaths: true
2183
+ });
2184
+ const database = resolve2(payload, "bazilion.db");
2185
+ const authFile = resolve2(payload, "auth.json");
2186
+ if (!lstatSync2(database).isFile() || !lstatSync2(authFile).isFile()) {
2187
+ throw new Error("backup required files did not extract as regular files");
2188
+ }
2189
+ const authToken = validateAuthFile(authFile);
2190
+ validateDatabase(database, authToken);
2191
+ rebaseRestoredHomeDirectories(database, payload, targetHome);
2192
+ validateDatabase(database, authToken);
2193
+ removeRebuildableQmdIndexes(payload);
2194
+ console.log(`installing validated backup ${file} \u2192 ${targetHome}`);
2195
+ try {
2196
+ installValidatedPayload(payload, targetHome, staging, restoreLock);
2197
+ } catch (error) {
2198
+ if (error instanceof RestoreRecoveryRequiredError) {
2199
+ preserveStaging = true;
2200
+ }
2201
+ throw error;
2202
+ }
2203
+ } finally {
2204
+ if (!preserveStaging) {
2205
+ try {
2206
+ rmSync2(staging, { recursive: true, force: true });
2207
+ } catch (error) {
2208
+ const detail = error instanceof Error ? error.message : String(error);
2209
+ console.warn(
2210
+ `warning: could not remove restore staging directory ${staging}: ${detail}`
2211
+ );
2212
+ }
2213
+ }
2214
+ }
2215
+ } finally {
2216
+ restoreLock.release();
1153
2217
  }
1154
- mkdirSync(targetHome, { recursive: true });
1155
- console.log(`extracting ${file} \u2192 ${targetHome}`);
1156
- await new Promise((res, rej) => {
1157
- const proc = spawn("tar", ["-xzf", file, "-C", targetHome], { stdio: "inherit" });
1158
- proc.on("error", rej);
1159
- proc.on("exit", (code) => {
1160
- if (code === 0) res();
1161
- else rej(new Error(`tar exited with code ${code}`));
1162
- });
1163
- });
1164
2218
  console.log(`restored to ${targetHome}`);
1165
2219
  console.log("start the server with: bazilion serve (migrations apply automatically)");
1166
2220
  }
@@ -1389,19 +2443,19 @@ var configCommand = defineCommand6({
1389
2443
  });
1390
2444
 
1391
2445
  // src/commands/dashboard.ts
1392
- import { spawn as spawn2 } from "child_process";
2446
+ import { spawn } from "child_process";
1393
2447
  import { existsSync as existsSync3 } from "fs";
1394
2448
  import { connect } from "net";
1395
- import { join as join2 } from "path";
2449
+ import { join as join3 } from "path";
1396
2450
  import { defineCommand as defineCommand7 } from "citty";
1397
- var bundledDaemonEntry = join2(import.meta.dirname, "daemon.js");
1398
- var sourceDaemonEntry = join2(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
2451
+ var bundledDaemonEntry = join3(import.meta.dirname, "daemon.js");
2452
+ var sourceDaemonEntry = join3(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
1399
2453
  var daemonEntry = existsSync3(bundledDaemonEntry) ? bundledDaemonEntry : sourceDaemonEntry;
1400
- var bundledWebServerEntry = join2(import.meta.dirname, "web-server.js");
1401
- var sourceWebServerEntry = join2(import.meta.dirname, "..", "web-server.ts");
2454
+ var bundledWebServerEntry = join3(import.meta.dirname, "web-server.js");
2455
+ var sourceWebServerEntry = join3(import.meta.dirname, "..", "web-server.ts");
1402
2456
  var webServerEntry = existsSync3(bundledWebServerEntry) ? bundledWebServerEntry : sourceWebServerEntry;
1403
- var bundledWebDist = join2(import.meta.dirname, "web");
1404
- var sourceWebDist = join2(import.meta.dirname, "..", "..", "..", "web", "dist");
2457
+ var bundledWebDist = join3(import.meta.dirname, "web");
2458
+ var sourceWebDist = join3(import.meta.dirname, "..", "..", "..", "web", "dist");
1405
2459
  var webDist = existsSync3(bundledWebDist) ? bundledWebDist : sourceWebDist;
1406
2460
  function nodeArgs(entry2) {
1407
2461
  return entry2.endsWith(".ts") ? ["--import", "tsx/esm", entry2] : [entry2];
@@ -1420,12 +2474,12 @@ function probeHost(host) {
1420
2474
  return host;
1421
2475
  }
1422
2476
  function isTcpPortOpen(host, port) {
1423
- return new Promise((resolve3) => {
2477
+ return new Promise((resolve4) => {
1424
2478
  const socket = connect({ host: probeHost(host), port: Number(port) });
1425
2479
  const done = (open) => {
1426
2480
  socket.removeAllListeners();
1427
2481
  socket.destroy();
1428
- resolve3(open);
2482
+ resolve4(open);
1429
2483
  };
1430
2484
  socket.setTimeout(600);
1431
2485
  socket.once("connect", () => done(true));
@@ -1475,14 +2529,14 @@ function openBrowser(url) {
1475
2529
  const platform = process.platform;
1476
2530
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : process.env.BROWSER || "xdg-open";
1477
2531
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1478
- const proc = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2532
+ const proc = spawn(cmd, args, { detached: true, stdio: "ignore" });
1479
2533
  proc.unref();
1480
2534
  }
1481
2535
  function spawnProcess(label, entry2, env) {
1482
2536
  if (!existsSync3(entry2)) {
1483
2537
  throw new Error(`${label} entry not found at ${entry2}`);
1484
2538
  }
1485
- return spawn2(process.execPath, nodeArgs(entry2), {
2539
+ return spawn(process.execPath, nodeArgs(entry2), {
1486
2540
  env,
1487
2541
  stdio: "inherit"
1488
2542
  });
@@ -1564,7 +2618,7 @@ var dashboardCommand = defineCommand7({
1564
2618
  `web UI port ${webPort} is already in use. Re-run with --port <free-port>.`
1565
2619
  );
1566
2620
  }
1567
- if (!existsSync3(join2(webDist, "server", "server.js"))) {
2621
+ if (!existsSync3(join3(webDist, "server", "server.js"))) {
1568
2622
  throw new Error(
1569
2623
  `bazilion web UI bundle not found at ${webDist}. Run "pnpm --filter @bazilion/web build" from a source checkout, or reinstall the published package.`
1570
2624
  );
@@ -1591,13 +2645,13 @@ var dashboardCommand = defineCommand7({
1591
2645
  if (children.length === 0) return;
1592
2646
  process.on("SIGINT", terminateChildren);
1593
2647
  process.on("SIGTERM", terminateChildren);
1594
- await new Promise((resolve3) => {
2648
+ await new Promise((resolve4) => {
1595
2649
  let remaining = children.length;
1596
2650
  for (const child of children) {
1597
2651
  child.on("close", (code) => {
1598
2652
  remaining--;
1599
2653
  if (!signalled && code && code !== 0) process.exitCode = code;
1600
- if (remaining === 0) resolve3();
2654
+ if (remaining === 0) resolve4();
1601
2655
  });
1602
2656
  }
1603
2657
  });
@@ -1688,6 +2742,36 @@ var doctorCommand = defineCommand8({
1688
2742
  ` ${r.scheduler.enabled ? "\u2713" : "-"} scheduler${r.scheduler.enabled ? ` (tick ${r.scheduler.tickMs}ms)` : " (BAZILION_SCHEDULER=off)"}`
1689
2743
  );
1690
2744
  console.log();
2745
+ console.log("agent shell security");
2746
+ if (!r.shellSecurity.ok) {
2747
+ check("valid shell-security configuration", false, r.shellSecurity.error);
2748
+ } else {
2749
+ if (r.shellSecurity.approvalMode === "dangerous") {
2750
+ console.log(" - dangerous-command approval enabled");
2751
+ console.log(
2752
+ " web and TTY CLI turns can allow once; background and non-TTY turns auto-deny"
2753
+ );
2754
+ } else {
2755
+ console.log(" - dangerous-command approval off");
2756
+ console.log(" set BAZILION_BASH_APPROVAL=dangerous to opt in");
2757
+ }
2758
+ if (r.shellSecurity.sandboxMode === "docker") {
2759
+ console.log(` - Docker sandbox configured (${r.shellSecurity.sandboxImage})`);
2760
+ console.log(
2761
+ " workspace-only writable mount \xB7 bounded inputs/skills/memory read-only \xB7 network disabled"
2762
+ );
2763
+ console.log(" host coding tools hidden");
2764
+ console.log(
2765
+ " policy syntax is valid; Docker and image availability are checked on execution"
2766
+ );
2767
+ console.log(" commands fail closed if either is unavailable");
2768
+ } else {
2769
+ console.log(" - sandbox off (agent shell and coding tools run on the host)");
2770
+ console.log(" approval is a host-execution tripwire, not a filesystem sandbox");
2771
+ console.log(" set BAZILION_BASH_SANDBOX=docker to opt in");
2772
+ }
2773
+ }
2774
+ console.log();
1691
2775
  console.log("operational counts");
1692
2776
  console.log(` ${r.triggers.active} active trigger(s), ${r.triggers.disabled} disabled`);
1693
2777
  console.log(` ${r.tokens.active} active web token(s)`);
@@ -1795,7 +2879,7 @@ var inboxCommand = defineCommand9({
1795
2879
  });
1796
2880
 
1797
2881
  // src/commands/login.ts
1798
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2882
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1799
2883
  import { defineCommand as defineCommand10 } from "citty";
1800
2884
  function normalizeServer(raw) {
1801
2885
  let url;
@@ -1834,7 +2918,7 @@ var loginCommand = defineCommand10({
1834
2918
  `${paths.authFile} not found. Start the daemon with "bazilion serve" first \u2014 login stores the remote there.`
1835
2919
  );
1836
2920
  }
1837
- const auth = JSON.parse(readFileSync3(paths.authFile, "utf8"));
2921
+ const auth = JSON.parse(readFileSync5(paths.authFile, "utf8"));
1838
2922
  if (args.clear) {
1839
2923
  if (!auth.remote) {
1840
2924
  console.log("no remote stored \u2014 nothing to clear");
@@ -2082,11 +3166,11 @@ var memoryCommand = defineCommand12({
2082
3166
  });
2083
3167
 
2084
3168
  // src/commands/profile.ts
2085
- import { spawn as spawn3 } from "child_process";
3169
+ import { spawn as spawn2 } from "child_process";
2086
3170
  import { randomBytes } from "crypto";
2087
- import { readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2088
- import { tmpdir } from "os";
2089
- import { join as join3 } from "path";
3171
+ import { readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3172
+ import { tmpdir as tmpdir2 } from "os";
3173
+ import { join as join4 } from "path";
2090
3174
 
2091
3175
  // ../../packages/api-types/src/index.ts
2092
3176
  var PROFILE_FILES = [
@@ -2155,7 +3239,7 @@ var createCmd2 = defineCommand13({
2155
3239
  },
2156
3240
  async run({ args }) {
2157
3241
  const skillsMode = parseSkillsMode(args["skills-mode"]);
2158
- const readTemplate = (path) => path ? readFileSync4(path, "utf8") : void 0;
3242
+ const readTemplate = (path) => path ? readFileSync6(path, "utf8") : void 0;
2159
3243
  const body = {
2160
3244
  id: args.id,
2161
3245
  name: args.name,
@@ -2177,12 +3261,12 @@ var listCmd6 = defineCommand13({
2177
3261
  meta: { name: "list", description: "List profiles" },
2178
3262
  async run() {
2179
3263
  const client = createClient2();
2180
- const list3 = await client.get("/api/profiles");
2181
- if (list3.length === 0) {
3264
+ const list4 = await client.get("/api/profiles");
3265
+ if (list4.length === 0) {
2182
3266
  console.log("(no profiles)");
2183
3267
  return;
2184
3268
  }
2185
- const rows = list3.map((p) => [p.id, p.defaultModel, p.name]);
3269
+ const rows = list4.map((p) => [p.id, p.defaultModel, p.name]);
2186
3270
  for (const line of columnize(rows)) console.log(line);
2187
3271
  }
2188
3272
  });
@@ -2230,17 +3314,17 @@ var editCmd2 = defineCommand13({
2230
3314
  const { content } = await client.get(
2231
3315
  `/api/profiles/${args.id}/files/${file}`
2232
3316
  );
2233
- const tmpPath = join3(tmpdir(), `bazilion-${args.id}-${file}-${randomBytes(4).toString("hex")}`);
3317
+ const tmpPath = join4(tmpdir2(), `bazilion-${args.id}-${file}-${randomBytes(4).toString("hex")}`);
2234
3318
  writeFileSync3(tmpPath, content);
2235
3319
  try {
2236
3320
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
2237
- const code = await new Promise((resolve3, reject) => {
2238
- const child = spawn3(editor, [tmpPath], { stdio: "inherit" });
3321
+ const code = await new Promise((resolve4, reject) => {
3322
+ const child = spawn2(editor, [tmpPath], { stdio: "inherit" });
2239
3323
  child.on("error", reject);
2240
- child.on("close", (c) => resolve3(c ?? 0));
3324
+ child.on("close", (c) => resolve4(c ?? 0));
2241
3325
  });
2242
3326
  if (code !== 0) throw new Error(`editor exited with code ${code}`);
2243
- const updated = readFileSync4(tmpPath, "utf8");
3327
+ const updated = readFileSync6(tmpPath, "utf8");
2244
3328
  if (updated === content) {
2245
3329
  console.log("(no changes)");
2246
3330
  return;
@@ -2250,7 +3334,7 @@ var editCmd2 = defineCommand13({
2250
3334
  console.log(`saved ${file}`);
2251
3335
  } finally {
2252
3336
  try {
2253
- rmSync2(tmpPath, { force: true });
3337
+ rmSync3(tmpPath, { force: true });
2254
3338
  } catch {
2255
3339
  }
2256
3340
  }
@@ -2495,12 +3579,12 @@ var sendCommand = defineCommand15({
2495
3579
  });
2496
3580
 
2497
3581
  // src/commands/serve.ts
2498
- import { spawn as spawn4 } from "child_process";
3582
+ import { spawn as spawn3 } from "child_process";
2499
3583
  import { existsSync as existsSync5 } from "fs";
2500
- import { join as join4 } from "path";
3584
+ import { join as join5 } from "path";
2501
3585
  import { defineCommand as defineCommand16 } from "citty";
2502
- var bundledDaemonEntry2 = join4(import.meta.dirname, "daemon.js");
2503
- var sourceDaemonEntry2 = join4(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
3586
+ var bundledDaemonEntry2 = join5(import.meta.dirname, "daemon.js");
3587
+ var sourceDaemonEntry2 = join5(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
2504
3588
  var daemonEntry2 = existsSync5(bundledDaemonEntry2) ? bundledDaemonEntry2 : sourceDaemonEntry2;
2505
3589
  var serveCommand = defineCommand16({
2506
3590
  meta: {
@@ -2535,7 +3619,7 @@ var serveCommand = defineCommand16({
2535
3619
  console.log(`starting bazilion daemon at http://${host}:${port}`);
2536
3620
  console.log("(web UI runs separately: cd apps/web && pnpm dev)");
2537
3621
  const nodeArgs2 = daemonEntry2.endsWith(".ts") ? ["--import", "tsx/esm", daemonEntry2] : [daemonEntry2];
2538
- const proc = spawn4("node", nodeArgs2, {
3622
+ const proc = spawn3("node", nodeArgs2, {
2539
3623
  env,
2540
3624
  stdio: "inherit"
2541
3625
  });
@@ -2551,17 +3635,17 @@ var serveCommand = defineCommand16({
2551
3635
  signalled = true;
2552
3636
  });
2553
3637
  process.on("SIGTERM", onSigterm);
2554
- await new Promise((resolve3, reject) => {
3638
+ await new Promise((resolve4, reject) => {
2555
3639
  proc.on("error", (err) => reject(err));
2556
3640
  proc.on("close", (code) => {
2557
3641
  if (signalled) {
2558
3642
  process.exitCode = 0;
2559
- resolve3();
3643
+ resolve4();
2560
3644
  } else if (code === 0 || code === null) {
2561
- resolve3();
3645
+ resolve4();
2562
3646
  } else {
2563
3647
  process.exitCode = code;
2564
- resolve3();
3648
+ resolve4();
2565
3649
  }
2566
3650
  });
2567
3651
  });
@@ -2569,8 +3653,8 @@ var serveCommand = defineCommand16({
2569
3653
  });
2570
3654
 
2571
3655
  // src/commands/skill.ts
2572
- import { existsSync as existsSync6, readFileSync as readFileSync5, statSync } from "fs";
2573
- import { basename as basename2, resolve as resolve2 } from "path";
3656
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync } from "fs";
3657
+ import { basename as basename4, resolve as resolve3 } from "path";
2574
3658
  import { defineCommand as defineCommand17 } from "citty";
2575
3659
  function printFindings(findings, prefix = " ") {
2576
3660
  for (const f of findings) {
@@ -2634,13 +3718,13 @@ var importCmd = defineCommand17({
2634
3718
  },
2635
3719
  async run({ args }) {
2636
3720
  const client = createClient2();
2637
- const absFrom = resolve2(args.from);
3721
+ const absFrom = resolve3(args.from);
2638
3722
  const isLocalZip = args.from.toLowerCase().endsWith(".zip") && existsSync6(absFrom) && statSync(absFrom).isFile();
2639
3723
  let result;
2640
3724
  try {
2641
3725
  if (isLocalZip) {
2642
- const bytes = readFileSync5(absFrom);
2643
- const file = new File([bytes], basename2(absFrom), { type: "application/zip" });
3726
+ const bytes = readFileSync7(absFrom);
3727
+ const file = new File([bytes], basename4(absFrom), { type: "application/zip" });
2644
3728
  const fd = new FormData();
2645
3729
  fd.set("file", file);
2646
3730
  if (args.force) fd.set("force", "true");
@@ -2709,12 +3793,12 @@ var skillCommand = defineCommand17({
2709
3793
  });
2710
3794
 
2711
3795
  // src/commands/team.ts
2712
- import { readFileSync as readFileSync7 } from "fs";
3796
+ import { readFileSync as readFileSync9 } from "fs";
2713
3797
  import { stdin as stdin2 } from "process";
2714
3798
  import { defineCommand as defineCommand19 } from "citty";
2715
3799
 
2716
3800
  // src/commands/team-policy.ts
2717
- import { readFileSync as readFileSync6 } from "fs";
3801
+ import { readFileSync as readFileSync8 } from "fs";
2718
3802
  import { defineCommand as defineCommand18 } from "citty";
2719
3803
 
2720
3804
  // src/team-interchange.ts
@@ -2931,7 +4015,7 @@ function edgeDiff(before, after) {
2931
4015
  // src/commands/team-policy.ts
2932
4016
  function readJson(path) {
2933
4017
  try {
2934
- return JSON.parse(readFileSync6(path, "utf8"));
4018
+ return JSON.parse(readFileSync8(path, "utf8"));
2935
4019
  } catch (error) {
2936
4020
  throw new Error(`invalid JSON: ${error.message}`);
2937
4021
  }
@@ -3201,12 +4285,12 @@ var listCmd9 = defineCommand19({
3201
4285
  meta: { name: "list", description: "List teams" },
3202
4286
  async run() {
3203
4287
  const client = createClient2();
3204
- const list3 = await client.get("/api/teams");
3205
- if (list3.length === 0) {
4288
+ const list4 = await client.get("/api/teams");
4289
+ if (list4.length === 0) {
3206
4290
  console.log("(no teams)");
3207
4291
  return;
3208
4292
  }
3209
- const rows = list3.map((g) => [g.id, g.path]);
4293
+ const rows = list4.map((g) => [g.id, g.path]);
3210
4294
  for (const line of columnize(rows)) console.log(line);
3211
4295
  }
3212
4296
  });
@@ -3255,7 +4339,7 @@ var userMdSetCmd = defineCommand19({
3255
4339
  async run({ args }) {
3256
4340
  let content;
3257
4341
  if (args["from-stdin"]) content = await readStdin();
3258
- else if (args.file) content = readFileSync7(args.file, "utf8");
4342
+ else if (args.file) content = readFileSync9(args.file, "utf8");
3259
4343
  else if (args.text !== void 0) content = args.text;
3260
4344
  else {
3261
4345
  console.error("team user-md set: provide --file, --from-stdin, or --text");
@@ -3348,11 +4432,11 @@ var teamCommand = defineCommand19({
3348
4432
  });
3349
4433
 
3350
4434
  // src/commands/team-template.ts
3351
- import { readFileSync as readFileSync8 } from "fs";
4435
+ import { readFileSync as readFileSync10 } from "fs";
3352
4436
  import { defineCommand as defineCommand20 } from "citty";
3353
4437
  function jsonFile(path) {
3354
4438
  try {
3355
- return JSON.parse(readFileSync8(path, "utf8"));
4439
+ return JSON.parse(readFileSync10(path, "utf8"));
3356
4440
  } catch (error) {
3357
4441
  throw new Error(`invalid JSON: ${error.message}`);
3358
4442
  }
@@ -3377,7 +4461,7 @@ function printTeam(detail) {
3377
4461
  console.log(line);
3378
4462
  }
3379
4463
  }
3380
- var list2 = defineCommand20({
4464
+ var list3 = defineCommand20({
3381
4465
  meta: { name: "list", description: "List canonical Team templates" },
3382
4466
  args: { json: { type: "boolean", description: "Emit stable JSON" } },
3383
4467
  async run({ args }) {
@@ -3531,7 +4615,7 @@ var teamTemplateCommand = defineCommand20({
3531
4615
  name: "team-template",
3532
4616
  description: "Inspect and exchange canonical Team templates"
3533
4617
  },
3534
- subCommands: { list: list2, show: show3, export: exportCommand2, import: importCommand2 }
4618
+ subCommands: { list: list3, show: show3, export: exportCommand2, import: importCommand2 }
3535
4619
  });
3536
4620
 
3537
4621
  // src/commands/telegram.ts
@@ -4024,6 +5108,34 @@ var disableCmd3 = defineCommand23({
4024
5108
  console.log(`disabled trigger ${args.id}`);
4025
5109
  }
4026
5110
  });
5111
+ var historyCmd = defineCommand23({
5112
+ meta: { name: "history", description: "Show recent dispatches for a trigger" },
5113
+ args: {
5114
+ id: { type: "positional", required: true },
5115
+ limit: { type: "string", description: "Maximum rows (default 20)" }
5116
+ },
5117
+ async run({ args }) {
5118
+ const limit = args.limit ? Number(args.limit) : 20;
5119
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
5120
+ throw new Error("--limit must be an integer from 1 to 100");
5121
+ }
5122
+ const client = createClient2();
5123
+ const { dispatches } = await client.get(
5124
+ `/api/triggers/${args.id}/dispatches?limit=${limit}`
5125
+ );
5126
+ if (dispatches.length === 0) {
5127
+ console.log("(no dispatches)");
5128
+ return;
5129
+ }
5130
+ const rows = dispatches.map((dispatch) => [
5131
+ new Date(dispatch.scheduledAt).toISOString(),
5132
+ dispatch.status,
5133
+ `attempts: ${dispatch.attemptCount}`,
5134
+ dispatch.lastError ?? ""
5135
+ ]);
5136
+ for (const line of columnize(rows)) console.log(line);
5137
+ }
5138
+ });
4027
5139
  var triggerCommand = defineCommand23({
4028
5140
  meta: { name: "trigger", description: "Manage scheduled agent triggers" },
4029
5141
  subCommands: {
@@ -4031,13 +5143,14 @@ var triggerCommand = defineCommand23({
4031
5143
  list: listCmd11,
4032
5144
  rm: rmCmd6,
4033
5145
  enable: enableCmd3,
4034
- disable: disableCmd3
5146
+ disable: disableCmd3,
5147
+ history: historyCmd
4035
5148
  }
4036
5149
  });
4037
5150
 
4038
5151
  // src/commands/uninstall.ts
4039
- import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
4040
- import { join as join5 } from "path";
5152
+ import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as rmSync4 } from "fs";
5153
+ import { join as join6 } from "path";
4041
5154
  import { defineCommand as defineCommand24 } from "citty";
4042
5155
  function makeLineReader() {
4043
5156
  let buffer = "";
@@ -4048,8 +5161,8 @@ function makeLineReader() {
4048
5161
  const nlIdx = buffer.indexOf("\n");
4049
5162
  if (nlIdx === -1) {
4050
5163
  if (ended) {
4051
- const resolve3 = queue.shift();
4052
- resolve3?.(buffer);
5164
+ const resolve4 = queue.shift();
5165
+ resolve4?.(buffer);
4053
5166
  buffer = "";
4054
5167
  } else {
4055
5168
  return;
@@ -4057,8 +5170,8 @@ function makeLineReader() {
4057
5170
  } else {
4058
5171
  const line = buffer.slice(0, nlIdx);
4059
5172
  buffer = buffer.slice(nlIdx + 1);
4060
- const resolve3 = queue.shift();
4061
- resolve3?.(line);
5173
+ const resolve4 = queue.shift();
5174
+ resolve4?.(line);
4062
5175
  }
4063
5176
  }
4064
5177
  };
@@ -4071,8 +5184,8 @@ function makeLineReader() {
4071
5184
  ended = true;
4072
5185
  tryDeliver();
4073
5186
  });
4074
- return () => new Promise((resolve3) => {
4075
- queue.push(resolve3);
5187
+ return () => new Promise((resolve4) => {
5188
+ queue.push(resolve4);
4076
5189
  tryDeliver();
4077
5190
  });
4078
5191
  }
@@ -4083,7 +5196,7 @@ async function askYes(readLine, prompt) {
4083
5196
  }
4084
5197
  function removePath(p) {
4085
5198
  if (!existsSync7(p)) return false;
4086
- rmSync3(p, { recursive: true, force: true });
5199
+ rmSync4(p, { recursive: true, force: true });
4087
5200
  return true;
4088
5201
  }
4089
5202
  var uninstallCommand = defineCommand24({
@@ -4104,12 +5217,12 @@ var uninstallCommand = defineCommand24({
4104
5217
  },
4105
5218
  async run({ args }) {
4106
5219
  const paths = resolveCliPaths(args.home);
4107
- const dbFile = join5(paths.home, "bazilion.db");
4108
- const profilesDir = join5(paths.home, "profiles");
4109
- const agentsDir = join5(paths.home, "agents");
4110
- const teamsDir = join5(paths.home, "teams");
4111
- const skillsDir = join5(paths.home, "skills");
4112
- const logsDir = join5(paths.home, "logs");
5220
+ const dbFile = join6(paths.home, "bazilion.db");
5221
+ const profilesDir = join6(paths.home, "profiles");
5222
+ const agentsDir = join6(paths.home, "agents");
5223
+ const teamsDir = join6(paths.home, "teams");
5224
+ const skillsDir = join6(paths.home, "skills");
5225
+ const logsDir = join6(paths.home, "logs");
4113
5226
  if (!existsSync7(paths.home)) {
4114
5227
  console.log(`nothing to remove: ${paths.home} does not exist`);
4115
5228
  return;
@@ -4142,7 +5255,7 @@ var uninstallCommand = defineCommand24({
4142
5255
  if (removePath(p)) console.log(`removed ${p}`);
4143
5256
  }
4144
5257
  if (readdirSync2(paths.home).length === 0) {
4145
- rmSync3(paths.home, { recursive: true, force: true });
5258
+ rmSync4(paths.home, { recursive: true, force: true });
4146
5259
  console.log(`removed ${paths.home}`);
4147
5260
  } else {
4148
5261
  console.log(`${paths.home} still has unmanaged files; left in place`);