@hasna/machines 0.0.45 → 0.0.47

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 (110) hide show
  1. package/README.md +53 -4
  2. package/dist/agent/index.d.ts +0 -1
  3. package/dist/agent/index.js +250 -15
  4. package/dist/agent/runtime.d.ts +0 -1
  5. package/dist/cli/index.d.ts +0 -1
  6. package/dist/cli/index.js +1659 -233
  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/hosts.d.ts +81 -0
  21. package/dist/commands/install-claude.d.ts +5 -3
  22. package/dist/commands/install-tailscale.d.ts +5 -3
  23. package/dist/commands/manifest.d.ts +0 -1
  24. package/dist/commands/mutation-approval.d.ts +54 -0
  25. package/dist/commands/notifications.d.ts +14 -2
  26. package/dist/commands/ports.d.ts +0 -1
  27. package/dist/commands/runtime.d.ts +15 -1
  28. package/dist/commands/screen.d.ts +4 -1
  29. package/dist/commands/self-test.d.ts +0 -1
  30. package/dist/commands/serve.d.ts +0 -1
  31. package/dist/commands/setup.d.ts +5 -3
  32. package/dist/commands/ssh.d.ts +8 -1
  33. package/dist/commands/status.d.ts +0 -1
  34. package/dist/commands/sync.d.ts +5 -3
  35. package/dist/commands/workspace.d.ts +0 -1
  36. package/dist/compatibility.d.ts +0 -1
  37. package/dist/consumer-schema.d.ts +0 -1
  38. package/dist/consumer.d.ts +0 -1
  39. package/dist/consumer.js +253 -12
  40. package/dist/cross-project-types.d.ts +0 -1
  41. package/dist/db.d.ts +0 -1
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.js +1092 -185
  44. package/dist/manifests.d.ts +0 -1
  45. package/dist/mcp/http.d.ts +26 -2
  46. package/dist/mcp/index.d.ts +0 -1
  47. package/dist/mcp/index.js +1004 -162
  48. package/dist/mcp/server.d.ts +5 -3
  49. package/dist/paths.d.ts +0 -1
  50. package/dist/pg-migrations.d.ts +0 -1
  51. package/dist/redaction.d.ts +0 -1
  52. package/dist/remote-storage.d.ts +0 -1
  53. package/dist/remote.d.ts +14 -5
  54. package/dist/storage-sync.d.ts +0 -1
  55. package/dist/storage.d.ts +0 -1
  56. package/dist/storage.js +18 -0
  57. package/dist/topology.d.ts +0 -1
  58. package/dist/types.d.ts +3 -1
  59. package/dist/version.d.ts +0 -1
  60. package/package.json +5 -3
  61. package/dist/agent/index.d.ts.map +0 -1
  62. package/dist/agent/runtime.d.ts.map +0 -1
  63. package/dist/cli/index.d.ts.map +0 -1
  64. package/dist/cli-utils.d.ts.map +0 -1
  65. package/dist/commands/apps.d.ts.map +0 -1
  66. package/dist/commands/backup.d.ts.map +0 -1
  67. package/dist/commands/cert.d.ts.map +0 -1
  68. package/dist/commands/clipboard-daemon.d.ts.map +0 -1
  69. package/dist/commands/clipboard-server.d.ts.map +0 -1
  70. package/dist/commands/clipboard.d.ts.map +0 -1
  71. package/dist/commands/daemon.d.ts.map +0 -1
  72. package/dist/commands/diff.d.ts.map +0 -1
  73. package/dist/commands/dns.d.ts.map +0 -1
  74. package/dist/commands/doctor.d.ts.map +0 -1
  75. package/dist/commands/heal-daemon.d.ts.map +0 -1
  76. package/dist/commands/heal.d.ts.map +0 -1
  77. package/dist/commands/install-claude.d.ts.map +0 -1
  78. package/dist/commands/install-tailscale.d.ts.map +0 -1
  79. package/dist/commands/manifest.d.ts.map +0 -1
  80. package/dist/commands/notifications.d.ts.map +0 -1
  81. package/dist/commands/ports.d.ts.map +0 -1
  82. package/dist/commands/runtime.d.ts.map +0 -1
  83. package/dist/commands/screen.d.ts.map +0 -1
  84. package/dist/commands/self-test.d.ts.map +0 -1
  85. package/dist/commands/serve.d.ts.map +0 -1
  86. package/dist/commands/setup.d.ts.map +0 -1
  87. package/dist/commands/ssh.d.ts.map +0 -1
  88. package/dist/commands/status.d.ts.map +0 -1
  89. package/dist/commands/sync.d.ts.map +0 -1
  90. package/dist/commands/workspace.d.ts.map +0 -1
  91. package/dist/compatibility.d.ts.map +0 -1
  92. package/dist/consumer-schema.d.ts.map +0 -1
  93. package/dist/consumer.d.ts.map +0 -1
  94. package/dist/cross-project-types.d.ts.map +0 -1
  95. package/dist/db.d.ts.map +0 -1
  96. package/dist/index.d.ts.map +0 -1
  97. package/dist/manifests.d.ts.map +0 -1
  98. package/dist/mcp/http.d.ts.map +0 -1
  99. package/dist/mcp/index.d.ts.map +0 -1
  100. package/dist/mcp/server.d.ts.map +0 -1
  101. package/dist/paths.d.ts.map +0 -1
  102. package/dist/pg-migrations.d.ts.map +0 -1
  103. package/dist/redaction.d.ts.map +0 -1
  104. package/dist/remote-storage.d.ts.map +0 -1
  105. package/dist/remote.d.ts.map +0 -1
  106. package/dist/storage-sync.d.ts.map +0 -1
  107. package/dist/storage.d.ts.map +0 -1
  108. package/dist/topology.d.ts.map +0 -1
  109. package/dist/types.d.ts.map +0 -1
  110. 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
+ }
12666
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
+ }
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,15 +14260,15 @@ 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";
14271
+ import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync5, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
13732
14272
  import { delimiter, dirname as dirname4 } from "path";
13733
14273
  import { platform as osPlatform } from "os";
13734
14274
  var DEFAULT_SERVICE_NAME = "machines-agent";
@@ -14166,7 +14706,7 @@ function bunRuntimeCandidates(executable) {
14166
14706
  }
14167
14707
  function isBunShebangScript(executable) {
14168
14708
  try {
14169
- const content = readFileSync4(executable, "utf8").slice(0, 256);
14709
+ const content = readFileSync5(executable, "utf8").slice(0, 256);
14170
14710
  const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
14171
14711
  return /^#!.*\bbun\b/.test(firstLine2);
14172
14712
  } catch {
@@ -14174,7 +14714,7 @@ function isBunShebangScript(executable) {
14174
14714
  }
14175
14715
  }
14176
14716
  function isExecutableFile(path) {
14177
- if (!existsSync6(path))
14717
+ if (!existsSync7(path))
14178
14718
  return false;
14179
14719
  try {
14180
14720
  const stats = statSync(path);
@@ -14352,12 +14892,12 @@ function parseProbe(tool, stdout) {
14352
14892
  }
14353
14893
  function buildClaudeInstallPlan(machineId, tools) {
14354
14894
  const machine = resolveMachine2(machineId);
14355
- return {
14895
+ return attachMutationPlanDigest({
14356
14896
  machineId: machine.id,
14357
14897
  mode: "plan",
14358
14898
  steps: buildInstallSteps(machine, tools),
14359
14899
  executed: 0
14360
- };
14900
+ });
14361
14901
  }
14362
14902
  function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
14363
14903
  const machine = resolveMachine2(machineId);
@@ -14382,8 +14922,12 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
14382
14922
  }
14383
14923
  function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
14384
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);
14385
14929
  if (!options.apply)
14386
- return plan;
14930
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
14387
14931
  if (!options.yes) {
14388
14932
  throw new Error("Claude CLI installation requires --yes.");
14389
14933
  }
@@ -14392,12 +14936,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
14392
14936
  requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
14393
14937
  executed += 1;
14394
14938
  }
14395
- return {
14939
+ return attachMutationPlanDigest({
14396
14940
  machineId: plan.machineId,
14397
14941
  mode: "apply",
14398
14942
  steps: plan.steps,
14399
14943
  executed
14400
- };
14944
+ });
14401
14945
  }
14402
14946
  // src/commands/install-tailscale.ts
14403
14947
  function buildInstallSteps2(machine) {
@@ -14436,17 +14980,21 @@ function buildTailscaleInstallPlan(machineId) {
14436
14980
  if (!machine) {
14437
14981
  throw new Error(`Machine not found in manifest: ${machineId}`);
14438
14982
  }
14439
- return {
14983
+ return attachMutationPlanDigest({
14440
14984
  machineId: machine.id,
14441
14985
  mode: "plan",
14442
14986
  steps: buildInstallSteps2(machine),
14443
14987
  executed: 0
14444
- };
14988
+ });
14445
14989
  }
14446
14990
  function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
14447
14991
  const plan = buildTailscaleInstallPlan(machineId);
14992
+ return runTailscaleInstallPlan(plan, options, runner);
14993
+ }
14994
+ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
14995
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
14448
14996
  if (!options.apply)
14449
- return plan;
14997
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
14450
14998
  if (!options.yes) {
14451
14999
  throw new Error("Tailscale install requires --yes.");
14452
15000
  }
@@ -14455,19 +15003,21 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
14455
15003
  requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
14456
15004
  executed += 1;
14457
15005
  }
14458
- return {
15006
+ return attachMutationPlanDigest({
14459
15007
  machineId: plan.machineId,
14460
15008
  mode: "apply",
14461
15009
  steps: plan.steps,
14462
15010
  executed
14463
- };
15011
+ });
14464
15012
  }
14465
15013
  // src/commands/notifications.ts
14466
- 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";
14467
15016
  var notificationChannelSchema = exports_external.object({
14468
15017
  id: exports_external.string(),
14469
15018
  type: exports_external.enum(["email", "webhook", "command"]),
14470
15019
  target: exports_external.string(),
15020
+ commandArgs: exports_external.array(exports_external.string()).optional(),
14471
15021
  events: exports_external.array(exports_external.string()),
14472
15022
  enabled: exports_external.boolean()
14473
15023
  });
@@ -14476,19 +15026,31 @@ var notificationConfigSchema = exports_external.object({
14476
15026
  updatedAt: exports_external.string().optional(),
14477
15027
  channels: exports_external.array(notificationChannelSchema)
14478
15028
  });
15029
+ var trustedNotificationApproval = Symbol("trustedNotificationApproval");
15030
+ function createTrustedNotificationApproval() {
15031
+ return { [trustedNotificationApproval]: true };
15032
+ }
15033
+ function isTrustedNotificationApproval(approval) {
15034
+ return approval?.[trustedNotificationApproval] === true;
15035
+ }
14479
15036
  function sortChannels(channels) {
14480
15037
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
14481
15038
  }
14482
- function shellQuote6(value) {
14483
- return `'${value.replace(/'/g, `'\\''`)}'`;
14484
- }
14485
15039
  function hasCommand2(binary) {
14486
- const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
14487
- stdout: "ignore",
14488
- stderr: "ignore",
14489
- env: process.env
14490
- });
14491
- 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;
14492
15054
  }
14493
15055
  function buildNotificationPreview(channel, event, message) {
14494
15056
  if (channel.type === "email") {
@@ -14497,7 +15059,8 @@ function buildNotificationPreview(channel, event, message) {
14497
15059
  if (channel.type === "webhook") {
14498
15060
  return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
14499
15061
  }
14500
- 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`;
14501
15064
  }
14502
15065
  async function dispatchEmail(channel, event, message) {
14503
15066
  const subject = `[${event}] machines notification`;
@@ -14508,7 +15071,7 @@ Content-Type: text/plain; charset=utf-8
14508
15071
  ${message}
14509
15072
  `;
14510
15073
  if (hasCommand2("sendmail")) {
14511
- const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
15074
+ const result = Bun.spawnSync(["sendmail", "-t"], {
14512
15075
  stdin: new TextEncoder().encode(body),
14513
15076
  stdout: "pipe",
14514
15077
  stderr: "pipe",
@@ -14526,8 +15089,9 @@ ${message}
14526
15089
  };
14527
15090
  }
14528
15091
  if (hasCommand2("mail")) {
14529
- const command2 = `printf %s ${shellQuote6(message)} | mail -s ${shellQuote6(subject)} ${shellQuote6(channel.target)}`;
14530
- const result = Bun.spawnSync(["bash", "-lc", command2], {
15092
+ const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
15093
+ stdin: new TextEncoder().encode(`${message}
15094
+ `),
14531
15095
  stdout: "pipe",
14532
15096
  stderr: "pipe",
14533
15097
  env: process.env
@@ -14570,8 +15134,20 @@ async function dispatchWebhook(channel, event, message) {
14570
15134
  detail: `Webhook accepted with HTTP ${response.status}`
14571
15135
  };
14572
15136
  }
14573
- async function dispatchCommand(channel, event, message) {
14574
- 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 ?? []], {
14575
15151
  stdout: "pipe",
14576
15152
  stderr: "pipe",
14577
15153
  env: {
@@ -14593,7 +15169,7 @@ async function dispatchCommand(channel, event, message) {
14593
15169
  detail: stdout || "Command completed successfully"
14594
15170
  };
14595
15171
  }
14596
- async function dispatchChannel(channel, event, message) {
15172
+ async function dispatchChannel(channel, event, message, options = {}) {
14597
15173
  if (!channel.enabled) {
14598
15174
  return {
14599
15175
  channelId: channel.id,
@@ -14609,7 +15185,7 @@ async function dispatchChannel(channel, event, message) {
14609
15185
  if (channel.type === "webhook") {
14610
15186
  return dispatchWebhook(channel, event, message);
14611
15187
  }
14612
- return dispatchCommand(channel, event, message);
15188
+ return dispatchCommand(channel, event, message, options);
14613
15189
  }
14614
15190
  function getDefaultNotificationConfig() {
14615
15191
  return {
@@ -14619,10 +15195,10 @@ function getDefaultNotificationConfig() {
14619
15195
  };
14620
15196
  }
14621
15197
  function readNotificationConfig(path = getNotificationsPath()) {
14622
- if (!existsSync7(path)) {
15198
+ if (!existsSync8(path)) {
14623
15199
  return getDefaultNotificationConfig();
14624
15200
  }
14625
- return notificationConfigSchema.parse(JSON.parse(readFileSync5(path, "utf8")));
15201
+ return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
14626
15202
  }
14627
15203
  function writeNotificationConfig(config, path = getNotificationsPath()) {
14628
15204
  ensureParentDir(path);
@@ -14638,11 +15214,20 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
14638
15214
  function listNotificationChannels() {
14639
15215
  return readNotificationConfig();
14640
15216
  }
14641
- 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
+ }
14642
15226
  const config = readNotificationConfig();
14643
15227
  const channels = config.channels.filter((entry) => entry.id !== channel.id);
14644
15228
  channels.push({
14645
15229
  ...channel,
15230
+ commandArgs: channel.commandArgs?.map(String),
14646
15231
  events: [...new Set(channel.events)]
14647
15232
  });
14648
15233
  return writeNotificationConfig({ ...config, channels });
@@ -14664,7 +15249,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
14664
15249
  const deliveries = [];
14665
15250
  for (const channel of channels) {
14666
15251
  try {
14667
- deliveries.push(await dispatchChannel(channel, event, message));
15252
+ deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
14668
15253
  } catch (error) {
14669
15254
  deliveries.push({
14670
15255
  channelId: channel.id,
@@ -14699,7 +15284,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
14699
15284
  if (!options.yes) {
14700
15285
  throw new Error("Notification test execution requires --yes.");
14701
15286
  }
14702
- const delivery = await dispatchChannel(channel, event, message);
15287
+ const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
14703
15288
  return {
14704
15289
  channelId,
14705
15290
  mode: "apply",
@@ -14765,6 +15350,46 @@ function listPorts(machineId) {
14765
15350
  import { spawnSync as spawnSync4 } from "child_process";
14766
15351
  import { setTimeout as sleep2 } from "timers/promises";
14767
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
+ }
14768
15393
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
14769
15394
  const checkedAt = new Date().toISOString();
14770
15395
  const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
@@ -14850,7 +15475,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
14850
15475
  return client.emit(input, { deliver });
14851
15476
  }
14852
15477
  // src/commands/serve.ts
14853
- 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";
14854
15480
 
14855
15481
  // src/commands/status.ts
14856
15482
  function parseJsonObject2(value) {
@@ -14905,7 +15531,7 @@ function escapeHtml(value) {
14905
15531
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
14906
15532
  }
14907
15533
  function getServeInfo(options = {}) {
14908
- const host = options.host || "0.0.0.0";
15534
+ const host = options.host || "127.0.0.1";
14909
15535
  const port = options.port || 7676;
14910
15536
  return {
14911
15537
  host,
@@ -15108,6 +15734,59 @@ async function parseJsonBody(request) {
15108
15734
  function jsonError(message, status = 400) {
15109
15735
  return Response.json({ error: message }, { status });
15110
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
+ }
15111
15790
  function privateOutputWarnings(requested, allowed) {
15112
15791
  return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
15113
15792
  }
@@ -15119,6 +15798,7 @@ function appendWarnings(payload, warnings) {
15119
15798
  function startDashboardServer(options = {}) {
15120
15799
  const info = getServeInfo(options);
15121
15800
  const events = new EventsClient2;
15801
+ const trustedNotificationApproval2 = createTrustedNotificationApproval();
15122
15802
  return Bun.serve({
15123
15803
  hostname: info.host,
15124
15804
  port: info.port,
@@ -15183,8 +15863,25 @@ function startDashboardServer(options = {}) {
15183
15863
  const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
15184
15864
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
15185
15865
  const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
15186
- const data = body["data"] && typeof body["data"] === "object" && !Array.isArray(body["data"]) ? body["data"] : {};
15187
- 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;
15188
15885
  return Response.json(await events.emit({
15189
15886
  source,
15190
15887
  type,
@@ -15227,8 +15924,20 @@ function startDashboardServer(options = {}) {
15227
15924
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
15228
15925
  const apply = body["apply"] === true;
15229
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;
15230
15935
  try {
15231
- 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
+ }));
15232
15941
  } catch (error) {
15233
15942
  return jsonError(error instanceof Error ? error.message : String(error));
15234
15943
  }
@@ -15243,13 +15952,22 @@ function startDashboardServer(options = {}) {
15243
15952
  return jsonError("channelId is required.");
15244
15953
  }
15245
15954
  const type = typeof body["type"] === "string" ? body["type"] : "events.test";
15246
- 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;
15247
15964
  try {
15248
15965
  return Response.json(await events.testChannel(channelId, {
15249
15966
  source: "machines",
15250
15967
  type,
15251
- subject: channelId,
15252
- message
15968
+ subject,
15969
+ message,
15970
+ data
15253
15971
  }));
15254
15972
  } catch (error) {
15255
15973
  return jsonError(error instanceof Error ? error.message : String(error));
@@ -15397,17 +16115,21 @@ function buildSetupPlan(machineId) {
15397
16115
  workspacePath: `${homedir4()}/workspace`
15398
16116
  };
15399
16117
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
15400
- return {
16118
+ return attachMutationPlanDigest({
15401
16119
  machineId: target.id,
15402
16120
  mode: "plan",
15403
16121
  steps,
15404
16122
  executed: 0
15405
- };
16123
+ });
15406
16124
  }
15407
16125
  function runSetup(machineId, options = {}, runner = runMachineCommand) {
15408
16126
  const plan = buildSetupPlan(machineId);
16127
+ return runSetupPlan(plan, options, runner);
16128
+ }
16129
+ function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
16130
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
15409
16131
  if (!options.apply) {
15410
- return plan;
16132
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
15411
16133
  }
15412
16134
  if (!options.yes) {
15413
16135
  throw new Error("Setup execution requires --yes.");
@@ -15428,12 +16150,12 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
15428
16150
  }
15429
16151
  executed += 1;
15430
16152
  }
15431
- const summary = {
16153
+ const summary = attachMutationPlanDigest({
15432
16154
  machineId: plan.machineId,
15433
16155
  mode: "apply",
15434
16156
  steps: plan.steps,
15435
16157
  executed
15436
- };
16158
+ });
15437
16159
  recordSetupRun(plan.machineId, "completed", summary);
15438
16160
  return summary;
15439
16161
  }
@@ -15552,16 +16274,22 @@ function buildScreenEnableCommand(machineId, options = {}) {
15552
16274
  }
15553
16275
  const secretsCommand = options.secretsCommand || "secrets";
15554
16276
  const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
16277
+ const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
16278
+ const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
15555
16279
  return {
15556
16280
  machineId: credentials.machineId,
15557
16281
  user: credentials.user,
15558
16282
  passwordSecretKey: credentials.passwordSecretKey,
15559
16283
  remoteCommand,
15560
- 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}`
15561
16289
  };
15562
16290
  }
15563
16291
  // src/commands/sync.ts
15564
- 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";
15565
16293
  import { homedir as homedir5 } from "os";
15566
16294
  function quote4(value) {
15567
16295
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -15614,15 +16342,15 @@ function detectFileActions(machine) {
15614
16342
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
15615
16343
  }
15616
16344
  return (machine.files || []).map((file, index) => {
15617
- const sourceExists = existsSync8(file.source);
15618
- const targetExists = existsSync8(file.target);
16345
+ const sourceExists = existsSync9(file.source);
16346
+ const targetExists = existsSync9(file.target);
15619
16347
  let status = "missing";
15620
16348
  if (sourceExists && targetExists) {
15621
16349
  if (file.mode === "symlink") {
15622
16350
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
15623
16351
  } else {
15624
- const source = readFileSync6(file.source, "utf8");
15625
- const target = readFileSync6(file.target, "utf8");
16352
+ const source = readFileSync7(file.source, "utf8");
16353
+ const target = readFileSync7(file.target, "utf8");
15626
16354
  status = source === target ? "ok" : "drifted";
15627
16355
  }
15628
16356
  }
@@ -15652,12 +16380,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
15652
16380
  ...detectPackageActions(target, runner),
15653
16381
  ...detectFileActions(target)
15654
16382
  ];
15655
- return {
16383
+ return attachMutationPlanDigest({
15656
16384
  machineId: target.id,
15657
16385
  mode: "plan",
15658
16386
  actions,
15659
16387
  executed: 0
15660
- };
16388
+ });
15661
16389
  }
15662
16390
  function applyFileAction(command2) {
15663
16391
  const [verb, source, target] = command2.split(" ");
@@ -15681,8 +16409,12 @@ function applyFileAction(command2) {
15681
16409
  }
15682
16410
  function runSync(machineId, options = {}, runner = runMachineCommand) {
15683
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);
15684
16416
  if (!options.apply) {
15685
- return plan;
16417
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
15686
16418
  }
15687
16419
  if (!options.yes) {
15688
16420
  throw new Error("Sync execution requires --yes.");
@@ -15709,12 +16441,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
15709
16441
  }
15710
16442
  executed += 1;
15711
16443
  }
15712
- const summary = {
16444
+ const summary = attachMutationPlanDigest({
15713
16445
  machineId: plan.machineId,
15714
16446
  mode: "apply",
15715
16447
  actions: plan.actions,
15716
16448
  executed
15717
- };
16449
+ });
15718
16450
  recordSyncRun(plan.machineId, "completed", summary);
15719
16451
  return summary;
15720
16452
  }
@@ -19194,7 +19926,7 @@ class JSONSchemaGenerator {
19194
19926
  if (val === undefined) {
19195
19927
  if (this.unrepresentable === "throw") {
19196
19928
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
19197
- }
19929
+ } else {}
19198
19930
  } else if (typeof val === "bigint") {
19199
19931
  if (this.unrepresentable === "throw") {
19200
19932
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -23099,7 +23831,7 @@ class Protocol {
23099
23831
  return;
23100
23832
  }
23101
23833
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
23102
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
23834
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
23103
23835
  options?.signal?.throwIfAborted();
23104
23836
  }
23105
23837
  } catch (error2) {
@@ -23111,7 +23843,7 @@ class Protocol {
23111
23843
  }
23112
23844
  request(request, resultSchema, options) {
23113
23845
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
23114
- return new Promise((resolve2, reject) => {
23846
+ return new Promise((resolve4, reject) => {
23115
23847
  const earlyReject = (error2) => {
23116
23848
  reject(error2);
23117
23849
  };
@@ -23189,7 +23921,7 @@ class Protocol {
23189
23921
  if (!parseResult.success) {
23190
23922
  reject(parseResult.error);
23191
23923
  } else {
23192
- resolve2(parseResult.data);
23924
+ resolve4(parseResult.data);
23193
23925
  }
23194
23926
  } catch (error2) {
23195
23927
  reject(error2);
@@ -23380,12 +24112,12 @@ class Protocol {
23380
24112
  interval = task.pollInterval;
23381
24113
  }
23382
24114
  } catch {}
23383
- return new Promise((resolve2, reject) => {
24115
+ return new Promise((resolve4, reject) => {
23384
24116
  if (signal.aborted) {
23385
24117
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
23386
24118
  return;
23387
24119
  }
23388
- const timeoutId = setTimeout(resolve2, interval);
24120
+ const timeoutId = setTimeout(resolve4, interval);
23389
24121
  signal.addEventListener("abort", () => {
23390
24122
  clearTimeout(timeoutId);
23391
24123
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -24239,7 +24971,7 @@ class McpServer {
24239
24971
  let task = createTaskResult.task;
24240
24972
  const pollInterval = task.pollInterval ?? 5000;
24241
24973
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
24242
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
24974
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
24243
24975
  const updatedTask = await extra.taskStore.getTask(taskId);
24244
24976
  if (!updatedTask) {
24245
24977
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -24838,8 +25570,8 @@ var MACHINE_MCP_TOOL_NAMES = [
24838
25570
  "storage_pull",
24839
25571
  "storage_sync"
24840
25572
  ];
24841
- function buildServer(version2 = getPackageVersion()) {
24842
- return createMcpServer(version2);
25573
+ function buildServer(version2 = getPackageVersion(), options = {}) {
25574
+ return createMcpServer(version2, options);
24843
25575
  }
24844
25576
  function privateMetadataAllowed(requested) {
24845
25577
  return requested === true && isPrivateOutputEnabled();
@@ -24853,9 +25585,50 @@ function appendWarnings2(payload, warnings) {
24853
25585
  const currentWarnings = typeof payload === "object" && payload && "warnings" in payload && Array.isArray(payload.warnings) ? payload.warnings : [];
24854
25586
  return { ...payload, warnings: [...currentWarnings, ...warnings] };
24855
25587
  }
24856
- 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 = {}) {
24857
25625
  const server = new McpServer({ name: "machines", version: version2 });
24858
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
+ }
24859
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 }) => {
24860
25633
  const privateMetadata = privateMetadataAllowed(private_metadata);
24861
25634
  const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
@@ -24869,18 +25642,31 @@ function createMcpServer(version2) {
24869
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) }] }));
24870
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) }] }));
24871
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) }] }));
24872
- 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
+ });
24873
25655
  server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
24874
25656
  content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
24875
25657
  }));
24876
25658
  server.tool("machines_manifest_validate", "Validate the current fleet manifest.", {}, async () => ({
24877
25659
  content: [{ type: "text", text: JSON.stringify(manifestValidate(), null, 2) }]
24878
25660
  }));
24879
- server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () => ({
24880
- content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }]
24881
- }));
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
+ });
24882
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) }] }));
24883
- 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
+ });
24884
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 }) => {
24885
25671
  const privateMetadata = privateMetadataAllowed(private_metadata);
24886
25672
  const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
@@ -24932,9 +25718,27 @@ function createMcpServer(version2) {
24932
25718
  }]
24933
25719
  }));
24934
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) }] }));
24935
- 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
+ });
24936
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) }] }));
24937
- 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
+ });
24938
25742
  server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
24939
25743
  include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
24940
25744
  private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
@@ -24989,12 +25793,31 @@ function createMcpServer(version2) {
24989
25793
  server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
24990
25794
  machine_id: exports_external.string().optional().describe("Machine identifier"),
24991
25795
  tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
24992
- yes: exports_external.boolean().describe("Confirmation flag for execution")
24993
- }, async ({ machine_id, tools, yes }) => ({
24994
- content: [{ type: "text", text: JSON.stringify(runClaudeInstall(machine_id, tools, { apply: true, yes }), null, 2) }]
24995
- }));
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
+ });
24996
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) }] }));
24997
- 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
+ });
24998
25821
  server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
24999
25822
  machine_id: exports_external.string().describe("Machine identifier"),
25000
25823
  include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
@@ -25062,41 +25885,72 @@ function createMcpServer(version2) {
25062
25885
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
25063
25886
  }));
25064
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) }] }));
25065
- 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
+ });
25066
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) }] }));
25067
- 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) }] }));
25068
- 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
+ });
25069
25902
  server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
25070
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) }] }));
25071
25904
  server.tool("machines_notifications_add", "Add or replace a notification channel.", {
25072
25905
  channel_id: exports_external.string().describe("Channel identifier"),
25073
25906
  type: exports_external.enum(["email", "webhook", "command"]).describe("Notification transport"),
25074
- 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"),
25075
25909
  events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
25076
- enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
25077
- }, async ({ channel_id, type, target, events: events2, enabled }) => ({
25078
- content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events: events2, enabled: enabled ?? true }), null, 2) }]
25079
- }));
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
+ });
25080
25921
  server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
25081
25922
  content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
25082
25923
  }));
25083
- 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 }) => ({
25084
- content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2) }]
25085
- }));
25086
- 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) }] }));
25087
- 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
+ });
25088
25939
  server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
25089
25940
  channel_id: exports_external.string().describe("Channel identifier"),
25090
25941
  url: exports_external.string().url().describe("Webhook URL"),
25091
25942
  event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
25092
25943
  source: exports_external.string().optional().describe("Optional source filter"),
25093
25944
  secret: exports_external.string().optional().describe("Optional HMAC secret"),
25094
- enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
25095
- }, 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 } });
25096
25950
  const now = new Date().toISOString();
25097
25951
  const channel = await events.addChannel({
25098
25952
  id: channel_id,
25099
- enabled: enabled ?? true,
25953
+ enabled: resolvedEnabled,
25100
25954
  transport: "webhook",
25101
25955
  filters: event_type || source ? [{ type: event_type, source }] : undefined,
25102
25956
  webhook: { url, secret },
@@ -25108,10 +25962,16 @@ function createMcpServer(version2) {
25108
25962
  server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
25109
25963
  content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
25110
25964
  }));
25111
- 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 }) => ({
25112
- content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
25113
- }));
25114
- 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
+ });
25115
25975
  server.tool("machines_events_emit", "Emit a shared event from machines.", {
25116
25976
  event_type: exports_external.string().describe("Event type"),
25117
25977
  subject: exports_external.string().optional().describe("Event subject"),
@@ -25120,25 +25980,36 @@ function createMcpServer(version2) {
25120
25980
  data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
25121
25981
  metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
25122
25982
  dedupe_key: exports_external.string().optional().describe("Dedupe key"),
25123
- deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
25124
- }, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver }) => ({
25125
- content: [{ type: "text", text: JSON.stringify(await events.emit({
25126
- source: "machines",
25127
- type: event_type,
25128
- subject,
25129
- severity,
25130
- message,
25131
- data: data ?? {},
25132
- metadata: metadata ?? {},
25133
- dedupeKey: dedupe_key
25134
- }, { deliver: deliver !== false }), null, 2) }]
25135
- }));
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
+ });
25136
26003
  server.tool("machines_events_list", "List shared events.", {}, async () => ({
25137
26004
  content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
25138
26005
  }));
25139
- 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 }) => ({
25140
- content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
25141
- }));
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
+ });
25142
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) }] }));
25143
26014
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
25144
26015
  content: [{ type: "text", text: renderDashboardHtml() }]
@@ -25146,9 +26017,21 @@ function createMcpServer(version2) {
25146
26017
  server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
25147
26018
  content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
25148
26019
  }));
25149
- 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) }] }));
25150
- 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) }] }));
25151
- 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
+ });
25152
26035
  return server;
25153
26036
  }
25154
26037
  export {
@@ -25157,6 +26040,8 @@ export {
25157
26040
  writeHeartbeatTick,
25158
26041
  writeHeartbeat,
25159
26042
  watchTmuxPane,
26043
+ verifyMutationApprovalToken,
26044
+ validateSshTarget,
25160
26045
  validateManifest,
25161
26046
  upsertHeartbeat,
25162
26047
  testNotificationChannel,
@@ -25166,16 +26051,21 @@ export {
25166
26051
  startDashboardServer,
25167
26052
  setHeartbeatStatus,
25168
26053
  sanitizePublicString,
26054
+ runTailscaleInstallPlan,
25169
26055
  runTailscaleInstall,
26056
+ runSyncPlan,
25170
26057
  runSync,
25171
26058
  runStorageMigrations,
26059
+ runSetupPlan,
25172
26060
  runSetup,
25173
26061
  runSelfTest,
25174
26062
  runDoctor,
25175
26063
  runDaemonServicePlan,
26064
+ runClaudeInstallPlan,
25176
26065
  runClaudeInstall,
25177
26066
  runCertPlan,
25178
26067
  runBackup,
26068
+ runAppsPlan,
25179
26069
  runAppsInstall,
25180
26070
  resolveTables,
25181
26071
  resolveSshTarget,
@@ -25209,6 +26099,8 @@ export {
25209
26099
  probeTmuxPane,
25210
26100
  parseStorageTables,
25211
26101
  parsePortOutput,
26102
+ mutationPlanDigest,
26103
+ mutationArgsSha256,
25212
26104
  markOffline,
25213
26105
  manifestValidate,
25214
26106
  manifestRemove,
@@ -25227,6 +26119,7 @@ export {
25227
26119
  isSensitiveKey,
25228
26120
  isPrivateOutputEnabled,
25229
26121
  isPrivateMetadataEnabled,
26122
+ isMutationApproved,
25230
26123
  getSyncMetaAll,
25231
26124
  getStorageStatus,
25232
26125
  getStoragePg,
@@ -25265,13 +26158,18 @@ export {
25265
26158
  diffApps,
25266
26159
  detectCurrentMachineManifest,
25267
26160
  defaultScreenPasswordSecretKey,
26161
+ createMutationApprovalToken,
25268
26162
  createMcpServer,
25269
26163
  createMachineResolverSnapshot,
25270
26164
  countRuns,
25271
26165
  closeDb,
25272
26166
  checkMachineCompatibility,
26167
+ canonicalMutationArgs,
26168
+ buildTmuxPaneDiedHookPlan,
25273
26169
  buildTailscaleInstallPlan,
25274
26170
  buildSyncPlan,
26171
+ buildSshCommandPlan,
26172
+ buildSshCommandArgs,
25275
26173
  buildSshCommand,
25276
26174
  buildSetupPlan,
25277
26175
  buildServer,
@@ -25289,6 +26187,9 @@ export {
25289
26187
  buildCertPlan,
25290
26188
  buildBackupPlan,
25291
26189
  buildAppsPlan,
26190
+ attachMutationPlanDigest,
26191
+ assertMutationPlanDigest,
26192
+ assertMutationApproved,
25292
26193
  addNotificationChannel,
25293
26194
  addDomainMapping,
25294
26195
  SqliteAdapter,
@@ -25304,6 +26205,11 @@ export {
25304
26205
  PRIVATE_METADATA_ENV,
25305
26206
  PRIVATE_MANIFEST_REF_ENV,
25306
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,
25307
26213
  MACHINE_MCP_TOOL_NAMES,
25308
26214
  MACHINES_STORAGE_TABLES,
25309
26215
  MACHINES_STORAGE_MODE_FALLBACK_ENV,
@@ -25322,6 +26228,7 @@ export {
25322
26228
  MACHINES_BACKUP_PREFIX_ENV,
25323
26229
  MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
25324
26230
  MACHINES_BACKUP_BUCKET_ENV,
26231
+ LEGACY_MUTATION_APPROVAL_FLAG_ENV,
25325
26232
  DOCTOR_OPTIONAL_ADAPTER_DOMAINS,
25326
26233
  DEFAULT_SCREEN_SECRET_NAMESPACE,
25327
26234
  DEFAULT_MACHINE_RESOLVER_TTL_MS,