@hasna/machines 0.0.44 → 0.0.46

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 (109) hide show
  1. package/README.md +27 -4
  2. package/dist/agent/index.d.ts +0 -1
  3. package/dist/agent/index.js +249 -14
  4. package/dist/agent/runtime.d.ts +0 -1
  5. package/dist/cli/index.d.ts +0 -1
  6. package/dist/cli/index.js +1316 -213
  7. package/dist/cli-utils.d.ts +0 -1
  8. package/dist/commands/apps.d.ts +7 -5
  9. package/dist/commands/backup.d.ts +0 -1
  10. package/dist/commands/cert.d.ts +0 -1
  11. package/dist/commands/clipboard-daemon.d.ts +0 -1
  12. package/dist/commands/clipboard-server.d.ts +0 -1
  13. package/dist/commands/clipboard.d.ts +0 -1
  14. package/dist/commands/daemon.d.ts +0 -1
  15. package/dist/commands/diff.d.ts +0 -1
  16. package/dist/commands/dns.d.ts +0 -1
  17. package/dist/commands/doctor.d.ts +0 -1
  18. package/dist/commands/heal-daemon.d.ts +0 -1
  19. package/dist/commands/heal.d.ts +0 -1
  20. package/dist/commands/install-claude.d.ts +5 -3
  21. package/dist/commands/install-tailscale.d.ts +5 -3
  22. package/dist/commands/manifest.d.ts +0 -1
  23. package/dist/commands/mutation-approval.d.ts +54 -0
  24. package/dist/commands/notifications.d.ts +14 -2
  25. package/dist/commands/ports.d.ts +0 -1
  26. package/dist/commands/runtime.d.ts +15 -1
  27. package/dist/commands/screen.d.ts +4 -1
  28. package/dist/commands/self-test.d.ts +0 -1
  29. package/dist/commands/serve.d.ts +0 -1
  30. package/dist/commands/setup.d.ts +5 -3
  31. package/dist/commands/ssh.d.ts +8 -1
  32. package/dist/commands/status.d.ts +0 -1
  33. package/dist/commands/sync.d.ts +5 -3
  34. package/dist/commands/workspace.d.ts +0 -1
  35. package/dist/compatibility.d.ts +0 -1
  36. package/dist/consumer-schema.d.ts +0 -1
  37. package/dist/consumer.d.ts +0 -1
  38. package/dist/consumer.js +253 -12
  39. package/dist/cross-project-types.d.ts +0 -1
  40. package/dist/db.d.ts +0 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +1108 -189
  43. package/dist/manifests.d.ts +0 -1
  44. package/dist/mcp/http.d.ts +26 -2
  45. package/dist/mcp/index.d.ts +0 -1
  46. package/dist/mcp/index.js +1021 -167
  47. package/dist/mcp/server.d.ts +5 -3
  48. package/dist/paths.d.ts +0 -1
  49. package/dist/pg-migrations.d.ts +0 -1
  50. package/dist/redaction.d.ts +0 -1
  51. package/dist/remote-storage.d.ts +0 -1
  52. package/dist/remote.d.ts +14 -5
  53. package/dist/storage-sync.d.ts +0 -1
  54. package/dist/storage.d.ts +0 -1
  55. package/dist/storage.js +18 -0
  56. package/dist/topology.d.ts +0 -1
  57. package/dist/types.d.ts +3 -1
  58. package/dist/version.d.ts +0 -1
  59. package/package.json +5 -3
  60. package/dist/agent/index.d.ts.map +0 -1
  61. package/dist/agent/runtime.d.ts.map +0 -1
  62. package/dist/cli/index.d.ts.map +0 -1
  63. package/dist/cli-utils.d.ts.map +0 -1
  64. package/dist/commands/apps.d.ts.map +0 -1
  65. package/dist/commands/backup.d.ts.map +0 -1
  66. package/dist/commands/cert.d.ts.map +0 -1
  67. package/dist/commands/clipboard-daemon.d.ts.map +0 -1
  68. package/dist/commands/clipboard-server.d.ts.map +0 -1
  69. package/dist/commands/clipboard.d.ts.map +0 -1
  70. package/dist/commands/daemon.d.ts.map +0 -1
  71. package/dist/commands/diff.d.ts.map +0 -1
  72. package/dist/commands/dns.d.ts.map +0 -1
  73. package/dist/commands/doctor.d.ts.map +0 -1
  74. package/dist/commands/heal-daemon.d.ts.map +0 -1
  75. package/dist/commands/heal.d.ts.map +0 -1
  76. package/dist/commands/install-claude.d.ts.map +0 -1
  77. package/dist/commands/install-tailscale.d.ts.map +0 -1
  78. package/dist/commands/manifest.d.ts.map +0 -1
  79. package/dist/commands/notifications.d.ts.map +0 -1
  80. package/dist/commands/ports.d.ts.map +0 -1
  81. package/dist/commands/runtime.d.ts.map +0 -1
  82. package/dist/commands/screen.d.ts.map +0 -1
  83. package/dist/commands/self-test.d.ts.map +0 -1
  84. package/dist/commands/serve.d.ts.map +0 -1
  85. package/dist/commands/setup.d.ts.map +0 -1
  86. package/dist/commands/ssh.d.ts.map +0 -1
  87. package/dist/commands/status.d.ts.map +0 -1
  88. package/dist/commands/sync.d.ts.map +0 -1
  89. package/dist/commands/workspace.d.ts.map +0 -1
  90. package/dist/compatibility.d.ts.map +0 -1
  91. package/dist/consumer-schema.d.ts.map +0 -1
  92. package/dist/consumer.d.ts.map +0 -1
  93. package/dist/cross-project-types.d.ts.map +0 -1
  94. package/dist/db.d.ts.map +0 -1
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/manifests.d.ts.map +0 -1
  97. package/dist/mcp/http.d.ts.map +0 -1
  98. package/dist/mcp/index.d.ts.map +0 -1
  99. package/dist/mcp/server.d.ts.map +0 -1
  100. package/dist/paths.d.ts.map +0 -1
  101. package/dist/pg-migrations.d.ts.map +0 -1
  102. package/dist/redaction.d.ts.map +0 -1
  103. package/dist/remote-storage.d.ts.map +0 -1
  104. package/dist/remote.d.ts.map +0 -1
  105. package/dist/storage-sync.d.ts.map +0 -1
  106. package/dist/storage.d.ts.map +0 -1
  107. package/dist/topology.d.ts.map +0 -1
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/version.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -422,11 +422,11 @@ var require_codegen = __commonJS((exports) => {
422
422
  const rhs = this.rhs === undefined ? "" : ` = ${this.rhs}`;
423
423
  return `${varKind} ${this.name}${rhs};` + _n;
424
424
  }
425
- optimizeNames(names, constants) {
425
+ optimizeNames(names, constants2) {
426
426
  if (!names[this.name.str])
427
427
  return;
428
428
  if (this.rhs)
429
- this.rhs = optimizeExpr(this.rhs, names, constants);
429
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
430
430
  return this;
431
431
  }
432
432
  get names() {
@@ -444,10 +444,10 @@ var require_codegen = __commonJS((exports) => {
444
444
  render({ _n }) {
445
445
  return `${this.lhs} = ${this.rhs};` + _n;
446
446
  }
447
- optimizeNames(names, constants) {
447
+ optimizeNames(names, constants2) {
448
448
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
449
449
  return;
450
- this.rhs = optimizeExpr(this.rhs, names, constants);
450
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
451
451
  return this;
452
452
  }
453
453
  get names() {
@@ -513,8 +513,8 @@ var require_codegen = __commonJS((exports) => {
513
513
  optimizeNodes() {
514
514
  return `${this.code}` ? this : undefined;
515
515
  }
516
- optimizeNames(names, constants) {
517
- this.code = optimizeExpr(this.code, names, constants);
516
+ optimizeNames(names, constants2) {
517
+ this.code = optimizeExpr(this.code, names, constants2);
518
518
  return this;
519
519
  }
520
520
  get names() {
@@ -544,12 +544,12 @@ var require_codegen = __commonJS((exports) => {
544
544
  }
545
545
  return nodes.length > 0 ? this : undefined;
546
546
  }
547
- optimizeNames(names, constants) {
547
+ optimizeNames(names, constants2) {
548
548
  const { nodes } = this;
549
549
  let i = nodes.length;
550
550
  while (i--) {
551
551
  const n = nodes[i];
552
- if (n.optimizeNames(names, constants))
552
+ if (n.optimizeNames(names, constants2))
553
553
  continue;
554
554
  subtractNames(names, n.names);
555
555
  nodes.splice(i, 1);
@@ -606,12 +606,12 @@ var require_codegen = __commonJS((exports) => {
606
606
  return;
607
607
  return this;
608
608
  }
609
- optimizeNames(names, constants) {
609
+ optimizeNames(names, constants2) {
610
610
  var _a;
611
- this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants);
612
- if (!(super.optimizeNames(names, constants) || this.else))
611
+ this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants2);
612
+ if (!(super.optimizeNames(names, constants2) || this.else))
613
613
  return;
614
- this.condition = optimizeExpr(this.condition, names, constants);
614
+ this.condition = optimizeExpr(this.condition, names, constants2);
615
615
  return this;
616
616
  }
617
617
  get names() {
@@ -636,10 +636,10 @@ var require_codegen = __commonJS((exports) => {
636
636
  render(opts) {
637
637
  return `for(${this.iteration})` + super.render(opts);
638
638
  }
639
- optimizeNames(names, constants) {
640
- if (!super.optimizeNames(names, constants))
639
+ optimizeNames(names, constants2) {
640
+ if (!super.optimizeNames(names, constants2))
641
641
  return;
642
- this.iteration = optimizeExpr(this.iteration, names, constants);
642
+ this.iteration = optimizeExpr(this.iteration, names, constants2);
643
643
  return this;
644
644
  }
645
645
  get names() {
@@ -677,10 +677,10 @@ var require_codegen = __commonJS((exports) => {
677
677
  render(opts) {
678
678
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
679
679
  }
680
- optimizeNames(names, constants) {
681
- if (!super.optimizeNames(names, constants))
680
+ optimizeNames(names, constants2) {
681
+ if (!super.optimizeNames(names, constants2))
682
682
  return;
683
- this.iterable = optimizeExpr(this.iterable, names, constants);
683
+ this.iterable = optimizeExpr(this.iterable, names, constants2);
684
684
  return this;
685
685
  }
686
686
  get names() {
@@ -725,11 +725,11 @@ var require_codegen = __commonJS((exports) => {
725
725
  (_b = this.finally) === null || _b === undefined || _b.optimizeNodes();
726
726
  return this;
727
727
  }
728
- optimizeNames(names, constants) {
728
+ optimizeNames(names, constants2) {
729
729
  var _a, _b;
730
- super.optimizeNames(names, constants);
731
- (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants);
732
- (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants);
730
+ super.optimizeNames(names, constants2);
731
+ (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants2);
732
+ (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants2);
733
733
  return this;
734
734
  }
735
735
  get names() {
@@ -1003,7 +1003,7 @@ var require_codegen = __commonJS((exports) => {
1003
1003
  function addExprNames(names, from) {
1004
1004
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1005
1005
  }
1006
- function optimizeExpr(expr, names, constants) {
1006
+ function optimizeExpr(expr, names, constants2) {
1007
1007
  if (expr instanceof code_1.Name)
1008
1008
  return replaceName(expr);
1009
1009
  if (!canOptimize(expr))
@@ -1018,14 +1018,14 @@ var require_codegen = __commonJS((exports) => {
1018
1018
  return items;
1019
1019
  }, []));
1020
1020
  function replaceName(n) {
1021
- const c = constants[n.str];
1021
+ const c = constants2[n.str];
1022
1022
  if (c === undefined || names[n.str] !== 1)
1023
1023
  return n;
1024
1024
  delete names[n.str];
1025
1025
  return c;
1026
1026
  }
1027
1027
  function canOptimize(e) {
1028
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== undefined);
1028
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== undefined);
1029
1029
  }
1030
1030
  }
1031
1031
  function subtractNames(names, from) {
@@ -2930,7 +2930,7 @@ var require_compile = __commonJS((exports) => {
2930
2930
  const schOrFunc = root.refs[ref];
2931
2931
  if (schOrFunc)
2932
2932
  return schOrFunc;
2933
- let _sch = resolve2.call(this, root, ref);
2933
+ let _sch = resolve4.call(this, root, ref);
2934
2934
  if (_sch === undefined) {
2935
2935
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
2936
2936
  const { schemaId } = this.opts;
@@ -2957,7 +2957,7 @@ var require_compile = __commonJS((exports) => {
2957
2957
  function sameSchemaEnv(s1, s2) {
2958
2958
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
2959
2959
  }
2960
- function resolve2(root, ref) {
2960
+ function resolve4(root, ref) {
2961
2961
  let sch;
2962
2962
  while (typeof (sch = this.refs[ref]) == "string")
2963
2963
  ref = sch;
@@ -3487,7 +3487,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3487
3487
  }
3488
3488
  return uri;
3489
3489
  }
3490
- function resolve2(baseURI, relativeURI, options) {
3490
+ function resolve4(baseURI, relativeURI, options) {
3491
3491
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3492
3492
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
3493
3493
  schemelessOptions.skipEscape = true;
@@ -3715,7 +3715,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3715
3715
  var fastUri = {
3716
3716
  SCHEMES,
3717
3717
  normalize,
3718
- resolve: resolve2,
3718
+ resolve: resolve4,
3719
3719
  resolveComponent,
3720
3720
  equal,
3721
3721
  serialize,
@@ -6563,6 +6563,7 @@ class SqliteAdapter {
6563
6563
  raw;
6564
6564
  constructor(path) {
6565
6565
  this.raw = new Database(path);
6566
+ this.raw.exec("PRAGMA busy_timeout = 5000");
6566
6567
  }
6567
6568
  close() {
6568
6569
  this.raw.close();
@@ -6628,6 +6629,23 @@ function createTables(db) {
6628
6629
  updated_at TEXT NOT NULL
6629
6630
  )
6630
6631
  `);
6632
+ db.exec(`
6633
+ CREATE TABLE IF NOT EXISTS mutation_approval_nonces (
6634
+ nonce_sha256 TEXT PRIMARY KEY,
6635
+ token_sha256 TEXT NOT NULL,
6636
+ surface TEXT NOT NULL,
6637
+ operation TEXT NOT NULL,
6638
+ caller_id TEXT NOT NULL,
6639
+ run_id TEXT NOT NULL,
6640
+ transport TEXT NOT NULL,
6641
+ expires_at INTEGER NOT NULL,
6642
+ used_at INTEGER NOT NULL
6643
+ )
6644
+ `);
6645
+ db.exec(`
6646
+ CREATE INDEX IF NOT EXISTS mutation_approval_nonces_expires_at_idx
6647
+ ON mutation_approval_nonces (expires_at)
6648
+ `);
6631
6649
  }
6632
6650
  function migrateAgentHeartbeats(db) {
6633
6651
  const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
@@ -12599,12 +12617,24 @@ function getLocalMachineTopology(options = {}) {
12599
12617
  }
12600
12618
  // src/remote.ts
12601
12619
  import { spawnSync as spawnSync2 } from "child_process";
12602
- import { hostname as hostname4 } from "os";
12620
+ import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync3, rmSync } from "fs";
12621
+ import { hostname as hostname4, tmpdir } from "os";
12622
+ import { join as join3 } from "path";
12603
12623
 
12604
12624
  // src/commands/ssh.ts
12605
12625
  function shellQuote2(value) {
12606
12626
  return `'${value.replace(/'/g, "'\\''")}'`;
12607
12627
  }
12628
+ function validateSshTarget(target) {
12629
+ const trimmed = target.trim();
12630
+ if (!trimmed || trimmed.startsWith("-") || /[\s"'`$\\;&|<>()[\]{}]/.test(trimmed)) {
12631
+ throw new Error(`Unsafe SSH target: ${target}`);
12632
+ }
12633
+ if (!/^(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9._:-]+$/.test(trimmed)) {
12634
+ throw new Error(`Unsafe SSH target: ${target}`);
12635
+ }
12636
+ return trimmed;
12637
+ }
12608
12638
  function resolveSshTarget(machineId, options = {}) {
12609
12639
  const resolved = resolveMachineRoute(machineId, options);
12610
12640
  if (!resolved.ok || !resolved.target) {
@@ -12615,15 +12645,28 @@ function resolveSshTarget(machineId, options = {}) {
12615
12645
  }
12616
12646
  return {
12617
12647
  machineId: resolved.machine_id ?? machineId,
12618
- target: resolved.command_target ?? resolved.target,
12648
+ target: validateSshTarget(resolved.command_target ?? resolved.target),
12619
12649
  route: resolved.route,
12620
12650
  confidence: resolved.confidence,
12621
12651
  warnings: resolved.warnings
12622
12652
  };
12623
12653
  }
12624
12654
  function buildSshCommand(machineId, remoteCommand, options = {}) {
12655
+ return buildSshCommandPlan(machineId, remoteCommand, options).shellCommand;
12656
+ }
12657
+ function buildSshCommandArgs(machineId, remoteCommand, options = {}) {
12658
+ return buildSshCommandPlan(machineId, remoteCommand, options).args;
12659
+ }
12660
+ function buildSshCommandPlan(machineId, remoteCommand, options = {}) {
12625
12661
  const resolved = resolveSshTarget(machineId, options);
12626
- return remoteCommand ? `ssh ${resolved.target} ${shellQuote2(remoteCommand)}` : `ssh ${resolved.target}`;
12662
+ const args = remoteCommand ? [resolved.target, remoteCommand] : [resolved.target];
12663
+ const shellCommand2 = `ssh ${args.map(shellQuote2).join(" ")}`;
12664
+ return {
12665
+ ...resolved,
12666
+ command: "ssh",
12667
+ args,
12668
+ shellCommand: shellCommand2
12669
+ };
12627
12670
  }
12628
12671
 
12629
12672
  // src/remote.ts
@@ -12635,35 +12678,233 @@ function machineIsLocal(machineId, localMachineId) {
12635
12678
  }
12636
12679
  function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
12637
12680
  if (machineIsLocal(machineId, localMachineId)) {
12638
- return { source: "local", shellCommand: command };
12681
+ return { source: "local", command: "bash", args: ["-c", command], shellCommand: command, usesShell: true };
12639
12682
  }
12640
12683
  try {
12684
+ const plan = buildSshCommandPlan(machineId, command);
12641
12685
  return {
12642
- source: resolveSshTarget(machineId).route,
12643
- shellCommand: buildSshCommand(machineId, command)
12686
+ source: plan.route,
12687
+ command: plan.command,
12688
+ args: plan.args,
12689
+ shellCommand: plan.shellCommand,
12690
+ usesShell: false
12644
12691
  };
12645
12692
  } catch (error) {
12646
12693
  const message = String(error.message ?? error);
12647
12694
  if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
12648
- return { source: "ssh", shellCommand: `ssh ${shellQuote3(machineId)} ${shellQuote3(command)}` };
12695
+ const target = validateSshTarget(machineId);
12696
+ return {
12697
+ source: "ssh",
12698
+ command: "ssh",
12699
+ args: [target, command],
12700
+ shellCommand: `ssh ${shellQuote3(target)} ${shellQuote3(command)}`,
12701
+ usesShell: false
12702
+ };
12649
12703
  }
12650
12704
  throw error;
12651
12705
  }
12652
12706
  }
12653
- function runMachineCommand(machineId, command) {
12707
+ function runMachineCommand(machineId, command, options = {}) {
12654
12708
  const resolved = resolveMachineCommand(machineId, command);
12655
- const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
12709
+ if (options.timeoutMs && options.timeoutMs > 0 && process.platform !== "win32") {
12710
+ return runMachineCommandWithProcessGroupTimeout(machineId, resolved, options);
12711
+ }
12712
+ const result = spawnSync2(resolved.command, resolved.args, {
12656
12713
  encoding: "utf8",
12657
- env: process.env
12714
+ env: process.env,
12715
+ timeout: options.timeoutMs,
12716
+ killSignal: "SIGTERM"
12658
12717
  });
12718
+ const timedOut = Boolean(result.error && "code" in result.error && result.error.code === "ETIMEDOUT");
12719
+ const timeoutMessage = timedOut ? `Command timed out after ${options.timeoutMs}ms.` : "";
12720
+ const stderr = [result.stderr || "", timeoutMessage].filter(Boolean).join(result.stderr ? `
12721
+ ` : "");
12659
12722
  return {
12660
12723
  machineId,
12661
12724
  source: resolved.source,
12662
12725
  stdout: result.stdout || "",
12663
- stderr: result.stderr || "",
12664
- exitCode: result.status ?? 1
12665
- };
12726
+ stderr,
12727
+ exitCode: timedOut ? 124 : result.status ?? 1,
12728
+ timedOut,
12729
+ signal: result.signal
12730
+ };
12731
+ }
12732
+ function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
12733
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
12734
+ const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
12735
+ const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
12736
+ const pgidFile = join3(helperDir, "pgid");
12737
+ const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
12738
+ input: JSON.stringify({ command: resolved.command, args: resolved.args }),
12739
+ encoding: "utf8",
12740
+ env: {
12741
+ ...process.env,
12742
+ HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
12743
+ HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
12744
+ HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
12745
+ },
12746
+ timeout: timeoutMs + killGraceMs + 2000,
12747
+ killSignal: "SIGKILL",
12748
+ maxBuffer: 64 * 1024 * 1024
12749
+ });
12750
+ try {
12751
+ const parsed = parseHelperResult(helper.stdout);
12752
+ if (parsed) {
12753
+ return {
12754
+ machineId,
12755
+ source: resolved.source,
12756
+ stdout: parsed.stdout,
12757
+ stderr: parsed.stderr,
12758
+ exitCode: parsed.exitCode,
12759
+ timedOut: parsed.timedOut,
12760
+ signal: parsed.signal
12761
+ };
12762
+ }
12763
+ const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
12764
+ if (helperTimedOut)
12765
+ killPublishedProcessGroup(pgidFile);
12766
+ const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
12767
+ const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
12768
+ ` : "");
12769
+ return {
12770
+ machineId,
12771
+ source: resolved.source,
12772
+ stdout: "",
12773
+ stderr,
12774
+ exitCode: helperTimedOut ? 124 : helper.status ?? 1,
12775
+ timedOut: helperTimedOut,
12776
+ signal: helper.signal
12777
+ };
12778
+ } finally {
12779
+ rmSync(helperDir, { recursive: true, force: true });
12780
+ }
12781
+ }
12782
+ function killPublishedProcessGroup(pgidFile) {
12783
+ if (!existsSync5(pgidFile))
12784
+ return;
12785
+ try {
12786
+ const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
12787
+ if (!Number.isInteger(pid) || pid <= 1)
12788
+ return;
12789
+ process.kill(-pid, "SIGKILL");
12790
+ } catch {}
12791
+ }
12792
+ function parseHelperResult(stdout) {
12793
+ if (!stdout)
12794
+ return null;
12795
+ try {
12796
+ const parsed = JSON.parse(stdout);
12797
+ if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
12798
+ return null;
12799
+ return {
12800
+ machineId: "",
12801
+ source: "local",
12802
+ stdout: parsed.stdout,
12803
+ stderr: parsed.stderr,
12804
+ exitCode: parsed.exitCode,
12805
+ timedOut: parsed.timedOut === true,
12806
+ signal: typeof parsed.signal === "string" ? parsed.signal : null
12807
+ };
12808
+ } catch {
12809
+ return null;
12810
+ }
12666
12811
  }
12812
+ var PROCESS_GROUP_TIMEOUT_HELPER = `
12813
+ const { spawn } = require("node:child_process");
12814
+ const { readFileSync, writeFileSync } = require("node:fs");
12815
+
12816
+ const plan = JSON.parse(readFileSync(0, "utf8"));
12817
+ const command = String(plan.command || "");
12818
+ const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
12819
+ const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
12820
+ const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
12821
+ const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
12822
+ let stdout = "";
12823
+ let stderr = "";
12824
+ let timedOut = false;
12825
+ let finished = false;
12826
+ let timeoutTimer;
12827
+ let killTimer;
12828
+ let sigkillSent = false;
12829
+ let pendingExit = null;
12830
+
12831
+ const child = spawn(command, args, {
12832
+ detached: true,
12833
+ stdio: ["ignore", "pipe", "pipe"],
12834
+ env: process.env,
12835
+ });
12836
+
12837
+ if (pgidFile && child.pid) {
12838
+ try {
12839
+ writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
12840
+ } catch {}
12841
+ }
12842
+
12843
+ function appendText(target, chunk) {
12844
+ return target + String(chunk);
12845
+ }
12846
+
12847
+ function killTarget(signal) {
12848
+ if (!child.pid) return;
12849
+ if (process.platform === "win32") {
12850
+ try {
12851
+ process.kill(child.pid, signal);
12852
+ } catch {}
12853
+ return;
12854
+ }
12855
+ try {
12856
+ process.kill(-child.pid, signal);
12857
+ } catch {}
12858
+ }
12859
+
12860
+ function finish(code, signal) {
12861
+ if (finished) return;
12862
+ if (timedOut && !sigkillSent) {
12863
+ pendingExit = { code, signal };
12864
+ return;
12865
+ }
12866
+ finished = true;
12867
+ if (timeoutTimer) clearTimeout(timeoutTimer);
12868
+ if (killTimer) clearTimeout(killTimer);
12869
+ if (timedOut) {
12870
+ stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
12871
+ }
12872
+ const exitCode = timedOut ? 124 : code ?? 1;
12873
+ process.stdout.write(JSON.stringify({
12874
+ stdout,
12875
+ stderr,
12876
+ exitCode,
12877
+ timedOut,
12878
+ signal: signal ?? null,
12879
+ }), () => process.exit(exitCode));
12880
+ }
12881
+
12882
+ child.stdout.setEncoding("utf8");
12883
+ child.stderr.setEncoding("utf8");
12884
+ child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
12885
+ child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
12886
+ let childExit = { code: null, signal: null };
12887
+ child.on("error", (error) => {
12888
+ stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
12889
+ finish(1, null);
12890
+ });
12891
+ child.on("exit", (code, signal) => {
12892
+ childExit = { code, signal };
12893
+ });
12894
+ child.on("close", (code, signal) => {
12895
+ finish(code ?? childExit.code, signal ?? childExit.signal);
12896
+ });
12897
+
12898
+ timeoutTimer = setTimeout(() => {
12899
+ timedOut = true;
12900
+ killTarget("SIGTERM");
12901
+ killTimer = setTimeout(() => {
12902
+ sigkillSent = true;
12903
+ killTarget("SIGKILL");
12904
+ if (pendingExit) finish(pendingExit.code, pendingExit.signal);
12905
+ }, killGraceMs);
12906
+ }, timeoutMs);
12907
+ `;
12667
12908
  function describeMachineCommandFailure(operation, result) {
12668
12909
  const detail = (result.stderr || result.stdout || "").trim();
12669
12910
  const suffix = detail ? `: ${detail}` : "";
@@ -13361,7 +13602,7 @@ function getAgentStatus(machineId, options = {}) {
13361
13602
  }
13362
13603
  // src/commands/backup.ts
13363
13604
  import { homedir as homedir2, hostname as hostname6 } from "os";
13364
- import { join as join3 } from "path";
13605
+ import { join as join4 } from "path";
13365
13606
  var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
13366
13607
  var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
13367
13608
  var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
@@ -13411,14 +13652,14 @@ function resolveBackupTarget(options = {}) {
13411
13652
  function defaultBackupSources() {
13412
13653
  const home = homedir2();
13413
13654
  return [
13414
- join3(home, ".hasna"),
13415
- join3(home, ".ssh"),
13416
- join3(home, ".secrets")
13655
+ join4(home, ".hasna"),
13656
+ join4(home, ".ssh"),
13657
+ join4(home, ".secrets")
13417
13658
  ];
13418
13659
  }
13419
13660
  function buildBackupPlan(bucket, prefix) {
13420
13661
  const target = resolveBackupTarget({ bucket, prefix });
13421
- const archivePath = join3(homedir2(), ".hasna", "machines", "backup.tgz");
13662
+ const archivePath = join4(homedir2(), ".hasna", "machines", "backup.tgz");
13422
13663
  const sources = defaultBackupSources();
13423
13664
  const steps = [
13424
13665
  {
@@ -13467,6 +13708,301 @@ function runBackup(bucket, prefix, options = {}) {
13467
13708
  executed
13468
13709
  };
13469
13710
  }
13711
+ // src/commands/mutation-approval.ts
13712
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
13713
+ import { resolve as resolve2 } from "path";
13714
+ var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
13715
+ var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
13716
+ var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
13717
+ var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
13718
+ var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
13719
+ var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
13720
+ var TOKEN_PREFIX = "machines-mut-v1";
13721
+ var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
13722
+ var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
13723
+ var MAX_CLOCK_SKEW_MS = 30000;
13724
+ function isTruthy(value) {
13725
+ return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
13726
+ }
13727
+ function nowMs(now) {
13728
+ if (typeof now === "number")
13729
+ return now;
13730
+ if (now instanceof Date)
13731
+ return now.getTime();
13732
+ return Date.now();
13733
+ }
13734
+ function signingSecret(env, explicitSecret) {
13735
+ return explicitSecret?.trim() || env[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
13736
+ }
13737
+ function base64Url(value) {
13738
+ return Buffer.from(value).toString("base64url");
13739
+ }
13740
+ function hmac(payload, secret) {
13741
+ return createHmac("sha256", secret).update(payload).digest("base64url");
13742
+ }
13743
+ function sha256Hex(payload) {
13744
+ return createHash("sha256").update(payload).digest("hex");
13745
+ }
13746
+ function replayDbPath(env) {
13747
+ const configured = env[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
13748
+ return configured ? resolve2(configured) : undefined;
13749
+ }
13750
+ function replayNonceKey(claims) {
13751
+ return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
13752
+ }
13753
+ function recordReplayNonce(env, claims, tokenPayload, now) {
13754
+ const dbPath = replayDbPath(env);
13755
+ if (!dbPath)
13756
+ return;
13757
+ if (!claims.nonce) {
13758
+ return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
13759
+ }
13760
+ try {
13761
+ const db = getDb(dbPath);
13762
+ db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
13763
+ const result = db.query(`
13764
+ INSERT OR IGNORE INTO mutation_approval_nonces (
13765
+ nonce_sha256,
13766
+ token_sha256,
13767
+ surface,
13768
+ operation,
13769
+ caller_id,
13770
+ run_id,
13771
+ transport,
13772
+ expires_at,
13773
+ used_at
13774
+ )
13775
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
13776
+ `).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
13777
+ if (result.changes !== 1) {
13778
+ return { approved: false, reason: "approval_token nonce has already been used." };
13779
+ }
13780
+ return;
13781
+ } catch (error) {
13782
+ const message = error instanceof Error ? error.message : String(error);
13783
+ return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
13784
+ }
13785
+ }
13786
+ function safeEqual(left, right) {
13787
+ const leftBuffer = Buffer.from(left);
13788
+ const rightBuffer = Buffer.from(right);
13789
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
13790
+ }
13791
+ function normalizeScope(scope) {
13792
+ return {
13793
+ surface: scope.surface,
13794
+ operation: scope.operation,
13795
+ machineId: scope.machineId || undefined,
13796
+ resourceId: scope.resourceId || undefined,
13797
+ callerId: scope.callerId || undefined,
13798
+ runId: scope.runId || undefined,
13799
+ transport: scope.transport || undefined,
13800
+ argsSha256: scope.argsSha256 || (scope.args === undefined ? undefined : mutationArgsSha256(scope.args))
13801
+ };
13802
+ }
13803
+ function canonicalizeMutationArg(value, inArray = false) {
13804
+ if (value === undefined)
13805
+ return inArray ? null : undefined;
13806
+ if (value === null || typeof value === "boolean" || typeof value === "string")
13807
+ return value;
13808
+ if (typeof value === "number")
13809
+ return Number.isFinite(value) ? value : null;
13810
+ if (Array.isArray(value)) {
13811
+ return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
13812
+ }
13813
+ if (value instanceof Date)
13814
+ return value.toISOString();
13815
+ if (typeof value === "object") {
13816
+ const result = {};
13817
+ for (const key of Object.keys(value).sort()) {
13818
+ if (key === "approval_token" || key === "approvalToken")
13819
+ continue;
13820
+ const canonicalValue = canonicalizeMutationArg(value[key]);
13821
+ if (canonicalValue !== undefined)
13822
+ result[key] = canonicalValue;
13823
+ }
13824
+ return result;
13825
+ }
13826
+ return inArray ? null : undefined;
13827
+ }
13828
+ function canonicalMutationArgs(value) {
13829
+ return JSON.stringify(canonicalizeMutationArg(value) ?? {});
13830
+ }
13831
+ function mutationArgsSha256(value) {
13832
+ return sha256Hex(canonicalMutationArgs(value));
13833
+ }
13834
+ function stripPlanRuntimeFields(value) {
13835
+ if (Array.isArray(value))
13836
+ return value.map(stripPlanRuntimeFields);
13837
+ if (value instanceof Date)
13838
+ return value;
13839
+ if (value && typeof value === "object") {
13840
+ const result = {};
13841
+ for (const [key, entry] of Object.entries(value)) {
13842
+ if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
13843
+ continue;
13844
+ result[key] = stripPlanRuntimeFields(entry);
13845
+ }
13846
+ return result;
13847
+ }
13848
+ return value;
13849
+ }
13850
+ function mutationPlanDigest(plan) {
13851
+ return mutationArgsSha256(stripPlanRuntimeFields(plan));
13852
+ }
13853
+ function attachMutationPlanDigest(plan) {
13854
+ return {
13855
+ ...plan,
13856
+ planDigest: mutationPlanDigest(plan)
13857
+ };
13858
+ }
13859
+ function assertMutationPlanDigest(plan, expectedPlanDigest) {
13860
+ if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
13861
+ throw new Error("Approved plan digest does not match the current execution plan.");
13862
+ }
13863
+ }
13864
+ function createMutationApprovalToken(scope, options = {}) {
13865
+ const env = options.env ?? process.env;
13866
+ const secret = signingSecret(env, options.secret);
13867
+ if (!secret)
13868
+ throw new Error(`${MUTATION_APPROVAL_TOKEN_ENV} is required to sign mutation approval tokens.`);
13869
+ const issuedAt = nowMs(options.now);
13870
+ const claims = {
13871
+ version: 1,
13872
+ ...normalizeScope(scope),
13873
+ callerId: scope.callerId,
13874
+ runId: scope.runId,
13875
+ transport: scope.transport,
13876
+ issuedAt,
13877
+ expiresAt: issuedAt + Math.max(1, options.ttlMs ?? DEFAULT_TOKEN_TTL_MS),
13878
+ nonce: options.nonce ?? randomUUID()
13879
+ };
13880
+ claims.args_sha256 = claims.argsSha256;
13881
+ delete claims.args;
13882
+ delete claims.argsSha256;
13883
+ const payload = base64Url(JSON.stringify(claims));
13884
+ return `${TOKEN_PREFIX}.${payload}.${hmac(payload, secret)}`;
13885
+ }
13886
+ function parseToken(token) {
13887
+ if (!token)
13888
+ return null;
13889
+ const parts = token.split(".");
13890
+ if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
13891
+ return null;
13892
+ try {
13893
+ const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
13894
+ return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
13895
+ } catch {
13896
+ return null;
13897
+ }
13898
+ }
13899
+ function claimMatches(expected, actual) {
13900
+ if (expected === undefined)
13901
+ return actual === undefined;
13902
+ return actual === expected;
13903
+ }
13904
+ function verifyMutationApprovalToken(options) {
13905
+ const env = options.env ?? process.env;
13906
+ const secret = signingSecret(env);
13907
+ if (!secret)
13908
+ return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
13909
+ const parsed = parseToken(options.approvalToken);
13910
+ if (!parsed)
13911
+ return { approved: false, reason: "approval_token is not a scoped mutation token." };
13912
+ if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
13913
+ return { approved: false, reason: "approval_token signature is invalid." };
13914
+ }
13915
+ const claims = parsed.claims;
13916
+ if (claims.version !== 1)
13917
+ return { approved: false, reason: "approval_token version is unsupported." };
13918
+ if (!claims.callerId || !claims.runId) {
13919
+ return { approved: false, reason: "approval_token must include caller and run claims." };
13920
+ }
13921
+ if (!claims.transport) {
13922
+ return { approved: false, reason: "approval_token must include a transport claim." };
13923
+ }
13924
+ if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
13925
+ return { approved: false, reason: "approval_token is expired." };
13926
+ }
13927
+ const now = nowMs(options.now);
13928
+ if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
13929
+ return { approved: false, reason: "approval_token issue time is invalid." };
13930
+ }
13931
+ if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
13932
+ return { approved: false, reason: "approval_token TTL is too long." };
13933
+ }
13934
+ for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
13935
+ if (!claimMatches(options[key], claims[key])) {
13936
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
13937
+ }
13938
+ }
13939
+ for (const key of ["callerId", "runId"]) {
13940
+ if (options[key] !== undefined && options[key] !== claims[key]) {
13941
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
13942
+ }
13943
+ }
13944
+ const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
13945
+ if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
13946
+ return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
13947
+ }
13948
+ const replayDecision = recordReplayNonce(env, claims, parsed.payload, now);
13949
+ if (replayDecision)
13950
+ return replayDecision;
13951
+ return { approved: true, claims };
13952
+ }
13953
+ function isMutationApproved(options = {}) {
13954
+ const env = options.env ?? process.env;
13955
+ const surface = options.surface ?? "cli";
13956
+ if (surface === "mcp") {
13957
+ if (!options.operation)
13958
+ return false;
13959
+ return verifyMutationApprovalToken({
13960
+ surface,
13961
+ operation: options.operation,
13962
+ machineId: options.machineId,
13963
+ resourceId: options.resourceId,
13964
+ callerId: options.callerId,
13965
+ runId: options.runId,
13966
+ transport: options.transport ?? "mcp",
13967
+ args: options.args,
13968
+ argsSha256: options.argsSha256,
13969
+ approvalToken: options.approvalToken,
13970
+ env,
13971
+ now: options.now
13972
+ }).approved;
13973
+ }
13974
+ if (options.approvalToken) {
13975
+ const decision = options.operation ? verifyMutationApprovalToken({
13976
+ surface,
13977
+ operation: options.operation,
13978
+ machineId: options.machineId,
13979
+ resourceId: options.resourceId,
13980
+ callerId: options.callerId,
13981
+ runId: options.runId,
13982
+ transport: options.transport ?? surface,
13983
+ args: options.args,
13984
+ argsSha256: options.argsSha256,
13985
+ approvalToken: options.approvalToken,
13986
+ env,
13987
+ now: options.now
13988
+ }) : { approved: false };
13989
+ if (decision.approved)
13990
+ return true;
13991
+ if (env[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
13992
+ return false;
13993
+ }
13994
+ return isTruthy(env[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
13995
+ }
13996
+ function assertMutationApproved(options) {
13997
+ if (isMutationApproved(options)) {
13998
+ return;
13999
+ }
14000
+ const env = options.env ?? process.env;
14001
+ const tokenConfigured = Boolean(env[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
14002
+ const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
14003
+ throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
14004
+ }
14005
+
13470
14006
  // src/commands/apps.ts
13471
14007
  function getPackageName(app) {
13472
14008
  return app.packageName || app.name;
@@ -13559,12 +14095,12 @@ function listApps(machineId) {
13559
14095
  }
13560
14096
  function buildAppsPlan(machineId) {
13561
14097
  const machine = resolveMachine(machineId);
13562
- return {
14098
+ return attachMutationPlanDigest({
13563
14099
  machineId: machine.id,
13564
14100
  mode: "plan",
13565
14101
  steps: buildAppSteps(machine),
13566
14102
  executed: 0
13567
- };
14103
+ });
13568
14104
  }
13569
14105
  function getAppsStatus(machineId, runner = runMachineCommand) {
13570
14106
  const machine = resolveMachine(machineId);
@@ -13589,8 +14125,12 @@ function diffApps(machineId, runner = runMachineCommand) {
13589
14125
  }
13590
14126
  function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
13591
14127
  const plan = buildAppsPlan(machineId);
14128
+ return runAppsPlan(plan, options, runner);
14129
+ }
14130
+ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
14131
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
13592
14132
  if (!options.apply)
13593
- return plan;
14133
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
13594
14134
  if (!options.yes) {
13595
14135
  throw new Error("App installation requires --yes.");
13596
14136
  }
@@ -13599,29 +14139,29 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
13599
14139
  requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
13600
14140
  executed += 1;
13601
14141
  }
13602
- return {
14142
+ return attachMutationPlanDigest({
13603
14143
  machineId: plan.machineId,
13604
14144
  mode: "apply",
13605
14145
  steps: plan.steps,
13606
14146
  executed
13607
- };
14147
+ });
13608
14148
  }
13609
14149
  // src/commands/cert.ts
13610
14150
  import { homedir as homedir3, platform as platform4 } from "os";
13611
- import { join as join4 } from "path";
14151
+ import { join as join5 } from "path";
13612
14152
  function quote2(value) {
13613
14153
  return `'${value.replace(/'/g, `'\\''`)}'`;
13614
14154
  }
13615
14155
  function certDir() {
13616
- return join4(homedir3(), ".hasna", "machines", "certs");
14156
+ return join5(homedir3(), ".hasna", "machines", "certs");
13617
14157
  }
13618
14158
  function buildCertPlan(domains) {
13619
14159
  if (domains.length === 0) {
13620
14160
  throw new Error("At least one domain is required.");
13621
14161
  }
13622
14162
  const primary = domains[0];
13623
- const certPath = join4(certDir(), `${primary}.pem`);
13624
- const keyPath = join4(certDir(), `${primary}-key.pem`);
14163
+ const certPath = join5(certDir(), `${primary}.pem`);
14164
+ const keyPath = join5(certDir(), `${primary}-key.pem`);
13625
14165
  const steps = [];
13626
14166
  if (platform4() === "darwin") {
13627
14167
  steps.push({
@@ -13684,16 +14224,16 @@ function runCertPlan(domains, options = {}) {
13684
14224
  };
13685
14225
  }
13686
14226
  // src/commands/dns.ts
13687
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
13688
- import { join as join5 } from "path";
14227
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
14228
+ import { join as join6 } from "path";
13689
14229
  function getDnsPath() {
13690
- return join5(getDataDir(), "dns.json");
14230
+ return join6(getDataDir(), "dns.json");
13691
14231
  }
13692
14232
  function readMappings() {
13693
14233
  const path = getDnsPath();
13694
- if (!existsSync5(path))
14234
+ if (!existsSync6(path))
13695
14235
  return [];
13696
- return JSON.parse(readFileSync3(path, "utf8"));
14236
+ return JSON.parse(readFileSync4(path, "utf8"));
13697
14237
  }
13698
14238
  function writeMappings(mappings) {
13699
14239
  const path = getDnsPath();
@@ -13720,16 +14260,16 @@ function renderDomainMapping(domain) {
13720
14260
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
13721
14261
  caddySnippet: `${entry.domain} {
13722
14262
  reverse_proxy 127.0.0.1:${entry.port}
13723
- tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
14263
+ tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
13724
14264
  }`,
13725
- certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
13726
- keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
14265
+ certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
14266
+ keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
13727
14267
  };
13728
14268
  }
13729
14269
  // src/commands/daemon.ts
13730
14270
  import { execFileSync as execFileSync2 } from "child_process";
13731
- import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync4, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
13732
- import { dirname as dirname4 } from "path";
14271
+ import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync5, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
14272
+ import { delimiter, dirname as dirname4 } from "path";
13733
14273
  import { platform as osPlatform } from "os";
13734
14274
  var DEFAULT_SERVICE_NAME = "machines-agent";
13735
14275
  var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
@@ -14142,19 +14682,31 @@ WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
14142
14682
  `;
14143
14683
  }
14144
14684
  function daemonProgramArguments(options) {
14145
- const bunRuntime = siblingBunRuntime(options.executable);
14685
+ const bunRuntime = bunRuntimeForExecutable(options.executable);
14146
14686
  const base = bunRuntime ? [bunRuntime, options.executable] : [options.executable];
14147
14687
  return [...base, "--interval-ms", String(options.intervalMs)];
14148
14688
  }
14149
- function siblingBunRuntime(executable) {
14689
+ function bunRuntimeForExecutable(executable) {
14150
14690
  if (!isBunShebangScript(executable))
14151
14691
  return null;
14152
- const candidate = `${dirname4(executable)}/bun`;
14153
- return isExecutableFile(candidate) ? candidate : null;
14692
+ for (const candidate of bunRuntimeCandidates(executable)) {
14693
+ if (isExecutableFile(candidate))
14694
+ return candidate;
14695
+ }
14696
+ return null;
14697
+ }
14698
+ function bunRuntimeCandidates(executable) {
14699
+ const candidates = [
14700
+ `${dirname4(executable)}/bun`,
14701
+ process.env["BUN_INSTALL"] ? `${process.env["BUN_INSTALL"]}/bin/bun` : null,
14702
+ process.env["HOME"] ? `${process.env["HOME"]}/.bun/bin/bun` : null,
14703
+ ...(process.env["PATH"] ?? "").split(delimiter).filter(Boolean).map((entry) => `${entry}/bun`)
14704
+ ].filter((value) => Boolean(value));
14705
+ return [...new Set(candidates)];
14154
14706
  }
14155
14707
  function isBunShebangScript(executable) {
14156
14708
  try {
14157
- const content = readFileSync4(executable, "utf8").slice(0, 256);
14709
+ const content = readFileSync5(executable, "utf8").slice(0, 256);
14158
14710
  const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
14159
14711
  return /^#!.*\bbun\b/.test(firstLine2);
14160
14712
  } catch {
@@ -14162,7 +14714,7 @@ function isBunShebangScript(executable) {
14162
14714
  }
14163
14715
  }
14164
14716
  function isExecutableFile(path) {
14165
- if (!existsSync6(path))
14717
+ if (!existsSync7(path))
14166
14718
  return false;
14167
14719
  try {
14168
14720
  const stats = statSync(path);
@@ -14340,12 +14892,12 @@ function parseProbe(tool, stdout) {
14340
14892
  }
14341
14893
  function buildClaudeInstallPlan(machineId, tools) {
14342
14894
  const machine = resolveMachine2(machineId);
14343
- return {
14895
+ return attachMutationPlanDigest({
14344
14896
  machineId: machine.id,
14345
14897
  mode: "plan",
14346
14898
  steps: buildInstallSteps(machine, tools),
14347
14899
  executed: 0
14348
- };
14900
+ });
14349
14901
  }
14350
14902
  function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
14351
14903
  const machine = resolveMachine2(machineId);
@@ -14370,8 +14922,12 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
14370
14922
  }
14371
14923
  function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
14372
14924
  const plan = buildClaudeInstallPlan(machineId, tools);
14925
+ return runClaudeInstallPlan(plan, options, runner);
14926
+ }
14927
+ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
14928
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
14373
14929
  if (!options.apply)
14374
- return plan;
14930
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
14375
14931
  if (!options.yes) {
14376
14932
  throw new Error("Claude CLI installation requires --yes.");
14377
14933
  }
@@ -14380,12 +14936,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
14380
14936
  requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
14381
14937
  executed += 1;
14382
14938
  }
14383
- return {
14939
+ return attachMutationPlanDigest({
14384
14940
  machineId: plan.machineId,
14385
14941
  mode: "apply",
14386
14942
  steps: plan.steps,
14387
14943
  executed
14388
- };
14944
+ });
14389
14945
  }
14390
14946
  // src/commands/install-tailscale.ts
14391
14947
  function buildInstallSteps2(machine) {
@@ -14424,17 +14980,21 @@ function buildTailscaleInstallPlan(machineId) {
14424
14980
  if (!machine) {
14425
14981
  throw new Error(`Machine not found in manifest: ${machineId}`);
14426
14982
  }
14427
- return {
14983
+ return attachMutationPlanDigest({
14428
14984
  machineId: machine.id,
14429
14985
  mode: "plan",
14430
14986
  steps: buildInstallSteps2(machine),
14431
14987
  executed: 0
14432
- };
14988
+ });
14433
14989
  }
14434
14990
  function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
14435
14991
  const plan = buildTailscaleInstallPlan(machineId);
14992
+ return runTailscaleInstallPlan(plan, options, runner);
14993
+ }
14994
+ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
14995
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
14436
14996
  if (!options.apply)
14437
- return plan;
14997
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
14438
14998
  if (!options.yes) {
14439
14999
  throw new Error("Tailscale install requires --yes.");
14440
15000
  }
@@ -14443,19 +15003,21 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
14443
15003
  requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
14444
15004
  executed += 1;
14445
15005
  }
14446
- return {
15006
+ return attachMutationPlanDigest({
14447
15007
  machineId: plan.machineId,
14448
15008
  mode: "apply",
14449
15009
  steps: plan.steps,
14450
15010
  executed
14451
- };
15011
+ });
14452
15012
  }
14453
15013
  // src/commands/notifications.ts
14454
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
15014
+ import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
15015
+ import { delimiter as delimiter2, isAbsolute, join as join7 } from "path";
14455
15016
  var notificationChannelSchema = exports_external.object({
14456
15017
  id: exports_external.string(),
14457
15018
  type: exports_external.enum(["email", "webhook", "command"]),
14458
15019
  target: exports_external.string(),
15020
+ commandArgs: exports_external.array(exports_external.string()).optional(),
14459
15021
  events: exports_external.array(exports_external.string()),
14460
15022
  enabled: exports_external.boolean()
14461
15023
  });
@@ -14464,19 +15026,31 @@ var notificationConfigSchema = exports_external.object({
14464
15026
  updatedAt: exports_external.string().optional(),
14465
15027
  channels: exports_external.array(notificationChannelSchema)
14466
15028
  });
15029
+ var trustedNotificationApproval = Symbol("trustedNotificationApproval");
15030
+ function createTrustedNotificationApproval() {
15031
+ return { [trustedNotificationApproval]: true };
15032
+ }
15033
+ function isTrustedNotificationApproval(approval) {
15034
+ return approval?.[trustedNotificationApproval] === true;
15035
+ }
14467
15036
  function sortChannels(channels) {
14468
15037
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
14469
15038
  }
14470
- function shellQuote6(value) {
14471
- return `'${value.replace(/'/g, `'\\''`)}'`;
14472
- }
14473
15039
  function hasCommand2(binary) {
14474
- const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
14475
- stdout: "ignore",
14476
- stderr: "ignore",
14477
- env: process.env
14478
- });
14479
- return result.exitCode === 0;
15040
+ return Boolean(resolveExecutable(binary));
15041
+ }
15042
+ function resolveExecutable(binary) {
15043
+ const trimmed = binary.trim();
15044
+ if (!trimmed)
15045
+ return null;
15046
+ const candidates = isAbsolute(trimmed) ? [trimmed] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).map((dir) => join7(dir, trimmed));
15047
+ for (const candidate of candidates) {
15048
+ try {
15049
+ accessSync(candidate, constants.X_OK);
15050
+ return candidate;
15051
+ } catch {}
15052
+ }
15053
+ return null;
14480
15054
  }
14481
15055
  function buildNotificationPreview(channel, event, message) {
14482
15056
  if (channel.type === "email") {
@@ -14485,7 +15059,8 @@ function buildNotificationPreview(channel, event, message) {
14485
15059
  if (channel.type === "webhook") {
14486
15060
  return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
14487
15061
  }
14488
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
15062
+ const args = channel.commandArgs?.length ? ` ${channel.commandArgs.join(" ")}` : "";
15063
+ return `${channel.target}${args} with HASNA_MACHINES_NOTIFICATION_* environment`;
14489
15064
  }
14490
15065
  async function dispatchEmail(channel, event, message) {
14491
15066
  const subject = `[${event}] machines notification`;
@@ -14496,7 +15071,7 @@ Content-Type: text/plain; charset=utf-8
14496
15071
  ${message}
14497
15072
  `;
14498
15073
  if (hasCommand2("sendmail")) {
14499
- const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
15074
+ const result = Bun.spawnSync(["sendmail", "-t"], {
14500
15075
  stdin: new TextEncoder().encode(body),
14501
15076
  stdout: "pipe",
14502
15077
  stderr: "pipe",
@@ -14514,8 +15089,9 @@ ${message}
14514
15089
  };
14515
15090
  }
14516
15091
  if (hasCommand2("mail")) {
14517
- const command2 = `printf %s ${shellQuote6(message)} | mail -s ${shellQuote6(subject)} ${shellQuote6(channel.target)}`;
14518
- const result = Bun.spawnSync(["bash", "-lc", command2], {
15092
+ const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
15093
+ stdin: new TextEncoder().encode(`${message}
15094
+ `),
14519
15095
  stdout: "pipe",
14520
15096
  stderr: "pipe",
14521
15097
  env: process.env
@@ -14558,8 +15134,20 @@ async function dispatchWebhook(channel, event, message) {
14558
15134
  detail: `Webhook accepted with HTTP ${response.status}`
14559
15135
  };
14560
15136
  }
14561
- async function dispatchCommand(channel, event, message) {
14562
- const result = Bun.spawnSync(["bash", "-lc", channel.target], {
15137
+ async function dispatchCommand(channel, event, message, options = {}) {
15138
+ if (!isTrustedNotificationApproval(options.trustedApproval)) {
15139
+ assertMutationApproved({
15140
+ surface: "notifications",
15141
+ operation: "dispatch_command",
15142
+ resourceId: channel.id,
15143
+ approvalToken: options.approvalToken
15144
+ });
15145
+ }
15146
+ const executable = resolveExecutable(channel.target);
15147
+ if (!executable) {
15148
+ throw new Error(`Command executable not found or not executable: ${channel.target}`);
15149
+ }
15150
+ const result = Bun.spawnSync([executable, ...channel.commandArgs ?? []], {
14563
15151
  stdout: "pipe",
14564
15152
  stderr: "pipe",
14565
15153
  env: {
@@ -14581,7 +15169,7 @@ async function dispatchCommand(channel, event, message) {
14581
15169
  detail: stdout || "Command completed successfully"
14582
15170
  };
14583
15171
  }
14584
- async function dispatchChannel(channel, event, message) {
15172
+ async function dispatchChannel(channel, event, message, options = {}) {
14585
15173
  if (!channel.enabled) {
14586
15174
  return {
14587
15175
  channelId: channel.id,
@@ -14597,7 +15185,7 @@ async function dispatchChannel(channel, event, message) {
14597
15185
  if (channel.type === "webhook") {
14598
15186
  return dispatchWebhook(channel, event, message);
14599
15187
  }
14600
- return dispatchCommand(channel, event, message);
15188
+ return dispatchCommand(channel, event, message, options);
14601
15189
  }
14602
15190
  function getDefaultNotificationConfig() {
14603
15191
  return {
@@ -14607,10 +15195,10 @@ function getDefaultNotificationConfig() {
14607
15195
  };
14608
15196
  }
14609
15197
  function readNotificationConfig(path = getNotificationsPath()) {
14610
- if (!existsSync7(path)) {
15198
+ if (!existsSync8(path)) {
14611
15199
  return getDefaultNotificationConfig();
14612
15200
  }
14613
- return notificationConfigSchema.parse(JSON.parse(readFileSync5(path, "utf8")));
15201
+ return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
14614
15202
  }
14615
15203
  function writeNotificationConfig(config, path = getNotificationsPath()) {
14616
15204
  ensureParentDir(path);
@@ -14626,11 +15214,20 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
14626
15214
  function listNotificationChannels() {
14627
15215
  return readNotificationConfig();
14628
15216
  }
14629
- function addNotificationChannel(channel) {
15217
+ function addNotificationChannel(channel, options = {}) {
15218
+ if (channel.type === "command" && !isTrustedNotificationApproval(options.trustedApproval)) {
15219
+ assertMutationApproved({
15220
+ surface: "notifications",
15221
+ operation: "add_command_channel",
15222
+ resourceId: channel.id,
15223
+ approvalToken: options.approvalToken
15224
+ });
15225
+ }
14630
15226
  const config = readNotificationConfig();
14631
15227
  const channels = config.channels.filter((entry) => entry.id !== channel.id);
14632
15228
  channels.push({
14633
15229
  ...channel,
15230
+ commandArgs: channel.commandArgs?.map(String),
14634
15231
  events: [...new Set(channel.events)]
14635
15232
  });
14636
15233
  return writeNotificationConfig({ ...config, channels });
@@ -14652,7 +15249,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
14652
15249
  const deliveries = [];
14653
15250
  for (const channel of channels) {
14654
15251
  try {
14655
- deliveries.push(await dispatchChannel(channel, event, message));
15252
+ deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
14656
15253
  } catch (error) {
14657
15254
  deliveries.push({
14658
15255
  channelId: channel.id,
@@ -14687,7 +15284,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
14687
15284
  if (!options.yes) {
14688
15285
  throw new Error("Notification test execution requires --yes.");
14689
15286
  }
14690
- const delivery = await dispatchChannel(channel, event, message);
15287
+ const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
14691
15288
  return {
14692
15289
  channelId,
14693
15290
  mode: "apply",
@@ -14753,6 +15350,46 @@ function listPorts(machineId) {
14753
15350
  import { spawnSync as spawnSync4 } from "child_process";
14754
15351
  import { setTimeout as sleep2 } from "timers/promises";
14755
15352
  import { EventsClient } from "@hasna/events";
15353
+ function shellQuote6(value) {
15354
+ return `'${value.replace(/'/g, "'\\''")}'`;
15355
+ }
15356
+ function buildTmuxPaneDiedHookPlan(options = {}) {
15357
+ const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
15358
+ const machinesCommand = options.machinesCommand ?? "machines";
15359
+ const deliver = options.deliver === true;
15360
+ const approvalToken = options.approvalToken?.trim();
15361
+ const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
15362
+ const emitArgs = [
15363
+ "events",
15364
+ "emit",
15365
+ "machines.tmux.pane_died",
15366
+ "--source",
15367
+ "machines",
15368
+ "--subject",
15369
+ "tmux:#{hook_pane}",
15370
+ "--severity",
15371
+ "warning",
15372
+ "--message",
15373
+ "tmux pane died: #{hook_pane}",
15374
+ "--data",
15375
+ '{"target":"#{hook_pane}","session":"#{session_name}","window":"#{window_index}"}'
15376
+ ];
15377
+ if (!deliver)
15378
+ emitArgs.push("--no-deliver");
15379
+ if (approvalToken)
15380
+ emitArgs.push("--approval-token", approvalToken);
15381
+ const command2 = [machinesCommand, ...emitArgs].map(shellQuote6).join(" ");
15382
+ const runShell = trustedLocalMutation ? `HASNA_MACHINES_ALLOW_MUTATIONS=1 ${command2}` : command2;
15383
+ const args = ["set-hook", "-g", "pane-died", `run-shell ${shellQuote6(runShell)}`];
15384
+ return {
15385
+ tmuxCommand,
15386
+ args,
15387
+ shellCommand: [tmuxCommand, ...args].map(shellQuote6).join(" "),
15388
+ eventType: "machines.tmux.pane_died",
15389
+ deliver,
15390
+ trustedLocalMutation
15391
+ };
15392
+ }
14756
15393
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
14757
15394
  const checkedAt = new Date().toISOString();
14758
15395
  const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
@@ -14838,7 +15475,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
14838
15475
  return client.emit(input, { deliver });
14839
15476
  }
14840
15477
  // src/commands/serve.ts
14841
- import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
15478
+ import { EventsClient as EventsClient2, getEventsDataDir, sanitizeChannelsForOutput } from "@hasna/events";
15479
+ import { resolve as resolve3 } from "path";
14842
15480
 
14843
15481
  // src/commands/status.ts
14844
15482
  function parseJsonObject2(value) {
@@ -14893,7 +15531,7 @@ function escapeHtml(value) {
14893
15531
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
14894
15532
  }
14895
15533
  function getServeInfo(options = {}) {
14896
- const host = options.host || "0.0.0.0";
15534
+ const host = options.host || "127.0.0.1";
14897
15535
  const port = options.port || 7676;
14898
15536
  return {
14899
15537
  host,
@@ -15096,6 +15734,59 @@ async function parseJsonBody(request) {
15096
15734
  function jsonError(message, status = 400) {
15097
15735
  return Response.json({ error: message }, { status });
15098
15736
  }
15737
+ function dashboardResourceId(kind, ...parts) {
15738
+ const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
15739
+ return values ? `${kind}:${values}` : kind;
15740
+ }
15741
+ function eventStoreDir() {
15742
+ return resolve3(getEventsDataDir());
15743
+ }
15744
+ function eventStoreScope() {
15745
+ return { event_store_dir: eventStoreDir() };
15746
+ }
15747
+ function eventStoreResourceId(kind, ...parts) {
15748
+ return dashboardResourceId(kind, mutationArgsSha256(eventStoreScope()), ...parts);
15749
+ }
15750
+ function withEventStoreScope(args) {
15751
+ return { event_store_dir: eventStoreDir(), ...args };
15752
+ }
15753
+ function dashboardMutationCallerId() {
15754
+ return process.env[MUTATION_APPROVAL_CALLER_ENV]?.trim() || "dashboard";
15755
+ }
15756
+ function dashboardMutationRunId() {
15757
+ return process.env[MUTATION_APPROVAL_RUN_ENV]?.trim() || "dashboard";
15758
+ }
15759
+ function approvalTokenFromRequest(request, body) {
15760
+ const bodyToken = typeof body["approval_token"] === "string" ? body["approval_token"] : typeof body["approvalToken"] === "string" ? body["approvalToken"] : undefined;
15761
+ if (bodyToken?.trim())
15762
+ return bodyToken;
15763
+ const headerToken = request.headers.get("x-hasna-approval-token")?.trim();
15764
+ if (headerToken)
15765
+ return headerToken;
15766
+ const authorization = request.headers.get("authorization")?.trim();
15767
+ if (authorization?.toLowerCase().startsWith("bearer ")) {
15768
+ return authorization.slice("bearer ".length).trim();
15769
+ }
15770
+ return;
15771
+ }
15772
+ function requireDashboardMutation(operation, request, body, scope = {}) {
15773
+ const decision = verifyMutationApprovalToken({
15774
+ surface: "dashboard",
15775
+ operation,
15776
+ transport: "dashboard:http",
15777
+ callerId: dashboardMutationCallerId(),
15778
+ runId: dashboardMutationRunId(),
15779
+ resourceId: scope.resourceId,
15780
+ args: scope.args,
15781
+ approvalToken: approvalTokenFromRequest(request, body)
15782
+ });
15783
+ if (decision.approved)
15784
+ return;
15785
+ return jsonError(`Mutation approval denied: ${decision.reason ?? "approval_token is invalid."}`, 403);
15786
+ }
15787
+ function objectBodyValue(value) {
15788
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
15789
+ }
15099
15790
  function privateOutputWarnings(requested, allowed) {
15100
15791
  return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
15101
15792
  }
@@ -15107,6 +15798,7 @@ function appendWarnings(payload, warnings) {
15107
15798
  function startDashboardServer(options = {}) {
15108
15799
  const info = getServeInfo(options);
15109
15800
  const events = new EventsClient2;
15801
+ const trustedNotificationApproval2 = createTrustedNotificationApproval();
15110
15802
  return Bun.serve({
15111
15803
  hostname: info.host,
15112
15804
  port: info.port,
@@ -15171,8 +15863,25 @@ function startDashboardServer(options = {}) {
15171
15863
  const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
15172
15864
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
15173
15865
  const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
15174
- const data = body["data"] && typeof body["data"] === "object" && !Array.isArray(body["data"]) ? body["data"] : {};
15175
- const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? body["metadata"] : {};
15866
+ const data = objectBodyValue(body["data"]);
15867
+ const metadata = objectBodyValue(body["metadata"]);
15868
+ const denied = requireDashboardMutation("machines_events_emit", request, body, {
15869
+ resourceId: eventStoreResourceId("event", type, subject, dedupeKey),
15870
+ args: withEventStoreScope({
15871
+ event_type: type,
15872
+ source,
15873
+ subject,
15874
+ severity,
15875
+ message,
15876
+ data,
15877
+ metadata,
15878
+ dedupe_key: dedupeKey,
15879
+ deliver: true,
15880
+ dedupe: true
15881
+ })
15882
+ });
15883
+ if (denied)
15884
+ return denied;
15176
15885
  return Response.json(await events.emit({
15177
15886
  source,
15178
15887
  type,
@@ -15215,8 +15924,20 @@ function startDashboardServer(options = {}) {
15215
15924
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
15216
15925
  const apply = body["apply"] === true;
15217
15926
  const yes = body["yes"] === true;
15927
+ const resolvedEvent = event ?? "manual.test";
15928
+ const resolvedMessage = message ?? "machines notification test";
15929
+ const denied = requireDashboardMutation("machines_notifications_test", request, body, {
15930
+ resourceId: dashboardResourceId("notification-test", channelId, resolvedEvent),
15931
+ args: { channel_id: channelId, event: resolvedEvent, message: resolvedMessage, apply, yes }
15932
+ });
15933
+ if (denied)
15934
+ return denied;
15218
15935
  try {
15219
- return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
15936
+ return Response.json(await testNotificationChannel(channelId, resolvedEvent, resolvedMessage, {
15937
+ apply,
15938
+ yes,
15939
+ trustedApproval: apply ? trustedNotificationApproval2 : undefined
15940
+ }));
15220
15941
  } catch (error) {
15221
15942
  return jsonError(error instanceof Error ? error.message : String(error));
15222
15943
  }
@@ -15231,13 +15952,22 @@ function startDashboardServer(options = {}) {
15231
15952
  return jsonError("channelId is required.");
15232
15953
  }
15233
15954
  const type = typeof body["type"] === "string" ? body["type"] : "events.test";
15234
- const message = typeof body["message"] === "string" ? body["message"] : undefined;
15955
+ const subject = channelId;
15956
+ const message = typeof body["message"] === "string" ? body["message"] : "Hasna events test delivery";
15957
+ const data = objectBodyValue(body["data"]);
15958
+ const denied = requireDashboardMutation("machines_webhooks_test", request, body, {
15959
+ resourceId: eventStoreResourceId("webhook-test", channelId, type),
15960
+ args: withEventStoreScope({ channel_id: channelId, event_type: type, subject, message, data })
15961
+ });
15962
+ if (denied)
15963
+ return denied;
15235
15964
  try {
15236
15965
  return Response.json(await events.testChannel(channelId, {
15237
15966
  source: "machines",
15238
15967
  type,
15239
- subject: channelId,
15240
- message
15968
+ subject,
15969
+ message,
15970
+ data
15241
15971
  }));
15242
15972
  } catch (error) {
15243
15973
  return jsonError(error instanceof Error ? error.message : String(error));
@@ -15385,17 +16115,21 @@ function buildSetupPlan(machineId) {
15385
16115
  workspacePath: `${homedir4()}/workspace`
15386
16116
  };
15387
16117
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
15388
- return {
16118
+ return attachMutationPlanDigest({
15389
16119
  machineId: target.id,
15390
16120
  mode: "plan",
15391
16121
  steps,
15392
16122
  executed: 0
15393
- };
16123
+ });
15394
16124
  }
15395
16125
  function runSetup(machineId, options = {}, runner = runMachineCommand) {
15396
16126
  const plan = buildSetupPlan(machineId);
16127
+ return runSetupPlan(plan, options, runner);
16128
+ }
16129
+ function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
16130
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
15397
16131
  if (!options.apply) {
15398
- return plan;
16132
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
15399
16133
  }
15400
16134
  if (!options.yes) {
15401
16135
  throw new Error("Setup execution requires --yes.");
@@ -15416,12 +16150,12 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
15416
16150
  }
15417
16151
  executed += 1;
15418
16152
  }
15419
- const summary = {
16153
+ const summary = attachMutationPlanDigest({
15420
16154
  machineId: plan.machineId,
15421
16155
  mode: "apply",
15422
16156
  steps: plan.steps,
15423
16157
  executed
15424
- };
16158
+ });
15425
16159
  recordSetupRun(plan.machineId, "completed", summary);
15426
16160
  return summary;
15427
16161
  }
@@ -15540,16 +16274,22 @@ function buildScreenEnableCommand(machineId, options = {}) {
15540
16274
  }
15541
16275
  const secretsCommand = options.secretsCommand || "secrets";
15542
16276
  const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
16277
+ const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
16278
+ const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
15543
16279
  return {
15544
16280
  machineId: credentials.machineId,
15545
16281
  user: credentials.user,
15546
16282
  passwordSecretKey: credentials.passwordSecretKey,
15547
16283
  remoteCommand,
15548
- command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
16284
+ secretsCommand,
16285
+ secretsCommandArgs,
16286
+ sshCommand: sshPlan.command,
16287
+ sshCommandArgs: sshPlan.args,
16288
+ command: `${shellCommand2(secretsCommandArgs)} | ${sshPlan.shellCommand}`
15549
16289
  };
15550
16290
  }
15551
16291
  // src/commands/sync.ts
15552
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync } from "fs";
16292
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, symlinkSync, copyFileSync } from "fs";
15553
16293
  import { homedir as homedir5 } from "os";
15554
16294
  function quote4(value) {
15555
16295
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -15602,15 +16342,15 @@ function detectFileActions(machine) {
15602
16342
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
15603
16343
  }
15604
16344
  return (machine.files || []).map((file, index) => {
15605
- const sourceExists = existsSync8(file.source);
15606
- const targetExists = existsSync8(file.target);
16345
+ const sourceExists = existsSync9(file.source);
16346
+ const targetExists = existsSync9(file.target);
15607
16347
  let status = "missing";
15608
16348
  if (sourceExists && targetExists) {
15609
16349
  if (file.mode === "symlink") {
15610
16350
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
15611
16351
  } else {
15612
- const source = readFileSync6(file.source, "utf8");
15613
- const target = readFileSync6(file.target, "utf8");
16352
+ const source = readFileSync7(file.source, "utf8");
16353
+ const target = readFileSync7(file.target, "utf8");
15614
16354
  status = source === target ? "ok" : "drifted";
15615
16355
  }
15616
16356
  }
@@ -15640,12 +16380,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
15640
16380
  ...detectPackageActions(target, runner),
15641
16381
  ...detectFileActions(target)
15642
16382
  ];
15643
- return {
16383
+ return attachMutationPlanDigest({
15644
16384
  machineId: target.id,
15645
16385
  mode: "plan",
15646
16386
  actions,
15647
16387
  executed: 0
15648
- };
16388
+ });
15649
16389
  }
15650
16390
  function applyFileAction(command2) {
15651
16391
  const [verb, source, target] = command2.split(" ");
@@ -15669,8 +16409,12 @@ function applyFileAction(command2) {
15669
16409
  }
15670
16410
  function runSync(machineId, options = {}, runner = runMachineCommand) {
15671
16411
  const plan = buildSyncPlan(machineId, runner);
16412
+ return runSyncPlan(plan, options, runner);
16413
+ }
16414
+ function runSyncPlan(plan, options = {}, runner = runMachineCommand) {
16415
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
15672
16416
  if (!options.apply) {
15673
- return plan;
16417
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
15674
16418
  }
15675
16419
  if (!options.yes) {
15676
16420
  throw new Error("Sync execution requires --yes.");
@@ -15697,12 +16441,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
15697
16441
  }
15698
16442
  executed += 1;
15699
16443
  }
15700
- const summary = {
16444
+ const summary = attachMutationPlanDigest({
15701
16445
  machineId: plan.machineId,
15702
16446
  mode: "apply",
15703
16447
  actions: plan.actions,
15704
16448
  executed
15705
- };
16449
+ });
15706
16450
  recordSyncRun(plan.machineId, "completed", summary);
15707
16451
  return summary;
15708
16452
  }
@@ -23087,7 +23831,7 @@ class Protocol {
23087
23831
  return;
23088
23832
  }
23089
23833
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
23090
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
23834
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
23091
23835
  options?.signal?.throwIfAborted();
23092
23836
  }
23093
23837
  } catch (error2) {
@@ -23099,7 +23843,7 @@ class Protocol {
23099
23843
  }
23100
23844
  request(request, resultSchema, options) {
23101
23845
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
23102
- return new Promise((resolve2, reject) => {
23846
+ return new Promise((resolve4, reject) => {
23103
23847
  const earlyReject = (error2) => {
23104
23848
  reject(error2);
23105
23849
  };
@@ -23177,7 +23921,7 @@ class Protocol {
23177
23921
  if (!parseResult.success) {
23178
23922
  reject(parseResult.error);
23179
23923
  } else {
23180
- resolve2(parseResult.data);
23924
+ resolve4(parseResult.data);
23181
23925
  }
23182
23926
  } catch (error2) {
23183
23927
  reject(error2);
@@ -23368,12 +24112,12 @@ class Protocol {
23368
24112
  interval = task.pollInterval;
23369
24113
  }
23370
24114
  } catch {}
23371
- return new Promise((resolve2, reject) => {
24115
+ return new Promise((resolve4, reject) => {
23372
24116
  if (signal.aborted) {
23373
24117
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
23374
24118
  return;
23375
24119
  }
23376
- const timeoutId = setTimeout(resolve2, interval);
24120
+ const timeoutId = setTimeout(resolve4, interval);
23377
24121
  signal.addEventListener("abort", () => {
23378
24122
  clearTimeout(timeoutId);
23379
24123
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -24227,7 +24971,7 @@ class McpServer {
24227
24971
  let task = createTaskResult.task;
24228
24972
  const pollInterval = task.pollInterval ?? 5000;
24229
24973
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
24230
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
24974
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
24231
24975
  const updatedTask = await extra.taskStore.getTask(taskId);
24232
24976
  if (!updatedTask) {
24233
24977
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -24826,8 +25570,8 @@ var MACHINE_MCP_TOOL_NAMES = [
24826
25570
  "storage_pull",
24827
25571
  "storage_sync"
24828
25572
  ];
24829
- function buildServer(version2 = getPackageVersion()) {
24830
- return createMcpServer(version2);
25573
+ function buildServer(version2 = getPackageVersion(), options = {}) {
25574
+ return createMcpServer(version2, options);
24831
25575
  }
24832
25576
  function privateMetadataAllowed(requested) {
24833
25577
  return requested === true && isPrivateOutputEnabled();
@@ -24841,9 +25585,50 @@ function appendWarnings2(payload, warnings) {
24841
25585
  const currentWarnings = typeof payload === "object" && payload && "warnings" in payload && Array.isArray(payload.warnings) ? payload.warnings : [];
24842
25586
  return { ...payload, warnings: [...currentWarnings, ...warnings] };
24843
25587
  }
24844
- function createMcpServer(version2) {
25588
+ var approvalTokenSchema = exports_external.string().optional().describe("Operator mutation approval token");
25589
+ function mutationMachineId(machineId) {
25590
+ return machineId?.trim() || "local";
25591
+ }
25592
+ function mutationResourceId(kind, ...parts) {
25593
+ const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
25594
+ return values ? `${kind}:${values}` : kind;
25595
+ }
25596
+ function mutationCallerId() {
25597
+ return process.env["HASNA_MACHINES_MUTATION_CALLER_ID"]?.trim() || "mcp";
25598
+ }
25599
+ function mutationRunId() {
25600
+ return process.env["HASNA_MACHINES_MUTATION_RUN_ID"]?.trim() || "mcp";
25601
+ }
25602
+ function assertScopedMcpMutation(operation, approvalToken, scope = {}, transport) {
25603
+ assertMutationApproved({
25604
+ surface: "mcp",
25605
+ operation,
25606
+ transport,
25607
+ callerId: mutationCallerId(),
25608
+ runId: mutationRunId(),
25609
+ machineId: scope.machineId === undefined ? undefined : mutationMachineId(scope.machineId),
25610
+ resourceId: scope.resourceId === undefined || scope.resourceId === null ? undefined : scope.resourceId,
25611
+ args: scope.args,
25612
+ approvalToken
25613
+ });
25614
+ }
25615
+ function mcpPlanApprovalArgs(args, plan) {
25616
+ return {
25617
+ ...args,
25618
+ plan_digest: mutationPlanDigest(plan)
25619
+ };
25620
+ }
25621
+ function mcpPlanResourceId(operation, machineId, plan) {
25622
+ return mutationResourceId("plan", operation, machineId, mutationPlanDigest(plan));
25623
+ }
25624
+ function createMcpServer(version2, options = {}) {
24845
25625
  const server = new McpServer({ name: "machines", version: version2 });
24846
25626
  const events = new EventsClient3;
25627
+ const trustedNotificationApproval2 = createTrustedNotificationApproval();
25628
+ const mutationTransport = options.mutationTransport ?? "mcp:stdio";
25629
+ function requireMcpMutation(operation, approvalToken, scope = {}) {
25630
+ assertScopedMcpMutation(operation, approvalToken, scope, mutationTransport);
25631
+ }
24847
25632
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", { private_metadata: exports_external.boolean().optional().describe("Include private local paths and machine identifiers") }, async ({ private_metadata }) => {
24848
25633
  const privateMetadata = privateMetadataAllowed(private_metadata);
24849
25634
  const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
@@ -24857,18 +25642,31 @@ function createMcpServer(version2) {
24857
25642
  server.tool("machines_apps_status", "Check installed state for manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(getAppsStatus(machine_id), null, 2) }] }));
24858
25643
  server.tool("machines_apps_diff", "Show missing and installed manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(diffApps(machine_id), null, 2) }] }));
24859
25644
  server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }] }));
24860
- server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runAppsInstall(machine_id, { apply: true, yes }), null, 2) }] }));
25645
+ server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
25646
+ const resolvedMachineId = mutationMachineId(machine_id);
25647
+ const plan = buildAppsPlan(machine_id);
25648
+ requireMcpMutation("machines_apps_apply", approval_token, {
25649
+ machineId: resolvedMachineId,
25650
+ resourceId: mcpPlanResourceId("machines_apps_apply", resolvedMachineId, plan),
25651
+ args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
25652
+ });
25653
+ return { content: [{ type: "text", text: JSON.stringify(runAppsPlan(plan, { apply: true, yes }), null, 2) }] };
25654
+ });
24861
25655
  server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
24862
25656
  content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
24863
25657
  }));
24864
25658
  server.tool("machines_manifest_validate", "Validate the current fleet manifest.", {}, async () => ({
24865
25659
  content: [{ type: "text", text: JSON.stringify(manifestValidate(), null, 2) }]
24866
25660
  }));
24867
- server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () => ({
24868
- content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }]
24869
- }));
25661
+ server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", { approval_token: approvalTokenSchema }, async ({ approval_token }) => {
25662
+ requireMcpMutation("machines_manifest_bootstrap", approval_token, { resourceId: "manifest:bootstrap", args: {} });
25663
+ return { content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }] };
25664
+ });
24870
25665
  server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
24871
- server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] }));
25666
+ server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier"), approval_token: approvalTokenSchema }, async ({ machine_id, approval_token }) => {
25667
+ requireMcpMutation("machines_manifest_remove", approval_token, { machineId: machine_id, args: { machine_id } });
25668
+ return { content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] };
25669
+ });
24872
25670
  server.tool("machines_agent_status", "List current machine agent heartbeats.", { private_metadata: exports_external.boolean().optional().describe("Include private heartbeat metadata") }, async ({ private_metadata }) => {
24873
25671
  const privateMetadata = privateMetadataAllowed(private_metadata);
24874
25672
  const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
@@ -24920,9 +25718,27 @@ function createMcpServer(version2) {
24920
25718
  }]
24921
25719
  }));
24922
25720
  server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
24923
- server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
25721
+ server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
25722
+ const resolvedMachineId = mutationMachineId(machine_id);
25723
+ const plan = buildSetupPlan(machine_id);
25724
+ requireMcpMutation("machines_setup_apply", approval_token, {
25725
+ machineId: resolvedMachineId,
25726
+ resourceId: mcpPlanResourceId("machines_setup_apply", resolvedMachineId, plan),
25727
+ args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
25728
+ });
25729
+ return { content: [{ type: "text", text: JSON.stringify(runSetupPlan(plan, { apply: true, yes }), null, 2) }] };
25730
+ });
24924
25731
  server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
24925
- server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
25732
+ server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
25733
+ const resolvedMachineId = mutationMachineId(machine_id);
25734
+ const plan = buildSyncPlan(machine_id);
25735
+ requireMcpMutation("machines_sync_apply", approval_token, {
25736
+ machineId: resolvedMachineId,
25737
+ resourceId: mcpPlanResourceId("machines_sync_apply", resolvedMachineId, plan),
25738
+ args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
25739
+ });
25740
+ return { content: [{ type: "text", text: JSON.stringify(runSyncPlan(plan, { apply: true, yes }), null, 2) }] };
25741
+ });
24926
25742
  server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
24927
25743
  include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
24928
25744
  private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
@@ -24977,12 +25793,31 @@ function createMcpServer(version2) {
24977
25793
  server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
24978
25794
  machine_id: exports_external.string().optional().describe("Machine identifier"),
24979
25795
  tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
24980
- yes: exports_external.boolean().describe("Confirmation flag for execution")
24981
- }, async ({ machine_id, tools, yes }) => ({
24982
- content: [{ type: "text", text: JSON.stringify(runClaudeInstall(machine_id, tools, { apply: true, yes }), null, 2) }]
24983
- }));
25796
+ yes: exports_external.boolean().describe("Confirmation flag for execution"),
25797
+ approval_token: approvalTokenSchema
25798
+ }, async ({ machine_id, tools, yes, approval_token }) => {
25799
+ const resolvedMachineId = mutationMachineId(machine_id);
25800
+ const plan = buildClaudeInstallPlan(machine_id, tools);
25801
+ requireMcpMutation("machines_install_claude_apply", approval_token, {
25802
+ machineId: resolvedMachineId,
25803
+ resourceId: mcpPlanResourceId("machines_install_claude_apply", resolvedMachineId, plan),
25804
+ args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, tools, yes }, plan)
25805
+ });
25806
+ return {
25807
+ content: [{ type: "text", text: JSON.stringify(runClaudeInstallPlan(plan, { apply: true, yes }), null, 2) }]
25808
+ };
25809
+ });
24984
25810
  server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
24985
- server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
25811
+ server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
25812
+ const resolvedMachineId = mutationMachineId(machine_id);
25813
+ const plan = buildTailscaleInstallPlan(machine_id);
25814
+ requireMcpMutation("machines_install_tailscale_apply", approval_token, {
25815
+ machineId: resolvedMachineId,
25816
+ resourceId: mcpPlanResourceId("machines_install_tailscale_apply", resolvedMachineId, plan),
25817
+ args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
25818
+ });
25819
+ return { content: [{ type: "text", text: JSON.stringify(runTailscaleInstallPlan(plan, { apply: true, yes }), null, 2) }] };
25820
+ });
24986
25821
  server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
24987
25822
  machine_id: exports_external.string().describe("Machine identifier"),
24988
25823
  include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
@@ -25050,41 +25885,72 @@ function createMcpServer(version2) {
25050
25885
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
25051
25886
  }));
25052
25887
  server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
25053
- server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] }));
25888
+ server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ bucket, prefix, yes, approval_token }) => {
25889
+ requireMcpMutation("machines_backup_apply", approval_token, { resourceId: mutationResourceId("backup", bucket, prefix), args: { bucket, prefix, yes } });
25890
+ return { content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] };
25891
+ });
25054
25892
  server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
25055
- server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] }));
25056
- server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host") }, async ({ domain, port, target_host }) => ({ content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }] }));
25893
+ server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ domains, yes, approval_token }) => {
25894
+ requireMcpMutation("machines_cert_apply", approval_token, { resourceId: mutationResourceId("cert", domains.join(",")), args: { domains, yes } });
25895
+ return { content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] };
25896
+ });
25897
+ server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host"), approval_token: approvalTokenSchema }, async ({ domain, port, target_host, approval_token }) => {
25898
+ const resolvedTargetHost = target_host ?? "127.0.0.1";
25899
+ requireMcpMutation("machines_dns_add", approval_token, { resourceId: mutationResourceId("dns", domain), args: { domain, port, target_host: resolvedTargetHost } });
25900
+ return { content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, resolvedTargetHost), null, 2) }] };
25901
+ });
25057
25902
  server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
25058
25903
  server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", { domain: exports_external.string().describe("Domain name") }, async ({ domain }) => ({ content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }] }));
25059
25904
  server.tool("machines_notifications_add", "Add or replace a notification channel.", {
25060
25905
  channel_id: exports_external.string().describe("Channel identifier"),
25061
25906
  type: exports_external.enum(["email", "webhook", "command"]).describe("Notification transport"),
25062
- target: exports_external.string().describe("Email, webhook URL, or shell command"),
25907
+ target: exports_external.string().describe("Email, webhook URL, or command executable"),
25908
+ command_args: exports_external.array(exports_external.string()).optional().describe("Arguments for command transports"),
25063
25909
  events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
25064
- enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
25065
- }, async ({ channel_id, type, target, events: events2, enabled }) => ({
25066
- content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events: events2, enabled: enabled ?? true }), null, 2) }]
25067
- }));
25910
+ enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
25911
+ approval_token: approvalTokenSchema
25912
+ }, async ({ channel_id, type, target, command_args, events: events2, enabled, approval_token }) => {
25913
+ const resolvedEnabled = enabled ?? true;
25914
+ const resolvedEvents = [...new Set(events2)];
25915
+ const commandArgs = command_args ?? [];
25916
+ requireMcpMutation("machines_notifications_add", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id, type, target, command_args: commandArgs, events: resolvedEvents, enabled: resolvedEnabled } });
25917
+ return {
25918
+ content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, commandArgs: type === "command" && commandArgs.length > 0 ? commandArgs : undefined, events: resolvedEvents, enabled: resolvedEnabled }, { trustedApproval: trustedNotificationApproval2 }), null, 2) }]
25919
+ };
25920
+ });
25068
25921
  server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
25069
25922
  content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
25070
25923
  }));
25071
- server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true") }, async ({ channel_id, event, message, yes }) => ({
25072
- content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2) }]
25073
- }));
25074
- server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
25075
- server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
25924
+ server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true"), approval_token: approvalTokenSchema }, async ({ channel_id, event, message, yes, approval_token }) => {
25925
+ if (yes === true)
25926
+ requireMcpMutation("machines_notifications_test", approval_token, { resourceId: mutationResourceId("notification-test", channel_id, event), args: { channel_id, event, message, yes: true } });
25927
+ return {
25928
+ content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes, trustedApproval: yes === true ? trustedNotificationApproval2 : undefined }), null, 2) }]
25929
+ };
25930
+ });
25931
+ server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel"), approval_token: approvalTokenSchema }, async ({ event, message, channel_id, approval_token }) => {
25932
+ requireMcpMutation("machines_notifications_dispatch", approval_token, { resourceId: mutationResourceId("notification-dispatch", channel_id, event), args: { event, message, channel_id } });
25933
+ return { content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id, trustedApproval: trustedNotificationApproval2 }), null, 2) }] };
25934
+ });
25935
+ server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
25936
+ requireMcpMutation("machines_notifications_remove", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id } });
25937
+ return { content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] };
25938
+ });
25076
25939
  server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
25077
25940
  channel_id: exports_external.string().describe("Channel identifier"),
25078
25941
  url: exports_external.string().url().describe("Webhook URL"),
25079
25942
  event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
25080
25943
  source: exports_external.string().optional().describe("Optional source filter"),
25081
25944
  secret: exports_external.string().optional().describe("Optional HMAC secret"),
25082
- enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
25083
- }, async ({ channel_id, url, event_type, source, secret, enabled }) => {
25945
+ enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
25946
+ approval_token: approvalTokenSchema
25947
+ }, async ({ channel_id, url, event_type, source, secret, enabled, approval_token }) => {
25948
+ const resolvedEnabled = enabled ?? true;
25949
+ requireMcpMutation("machines_webhooks_add", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id, url, event_type, source, secret, enabled: resolvedEnabled } });
25084
25950
  const now = new Date().toISOString();
25085
25951
  const channel = await events.addChannel({
25086
25952
  id: channel_id,
25087
- enabled: enabled ?? true,
25953
+ enabled: resolvedEnabled,
25088
25954
  transport: "webhook",
25089
25955
  filters: event_type || source ? [{ type: event_type, source }] : undefined,
25090
25956
  webhook: { url, secret },
@@ -25096,10 +25962,16 @@ function createMcpServer(version2) {
25096
25962
  server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
25097
25963
  content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
25098
25964
  }));
25099
- server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) => ({
25100
- content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
25101
- }));
25102
- server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] }));
25965
+ server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body"), approval_token: approvalTokenSchema }, async ({ channel_id, event_type, message, approval_token }) => {
25966
+ requireMcpMutation("machines_webhooks_test", approval_token, { resourceId: mutationResourceId("webhook-test", channel_id, event_type), args: { channel_id, event_type, message } });
25967
+ return {
25968
+ content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
25969
+ };
25970
+ });
25971
+ server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
25972
+ requireMcpMutation("machines_webhooks_remove", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id } });
25973
+ return { content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] };
25974
+ });
25103
25975
  server.tool("machines_events_emit", "Emit a shared event from machines.", {
25104
25976
  event_type: exports_external.string().describe("Event type"),
25105
25977
  subject: exports_external.string().optional().describe("Event subject"),
@@ -25108,25 +25980,36 @@ function createMcpServer(version2) {
25108
25980
  data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
25109
25981
  metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
25110
25982
  dedupe_key: exports_external.string().optional().describe("Dedupe key"),
25111
- deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
25112
- }, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver }) => ({
25113
- content: [{ type: "text", text: JSON.stringify(await events.emit({
25114
- source: "machines",
25115
- type: event_type,
25116
- subject,
25117
- severity,
25118
- message,
25119
- data: data ?? {},
25120
- metadata: metadata ?? {},
25121
- dedupeKey: dedupe_key
25122
- }, { deliver: deliver !== false }), null, 2) }]
25123
- }));
25983
+ deliver: exports_external.boolean().optional().describe("Deliver to matching channels"),
25984
+ approval_token: approvalTokenSchema
25985
+ }, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver, approval_token }) => {
25986
+ const resolvedData = data ?? {};
25987
+ const resolvedMetadata = metadata ?? {};
25988
+ const resolvedDeliver = deliver !== false;
25989
+ requireMcpMutation("machines_events_emit", approval_token, { resourceId: mutationResourceId("event", event_type, subject, dedupe_key), args: { event_type, subject, severity, message, data: resolvedData, metadata: resolvedMetadata, dedupe_key, deliver: resolvedDeliver } });
25990
+ return {
25991
+ content: [{ type: "text", text: JSON.stringify(await events.emit({
25992
+ source: "machines",
25993
+ type: event_type,
25994
+ subject,
25995
+ severity,
25996
+ message,
25997
+ data: resolvedData,
25998
+ metadata: resolvedMetadata,
25999
+ dedupeKey: dedupe_key
26000
+ }, { deliver: resolvedDeliver }), null, 2) }]
26001
+ };
26002
+ });
25124
26003
  server.tool("machines_events_list", "List shared events.", {}, async () => ({
25125
26004
  content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
25126
26005
  }));
25127
- server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) => ({
25128
- content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
25129
- }));
26006
+ server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery"), approval_token: approvalTokenSchema }, async ({ event_id, source, event_type, dry_run, approval_token }) => {
26007
+ if (dry_run !== true)
26008
+ requireMcpMutation("machines_events_replay", approval_token, { resourceId: mutationResourceId("event-replay", event_id, source, event_type), args: { event_id, source, event_type, dry_run: false } });
26009
+ return {
26010
+ content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
26011
+ };
26012
+ });
25130
26013
  server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
25131
26014
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
25132
26015
  content: [{ type: "text", text: renderDashboardHtml() }]
@@ -25134,9 +26017,21 @@ function createMcpServer(version2) {
25134
26017
  server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
25135
26018
  content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
25136
26019
  }));
25137
- server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(tables ? { tables } : undefined), null, 2) }] }));
25138
- server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(tables ? { tables } : undefined), null, 2) }] }));
25139
- server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(tables ? { tables } : undefined), null, 2) }] }));
26020
+ server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
26021
+ const resolvedTables = resolveTables(tables);
26022
+ requireMcpMutation("storage_push", approval_token, { resourceId: mutationResourceId("storage-push", resolvedTables.join(",")), args: { tables: resolvedTables } });
26023
+ return { content: [{ type: "text", text: JSON.stringify(await storagePush({ tables: resolvedTables }), null, 2) }] };
26024
+ });
26025
+ server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
26026
+ const resolvedTables = resolveTables(tables);
26027
+ requireMcpMutation("storage_pull", approval_token, { resourceId: mutationResourceId("storage-pull", resolvedTables.join(",")), args: { tables: resolvedTables } });
26028
+ return { content: [{ type: "text", text: JSON.stringify(await storagePull({ tables: resolvedTables }), null, 2) }] };
26029
+ });
26030
+ server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
26031
+ const resolvedTables = resolveTables(tables);
26032
+ requireMcpMutation("storage_sync", approval_token, { resourceId: mutationResourceId("storage-sync", resolvedTables.join(",")), args: { tables: resolvedTables } });
26033
+ return { content: [{ type: "text", text: JSON.stringify(await storageSync({ tables: resolvedTables }), null, 2) }] };
26034
+ });
25140
26035
  return server;
25141
26036
  }
25142
26037
  export {
@@ -25145,6 +26040,8 @@ export {
25145
26040
  writeHeartbeatTick,
25146
26041
  writeHeartbeat,
25147
26042
  watchTmuxPane,
26043
+ verifyMutationApprovalToken,
26044
+ validateSshTarget,
25148
26045
  validateManifest,
25149
26046
  upsertHeartbeat,
25150
26047
  testNotificationChannel,
@@ -25154,16 +26051,21 @@ export {
25154
26051
  startDashboardServer,
25155
26052
  setHeartbeatStatus,
25156
26053
  sanitizePublicString,
26054
+ runTailscaleInstallPlan,
25157
26055
  runTailscaleInstall,
26056
+ runSyncPlan,
25158
26057
  runSync,
25159
26058
  runStorageMigrations,
26059
+ runSetupPlan,
25160
26060
  runSetup,
25161
26061
  runSelfTest,
25162
26062
  runDoctor,
25163
26063
  runDaemonServicePlan,
26064
+ runClaudeInstallPlan,
25164
26065
  runClaudeInstall,
25165
26066
  runCertPlan,
25166
26067
  runBackup,
26068
+ runAppsPlan,
25167
26069
  runAppsInstall,
25168
26070
  resolveTables,
25169
26071
  resolveSshTarget,
@@ -25197,6 +26099,8 @@ export {
25197
26099
  probeTmuxPane,
25198
26100
  parseStorageTables,
25199
26101
  parsePortOutput,
26102
+ mutationPlanDigest,
26103
+ mutationArgsSha256,
25200
26104
  markOffline,
25201
26105
  manifestValidate,
25202
26106
  manifestRemove,
@@ -25215,6 +26119,7 @@ export {
25215
26119
  isSensitiveKey,
25216
26120
  isPrivateOutputEnabled,
25217
26121
  isPrivateMetadataEnabled,
26122
+ isMutationApproved,
25218
26123
  getSyncMetaAll,
25219
26124
  getStorageStatus,
25220
26125
  getStoragePg,
@@ -25253,13 +26158,18 @@ export {
25253
26158
  diffApps,
25254
26159
  detectCurrentMachineManifest,
25255
26160
  defaultScreenPasswordSecretKey,
26161
+ createMutationApprovalToken,
25256
26162
  createMcpServer,
25257
26163
  createMachineResolverSnapshot,
25258
26164
  countRuns,
25259
26165
  closeDb,
25260
26166
  checkMachineCompatibility,
26167
+ canonicalMutationArgs,
26168
+ buildTmuxPaneDiedHookPlan,
25261
26169
  buildTailscaleInstallPlan,
25262
26170
  buildSyncPlan,
26171
+ buildSshCommandPlan,
26172
+ buildSshCommandArgs,
25263
26173
  buildSshCommand,
25264
26174
  buildSetupPlan,
25265
26175
  buildServer,
@@ -25277,6 +26187,9 @@ export {
25277
26187
  buildCertPlan,
25278
26188
  buildBackupPlan,
25279
26189
  buildAppsPlan,
26190
+ attachMutationPlanDigest,
26191
+ assertMutationPlanDigest,
26192
+ assertMutationApproved,
25280
26193
  addNotificationChannel,
25281
26194
  addDomainMapping,
25282
26195
  SqliteAdapter,
@@ -25292,6 +26205,11 @@ export {
25292
26205
  PRIVATE_METADATA_ENV,
25293
26206
  PRIVATE_MANIFEST_REF_ENV,
25294
26207
  PRIVATE_MANIFEST_BACKEND_ENV,
26208
+ MUTATION_APPROVAL_TOKEN_ENV,
26209
+ MUTATION_APPROVAL_RUN_ENV,
26210
+ MUTATION_APPROVAL_REPLAY_PATH_ENV,
26211
+ MUTATION_APPROVAL_FLAG_ENV,
26212
+ MUTATION_APPROVAL_CALLER_ENV,
25295
26213
  MACHINE_MCP_TOOL_NAMES,
25296
26214
  MACHINES_STORAGE_TABLES,
25297
26215
  MACHINES_STORAGE_MODE_FALLBACK_ENV,
@@ -25310,6 +26228,7 @@ export {
25310
26228
  MACHINES_BACKUP_PREFIX_ENV,
25311
26229
  MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
25312
26230
  MACHINES_BACKUP_BUCKET_ENV,
26231
+ LEGACY_MUTATION_APPROVAL_FLAG_ENV,
25313
26232
  DOCTOR_OPTIONAL_ADAPTER_DOMAINS,
25314
26233
  DEFAULT_SCREEN_SECRET_NAMESPACE,
25315
26234
  DEFAULT_MACHINE_RESOLVER_TTL_MS,