@hasna/machines 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -4
- package/dist/agent/index.d.ts +0 -1
- package/dist/agent/index.js +249 -14
- package/dist/agent/runtime.d.ts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +1316 -213
- package/dist/cli-utils.d.ts +0 -1
- package/dist/commands/apps.d.ts +7 -5
- package/dist/commands/backup.d.ts +0 -1
- package/dist/commands/cert.d.ts +0 -1
- package/dist/commands/clipboard-daemon.d.ts +0 -1
- package/dist/commands/clipboard-server.d.ts +0 -1
- package/dist/commands/clipboard.d.ts +0 -1
- package/dist/commands/daemon.d.ts +0 -1
- package/dist/commands/diff.d.ts +0 -1
- package/dist/commands/dns.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/heal-daemon.d.ts +0 -1
- package/dist/commands/heal.d.ts +0 -1
- package/dist/commands/install-claude.d.ts +5 -3
- package/dist/commands/install-tailscale.d.ts +5 -3
- package/dist/commands/manifest.d.ts +0 -1
- package/dist/commands/mutation-approval.d.ts +54 -0
- package/dist/commands/notifications.d.ts +14 -2
- package/dist/commands/ports.d.ts +0 -1
- package/dist/commands/runtime.d.ts +15 -1
- package/dist/commands/screen.d.ts +4 -1
- package/dist/commands/self-test.d.ts +0 -1
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/setup.d.ts +5 -3
- package/dist/commands/ssh.d.ts +8 -1
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/sync.d.ts +5 -3
- package/dist/commands/workspace.d.ts +0 -1
- package/dist/compatibility.d.ts +0 -1
- package/dist/consumer-schema.d.ts +0 -1
- package/dist/consumer.d.ts +0 -1
- package/dist/consumer.js +253 -12
- package/dist/cross-project-types.d.ts +0 -1
- package/dist/db.d.ts +0 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1108 -189
- package/dist/manifests.d.ts +0 -1
- package/dist/mcp/http.d.ts +26 -2
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.js +1021 -167
- package/dist/mcp/server.d.ts +5 -3
- package/dist/paths.d.ts +0 -1
- package/dist/pg-migrations.d.ts +0 -1
- package/dist/redaction.d.ts +0 -1
- package/dist/remote-storage.d.ts +0 -1
- package/dist/remote.d.ts +14 -5
- package/dist/storage-sync.d.ts +0 -1
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +18 -0
- package/dist/topology.d.ts +0 -1
- package/dist/types.d.ts +3 -1
- package/dist/version.d.ts +0 -1
- package/package.json +5 -3
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/runtime.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli-utils.d.ts.map +0 -1
- package/dist/commands/apps.d.ts.map +0 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/cert.d.ts.map +0 -1
- package/dist/commands/clipboard-daemon.d.ts.map +0 -1
- package/dist/commands/clipboard-server.d.ts.map +0 -1
- package/dist/commands/clipboard.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/dns.d.ts.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/heal-daemon.d.ts.map +0 -1
- package/dist/commands/heal.d.ts.map +0 -1
- package/dist/commands/install-claude.d.ts.map +0 -1
- package/dist/commands/install-tailscale.d.ts.map +0 -1
- package/dist/commands/manifest.d.ts.map +0 -1
- package/dist/commands/notifications.d.ts.map +0 -1
- package/dist/commands/ports.d.ts.map +0 -1
- package/dist/commands/runtime.d.ts.map +0 -1
- package/dist/commands/screen.d.ts.map +0 -1
- package/dist/commands/self-test.d.ts.map +0 -1
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/ssh.d.ts.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/workspace.d.ts.map +0 -1
- package/dist/compatibility.d.ts.map +0 -1
- package/dist/consumer-schema.d.ts.map +0 -1
- package/dist/consumer.d.ts.map +0 -1
- package/dist/cross-project-types.d.ts.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/manifests.d.ts.map +0 -1
- package/dist/mcp/http.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/paths.d.ts.map +0 -1
- package/dist/pg-migrations.d.ts.map +0 -1
- package/dist/redaction.d.ts.map +0 -1
- package/dist/remote-storage.d.ts.map +0 -1
- package/dist/remote.d.ts.map +0 -1
- package/dist/storage-sync.d.ts.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/topology.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
517
|
-
this.code = optimizeExpr(this.code, names,
|
|
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,
|
|
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,
|
|
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,
|
|
609
|
+
optimizeNames(names, constants2) {
|
|
610
610
|
var _a;
|
|
611
|
-
this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names,
|
|
612
|
-
if (!(super.optimizeNames(names,
|
|
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,
|
|
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,
|
|
640
|
-
if (!super.optimizeNames(names,
|
|
639
|
+
optimizeNames(names, constants2) {
|
|
640
|
+
if (!super.optimizeNames(names, constants2))
|
|
641
641
|
return;
|
|
642
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
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,
|
|
681
|
-
if (!super.optimizeNames(names,
|
|
680
|
+
optimizeNames(names, constants2) {
|
|
681
|
+
if (!super.optimizeNames(names, constants2))
|
|
682
682
|
return;
|
|
683
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
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,
|
|
728
|
+
optimizeNames(names, constants2) {
|
|
729
729
|
var _a, _b;
|
|
730
|
-
super.optimizeNames(names,
|
|
731
|
-
(_a = this.catch) === null || _a === undefined || _a.optimizeNames(names,
|
|
732
|
-
(_b = this.finally) === null || _b === undefined || _b.optimizeNames(names,
|
|
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,
|
|
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 =
|
|
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 &&
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
12643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12664
|
-
exitCode: result.status ?? 1
|
|
12665
|
-
|
|
12726
|
+
stderr,
|
|
12727
|
+
exitCode: timedOut ? 124 : result.status ?? 1,
|
|
12728
|
+
timedOut,
|
|
12729
|
+
signal: result.signal
|
|
12730
|
+
};
|
|
12731
|
+
}
|
|
12732
|
+
function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
|
|
12733
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
|
|
12734
|
+
const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
|
|
12735
|
+
const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
|
|
12736
|
+
const pgidFile = join3(helperDir, "pgid");
|
|
12737
|
+
const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
|
|
12738
|
+
input: JSON.stringify({ command: resolved.command, args: resolved.args }),
|
|
12739
|
+
encoding: "utf8",
|
|
12740
|
+
env: {
|
|
12741
|
+
...process.env,
|
|
12742
|
+
HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
|
|
12743
|
+
HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
|
|
12744
|
+
HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
|
|
12745
|
+
},
|
|
12746
|
+
timeout: timeoutMs + killGraceMs + 2000,
|
|
12747
|
+
killSignal: "SIGKILL",
|
|
12748
|
+
maxBuffer: 64 * 1024 * 1024
|
|
12749
|
+
});
|
|
12750
|
+
try {
|
|
12751
|
+
const parsed = parseHelperResult(helper.stdout);
|
|
12752
|
+
if (parsed) {
|
|
12753
|
+
return {
|
|
12754
|
+
machineId,
|
|
12755
|
+
source: resolved.source,
|
|
12756
|
+
stdout: parsed.stdout,
|
|
12757
|
+
stderr: parsed.stderr,
|
|
12758
|
+
exitCode: parsed.exitCode,
|
|
12759
|
+
timedOut: parsed.timedOut,
|
|
12760
|
+
signal: parsed.signal
|
|
12761
|
+
};
|
|
12762
|
+
}
|
|
12763
|
+
const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
|
|
12764
|
+
if (helperTimedOut)
|
|
12765
|
+
killPublishedProcessGroup(pgidFile);
|
|
12766
|
+
const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
|
|
12767
|
+
const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
|
|
12768
|
+
` : "");
|
|
12769
|
+
return {
|
|
12770
|
+
machineId,
|
|
12771
|
+
source: resolved.source,
|
|
12772
|
+
stdout: "",
|
|
12773
|
+
stderr,
|
|
12774
|
+
exitCode: helperTimedOut ? 124 : helper.status ?? 1,
|
|
12775
|
+
timedOut: helperTimedOut,
|
|
12776
|
+
signal: helper.signal
|
|
12777
|
+
};
|
|
12778
|
+
} finally {
|
|
12779
|
+
rmSync(helperDir, { recursive: true, force: true });
|
|
12780
|
+
}
|
|
12781
|
+
}
|
|
12782
|
+
function killPublishedProcessGroup(pgidFile) {
|
|
12783
|
+
if (!existsSync5(pgidFile))
|
|
12784
|
+
return;
|
|
12785
|
+
try {
|
|
12786
|
+
const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
|
|
12787
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
12788
|
+
return;
|
|
12789
|
+
process.kill(-pid, "SIGKILL");
|
|
12790
|
+
} catch {}
|
|
12791
|
+
}
|
|
12792
|
+
function parseHelperResult(stdout) {
|
|
12793
|
+
if (!stdout)
|
|
12794
|
+
return null;
|
|
12795
|
+
try {
|
|
12796
|
+
const parsed = JSON.parse(stdout);
|
|
12797
|
+
if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
|
|
12798
|
+
return null;
|
|
12799
|
+
return {
|
|
12800
|
+
machineId: "",
|
|
12801
|
+
source: "local",
|
|
12802
|
+
stdout: parsed.stdout,
|
|
12803
|
+
stderr: parsed.stderr,
|
|
12804
|
+
exitCode: parsed.exitCode,
|
|
12805
|
+
timedOut: parsed.timedOut === true,
|
|
12806
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null
|
|
12807
|
+
};
|
|
12808
|
+
} catch {
|
|
12809
|
+
return null;
|
|
12810
|
+
}
|
|
12666
12811
|
}
|
|
12812
|
+
var PROCESS_GROUP_TIMEOUT_HELPER = `
|
|
12813
|
+
const { spawn } = require("node:child_process");
|
|
12814
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
12815
|
+
|
|
12816
|
+
const plan = JSON.parse(readFileSync(0, "utf8"));
|
|
12817
|
+
const command = String(plan.command || "");
|
|
12818
|
+
const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
|
|
12819
|
+
const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
|
|
12820
|
+
const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
|
|
12821
|
+
const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
|
|
12822
|
+
let stdout = "";
|
|
12823
|
+
let stderr = "";
|
|
12824
|
+
let timedOut = false;
|
|
12825
|
+
let finished = false;
|
|
12826
|
+
let timeoutTimer;
|
|
12827
|
+
let killTimer;
|
|
12828
|
+
let sigkillSent = false;
|
|
12829
|
+
let pendingExit = null;
|
|
12830
|
+
|
|
12831
|
+
const child = spawn(command, args, {
|
|
12832
|
+
detached: true,
|
|
12833
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12834
|
+
env: process.env,
|
|
12835
|
+
});
|
|
12836
|
+
|
|
12837
|
+
if (pgidFile && child.pid) {
|
|
12838
|
+
try {
|
|
12839
|
+
writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
|
|
12840
|
+
} catch {}
|
|
12841
|
+
}
|
|
12842
|
+
|
|
12843
|
+
function appendText(target, chunk) {
|
|
12844
|
+
return target + String(chunk);
|
|
12845
|
+
}
|
|
12846
|
+
|
|
12847
|
+
function killTarget(signal) {
|
|
12848
|
+
if (!child.pid) return;
|
|
12849
|
+
if (process.platform === "win32") {
|
|
12850
|
+
try {
|
|
12851
|
+
process.kill(child.pid, signal);
|
|
12852
|
+
} catch {}
|
|
12853
|
+
return;
|
|
12854
|
+
}
|
|
12855
|
+
try {
|
|
12856
|
+
process.kill(-child.pid, signal);
|
|
12857
|
+
} catch {}
|
|
12858
|
+
}
|
|
12859
|
+
|
|
12860
|
+
function finish(code, signal) {
|
|
12861
|
+
if (finished) return;
|
|
12862
|
+
if (timedOut && !sigkillSent) {
|
|
12863
|
+
pendingExit = { code, signal };
|
|
12864
|
+
return;
|
|
12865
|
+
}
|
|
12866
|
+
finished = true;
|
|
12867
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
12868
|
+
if (killTimer) clearTimeout(killTimer);
|
|
12869
|
+
if (timedOut) {
|
|
12870
|
+
stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12871
|
+
}
|
|
12872
|
+
const exitCode = timedOut ? 124 : code ?? 1;
|
|
12873
|
+
process.stdout.write(JSON.stringify({
|
|
12874
|
+
stdout,
|
|
12875
|
+
stderr,
|
|
12876
|
+
exitCode,
|
|
12877
|
+
timedOut,
|
|
12878
|
+
signal: signal ?? null,
|
|
12879
|
+
}), () => process.exit(exitCode));
|
|
12880
|
+
}
|
|
12881
|
+
|
|
12882
|
+
child.stdout.setEncoding("utf8");
|
|
12883
|
+
child.stderr.setEncoding("utf8");
|
|
12884
|
+
child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
|
|
12885
|
+
child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
|
|
12886
|
+
let childExit = { code: null, signal: null };
|
|
12887
|
+
child.on("error", (error) => {
|
|
12888
|
+
stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12889
|
+
finish(1, null);
|
|
12890
|
+
});
|
|
12891
|
+
child.on("exit", (code, signal) => {
|
|
12892
|
+
childExit = { code, signal };
|
|
12893
|
+
});
|
|
12894
|
+
child.on("close", (code, signal) => {
|
|
12895
|
+
finish(code ?? childExit.code, signal ?? childExit.signal);
|
|
12896
|
+
});
|
|
12897
|
+
|
|
12898
|
+
timeoutTimer = setTimeout(() => {
|
|
12899
|
+
timedOut = true;
|
|
12900
|
+
killTarget("SIGTERM");
|
|
12901
|
+
killTimer = setTimeout(() => {
|
|
12902
|
+
sigkillSent = true;
|
|
12903
|
+
killTarget("SIGKILL");
|
|
12904
|
+
if (pendingExit) finish(pendingExit.code, pendingExit.signal);
|
|
12905
|
+
}, killGraceMs);
|
|
12906
|
+
}, timeoutMs);
|
|
12907
|
+
`;
|
|
12667
12908
|
function describeMachineCommandFailure(operation, result) {
|
|
12668
12909
|
const detail = (result.stderr || result.stdout || "").trim();
|
|
12669
12910
|
const suffix = detail ? `: ${detail}` : "";
|
|
@@ -13361,7 +13602,7 @@ function getAgentStatus(machineId, options = {}) {
|
|
|
13361
13602
|
}
|
|
13362
13603
|
// src/commands/backup.ts
|
|
13363
13604
|
import { homedir as homedir2, hostname as hostname6 } from "os";
|
|
13364
|
-
import { join as
|
|
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
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
13624
|
-
const keyPath =
|
|
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
|
|
13688
|
-
import { join as
|
|
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
|
|
14230
|
+
return join6(getDataDir(), "dns.json");
|
|
13691
14231
|
}
|
|
13692
14232
|
function readMappings() {
|
|
13693
14233
|
const path = getDnsPath();
|
|
13694
|
-
if (!
|
|
14234
|
+
if (!existsSync6(path))
|
|
13695
14235
|
return [];
|
|
13696
|
-
return JSON.parse(
|
|
14236
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
13697
14237
|
}
|
|
13698
14238
|
function writeMappings(mappings) {
|
|
13699
14239
|
const path = getDnsPath();
|
|
@@ -13720,16 +14260,16 @@ function renderDomainMapping(domain) {
|
|
|
13720
14260
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
13721
14261
|
caddySnippet: `${entry.domain} {
|
|
13722
14262
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
13723
|
-
tls ${
|
|
14263
|
+
tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
13724
14264
|
}`,
|
|
13725
|
-
certPath:
|
|
13726
|
-
keyPath:
|
|
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
|
|
13732
|
-
import { dirname as dirname4 } from "path";
|
|
14271
|
+
import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync5, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
14272
|
+
import { delimiter, dirname as dirname4 } from "path";
|
|
13733
14273
|
import { platform as osPlatform } from "os";
|
|
13734
14274
|
var DEFAULT_SERVICE_NAME = "machines-agent";
|
|
13735
14275
|
var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
|
|
@@ -14142,19 +14682,31 @@ WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
|
|
|
14142
14682
|
`;
|
|
14143
14683
|
}
|
|
14144
14684
|
function daemonProgramArguments(options) {
|
|
14145
|
-
const bunRuntime =
|
|
14685
|
+
const bunRuntime = bunRuntimeForExecutable(options.executable);
|
|
14146
14686
|
const base = bunRuntime ? [bunRuntime, options.executable] : [options.executable];
|
|
14147
14687
|
return [...base, "--interval-ms", String(options.intervalMs)];
|
|
14148
14688
|
}
|
|
14149
|
-
function
|
|
14689
|
+
function bunRuntimeForExecutable(executable) {
|
|
14150
14690
|
if (!isBunShebangScript(executable))
|
|
14151
14691
|
return null;
|
|
14152
|
-
const candidate
|
|
14153
|
-
|
|
14692
|
+
for (const candidate of bunRuntimeCandidates(executable)) {
|
|
14693
|
+
if (isExecutableFile(candidate))
|
|
14694
|
+
return candidate;
|
|
14695
|
+
}
|
|
14696
|
+
return null;
|
|
14697
|
+
}
|
|
14698
|
+
function bunRuntimeCandidates(executable) {
|
|
14699
|
+
const candidates = [
|
|
14700
|
+
`${dirname4(executable)}/bun`,
|
|
14701
|
+
process.env["BUN_INSTALL"] ? `${process.env["BUN_INSTALL"]}/bin/bun` : null,
|
|
14702
|
+
process.env["HOME"] ? `${process.env["HOME"]}/.bun/bin/bun` : null,
|
|
14703
|
+
...(process.env["PATH"] ?? "").split(delimiter).filter(Boolean).map((entry) => `${entry}/bun`)
|
|
14704
|
+
].filter((value) => Boolean(value));
|
|
14705
|
+
return [...new Set(candidates)];
|
|
14154
14706
|
}
|
|
14155
14707
|
function isBunShebangScript(executable) {
|
|
14156
14708
|
try {
|
|
14157
|
-
const content =
|
|
14709
|
+
const content = readFileSync5(executable, "utf8").slice(0, 256);
|
|
14158
14710
|
const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
|
|
14159
14711
|
return /^#!.*\bbun\b/.test(firstLine2);
|
|
14160
14712
|
} catch {
|
|
@@ -14162,7 +14714,7 @@ function isBunShebangScript(executable) {
|
|
|
14162
14714
|
}
|
|
14163
14715
|
}
|
|
14164
14716
|
function isExecutableFile(path) {
|
|
14165
|
-
if (!
|
|
14717
|
+
if (!existsSync7(path))
|
|
14166
14718
|
return false;
|
|
14167
14719
|
try {
|
|
14168
14720
|
const stats = statSync(path);
|
|
@@ -14340,12 +14892,12 @@ function parseProbe(tool, stdout) {
|
|
|
14340
14892
|
}
|
|
14341
14893
|
function buildClaudeInstallPlan(machineId, tools) {
|
|
14342
14894
|
const machine = resolveMachine2(machineId);
|
|
14343
|
-
return {
|
|
14895
|
+
return attachMutationPlanDigest({
|
|
14344
14896
|
machineId: machine.id,
|
|
14345
14897
|
mode: "plan",
|
|
14346
14898
|
steps: buildInstallSteps(machine, tools),
|
|
14347
14899
|
executed: 0
|
|
14348
|
-
};
|
|
14900
|
+
});
|
|
14349
14901
|
}
|
|
14350
14902
|
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
14351
14903
|
const machine = resolveMachine2(machineId);
|
|
@@ -14370,8 +14922,12 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
|
14370
14922
|
}
|
|
14371
14923
|
function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
|
|
14372
14924
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
14925
|
+
return runClaudeInstallPlan(plan, options, runner);
|
|
14926
|
+
}
|
|
14927
|
+
function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
14928
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
14373
14929
|
if (!options.apply)
|
|
14374
|
-
return plan;
|
|
14930
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
14375
14931
|
if (!options.yes) {
|
|
14376
14932
|
throw new Error("Claude CLI installation requires --yes.");
|
|
14377
14933
|
}
|
|
@@ -14380,12 +14936,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
|
|
|
14380
14936
|
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
14381
14937
|
executed += 1;
|
|
14382
14938
|
}
|
|
14383
|
-
return {
|
|
14939
|
+
return attachMutationPlanDigest({
|
|
14384
14940
|
machineId: plan.machineId,
|
|
14385
14941
|
mode: "apply",
|
|
14386
14942
|
steps: plan.steps,
|
|
14387
14943
|
executed
|
|
14388
|
-
};
|
|
14944
|
+
});
|
|
14389
14945
|
}
|
|
14390
14946
|
// src/commands/install-tailscale.ts
|
|
14391
14947
|
function buildInstallSteps2(machine) {
|
|
@@ -14424,17 +14980,21 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
14424
14980
|
if (!machine) {
|
|
14425
14981
|
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
14426
14982
|
}
|
|
14427
|
-
return {
|
|
14983
|
+
return attachMutationPlanDigest({
|
|
14428
14984
|
machineId: machine.id,
|
|
14429
14985
|
mode: "plan",
|
|
14430
14986
|
steps: buildInstallSteps2(machine),
|
|
14431
14987
|
executed: 0
|
|
14432
|
-
};
|
|
14988
|
+
});
|
|
14433
14989
|
}
|
|
14434
14990
|
function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
14435
14991
|
const plan = buildTailscaleInstallPlan(machineId);
|
|
14992
|
+
return runTailscaleInstallPlan(plan, options, runner);
|
|
14993
|
+
}
|
|
14994
|
+
function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
14995
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
14436
14996
|
if (!options.apply)
|
|
14437
|
-
return plan;
|
|
14997
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
14438
14998
|
if (!options.yes) {
|
|
14439
14999
|
throw new Error("Tailscale install requires --yes.");
|
|
14440
15000
|
}
|
|
@@ -14443,19 +15003,21 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
14443
15003
|
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
14444
15004
|
executed += 1;
|
|
14445
15005
|
}
|
|
14446
|
-
return {
|
|
15006
|
+
return attachMutationPlanDigest({
|
|
14447
15007
|
machineId: plan.machineId,
|
|
14448
15008
|
mode: "apply",
|
|
14449
15009
|
steps: plan.steps,
|
|
14450
15010
|
executed
|
|
14451
|
-
};
|
|
15011
|
+
});
|
|
14452
15012
|
}
|
|
14453
15013
|
// src/commands/notifications.ts
|
|
14454
|
-
import { existsSync as
|
|
15014
|
+
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
15015
|
+
import { delimiter as delimiter2, isAbsolute, join as join7 } from "path";
|
|
14455
15016
|
var notificationChannelSchema = exports_external.object({
|
|
14456
15017
|
id: exports_external.string(),
|
|
14457
15018
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
14458
15019
|
target: exports_external.string(),
|
|
15020
|
+
commandArgs: exports_external.array(exports_external.string()).optional(),
|
|
14459
15021
|
events: exports_external.array(exports_external.string()),
|
|
14460
15022
|
enabled: exports_external.boolean()
|
|
14461
15023
|
});
|
|
@@ -14464,19 +15026,31 @@ var notificationConfigSchema = exports_external.object({
|
|
|
14464
15026
|
updatedAt: exports_external.string().optional(),
|
|
14465
15027
|
channels: exports_external.array(notificationChannelSchema)
|
|
14466
15028
|
});
|
|
15029
|
+
var trustedNotificationApproval = Symbol("trustedNotificationApproval");
|
|
15030
|
+
function createTrustedNotificationApproval() {
|
|
15031
|
+
return { [trustedNotificationApproval]: true };
|
|
15032
|
+
}
|
|
15033
|
+
function isTrustedNotificationApproval(approval) {
|
|
15034
|
+
return approval?.[trustedNotificationApproval] === true;
|
|
15035
|
+
}
|
|
14467
15036
|
function sortChannels(channels) {
|
|
14468
15037
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
14469
15038
|
}
|
|
14470
|
-
function shellQuote6(value) {
|
|
14471
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14472
|
-
}
|
|
14473
15039
|
function hasCommand2(binary) {
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
15040
|
+
return Boolean(resolveExecutable(binary));
|
|
15041
|
+
}
|
|
15042
|
+
function resolveExecutable(binary) {
|
|
15043
|
+
const trimmed = binary.trim();
|
|
15044
|
+
if (!trimmed)
|
|
15045
|
+
return null;
|
|
15046
|
+
const candidates = isAbsolute(trimmed) ? [trimmed] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).map((dir) => join7(dir, trimmed));
|
|
15047
|
+
for (const candidate of candidates) {
|
|
15048
|
+
try {
|
|
15049
|
+
accessSync(candidate, constants.X_OK);
|
|
15050
|
+
return candidate;
|
|
15051
|
+
} catch {}
|
|
15052
|
+
}
|
|
15053
|
+
return null;
|
|
14480
15054
|
}
|
|
14481
15055
|
function buildNotificationPreview(channel, event, message) {
|
|
14482
15056
|
if (channel.type === "email") {
|
|
@@ -14485,7 +15059,8 @@ function buildNotificationPreview(channel, event, message) {
|
|
|
14485
15059
|
if (channel.type === "webhook") {
|
|
14486
15060
|
return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
|
|
14487
15061
|
}
|
|
14488
|
-
|
|
15062
|
+
const args = channel.commandArgs?.length ? ` ${channel.commandArgs.join(" ")}` : "";
|
|
15063
|
+
return `${channel.target}${args} with HASNA_MACHINES_NOTIFICATION_* environment`;
|
|
14489
15064
|
}
|
|
14490
15065
|
async function dispatchEmail(channel, event, message) {
|
|
14491
15066
|
const subject = `[${event}] machines notification`;
|
|
@@ -14496,7 +15071,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
14496
15071
|
${message}
|
|
14497
15072
|
`;
|
|
14498
15073
|
if (hasCommand2("sendmail")) {
|
|
14499
|
-
const result = Bun.spawnSync(["
|
|
15074
|
+
const result = Bun.spawnSync(["sendmail", "-t"], {
|
|
14500
15075
|
stdin: new TextEncoder().encode(body),
|
|
14501
15076
|
stdout: "pipe",
|
|
14502
15077
|
stderr: "pipe",
|
|
@@ -14514,8 +15089,9 @@ ${message}
|
|
|
14514
15089
|
};
|
|
14515
15090
|
}
|
|
14516
15091
|
if (hasCommand2("mail")) {
|
|
14517
|
-
const
|
|
14518
|
-
|
|
15092
|
+
const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
|
|
15093
|
+
stdin: new TextEncoder().encode(`${message}
|
|
15094
|
+
`),
|
|
14519
15095
|
stdout: "pipe",
|
|
14520
15096
|
stderr: "pipe",
|
|
14521
15097
|
env: process.env
|
|
@@ -14558,8 +15134,20 @@ async function dispatchWebhook(channel, event, message) {
|
|
|
14558
15134
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
14559
15135
|
};
|
|
14560
15136
|
}
|
|
14561
|
-
async function dispatchCommand(channel, event, message) {
|
|
14562
|
-
|
|
15137
|
+
async function dispatchCommand(channel, event, message, options = {}) {
|
|
15138
|
+
if (!isTrustedNotificationApproval(options.trustedApproval)) {
|
|
15139
|
+
assertMutationApproved({
|
|
15140
|
+
surface: "notifications",
|
|
15141
|
+
operation: "dispatch_command",
|
|
15142
|
+
resourceId: channel.id,
|
|
15143
|
+
approvalToken: options.approvalToken
|
|
15144
|
+
});
|
|
15145
|
+
}
|
|
15146
|
+
const executable = resolveExecutable(channel.target);
|
|
15147
|
+
if (!executable) {
|
|
15148
|
+
throw new Error(`Command executable not found or not executable: ${channel.target}`);
|
|
15149
|
+
}
|
|
15150
|
+
const result = Bun.spawnSync([executable, ...channel.commandArgs ?? []], {
|
|
14563
15151
|
stdout: "pipe",
|
|
14564
15152
|
stderr: "pipe",
|
|
14565
15153
|
env: {
|
|
@@ -14581,7 +15169,7 @@ async function dispatchCommand(channel, event, message) {
|
|
|
14581
15169
|
detail: stdout || "Command completed successfully"
|
|
14582
15170
|
};
|
|
14583
15171
|
}
|
|
14584
|
-
async function dispatchChannel(channel, event, message) {
|
|
15172
|
+
async function dispatchChannel(channel, event, message, options = {}) {
|
|
14585
15173
|
if (!channel.enabled) {
|
|
14586
15174
|
return {
|
|
14587
15175
|
channelId: channel.id,
|
|
@@ -14597,7 +15185,7 @@ async function dispatchChannel(channel, event, message) {
|
|
|
14597
15185
|
if (channel.type === "webhook") {
|
|
14598
15186
|
return dispatchWebhook(channel, event, message);
|
|
14599
15187
|
}
|
|
14600
|
-
return dispatchCommand(channel, event, message);
|
|
15188
|
+
return dispatchCommand(channel, event, message, options);
|
|
14601
15189
|
}
|
|
14602
15190
|
function getDefaultNotificationConfig() {
|
|
14603
15191
|
return {
|
|
@@ -14607,10 +15195,10 @@ function getDefaultNotificationConfig() {
|
|
|
14607
15195
|
};
|
|
14608
15196
|
}
|
|
14609
15197
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
14610
|
-
if (!
|
|
15198
|
+
if (!existsSync8(path)) {
|
|
14611
15199
|
return getDefaultNotificationConfig();
|
|
14612
15200
|
}
|
|
14613
|
-
return notificationConfigSchema.parse(JSON.parse(
|
|
15201
|
+
return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
|
|
14614
15202
|
}
|
|
14615
15203
|
function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
14616
15204
|
ensureParentDir(path);
|
|
@@ -14626,11 +15214,20 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
|
14626
15214
|
function listNotificationChannels() {
|
|
14627
15215
|
return readNotificationConfig();
|
|
14628
15216
|
}
|
|
14629
|
-
function addNotificationChannel(channel) {
|
|
15217
|
+
function addNotificationChannel(channel, options = {}) {
|
|
15218
|
+
if (channel.type === "command" && !isTrustedNotificationApproval(options.trustedApproval)) {
|
|
15219
|
+
assertMutationApproved({
|
|
15220
|
+
surface: "notifications",
|
|
15221
|
+
operation: "add_command_channel",
|
|
15222
|
+
resourceId: channel.id,
|
|
15223
|
+
approvalToken: options.approvalToken
|
|
15224
|
+
});
|
|
15225
|
+
}
|
|
14630
15226
|
const config = readNotificationConfig();
|
|
14631
15227
|
const channels = config.channels.filter((entry) => entry.id !== channel.id);
|
|
14632
15228
|
channels.push({
|
|
14633
15229
|
...channel,
|
|
15230
|
+
commandArgs: channel.commandArgs?.map(String),
|
|
14634
15231
|
events: [...new Set(channel.events)]
|
|
14635
15232
|
});
|
|
14636
15233
|
return writeNotificationConfig({ ...config, channels });
|
|
@@ -14652,7 +15249,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
14652
15249
|
const deliveries = [];
|
|
14653
15250
|
for (const channel of channels) {
|
|
14654
15251
|
try {
|
|
14655
|
-
deliveries.push(await dispatchChannel(channel, event, message));
|
|
15252
|
+
deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
|
|
14656
15253
|
} catch (error) {
|
|
14657
15254
|
deliveries.push({
|
|
14658
15255
|
channelId: channel.id,
|
|
@@ -14687,7 +15284,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
14687
15284
|
if (!options.yes) {
|
|
14688
15285
|
throw new Error("Notification test execution requires --yes.");
|
|
14689
15286
|
}
|
|
14690
|
-
const delivery = await dispatchChannel(channel, event, message);
|
|
15287
|
+
const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
|
|
14691
15288
|
return {
|
|
14692
15289
|
channelId,
|
|
14693
15290
|
mode: "apply",
|
|
@@ -14753,6 +15350,46 @@ function listPorts(machineId) {
|
|
|
14753
15350
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14754
15351
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
14755
15352
|
import { EventsClient } from "@hasna/events";
|
|
15353
|
+
function shellQuote6(value) {
|
|
15354
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
15355
|
+
}
|
|
15356
|
+
function buildTmuxPaneDiedHookPlan(options = {}) {
|
|
15357
|
+
const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
|
|
15358
|
+
const machinesCommand = options.machinesCommand ?? "machines";
|
|
15359
|
+
const deliver = options.deliver === true;
|
|
15360
|
+
const approvalToken = options.approvalToken?.trim();
|
|
15361
|
+
const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
|
|
15362
|
+
const emitArgs = [
|
|
15363
|
+
"events",
|
|
15364
|
+
"emit",
|
|
15365
|
+
"machines.tmux.pane_died",
|
|
15366
|
+
"--source",
|
|
15367
|
+
"machines",
|
|
15368
|
+
"--subject",
|
|
15369
|
+
"tmux:#{hook_pane}",
|
|
15370
|
+
"--severity",
|
|
15371
|
+
"warning",
|
|
15372
|
+
"--message",
|
|
15373
|
+
"tmux pane died: #{hook_pane}",
|
|
15374
|
+
"--data",
|
|
15375
|
+
'{"target":"#{hook_pane}","session":"#{session_name}","window":"#{window_index}"}'
|
|
15376
|
+
];
|
|
15377
|
+
if (!deliver)
|
|
15378
|
+
emitArgs.push("--no-deliver");
|
|
15379
|
+
if (approvalToken)
|
|
15380
|
+
emitArgs.push("--approval-token", approvalToken);
|
|
15381
|
+
const command2 = [machinesCommand, ...emitArgs].map(shellQuote6).join(" ");
|
|
15382
|
+
const runShell = trustedLocalMutation ? `HASNA_MACHINES_ALLOW_MUTATIONS=1 ${command2}` : command2;
|
|
15383
|
+
const args = ["set-hook", "-g", "pane-died", `run-shell ${shellQuote6(runShell)}`];
|
|
15384
|
+
return {
|
|
15385
|
+
tmuxCommand,
|
|
15386
|
+
args,
|
|
15387
|
+
shellCommand: [tmuxCommand, ...args].map(shellQuote6).join(" "),
|
|
15388
|
+
eventType: "machines.tmux.pane_died",
|
|
15389
|
+
deliver,
|
|
15390
|
+
trustedLocalMutation
|
|
15391
|
+
};
|
|
15392
|
+
}
|
|
14756
15393
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
14757
15394
|
const checkedAt = new Date().toISOString();
|
|
14758
15395
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -14838,7 +15475,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
14838
15475
|
return client.emit(input, { deliver });
|
|
14839
15476
|
}
|
|
14840
15477
|
// src/commands/serve.ts
|
|
14841
|
-
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
15478
|
+
import { EventsClient as EventsClient2, getEventsDataDir, sanitizeChannelsForOutput } from "@hasna/events";
|
|
15479
|
+
import { resolve as resolve3 } from "path";
|
|
14842
15480
|
|
|
14843
15481
|
// src/commands/status.ts
|
|
14844
15482
|
function parseJsonObject2(value) {
|
|
@@ -14893,7 +15531,7 @@ function escapeHtml(value) {
|
|
|
14893
15531
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
14894
15532
|
}
|
|
14895
15533
|
function getServeInfo(options = {}) {
|
|
14896
|
-
const host = options.host || "
|
|
15534
|
+
const host = options.host || "127.0.0.1";
|
|
14897
15535
|
const port = options.port || 7676;
|
|
14898
15536
|
return {
|
|
14899
15537
|
host,
|
|
@@ -15096,6 +15734,59 @@ async function parseJsonBody(request) {
|
|
|
15096
15734
|
function jsonError(message, status = 400) {
|
|
15097
15735
|
return Response.json({ error: message }, { status });
|
|
15098
15736
|
}
|
|
15737
|
+
function dashboardResourceId(kind, ...parts) {
|
|
15738
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
15739
|
+
return values ? `${kind}:${values}` : kind;
|
|
15740
|
+
}
|
|
15741
|
+
function eventStoreDir() {
|
|
15742
|
+
return resolve3(getEventsDataDir());
|
|
15743
|
+
}
|
|
15744
|
+
function eventStoreScope() {
|
|
15745
|
+
return { event_store_dir: eventStoreDir() };
|
|
15746
|
+
}
|
|
15747
|
+
function eventStoreResourceId(kind, ...parts) {
|
|
15748
|
+
return dashboardResourceId(kind, mutationArgsSha256(eventStoreScope()), ...parts);
|
|
15749
|
+
}
|
|
15750
|
+
function withEventStoreScope(args) {
|
|
15751
|
+
return { event_store_dir: eventStoreDir(), ...args };
|
|
15752
|
+
}
|
|
15753
|
+
function dashboardMutationCallerId() {
|
|
15754
|
+
return process.env[MUTATION_APPROVAL_CALLER_ENV]?.trim() || "dashboard";
|
|
15755
|
+
}
|
|
15756
|
+
function dashboardMutationRunId() {
|
|
15757
|
+
return process.env[MUTATION_APPROVAL_RUN_ENV]?.trim() || "dashboard";
|
|
15758
|
+
}
|
|
15759
|
+
function approvalTokenFromRequest(request, body) {
|
|
15760
|
+
const bodyToken = typeof body["approval_token"] === "string" ? body["approval_token"] : typeof body["approvalToken"] === "string" ? body["approvalToken"] : undefined;
|
|
15761
|
+
if (bodyToken?.trim())
|
|
15762
|
+
return bodyToken;
|
|
15763
|
+
const headerToken = request.headers.get("x-hasna-approval-token")?.trim();
|
|
15764
|
+
if (headerToken)
|
|
15765
|
+
return headerToken;
|
|
15766
|
+
const authorization = request.headers.get("authorization")?.trim();
|
|
15767
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) {
|
|
15768
|
+
return authorization.slice("bearer ".length).trim();
|
|
15769
|
+
}
|
|
15770
|
+
return;
|
|
15771
|
+
}
|
|
15772
|
+
function requireDashboardMutation(operation, request, body, scope = {}) {
|
|
15773
|
+
const decision = verifyMutationApprovalToken({
|
|
15774
|
+
surface: "dashboard",
|
|
15775
|
+
operation,
|
|
15776
|
+
transport: "dashboard:http",
|
|
15777
|
+
callerId: dashboardMutationCallerId(),
|
|
15778
|
+
runId: dashboardMutationRunId(),
|
|
15779
|
+
resourceId: scope.resourceId,
|
|
15780
|
+
args: scope.args,
|
|
15781
|
+
approvalToken: approvalTokenFromRequest(request, body)
|
|
15782
|
+
});
|
|
15783
|
+
if (decision.approved)
|
|
15784
|
+
return;
|
|
15785
|
+
return jsonError(`Mutation approval denied: ${decision.reason ?? "approval_token is invalid."}`, 403);
|
|
15786
|
+
}
|
|
15787
|
+
function objectBodyValue(value) {
|
|
15788
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
15789
|
+
}
|
|
15099
15790
|
function privateOutputWarnings(requested, allowed) {
|
|
15100
15791
|
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
15101
15792
|
}
|
|
@@ -15107,6 +15798,7 @@ function appendWarnings(payload, warnings) {
|
|
|
15107
15798
|
function startDashboardServer(options = {}) {
|
|
15108
15799
|
const info = getServeInfo(options);
|
|
15109
15800
|
const events = new EventsClient2;
|
|
15801
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
15110
15802
|
return Bun.serve({
|
|
15111
15803
|
hostname: info.host,
|
|
15112
15804
|
port: info.port,
|
|
@@ -15171,8 +15863,25 @@ function startDashboardServer(options = {}) {
|
|
|
15171
15863
|
const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
|
|
15172
15864
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
15173
15865
|
const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
|
|
15174
|
-
const data =
|
|
15175
|
-
const metadata =
|
|
15866
|
+
const data = objectBodyValue(body["data"]);
|
|
15867
|
+
const metadata = objectBodyValue(body["metadata"]);
|
|
15868
|
+
const denied = requireDashboardMutation("machines_events_emit", request, body, {
|
|
15869
|
+
resourceId: eventStoreResourceId("event", type, subject, dedupeKey),
|
|
15870
|
+
args: withEventStoreScope({
|
|
15871
|
+
event_type: type,
|
|
15872
|
+
source,
|
|
15873
|
+
subject,
|
|
15874
|
+
severity,
|
|
15875
|
+
message,
|
|
15876
|
+
data,
|
|
15877
|
+
metadata,
|
|
15878
|
+
dedupe_key: dedupeKey,
|
|
15879
|
+
deliver: true,
|
|
15880
|
+
dedupe: true
|
|
15881
|
+
})
|
|
15882
|
+
});
|
|
15883
|
+
if (denied)
|
|
15884
|
+
return denied;
|
|
15176
15885
|
return Response.json(await events.emit({
|
|
15177
15886
|
source,
|
|
15178
15887
|
type,
|
|
@@ -15215,8 +15924,20 @@ function startDashboardServer(options = {}) {
|
|
|
15215
15924
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
15216
15925
|
const apply = body["apply"] === true;
|
|
15217
15926
|
const yes = body["yes"] === true;
|
|
15927
|
+
const resolvedEvent = event ?? "manual.test";
|
|
15928
|
+
const resolvedMessage = message ?? "machines notification test";
|
|
15929
|
+
const denied = requireDashboardMutation("machines_notifications_test", request, body, {
|
|
15930
|
+
resourceId: dashboardResourceId("notification-test", channelId, resolvedEvent),
|
|
15931
|
+
args: { channel_id: channelId, event: resolvedEvent, message: resolvedMessage, apply, yes }
|
|
15932
|
+
});
|
|
15933
|
+
if (denied)
|
|
15934
|
+
return denied;
|
|
15218
15935
|
try {
|
|
15219
|
-
return Response.json(await testNotificationChannel(channelId,
|
|
15936
|
+
return Response.json(await testNotificationChannel(channelId, resolvedEvent, resolvedMessage, {
|
|
15937
|
+
apply,
|
|
15938
|
+
yes,
|
|
15939
|
+
trustedApproval: apply ? trustedNotificationApproval2 : undefined
|
|
15940
|
+
}));
|
|
15220
15941
|
} catch (error) {
|
|
15221
15942
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
15222
15943
|
}
|
|
@@ -15231,13 +15952,22 @@ function startDashboardServer(options = {}) {
|
|
|
15231
15952
|
return jsonError("channelId is required.");
|
|
15232
15953
|
}
|
|
15233
15954
|
const type = typeof body["type"] === "string" ? body["type"] : "events.test";
|
|
15234
|
-
const
|
|
15955
|
+
const subject = channelId;
|
|
15956
|
+
const message = typeof body["message"] === "string" ? body["message"] : "Hasna events test delivery";
|
|
15957
|
+
const data = objectBodyValue(body["data"]);
|
|
15958
|
+
const denied = requireDashboardMutation("machines_webhooks_test", request, body, {
|
|
15959
|
+
resourceId: eventStoreResourceId("webhook-test", channelId, type),
|
|
15960
|
+
args: withEventStoreScope({ channel_id: channelId, event_type: type, subject, message, data })
|
|
15961
|
+
});
|
|
15962
|
+
if (denied)
|
|
15963
|
+
return denied;
|
|
15235
15964
|
try {
|
|
15236
15965
|
return Response.json(await events.testChannel(channelId, {
|
|
15237
15966
|
source: "machines",
|
|
15238
15967
|
type,
|
|
15239
|
-
subject
|
|
15240
|
-
message
|
|
15968
|
+
subject,
|
|
15969
|
+
message,
|
|
15970
|
+
data
|
|
15241
15971
|
}));
|
|
15242
15972
|
} catch (error) {
|
|
15243
15973
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
@@ -15385,17 +16115,21 @@ function buildSetupPlan(machineId) {
|
|
|
15385
16115
|
workspacePath: `${homedir4()}/workspace`
|
|
15386
16116
|
};
|
|
15387
16117
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
15388
|
-
return {
|
|
16118
|
+
return attachMutationPlanDigest({
|
|
15389
16119
|
machineId: target.id,
|
|
15390
16120
|
mode: "plan",
|
|
15391
16121
|
steps,
|
|
15392
16122
|
executed: 0
|
|
15393
|
-
};
|
|
16123
|
+
});
|
|
15394
16124
|
}
|
|
15395
16125
|
function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
15396
16126
|
const plan = buildSetupPlan(machineId);
|
|
16127
|
+
return runSetupPlan(plan, options, runner);
|
|
16128
|
+
}
|
|
16129
|
+
function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
|
|
16130
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
15397
16131
|
if (!options.apply) {
|
|
15398
|
-
return plan;
|
|
16132
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
15399
16133
|
}
|
|
15400
16134
|
if (!options.yes) {
|
|
15401
16135
|
throw new Error("Setup execution requires --yes.");
|
|
@@ -15416,12 +16150,12 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
15416
16150
|
}
|
|
15417
16151
|
executed += 1;
|
|
15418
16152
|
}
|
|
15419
|
-
const summary = {
|
|
16153
|
+
const summary = attachMutationPlanDigest({
|
|
15420
16154
|
machineId: plan.machineId,
|
|
15421
16155
|
mode: "apply",
|
|
15422
16156
|
steps: plan.steps,
|
|
15423
16157
|
executed
|
|
15424
|
-
};
|
|
16158
|
+
});
|
|
15425
16159
|
recordSetupRun(plan.machineId, "completed", summary);
|
|
15426
16160
|
return summary;
|
|
15427
16161
|
}
|
|
@@ -15540,16 +16274,22 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
15540
16274
|
}
|
|
15541
16275
|
const secretsCommand = options.secretsCommand || "secrets";
|
|
15542
16276
|
const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
|
|
16277
|
+
const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
|
|
16278
|
+
const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
|
|
15543
16279
|
return {
|
|
15544
16280
|
machineId: credentials.machineId,
|
|
15545
16281
|
user: credentials.user,
|
|
15546
16282
|
passwordSecretKey: credentials.passwordSecretKey,
|
|
15547
16283
|
remoteCommand,
|
|
15548
|
-
|
|
16284
|
+
secretsCommand,
|
|
16285
|
+
secretsCommandArgs,
|
|
16286
|
+
sshCommand: sshPlan.command,
|
|
16287
|
+
sshCommandArgs: sshPlan.args,
|
|
16288
|
+
command: `${shellCommand2(secretsCommandArgs)} | ${sshPlan.shellCommand}`
|
|
15549
16289
|
};
|
|
15550
16290
|
}
|
|
15551
16291
|
// src/commands/sync.ts
|
|
15552
|
-
import { existsSync as
|
|
16292
|
+
import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, symlinkSync, copyFileSync } from "fs";
|
|
15553
16293
|
import { homedir as homedir5 } from "os";
|
|
15554
16294
|
function quote4(value) {
|
|
15555
16295
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -15602,15 +16342,15 @@ function detectFileActions(machine) {
|
|
|
15602
16342
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
15603
16343
|
}
|
|
15604
16344
|
return (machine.files || []).map((file, index) => {
|
|
15605
|
-
const sourceExists =
|
|
15606
|
-
const targetExists =
|
|
16345
|
+
const sourceExists = existsSync9(file.source);
|
|
16346
|
+
const targetExists = existsSync9(file.target);
|
|
15607
16347
|
let status = "missing";
|
|
15608
16348
|
if (sourceExists && targetExists) {
|
|
15609
16349
|
if (file.mode === "symlink") {
|
|
15610
16350
|
status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
|
|
15611
16351
|
} else {
|
|
15612
|
-
const source =
|
|
15613
|
-
const target =
|
|
16352
|
+
const source = readFileSync7(file.source, "utf8");
|
|
16353
|
+
const target = readFileSync7(file.target, "utf8");
|
|
15614
16354
|
status = source === target ? "ok" : "drifted";
|
|
15615
16355
|
}
|
|
15616
16356
|
}
|
|
@@ -15640,12 +16380,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
15640
16380
|
...detectPackageActions(target, runner),
|
|
15641
16381
|
...detectFileActions(target)
|
|
15642
16382
|
];
|
|
15643
|
-
return {
|
|
16383
|
+
return attachMutationPlanDigest({
|
|
15644
16384
|
machineId: target.id,
|
|
15645
16385
|
mode: "plan",
|
|
15646
16386
|
actions,
|
|
15647
16387
|
executed: 0
|
|
15648
|
-
};
|
|
16388
|
+
});
|
|
15649
16389
|
}
|
|
15650
16390
|
function applyFileAction(command2) {
|
|
15651
16391
|
const [verb, source, target] = command2.split(" ");
|
|
@@ -15669,8 +16409,12 @@ function applyFileAction(command2) {
|
|
|
15669
16409
|
}
|
|
15670
16410
|
function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
15671
16411
|
const plan = buildSyncPlan(machineId, runner);
|
|
16412
|
+
return runSyncPlan(plan, options, runner);
|
|
16413
|
+
}
|
|
16414
|
+
function runSyncPlan(plan, options = {}, runner = runMachineCommand) {
|
|
16415
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
15672
16416
|
if (!options.apply) {
|
|
15673
|
-
return plan;
|
|
16417
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
15674
16418
|
}
|
|
15675
16419
|
if (!options.yes) {
|
|
15676
16420
|
throw new Error("Sync execution requires --yes.");
|
|
@@ -15697,12 +16441,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
|
15697
16441
|
}
|
|
15698
16442
|
executed += 1;
|
|
15699
16443
|
}
|
|
15700
|
-
const summary = {
|
|
16444
|
+
const summary = attachMutationPlanDigest({
|
|
15701
16445
|
machineId: plan.machineId,
|
|
15702
16446
|
mode: "apply",
|
|
15703
16447
|
actions: plan.actions,
|
|
15704
16448
|
executed
|
|
15705
|
-
};
|
|
16449
|
+
});
|
|
15706
16450
|
recordSyncRun(plan.machineId, "completed", summary);
|
|
15707
16451
|
return summary;
|
|
15708
16452
|
}
|
|
@@ -23087,7 +23831,7 @@ class Protocol {
|
|
|
23087
23831
|
return;
|
|
23088
23832
|
}
|
|
23089
23833
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
|
|
23090
|
-
await new Promise((
|
|
23834
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
23091
23835
|
options?.signal?.throwIfAborted();
|
|
23092
23836
|
}
|
|
23093
23837
|
} catch (error2) {
|
|
@@ -23099,7 +23843,7 @@ class Protocol {
|
|
|
23099
23843
|
}
|
|
23100
23844
|
request(request, resultSchema, options) {
|
|
23101
23845
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
23102
|
-
return new Promise((
|
|
23846
|
+
return new Promise((resolve4, reject) => {
|
|
23103
23847
|
const earlyReject = (error2) => {
|
|
23104
23848
|
reject(error2);
|
|
23105
23849
|
};
|
|
@@ -23177,7 +23921,7 @@ class Protocol {
|
|
|
23177
23921
|
if (!parseResult.success) {
|
|
23178
23922
|
reject(parseResult.error);
|
|
23179
23923
|
} else {
|
|
23180
|
-
|
|
23924
|
+
resolve4(parseResult.data);
|
|
23181
23925
|
}
|
|
23182
23926
|
} catch (error2) {
|
|
23183
23927
|
reject(error2);
|
|
@@ -23368,12 +24112,12 @@ class Protocol {
|
|
|
23368
24112
|
interval = task.pollInterval;
|
|
23369
24113
|
}
|
|
23370
24114
|
} catch {}
|
|
23371
|
-
return new Promise((
|
|
24115
|
+
return new Promise((resolve4, reject) => {
|
|
23372
24116
|
if (signal.aborted) {
|
|
23373
24117
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
23374
24118
|
return;
|
|
23375
24119
|
}
|
|
23376
|
-
const timeoutId = setTimeout(
|
|
24120
|
+
const timeoutId = setTimeout(resolve4, interval);
|
|
23377
24121
|
signal.addEventListener("abort", () => {
|
|
23378
24122
|
clearTimeout(timeoutId);
|
|
23379
24123
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -24227,7 +24971,7 @@ class McpServer {
|
|
|
24227
24971
|
let task = createTaskResult.task;
|
|
24228
24972
|
const pollInterval = task.pollInterval ?? 5000;
|
|
24229
24973
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
24230
|
-
await new Promise((
|
|
24974
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
24231
24975
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
24232
24976
|
if (!updatedTask) {
|
|
24233
24977
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -24826,8 +25570,8 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
24826
25570
|
"storage_pull",
|
|
24827
25571
|
"storage_sync"
|
|
24828
25572
|
];
|
|
24829
|
-
function buildServer(version2 = getPackageVersion()) {
|
|
24830
|
-
return createMcpServer(version2);
|
|
25573
|
+
function buildServer(version2 = getPackageVersion(), options = {}) {
|
|
25574
|
+
return createMcpServer(version2, options);
|
|
24831
25575
|
}
|
|
24832
25576
|
function privateMetadataAllowed(requested) {
|
|
24833
25577
|
return requested === true && isPrivateOutputEnabled();
|
|
@@ -24841,9 +25585,50 @@ function appendWarnings2(payload, warnings) {
|
|
|
24841
25585
|
const currentWarnings = typeof payload === "object" && payload && "warnings" in payload && Array.isArray(payload.warnings) ? payload.warnings : [];
|
|
24842
25586
|
return { ...payload, warnings: [...currentWarnings, ...warnings] };
|
|
24843
25587
|
}
|
|
24844
|
-
|
|
25588
|
+
var approvalTokenSchema = exports_external.string().optional().describe("Operator mutation approval token");
|
|
25589
|
+
function mutationMachineId(machineId) {
|
|
25590
|
+
return machineId?.trim() || "local";
|
|
25591
|
+
}
|
|
25592
|
+
function mutationResourceId(kind, ...parts) {
|
|
25593
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
25594
|
+
return values ? `${kind}:${values}` : kind;
|
|
25595
|
+
}
|
|
25596
|
+
function mutationCallerId() {
|
|
25597
|
+
return process.env["HASNA_MACHINES_MUTATION_CALLER_ID"]?.trim() || "mcp";
|
|
25598
|
+
}
|
|
25599
|
+
function mutationRunId() {
|
|
25600
|
+
return process.env["HASNA_MACHINES_MUTATION_RUN_ID"]?.trim() || "mcp";
|
|
25601
|
+
}
|
|
25602
|
+
function assertScopedMcpMutation(operation, approvalToken, scope = {}, transport) {
|
|
25603
|
+
assertMutationApproved({
|
|
25604
|
+
surface: "mcp",
|
|
25605
|
+
operation,
|
|
25606
|
+
transport,
|
|
25607
|
+
callerId: mutationCallerId(),
|
|
25608
|
+
runId: mutationRunId(),
|
|
25609
|
+
machineId: scope.machineId === undefined ? undefined : mutationMachineId(scope.machineId),
|
|
25610
|
+
resourceId: scope.resourceId === undefined || scope.resourceId === null ? undefined : scope.resourceId,
|
|
25611
|
+
args: scope.args,
|
|
25612
|
+
approvalToken
|
|
25613
|
+
});
|
|
25614
|
+
}
|
|
25615
|
+
function mcpPlanApprovalArgs(args, plan) {
|
|
25616
|
+
return {
|
|
25617
|
+
...args,
|
|
25618
|
+
plan_digest: mutationPlanDigest(plan)
|
|
25619
|
+
};
|
|
25620
|
+
}
|
|
25621
|
+
function mcpPlanResourceId(operation, machineId, plan) {
|
|
25622
|
+
return mutationResourceId("plan", operation, machineId, mutationPlanDigest(plan));
|
|
25623
|
+
}
|
|
25624
|
+
function createMcpServer(version2, options = {}) {
|
|
24845
25625
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
24846
25626
|
const events = new EventsClient3;
|
|
25627
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
25628
|
+
const mutationTransport = options.mutationTransport ?? "mcp:stdio";
|
|
25629
|
+
function requireMcpMutation(operation, approvalToken, scope = {}) {
|
|
25630
|
+
assertScopedMcpMutation(operation, approvalToken, scope, mutationTransport);
|
|
25631
|
+
}
|
|
24847
25632
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", { private_metadata: exports_external.boolean().optional().describe("Include private local paths and machine identifiers") }, async ({ private_metadata }) => {
|
|
24848
25633
|
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24849
25634
|
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
@@ -24857,18 +25642,31 @@ function createMcpServer(version2) {
|
|
|
24857
25642
|
server.tool("machines_apps_status", "Check installed state for manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(getAppsStatus(machine_id), null, 2) }] }));
|
|
24858
25643
|
server.tool("machines_apps_diff", "Show missing and installed manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(diffApps(machine_id), null, 2) }] }));
|
|
24859
25644
|
server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }] }));
|
|
24860
|
-
server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25645
|
+
server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25646
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25647
|
+
const plan = buildAppsPlan(machine_id);
|
|
25648
|
+
requireMcpMutation("machines_apps_apply", approval_token, {
|
|
25649
|
+
machineId: resolvedMachineId,
|
|
25650
|
+
resourceId: mcpPlanResourceId("machines_apps_apply", resolvedMachineId, plan),
|
|
25651
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25652
|
+
});
|
|
25653
|
+
return { content: [{ type: "text", text: JSON.stringify(runAppsPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25654
|
+
});
|
|
24861
25655
|
server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
|
|
24862
25656
|
content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
|
|
24863
25657
|
}));
|
|
24864
25658
|
server.tool("machines_manifest_validate", "Validate the current fleet manifest.", {}, async () => ({
|
|
24865
25659
|
content: [{ type: "text", text: JSON.stringify(manifestValidate(), null, 2) }]
|
|
24866
25660
|
}));
|
|
24867
|
-
server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () =>
|
|
24868
|
-
|
|
24869
|
-
|
|
25661
|
+
server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", { approval_token: approvalTokenSchema }, async ({ approval_token }) => {
|
|
25662
|
+
requireMcpMutation("machines_manifest_bootstrap", approval_token, { resourceId: "manifest:bootstrap", args: {} });
|
|
25663
|
+
return { content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }] };
|
|
25664
|
+
});
|
|
24870
25665
|
server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
|
|
24871
|
-
server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) =>
|
|
25666
|
+
server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier"), approval_token: approvalTokenSchema }, async ({ machine_id, approval_token }) => {
|
|
25667
|
+
requireMcpMutation("machines_manifest_remove", approval_token, { machineId: machine_id, args: { machine_id } });
|
|
25668
|
+
return { content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] };
|
|
25669
|
+
});
|
|
24872
25670
|
server.tool("machines_agent_status", "List current machine agent heartbeats.", { private_metadata: exports_external.boolean().optional().describe("Include private heartbeat metadata") }, async ({ private_metadata }) => {
|
|
24873
25671
|
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24874
25672
|
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
@@ -24920,9 +25718,27 @@ function createMcpServer(version2) {
|
|
|
24920
25718
|
}]
|
|
24921
25719
|
}));
|
|
24922
25720
|
server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
|
|
24923
|
-
server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25721
|
+
server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25722
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25723
|
+
const plan = buildSetupPlan(machine_id);
|
|
25724
|
+
requireMcpMutation("machines_setup_apply", approval_token, {
|
|
25725
|
+
machineId: resolvedMachineId,
|
|
25726
|
+
resourceId: mcpPlanResourceId("machines_setup_apply", resolvedMachineId, plan),
|
|
25727
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25728
|
+
});
|
|
25729
|
+
return { content: [{ type: "text", text: JSON.stringify(runSetupPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25730
|
+
});
|
|
24924
25731
|
server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
|
|
24925
|
-
server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25732
|
+
server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25733
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25734
|
+
const plan = buildSyncPlan(machine_id);
|
|
25735
|
+
requireMcpMutation("machines_sync_apply", approval_token, {
|
|
25736
|
+
machineId: resolvedMachineId,
|
|
25737
|
+
resourceId: mcpPlanResourceId("machines_sync_apply", resolvedMachineId, plan),
|
|
25738
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25739
|
+
});
|
|
25740
|
+
return { content: [{ type: "text", text: JSON.stringify(runSyncPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25741
|
+
});
|
|
24926
25742
|
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
|
|
24927
25743
|
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
24928
25744
|
private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
|
|
@@ -24977,12 +25793,31 @@ function createMcpServer(version2) {
|
|
|
24977
25793
|
server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
|
|
24978
25794
|
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
24979
25795
|
tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
|
|
24980
|
-
yes: exports_external.boolean().describe("Confirmation flag for execution")
|
|
24981
|
-
|
|
24982
|
-
|
|
24983
|
-
|
|
25796
|
+
yes: exports_external.boolean().describe("Confirmation flag for execution"),
|
|
25797
|
+
approval_token: approvalTokenSchema
|
|
25798
|
+
}, async ({ machine_id, tools, yes, approval_token }) => {
|
|
25799
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25800
|
+
const plan = buildClaudeInstallPlan(machine_id, tools);
|
|
25801
|
+
requireMcpMutation("machines_install_claude_apply", approval_token, {
|
|
25802
|
+
machineId: resolvedMachineId,
|
|
25803
|
+
resourceId: mcpPlanResourceId("machines_install_claude_apply", resolvedMachineId, plan),
|
|
25804
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, tools, yes }, plan)
|
|
25805
|
+
});
|
|
25806
|
+
return {
|
|
25807
|
+
content: [{ type: "text", text: JSON.stringify(runClaudeInstallPlan(plan, { apply: true, yes }), null, 2) }]
|
|
25808
|
+
};
|
|
25809
|
+
});
|
|
24984
25810
|
server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
|
|
24985
|
-
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25811
|
+
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25812
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25813
|
+
const plan = buildTailscaleInstallPlan(machine_id);
|
|
25814
|
+
requireMcpMutation("machines_install_tailscale_apply", approval_token, {
|
|
25815
|
+
machineId: resolvedMachineId,
|
|
25816
|
+
resourceId: mcpPlanResourceId("machines_install_tailscale_apply", resolvedMachineId, plan),
|
|
25817
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25818
|
+
});
|
|
25819
|
+
return { content: [{ type: "text", text: JSON.stringify(runTailscaleInstallPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25820
|
+
});
|
|
24986
25821
|
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
|
|
24987
25822
|
machine_id: exports_external.string().describe("Machine identifier"),
|
|
24988
25823
|
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
@@ -25050,41 +25885,72 @@ function createMcpServer(version2) {
|
|
|
25050
25885
|
content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
|
|
25051
25886
|
}));
|
|
25052
25887
|
server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
|
|
25053
|
-
server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) =>
|
|
25888
|
+
server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ bucket, prefix, yes, approval_token }) => {
|
|
25889
|
+
requireMcpMutation("machines_backup_apply", approval_token, { resourceId: mutationResourceId("backup", bucket, prefix), args: { bucket, prefix, yes } });
|
|
25890
|
+
return { content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] };
|
|
25891
|
+
});
|
|
25054
25892
|
server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
|
|
25055
|
-
server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) =>
|
|
25056
|
-
|
|
25893
|
+
server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ domains, yes, approval_token }) => {
|
|
25894
|
+
requireMcpMutation("machines_cert_apply", approval_token, { resourceId: mutationResourceId("cert", domains.join(",")), args: { domains, yes } });
|
|
25895
|
+
return { content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] };
|
|
25896
|
+
});
|
|
25897
|
+
server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host"), approval_token: approvalTokenSchema }, async ({ domain, port, target_host, approval_token }) => {
|
|
25898
|
+
const resolvedTargetHost = target_host ?? "127.0.0.1";
|
|
25899
|
+
requireMcpMutation("machines_dns_add", approval_token, { resourceId: mutationResourceId("dns", domain), args: { domain, port, target_host: resolvedTargetHost } });
|
|
25900
|
+
return { content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, resolvedTargetHost), null, 2) }] };
|
|
25901
|
+
});
|
|
25057
25902
|
server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
|
|
25058
25903
|
server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", { domain: exports_external.string().describe("Domain name") }, async ({ domain }) => ({ content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }] }));
|
|
25059
25904
|
server.tool("machines_notifications_add", "Add or replace a notification channel.", {
|
|
25060
25905
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
25061
25906
|
type: exports_external.enum(["email", "webhook", "command"]).describe("Notification transport"),
|
|
25062
|
-
target: exports_external.string().describe("Email, webhook URL, or
|
|
25907
|
+
target: exports_external.string().describe("Email, webhook URL, or command executable"),
|
|
25908
|
+
command_args: exports_external.array(exports_external.string()).optional().describe("Arguments for command transports"),
|
|
25063
25909
|
events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
|
|
25064
|
-
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
25065
|
-
|
|
25066
|
-
|
|
25067
|
-
|
|
25910
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
|
|
25911
|
+
approval_token: approvalTokenSchema
|
|
25912
|
+
}, async ({ channel_id, type, target, command_args, events: events2, enabled, approval_token }) => {
|
|
25913
|
+
const resolvedEnabled = enabled ?? true;
|
|
25914
|
+
const resolvedEvents = [...new Set(events2)];
|
|
25915
|
+
const commandArgs = command_args ?? [];
|
|
25916
|
+
requireMcpMutation("machines_notifications_add", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id, type, target, command_args: commandArgs, events: resolvedEvents, enabled: resolvedEnabled } });
|
|
25917
|
+
return {
|
|
25918
|
+
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, commandArgs: type === "command" && commandArgs.length > 0 ? commandArgs : undefined, events: resolvedEvents, enabled: resolvedEnabled }, { trustedApproval: trustedNotificationApproval2 }), null, 2) }]
|
|
25919
|
+
};
|
|
25920
|
+
});
|
|
25068
25921
|
server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
|
|
25069
25922
|
content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
|
|
25070
25923
|
}));
|
|
25071
|
-
server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true") }, async ({ channel_id, event, message, yes }) =>
|
|
25072
|
-
|
|
25073
|
-
|
|
25074
|
-
|
|
25075
|
-
|
|
25924
|
+
server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true"), approval_token: approvalTokenSchema }, async ({ channel_id, event, message, yes, approval_token }) => {
|
|
25925
|
+
if (yes === true)
|
|
25926
|
+
requireMcpMutation("machines_notifications_test", approval_token, { resourceId: mutationResourceId("notification-test", channel_id, event), args: { channel_id, event, message, yes: true } });
|
|
25927
|
+
return {
|
|
25928
|
+
content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes, trustedApproval: yes === true ? trustedNotificationApproval2 : undefined }), null, 2) }]
|
|
25929
|
+
};
|
|
25930
|
+
});
|
|
25931
|
+
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel"), approval_token: approvalTokenSchema }, async ({ event, message, channel_id, approval_token }) => {
|
|
25932
|
+
requireMcpMutation("machines_notifications_dispatch", approval_token, { resourceId: mutationResourceId("notification-dispatch", channel_id, event), args: { event, message, channel_id } });
|
|
25933
|
+
return { content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id, trustedApproval: trustedNotificationApproval2 }), null, 2) }] };
|
|
25934
|
+
});
|
|
25935
|
+
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
|
|
25936
|
+
requireMcpMutation("machines_notifications_remove", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id } });
|
|
25937
|
+
return { content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] };
|
|
25938
|
+
});
|
|
25076
25939
|
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
25077
25940
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
25078
25941
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
25079
25942
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
25080
25943
|
source: exports_external.string().optional().describe("Optional source filter"),
|
|
25081
25944
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
25082
|
-
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
25083
|
-
|
|
25945
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
|
|
25946
|
+
approval_token: approvalTokenSchema
|
|
25947
|
+
}, async ({ channel_id, url, event_type, source, secret, enabled, approval_token }) => {
|
|
25948
|
+
const resolvedEnabled = enabled ?? true;
|
|
25949
|
+
requireMcpMutation("machines_webhooks_add", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id, url, event_type, source, secret, enabled: resolvedEnabled } });
|
|
25084
25950
|
const now = new Date().toISOString();
|
|
25085
25951
|
const channel = await events.addChannel({
|
|
25086
25952
|
id: channel_id,
|
|
25087
|
-
enabled:
|
|
25953
|
+
enabled: resolvedEnabled,
|
|
25088
25954
|
transport: "webhook",
|
|
25089
25955
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
25090
25956
|
webhook: { url, secret },
|
|
@@ -25096,10 +25962,16 @@ function createMcpServer(version2) {
|
|
|
25096
25962
|
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
25097
25963
|
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
25098
25964
|
}));
|
|
25099
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) =>
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25965
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body"), approval_token: approvalTokenSchema }, async ({ channel_id, event_type, message, approval_token }) => {
|
|
25966
|
+
requireMcpMutation("machines_webhooks_test", approval_token, { resourceId: mutationResourceId("webhook-test", channel_id, event_type), args: { channel_id, event_type, message } });
|
|
25967
|
+
return {
|
|
25968
|
+
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
25969
|
+
};
|
|
25970
|
+
});
|
|
25971
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
|
|
25972
|
+
requireMcpMutation("machines_webhooks_remove", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id } });
|
|
25973
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] };
|
|
25974
|
+
});
|
|
25103
25975
|
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
25104
25976
|
event_type: exports_external.string().describe("Event type"),
|
|
25105
25977
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
@@ -25108,25 +25980,36 @@ function createMcpServer(version2) {
|
|
|
25108
25980
|
data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
|
|
25109
25981
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
|
|
25110
25982
|
dedupe_key: exports_external.string().optional().describe("Dedupe key"),
|
|
25111
|
-
deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
|
|
25983
|
+
deliver: exports_external.boolean().optional().describe("Deliver to matching channels"),
|
|
25984
|
+
approval_token: approvalTokenSchema
|
|
25985
|
+
}, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver, approval_token }) => {
|
|
25986
|
+
const resolvedData = data ?? {};
|
|
25987
|
+
const resolvedMetadata = metadata ?? {};
|
|
25988
|
+
const resolvedDeliver = deliver !== false;
|
|
25989
|
+
requireMcpMutation("machines_events_emit", approval_token, { resourceId: mutationResourceId("event", event_type, subject, dedupe_key), args: { event_type, subject, severity, message, data: resolvedData, metadata: resolvedMetadata, dedupe_key, deliver: resolvedDeliver } });
|
|
25990
|
+
return {
|
|
25991
|
+
content: [{ type: "text", text: JSON.stringify(await events.emit({
|
|
25992
|
+
source: "machines",
|
|
25993
|
+
type: event_type,
|
|
25994
|
+
subject,
|
|
25995
|
+
severity,
|
|
25996
|
+
message,
|
|
25997
|
+
data: resolvedData,
|
|
25998
|
+
metadata: resolvedMetadata,
|
|
25999
|
+
dedupeKey: dedupe_key
|
|
26000
|
+
}, { deliver: resolvedDeliver }), null, 2) }]
|
|
26001
|
+
};
|
|
26002
|
+
});
|
|
25124
26003
|
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
25125
26004
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
25126
26005
|
}));
|
|
25127
|
-
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) =>
|
|
25128
|
-
|
|
25129
|
-
|
|
26006
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery"), approval_token: approvalTokenSchema }, async ({ event_id, source, event_type, dry_run, approval_token }) => {
|
|
26007
|
+
if (dry_run !== true)
|
|
26008
|
+
requireMcpMutation("machines_events_replay", approval_token, { resourceId: mutationResourceId("event-replay", event_id, source, event_type), args: { event_id, source, event_type, dry_run: false } });
|
|
26009
|
+
return {
|
|
26010
|
+
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
26011
|
+
};
|
|
26012
|
+
});
|
|
25130
26013
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|
|
25131
26014
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
25132
26015
|
content: [{ type: "text", text: renderDashboardHtml() }]
|
|
@@ -25134,9 +26017,21 @@ function createMcpServer(version2) {
|
|
|
25134
26017
|
server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
|
|
25135
26018
|
content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
|
|
25136
26019
|
}));
|
|
25137
|
-
server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push")
|
|
25138
|
-
|
|
25139
|
-
|
|
26020
|
+
server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26021
|
+
const resolvedTables = resolveTables(tables);
|
|
26022
|
+
requireMcpMutation("storage_push", approval_token, { resourceId: mutationResourceId("storage-push", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26023
|
+
return { content: [{ type: "text", text: JSON.stringify(await storagePush({ tables: resolvedTables }), null, 2) }] };
|
|
26024
|
+
});
|
|
26025
|
+
server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26026
|
+
const resolvedTables = resolveTables(tables);
|
|
26027
|
+
requireMcpMutation("storage_pull", approval_token, { resourceId: mutationResourceId("storage-pull", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26028
|
+
return { content: [{ type: "text", text: JSON.stringify(await storagePull({ tables: resolvedTables }), null, 2) }] };
|
|
26029
|
+
});
|
|
26030
|
+
server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26031
|
+
const resolvedTables = resolveTables(tables);
|
|
26032
|
+
requireMcpMutation("storage_sync", approval_token, { resourceId: mutationResourceId("storage-sync", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26033
|
+
return { content: [{ type: "text", text: JSON.stringify(await storageSync({ tables: resolvedTables }), null, 2) }] };
|
|
26034
|
+
});
|
|
25140
26035
|
return server;
|
|
25141
26036
|
}
|
|
25142
26037
|
export {
|
|
@@ -25145,6 +26040,8 @@ export {
|
|
|
25145
26040
|
writeHeartbeatTick,
|
|
25146
26041
|
writeHeartbeat,
|
|
25147
26042
|
watchTmuxPane,
|
|
26043
|
+
verifyMutationApprovalToken,
|
|
26044
|
+
validateSshTarget,
|
|
25148
26045
|
validateManifest,
|
|
25149
26046
|
upsertHeartbeat,
|
|
25150
26047
|
testNotificationChannel,
|
|
@@ -25154,16 +26051,21 @@ export {
|
|
|
25154
26051
|
startDashboardServer,
|
|
25155
26052
|
setHeartbeatStatus,
|
|
25156
26053
|
sanitizePublicString,
|
|
26054
|
+
runTailscaleInstallPlan,
|
|
25157
26055
|
runTailscaleInstall,
|
|
26056
|
+
runSyncPlan,
|
|
25158
26057
|
runSync,
|
|
25159
26058
|
runStorageMigrations,
|
|
26059
|
+
runSetupPlan,
|
|
25160
26060
|
runSetup,
|
|
25161
26061
|
runSelfTest,
|
|
25162
26062
|
runDoctor,
|
|
25163
26063
|
runDaemonServicePlan,
|
|
26064
|
+
runClaudeInstallPlan,
|
|
25164
26065
|
runClaudeInstall,
|
|
25165
26066
|
runCertPlan,
|
|
25166
26067
|
runBackup,
|
|
26068
|
+
runAppsPlan,
|
|
25167
26069
|
runAppsInstall,
|
|
25168
26070
|
resolveTables,
|
|
25169
26071
|
resolveSshTarget,
|
|
@@ -25197,6 +26099,8 @@ export {
|
|
|
25197
26099
|
probeTmuxPane,
|
|
25198
26100
|
parseStorageTables,
|
|
25199
26101
|
parsePortOutput,
|
|
26102
|
+
mutationPlanDigest,
|
|
26103
|
+
mutationArgsSha256,
|
|
25200
26104
|
markOffline,
|
|
25201
26105
|
manifestValidate,
|
|
25202
26106
|
manifestRemove,
|
|
@@ -25215,6 +26119,7 @@ export {
|
|
|
25215
26119
|
isSensitiveKey,
|
|
25216
26120
|
isPrivateOutputEnabled,
|
|
25217
26121
|
isPrivateMetadataEnabled,
|
|
26122
|
+
isMutationApproved,
|
|
25218
26123
|
getSyncMetaAll,
|
|
25219
26124
|
getStorageStatus,
|
|
25220
26125
|
getStoragePg,
|
|
@@ -25253,13 +26158,18 @@ export {
|
|
|
25253
26158
|
diffApps,
|
|
25254
26159
|
detectCurrentMachineManifest,
|
|
25255
26160
|
defaultScreenPasswordSecretKey,
|
|
26161
|
+
createMutationApprovalToken,
|
|
25256
26162
|
createMcpServer,
|
|
25257
26163
|
createMachineResolverSnapshot,
|
|
25258
26164
|
countRuns,
|
|
25259
26165
|
closeDb,
|
|
25260
26166
|
checkMachineCompatibility,
|
|
26167
|
+
canonicalMutationArgs,
|
|
26168
|
+
buildTmuxPaneDiedHookPlan,
|
|
25261
26169
|
buildTailscaleInstallPlan,
|
|
25262
26170
|
buildSyncPlan,
|
|
26171
|
+
buildSshCommandPlan,
|
|
26172
|
+
buildSshCommandArgs,
|
|
25263
26173
|
buildSshCommand,
|
|
25264
26174
|
buildSetupPlan,
|
|
25265
26175
|
buildServer,
|
|
@@ -25277,6 +26187,9 @@ export {
|
|
|
25277
26187
|
buildCertPlan,
|
|
25278
26188
|
buildBackupPlan,
|
|
25279
26189
|
buildAppsPlan,
|
|
26190
|
+
attachMutationPlanDigest,
|
|
26191
|
+
assertMutationPlanDigest,
|
|
26192
|
+
assertMutationApproved,
|
|
25280
26193
|
addNotificationChannel,
|
|
25281
26194
|
addDomainMapping,
|
|
25282
26195
|
SqliteAdapter,
|
|
@@ -25292,6 +26205,11 @@ export {
|
|
|
25292
26205
|
PRIVATE_METADATA_ENV,
|
|
25293
26206
|
PRIVATE_MANIFEST_REF_ENV,
|
|
25294
26207
|
PRIVATE_MANIFEST_BACKEND_ENV,
|
|
26208
|
+
MUTATION_APPROVAL_TOKEN_ENV,
|
|
26209
|
+
MUTATION_APPROVAL_RUN_ENV,
|
|
26210
|
+
MUTATION_APPROVAL_REPLAY_PATH_ENV,
|
|
26211
|
+
MUTATION_APPROVAL_FLAG_ENV,
|
|
26212
|
+
MUTATION_APPROVAL_CALLER_ENV,
|
|
25295
26213
|
MACHINE_MCP_TOOL_NAMES,
|
|
25296
26214
|
MACHINES_STORAGE_TABLES,
|
|
25297
26215
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|
|
@@ -25310,6 +26228,7 @@ export {
|
|
|
25310
26228
|
MACHINES_BACKUP_PREFIX_ENV,
|
|
25311
26229
|
MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
|
|
25312
26230
|
MACHINES_BACKUP_BUCKET_ENV,
|
|
26231
|
+
LEGACY_MUTATION_APPROVAL_FLAG_ENV,
|
|
25313
26232
|
DOCTOR_OPTIONAL_ADAPTER_DOMAINS,
|
|
25314
26233
|
DEFAULT_SCREEN_SECRET_NAMESPACE,
|
|
25315
26234
|
DEFAULT_MACHINE_RESOLVER_TTL_MS,
|