@hasna/machines 0.0.45 → 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 +1301 -210
- 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 +1091 -184
- 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 +1004 -162
- 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
|
+
}
|
|
12666
12781
|
}
|
|
12782
|
+
function killPublishedProcessGroup(pgidFile) {
|
|
12783
|
+
if (!existsSync5(pgidFile))
|
|
12784
|
+
return;
|
|
12785
|
+
try {
|
|
12786
|
+
const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
|
|
12787
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
12788
|
+
return;
|
|
12789
|
+
process.kill(-pid, "SIGKILL");
|
|
12790
|
+
} catch {}
|
|
12791
|
+
}
|
|
12792
|
+
function parseHelperResult(stdout) {
|
|
12793
|
+
if (!stdout)
|
|
12794
|
+
return null;
|
|
12795
|
+
try {
|
|
12796
|
+
const parsed = JSON.parse(stdout);
|
|
12797
|
+
if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
|
|
12798
|
+
return null;
|
|
12799
|
+
return {
|
|
12800
|
+
machineId: "",
|
|
12801
|
+
source: "local",
|
|
12802
|
+
stdout: parsed.stdout,
|
|
12803
|
+
stderr: parsed.stderr,
|
|
12804
|
+
exitCode: parsed.exitCode,
|
|
12805
|
+
timedOut: parsed.timedOut === true,
|
|
12806
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null
|
|
12807
|
+
};
|
|
12808
|
+
} catch {
|
|
12809
|
+
return null;
|
|
12810
|
+
}
|
|
12811
|
+
}
|
|
12812
|
+
var PROCESS_GROUP_TIMEOUT_HELPER = `
|
|
12813
|
+
const { spawn } = require("node:child_process");
|
|
12814
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
12815
|
+
|
|
12816
|
+
const plan = JSON.parse(readFileSync(0, "utf8"));
|
|
12817
|
+
const command = String(plan.command || "");
|
|
12818
|
+
const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
|
|
12819
|
+
const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
|
|
12820
|
+
const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
|
|
12821
|
+
const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
|
|
12822
|
+
let stdout = "";
|
|
12823
|
+
let stderr = "";
|
|
12824
|
+
let timedOut = false;
|
|
12825
|
+
let finished = false;
|
|
12826
|
+
let timeoutTimer;
|
|
12827
|
+
let killTimer;
|
|
12828
|
+
let sigkillSent = false;
|
|
12829
|
+
let pendingExit = null;
|
|
12830
|
+
|
|
12831
|
+
const child = spawn(command, args, {
|
|
12832
|
+
detached: true,
|
|
12833
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12834
|
+
env: process.env,
|
|
12835
|
+
});
|
|
12836
|
+
|
|
12837
|
+
if (pgidFile && child.pid) {
|
|
12838
|
+
try {
|
|
12839
|
+
writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
|
|
12840
|
+
} catch {}
|
|
12841
|
+
}
|
|
12842
|
+
|
|
12843
|
+
function appendText(target, chunk) {
|
|
12844
|
+
return target + String(chunk);
|
|
12845
|
+
}
|
|
12846
|
+
|
|
12847
|
+
function killTarget(signal) {
|
|
12848
|
+
if (!child.pid) return;
|
|
12849
|
+
if (process.platform === "win32") {
|
|
12850
|
+
try {
|
|
12851
|
+
process.kill(child.pid, signal);
|
|
12852
|
+
} catch {}
|
|
12853
|
+
return;
|
|
12854
|
+
}
|
|
12855
|
+
try {
|
|
12856
|
+
process.kill(-child.pid, signal);
|
|
12857
|
+
} catch {}
|
|
12858
|
+
}
|
|
12859
|
+
|
|
12860
|
+
function finish(code, signal) {
|
|
12861
|
+
if (finished) return;
|
|
12862
|
+
if (timedOut && !sigkillSent) {
|
|
12863
|
+
pendingExit = { code, signal };
|
|
12864
|
+
return;
|
|
12865
|
+
}
|
|
12866
|
+
finished = true;
|
|
12867
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
12868
|
+
if (killTimer) clearTimeout(killTimer);
|
|
12869
|
+
if (timedOut) {
|
|
12870
|
+
stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12871
|
+
}
|
|
12872
|
+
const exitCode = timedOut ? 124 : code ?? 1;
|
|
12873
|
+
process.stdout.write(JSON.stringify({
|
|
12874
|
+
stdout,
|
|
12875
|
+
stderr,
|
|
12876
|
+
exitCode,
|
|
12877
|
+
timedOut,
|
|
12878
|
+
signal: signal ?? null,
|
|
12879
|
+
}), () => process.exit(exitCode));
|
|
12880
|
+
}
|
|
12881
|
+
|
|
12882
|
+
child.stdout.setEncoding("utf8");
|
|
12883
|
+
child.stderr.setEncoding("utf8");
|
|
12884
|
+
child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
|
|
12885
|
+
child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
|
|
12886
|
+
let childExit = { code: null, signal: null };
|
|
12887
|
+
child.on("error", (error) => {
|
|
12888
|
+
stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12889
|
+
finish(1, null);
|
|
12890
|
+
});
|
|
12891
|
+
child.on("exit", (code, signal) => {
|
|
12892
|
+
childExit = { code, signal };
|
|
12893
|
+
});
|
|
12894
|
+
child.on("close", (code, signal) => {
|
|
12895
|
+
finish(code ?? childExit.code, signal ?? childExit.signal);
|
|
12896
|
+
});
|
|
12897
|
+
|
|
12898
|
+
timeoutTimer = setTimeout(() => {
|
|
12899
|
+
timedOut = true;
|
|
12900
|
+
killTarget("SIGTERM");
|
|
12901
|
+
killTimer = setTimeout(() => {
|
|
12902
|
+
sigkillSent = true;
|
|
12903
|
+
killTarget("SIGKILL");
|
|
12904
|
+
if (pendingExit) finish(pendingExit.code, pendingExit.signal);
|
|
12905
|
+
}, killGraceMs);
|
|
12906
|
+
}, timeoutMs);
|
|
12907
|
+
`;
|
|
12667
12908
|
function describeMachineCommandFailure(operation, result) {
|
|
12668
12909
|
const detail = (result.stderr || result.stdout || "").trim();
|
|
12669
12910
|
const suffix = detail ? `: ${detail}` : "";
|
|
@@ -13361,7 +13602,7 @@ function getAgentStatus(machineId, options = {}) {
|
|
|
13361
13602
|
}
|
|
13362
13603
|
// src/commands/backup.ts
|
|
13363
13604
|
import { homedir as homedir2, hostname as hostname6 } from "os";
|
|
13364
|
-
import { join as
|
|
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,15 +14260,15 @@ function renderDomainMapping(domain) {
|
|
|
13720
14260
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
13721
14261
|
caddySnippet: `${entry.domain} {
|
|
13722
14262
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
13723
|
-
tls ${
|
|
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
|
|
14271
|
+
import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync5, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
13732
14272
|
import { delimiter, dirname as dirname4 } from "path";
|
|
13733
14273
|
import { platform as osPlatform } from "os";
|
|
13734
14274
|
var DEFAULT_SERVICE_NAME = "machines-agent";
|
|
@@ -14166,7 +14706,7 @@ function bunRuntimeCandidates(executable) {
|
|
|
14166
14706
|
}
|
|
14167
14707
|
function isBunShebangScript(executable) {
|
|
14168
14708
|
try {
|
|
14169
|
-
const content =
|
|
14709
|
+
const content = readFileSync5(executable, "utf8").slice(0, 256);
|
|
14170
14710
|
const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
|
|
14171
14711
|
return /^#!.*\bbun\b/.test(firstLine2);
|
|
14172
14712
|
} catch {
|
|
@@ -14174,7 +14714,7 @@ function isBunShebangScript(executable) {
|
|
|
14174
14714
|
}
|
|
14175
14715
|
}
|
|
14176
14716
|
function isExecutableFile(path) {
|
|
14177
|
-
if (!
|
|
14717
|
+
if (!existsSync7(path))
|
|
14178
14718
|
return false;
|
|
14179
14719
|
try {
|
|
14180
14720
|
const stats = statSync(path);
|
|
@@ -14352,12 +14892,12 @@ function parseProbe(tool, stdout) {
|
|
|
14352
14892
|
}
|
|
14353
14893
|
function buildClaudeInstallPlan(machineId, tools) {
|
|
14354
14894
|
const machine = resolveMachine2(machineId);
|
|
14355
|
-
return {
|
|
14895
|
+
return attachMutationPlanDigest({
|
|
14356
14896
|
machineId: machine.id,
|
|
14357
14897
|
mode: "plan",
|
|
14358
14898
|
steps: buildInstallSteps(machine, tools),
|
|
14359
14899
|
executed: 0
|
|
14360
|
-
};
|
|
14900
|
+
});
|
|
14361
14901
|
}
|
|
14362
14902
|
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
14363
14903
|
const machine = resolveMachine2(machineId);
|
|
@@ -14382,8 +14922,12 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
|
14382
14922
|
}
|
|
14383
14923
|
function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
|
|
14384
14924
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
14925
|
+
return runClaudeInstallPlan(plan, options, runner);
|
|
14926
|
+
}
|
|
14927
|
+
function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
14928
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
14385
14929
|
if (!options.apply)
|
|
14386
|
-
return plan;
|
|
14930
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
14387
14931
|
if (!options.yes) {
|
|
14388
14932
|
throw new Error("Claude CLI installation requires --yes.");
|
|
14389
14933
|
}
|
|
@@ -14392,12 +14936,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
|
|
|
14392
14936
|
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
14393
14937
|
executed += 1;
|
|
14394
14938
|
}
|
|
14395
|
-
return {
|
|
14939
|
+
return attachMutationPlanDigest({
|
|
14396
14940
|
machineId: plan.machineId,
|
|
14397
14941
|
mode: "apply",
|
|
14398
14942
|
steps: plan.steps,
|
|
14399
14943
|
executed
|
|
14400
|
-
};
|
|
14944
|
+
});
|
|
14401
14945
|
}
|
|
14402
14946
|
// src/commands/install-tailscale.ts
|
|
14403
14947
|
function buildInstallSteps2(machine) {
|
|
@@ -14436,17 +14980,21 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
14436
14980
|
if (!machine) {
|
|
14437
14981
|
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
14438
14982
|
}
|
|
14439
|
-
return {
|
|
14983
|
+
return attachMutationPlanDigest({
|
|
14440
14984
|
machineId: machine.id,
|
|
14441
14985
|
mode: "plan",
|
|
14442
14986
|
steps: buildInstallSteps2(machine),
|
|
14443
14987
|
executed: 0
|
|
14444
|
-
};
|
|
14988
|
+
});
|
|
14445
14989
|
}
|
|
14446
14990
|
function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
14447
14991
|
const plan = buildTailscaleInstallPlan(machineId);
|
|
14992
|
+
return runTailscaleInstallPlan(plan, options, runner);
|
|
14993
|
+
}
|
|
14994
|
+
function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
14995
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
14448
14996
|
if (!options.apply)
|
|
14449
|
-
return plan;
|
|
14997
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
14450
14998
|
if (!options.yes) {
|
|
14451
14999
|
throw new Error("Tailscale install requires --yes.");
|
|
14452
15000
|
}
|
|
@@ -14455,19 +15003,21 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
14455
15003
|
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
14456
15004
|
executed += 1;
|
|
14457
15005
|
}
|
|
14458
|
-
return {
|
|
15006
|
+
return attachMutationPlanDigest({
|
|
14459
15007
|
machineId: plan.machineId,
|
|
14460
15008
|
mode: "apply",
|
|
14461
15009
|
steps: plan.steps,
|
|
14462
15010
|
executed
|
|
14463
|
-
};
|
|
15011
|
+
});
|
|
14464
15012
|
}
|
|
14465
15013
|
// src/commands/notifications.ts
|
|
14466
|
-
import { existsSync as
|
|
15014
|
+
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
15015
|
+
import { delimiter as delimiter2, isAbsolute, join as join7 } from "path";
|
|
14467
15016
|
var notificationChannelSchema = exports_external.object({
|
|
14468
15017
|
id: exports_external.string(),
|
|
14469
15018
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
14470
15019
|
target: exports_external.string(),
|
|
15020
|
+
commandArgs: exports_external.array(exports_external.string()).optional(),
|
|
14471
15021
|
events: exports_external.array(exports_external.string()),
|
|
14472
15022
|
enabled: exports_external.boolean()
|
|
14473
15023
|
});
|
|
@@ -14476,19 +15026,31 @@ var notificationConfigSchema = exports_external.object({
|
|
|
14476
15026
|
updatedAt: exports_external.string().optional(),
|
|
14477
15027
|
channels: exports_external.array(notificationChannelSchema)
|
|
14478
15028
|
});
|
|
15029
|
+
var trustedNotificationApproval = Symbol("trustedNotificationApproval");
|
|
15030
|
+
function createTrustedNotificationApproval() {
|
|
15031
|
+
return { [trustedNotificationApproval]: true };
|
|
15032
|
+
}
|
|
15033
|
+
function isTrustedNotificationApproval(approval) {
|
|
15034
|
+
return approval?.[trustedNotificationApproval] === true;
|
|
15035
|
+
}
|
|
14479
15036
|
function sortChannels(channels) {
|
|
14480
15037
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
14481
15038
|
}
|
|
14482
|
-
function shellQuote6(value) {
|
|
14483
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14484
|
-
}
|
|
14485
15039
|
function hasCommand2(binary) {
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
15040
|
+
return Boolean(resolveExecutable(binary));
|
|
15041
|
+
}
|
|
15042
|
+
function resolveExecutable(binary) {
|
|
15043
|
+
const trimmed = binary.trim();
|
|
15044
|
+
if (!trimmed)
|
|
15045
|
+
return null;
|
|
15046
|
+
const candidates = isAbsolute(trimmed) ? [trimmed] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).map((dir) => join7(dir, trimmed));
|
|
15047
|
+
for (const candidate of candidates) {
|
|
15048
|
+
try {
|
|
15049
|
+
accessSync(candidate, constants.X_OK);
|
|
15050
|
+
return candidate;
|
|
15051
|
+
} catch {}
|
|
15052
|
+
}
|
|
15053
|
+
return null;
|
|
14492
15054
|
}
|
|
14493
15055
|
function buildNotificationPreview(channel, event, message) {
|
|
14494
15056
|
if (channel.type === "email") {
|
|
@@ -14497,7 +15059,8 @@ function buildNotificationPreview(channel, event, message) {
|
|
|
14497
15059
|
if (channel.type === "webhook") {
|
|
14498
15060
|
return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
|
|
14499
15061
|
}
|
|
14500
|
-
|
|
15062
|
+
const args = channel.commandArgs?.length ? ` ${channel.commandArgs.join(" ")}` : "";
|
|
15063
|
+
return `${channel.target}${args} with HASNA_MACHINES_NOTIFICATION_* environment`;
|
|
14501
15064
|
}
|
|
14502
15065
|
async function dispatchEmail(channel, event, message) {
|
|
14503
15066
|
const subject = `[${event}] machines notification`;
|
|
@@ -14508,7 +15071,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
14508
15071
|
${message}
|
|
14509
15072
|
`;
|
|
14510
15073
|
if (hasCommand2("sendmail")) {
|
|
14511
|
-
const result = Bun.spawnSync(["
|
|
15074
|
+
const result = Bun.spawnSync(["sendmail", "-t"], {
|
|
14512
15075
|
stdin: new TextEncoder().encode(body),
|
|
14513
15076
|
stdout: "pipe",
|
|
14514
15077
|
stderr: "pipe",
|
|
@@ -14526,8 +15089,9 @@ ${message}
|
|
|
14526
15089
|
};
|
|
14527
15090
|
}
|
|
14528
15091
|
if (hasCommand2("mail")) {
|
|
14529
|
-
const
|
|
14530
|
-
|
|
15092
|
+
const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
|
|
15093
|
+
stdin: new TextEncoder().encode(`${message}
|
|
15094
|
+
`),
|
|
14531
15095
|
stdout: "pipe",
|
|
14532
15096
|
stderr: "pipe",
|
|
14533
15097
|
env: process.env
|
|
@@ -14570,8 +15134,20 @@ async function dispatchWebhook(channel, event, message) {
|
|
|
14570
15134
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
14571
15135
|
};
|
|
14572
15136
|
}
|
|
14573
|
-
async function dispatchCommand(channel, event, message) {
|
|
14574
|
-
|
|
15137
|
+
async function dispatchCommand(channel, event, message, options = {}) {
|
|
15138
|
+
if (!isTrustedNotificationApproval(options.trustedApproval)) {
|
|
15139
|
+
assertMutationApproved({
|
|
15140
|
+
surface: "notifications",
|
|
15141
|
+
operation: "dispatch_command",
|
|
15142
|
+
resourceId: channel.id,
|
|
15143
|
+
approvalToken: options.approvalToken
|
|
15144
|
+
});
|
|
15145
|
+
}
|
|
15146
|
+
const executable = resolveExecutable(channel.target);
|
|
15147
|
+
if (!executable) {
|
|
15148
|
+
throw new Error(`Command executable not found or not executable: ${channel.target}`);
|
|
15149
|
+
}
|
|
15150
|
+
const result = Bun.spawnSync([executable, ...channel.commandArgs ?? []], {
|
|
14575
15151
|
stdout: "pipe",
|
|
14576
15152
|
stderr: "pipe",
|
|
14577
15153
|
env: {
|
|
@@ -14593,7 +15169,7 @@ async function dispatchCommand(channel, event, message) {
|
|
|
14593
15169
|
detail: stdout || "Command completed successfully"
|
|
14594
15170
|
};
|
|
14595
15171
|
}
|
|
14596
|
-
async function dispatchChannel(channel, event, message) {
|
|
15172
|
+
async function dispatchChannel(channel, event, message, options = {}) {
|
|
14597
15173
|
if (!channel.enabled) {
|
|
14598
15174
|
return {
|
|
14599
15175
|
channelId: channel.id,
|
|
@@ -14609,7 +15185,7 @@ async function dispatchChannel(channel, event, message) {
|
|
|
14609
15185
|
if (channel.type === "webhook") {
|
|
14610
15186
|
return dispatchWebhook(channel, event, message);
|
|
14611
15187
|
}
|
|
14612
|
-
return dispatchCommand(channel, event, message);
|
|
15188
|
+
return dispatchCommand(channel, event, message, options);
|
|
14613
15189
|
}
|
|
14614
15190
|
function getDefaultNotificationConfig() {
|
|
14615
15191
|
return {
|
|
@@ -14619,10 +15195,10 @@ function getDefaultNotificationConfig() {
|
|
|
14619
15195
|
};
|
|
14620
15196
|
}
|
|
14621
15197
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
14622
|
-
if (!
|
|
15198
|
+
if (!existsSync8(path)) {
|
|
14623
15199
|
return getDefaultNotificationConfig();
|
|
14624
15200
|
}
|
|
14625
|
-
return notificationConfigSchema.parse(JSON.parse(
|
|
15201
|
+
return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
|
|
14626
15202
|
}
|
|
14627
15203
|
function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
14628
15204
|
ensureParentDir(path);
|
|
@@ -14638,11 +15214,20 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
|
14638
15214
|
function listNotificationChannels() {
|
|
14639
15215
|
return readNotificationConfig();
|
|
14640
15216
|
}
|
|
14641
|
-
function addNotificationChannel(channel) {
|
|
15217
|
+
function addNotificationChannel(channel, options = {}) {
|
|
15218
|
+
if (channel.type === "command" && !isTrustedNotificationApproval(options.trustedApproval)) {
|
|
15219
|
+
assertMutationApproved({
|
|
15220
|
+
surface: "notifications",
|
|
15221
|
+
operation: "add_command_channel",
|
|
15222
|
+
resourceId: channel.id,
|
|
15223
|
+
approvalToken: options.approvalToken
|
|
15224
|
+
});
|
|
15225
|
+
}
|
|
14642
15226
|
const config = readNotificationConfig();
|
|
14643
15227
|
const channels = config.channels.filter((entry) => entry.id !== channel.id);
|
|
14644
15228
|
channels.push({
|
|
14645
15229
|
...channel,
|
|
15230
|
+
commandArgs: channel.commandArgs?.map(String),
|
|
14646
15231
|
events: [...new Set(channel.events)]
|
|
14647
15232
|
});
|
|
14648
15233
|
return writeNotificationConfig({ ...config, channels });
|
|
@@ -14664,7 +15249,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
14664
15249
|
const deliveries = [];
|
|
14665
15250
|
for (const channel of channels) {
|
|
14666
15251
|
try {
|
|
14667
|
-
deliveries.push(await dispatchChannel(channel, event, message));
|
|
15252
|
+
deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
|
|
14668
15253
|
} catch (error) {
|
|
14669
15254
|
deliveries.push({
|
|
14670
15255
|
channelId: channel.id,
|
|
@@ -14699,7 +15284,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
14699
15284
|
if (!options.yes) {
|
|
14700
15285
|
throw new Error("Notification test execution requires --yes.");
|
|
14701
15286
|
}
|
|
14702
|
-
const delivery = await dispatchChannel(channel, event, message);
|
|
15287
|
+
const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
|
|
14703
15288
|
return {
|
|
14704
15289
|
channelId,
|
|
14705
15290
|
mode: "apply",
|
|
@@ -14765,6 +15350,46 @@ function listPorts(machineId) {
|
|
|
14765
15350
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14766
15351
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
14767
15352
|
import { EventsClient } from "@hasna/events";
|
|
15353
|
+
function shellQuote6(value) {
|
|
15354
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
15355
|
+
}
|
|
15356
|
+
function buildTmuxPaneDiedHookPlan(options = {}) {
|
|
15357
|
+
const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
|
|
15358
|
+
const machinesCommand = options.machinesCommand ?? "machines";
|
|
15359
|
+
const deliver = options.deliver === true;
|
|
15360
|
+
const approvalToken = options.approvalToken?.trim();
|
|
15361
|
+
const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
|
|
15362
|
+
const emitArgs = [
|
|
15363
|
+
"events",
|
|
15364
|
+
"emit",
|
|
15365
|
+
"machines.tmux.pane_died",
|
|
15366
|
+
"--source",
|
|
15367
|
+
"machines",
|
|
15368
|
+
"--subject",
|
|
15369
|
+
"tmux:#{hook_pane}",
|
|
15370
|
+
"--severity",
|
|
15371
|
+
"warning",
|
|
15372
|
+
"--message",
|
|
15373
|
+
"tmux pane died: #{hook_pane}",
|
|
15374
|
+
"--data",
|
|
15375
|
+
'{"target":"#{hook_pane}","session":"#{session_name}","window":"#{window_index}"}'
|
|
15376
|
+
];
|
|
15377
|
+
if (!deliver)
|
|
15378
|
+
emitArgs.push("--no-deliver");
|
|
15379
|
+
if (approvalToken)
|
|
15380
|
+
emitArgs.push("--approval-token", approvalToken);
|
|
15381
|
+
const command2 = [machinesCommand, ...emitArgs].map(shellQuote6).join(" ");
|
|
15382
|
+
const runShell = trustedLocalMutation ? `HASNA_MACHINES_ALLOW_MUTATIONS=1 ${command2}` : command2;
|
|
15383
|
+
const args = ["set-hook", "-g", "pane-died", `run-shell ${shellQuote6(runShell)}`];
|
|
15384
|
+
return {
|
|
15385
|
+
tmuxCommand,
|
|
15386
|
+
args,
|
|
15387
|
+
shellCommand: [tmuxCommand, ...args].map(shellQuote6).join(" "),
|
|
15388
|
+
eventType: "machines.tmux.pane_died",
|
|
15389
|
+
deliver,
|
|
15390
|
+
trustedLocalMutation
|
|
15391
|
+
};
|
|
15392
|
+
}
|
|
14768
15393
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
14769
15394
|
const checkedAt = new Date().toISOString();
|
|
14770
15395
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -14850,7 +15475,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
14850
15475
|
return client.emit(input, { deliver });
|
|
14851
15476
|
}
|
|
14852
15477
|
// src/commands/serve.ts
|
|
14853
|
-
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
15478
|
+
import { EventsClient as EventsClient2, getEventsDataDir, sanitizeChannelsForOutput } from "@hasna/events";
|
|
15479
|
+
import { resolve as resolve3 } from "path";
|
|
14854
15480
|
|
|
14855
15481
|
// src/commands/status.ts
|
|
14856
15482
|
function parseJsonObject2(value) {
|
|
@@ -14905,7 +15531,7 @@ function escapeHtml(value) {
|
|
|
14905
15531
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
14906
15532
|
}
|
|
14907
15533
|
function getServeInfo(options = {}) {
|
|
14908
|
-
const host = options.host || "
|
|
15534
|
+
const host = options.host || "127.0.0.1";
|
|
14909
15535
|
const port = options.port || 7676;
|
|
14910
15536
|
return {
|
|
14911
15537
|
host,
|
|
@@ -15108,6 +15734,59 @@ async function parseJsonBody(request) {
|
|
|
15108
15734
|
function jsonError(message, status = 400) {
|
|
15109
15735
|
return Response.json({ error: message }, { status });
|
|
15110
15736
|
}
|
|
15737
|
+
function dashboardResourceId(kind, ...parts) {
|
|
15738
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
15739
|
+
return values ? `${kind}:${values}` : kind;
|
|
15740
|
+
}
|
|
15741
|
+
function eventStoreDir() {
|
|
15742
|
+
return resolve3(getEventsDataDir());
|
|
15743
|
+
}
|
|
15744
|
+
function eventStoreScope() {
|
|
15745
|
+
return { event_store_dir: eventStoreDir() };
|
|
15746
|
+
}
|
|
15747
|
+
function eventStoreResourceId(kind, ...parts) {
|
|
15748
|
+
return dashboardResourceId(kind, mutationArgsSha256(eventStoreScope()), ...parts);
|
|
15749
|
+
}
|
|
15750
|
+
function withEventStoreScope(args) {
|
|
15751
|
+
return { event_store_dir: eventStoreDir(), ...args };
|
|
15752
|
+
}
|
|
15753
|
+
function dashboardMutationCallerId() {
|
|
15754
|
+
return process.env[MUTATION_APPROVAL_CALLER_ENV]?.trim() || "dashboard";
|
|
15755
|
+
}
|
|
15756
|
+
function dashboardMutationRunId() {
|
|
15757
|
+
return process.env[MUTATION_APPROVAL_RUN_ENV]?.trim() || "dashboard";
|
|
15758
|
+
}
|
|
15759
|
+
function approvalTokenFromRequest(request, body) {
|
|
15760
|
+
const bodyToken = typeof body["approval_token"] === "string" ? body["approval_token"] : typeof body["approvalToken"] === "string" ? body["approvalToken"] : undefined;
|
|
15761
|
+
if (bodyToken?.trim())
|
|
15762
|
+
return bodyToken;
|
|
15763
|
+
const headerToken = request.headers.get("x-hasna-approval-token")?.trim();
|
|
15764
|
+
if (headerToken)
|
|
15765
|
+
return headerToken;
|
|
15766
|
+
const authorization = request.headers.get("authorization")?.trim();
|
|
15767
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) {
|
|
15768
|
+
return authorization.slice("bearer ".length).trim();
|
|
15769
|
+
}
|
|
15770
|
+
return;
|
|
15771
|
+
}
|
|
15772
|
+
function requireDashboardMutation(operation, request, body, scope = {}) {
|
|
15773
|
+
const decision = verifyMutationApprovalToken({
|
|
15774
|
+
surface: "dashboard",
|
|
15775
|
+
operation,
|
|
15776
|
+
transport: "dashboard:http",
|
|
15777
|
+
callerId: dashboardMutationCallerId(),
|
|
15778
|
+
runId: dashboardMutationRunId(),
|
|
15779
|
+
resourceId: scope.resourceId,
|
|
15780
|
+
args: scope.args,
|
|
15781
|
+
approvalToken: approvalTokenFromRequest(request, body)
|
|
15782
|
+
});
|
|
15783
|
+
if (decision.approved)
|
|
15784
|
+
return;
|
|
15785
|
+
return jsonError(`Mutation approval denied: ${decision.reason ?? "approval_token is invalid."}`, 403);
|
|
15786
|
+
}
|
|
15787
|
+
function objectBodyValue(value) {
|
|
15788
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
15789
|
+
}
|
|
15111
15790
|
function privateOutputWarnings(requested, allowed) {
|
|
15112
15791
|
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
15113
15792
|
}
|
|
@@ -15119,6 +15798,7 @@ function appendWarnings(payload, warnings) {
|
|
|
15119
15798
|
function startDashboardServer(options = {}) {
|
|
15120
15799
|
const info = getServeInfo(options);
|
|
15121
15800
|
const events = new EventsClient2;
|
|
15801
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
15122
15802
|
return Bun.serve({
|
|
15123
15803
|
hostname: info.host,
|
|
15124
15804
|
port: info.port,
|
|
@@ -15183,8 +15863,25 @@ function startDashboardServer(options = {}) {
|
|
|
15183
15863
|
const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
|
|
15184
15864
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
15185
15865
|
const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
|
|
15186
|
-
const data =
|
|
15187
|
-
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;
|
|
15188
15885
|
return Response.json(await events.emit({
|
|
15189
15886
|
source,
|
|
15190
15887
|
type,
|
|
@@ -15227,8 +15924,20 @@ function startDashboardServer(options = {}) {
|
|
|
15227
15924
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
15228
15925
|
const apply = body["apply"] === true;
|
|
15229
15926
|
const yes = body["yes"] === true;
|
|
15927
|
+
const resolvedEvent = event ?? "manual.test";
|
|
15928
|
+
const resolvedMessage = message ?? "machines notification test";
|
|
15929
|
+
const denied = requireDashboardMutation("machines_notifications_test", request, body, {
|
|
15930
|
+
resourceId: dashboardResourceId("notification-test", channelId, resolvedEvent),
|
|
15931
|
+
args: { channel_id: channelId, event: resolvedEvent, message: resolvedMessage, apply, yes }
|
|
15932
|
+
});
|
|
15933
|
+
if (denied)
|
|
15934
|
+
return denied;
|
|
15230
15935
|
try {
|
|
15231
|
-
return Response.json(await testNotificationChannel(channelId,
|
|
15936
|
+
return Response.json(await testNotificationChannel(channelId, resolvedEvent, resolvedMessage, {
|
|
15937
|
+
apply,
|
|
15938
|
+
yes,
|
|
15939
|
+
trustedApproval: apply ? trustedNotificationApproval2 : undefined
|
|
15940
|
+
}));
|
|
15232
15941
|
} catch (error) {
|
|
15233
15942
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
15234
15943
|
}
|
|
@@ -15243,13 +15952,22 @@ function startDashboardServer(options = {}) {
|
|
|
15243
15952
|
return jsonError("channelId is required.");
|
|
15244
15953
|
}
|
|
15245
15954
|
const type = typeof body["type"] === "string" ? body["type"] : "events.test";
|
|
15246
|
-
const
|
|
15955
|
+
const subject = channelId;
|
|
15956
|
+
const message = typeof body["message"] === "string" ? body["message"] : "Hasna events test delivery";
|
|
15957
|
+
const data = objectBodyValue(body["data"]);
|
|
15958
|
+
const denied = requireDashboardMutation("machines_webhooks_test", request, body, {
|
|
15959
|
+
resourceId: eventStoreResourceId("webhook-test", channelId, type),
|
|
15960
|
+
args: withEventStoreScope({ channel_id: channelId, event_type: type, subject, message, data })
|
|
15961
|
+
});
|
|
15962
|
+
if (denied)
|
|
15963
|
+
return denied;
|
|
15247
15964
|
try {
|
|
15248
15965
|
return Response.json(await events.testChannel(channelId, {
|
|
15249
15966
|
source: "machines",
|
|
15250
15967
|
type,
|
|
15251
|
-
subject
|
|
15252
|
-
message
|
|
15968
|
+
subject,
|
|
15969
|
+
message,
|
|
15970
|
+
data
|
|
15253
15971
|
}));
|
|
15254
15972
|
} catch (error) {
|
|
15255
15973
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
@@ -15397,17 +16115,21 @@ function buildSetupPlan(machineId) {
|
|
|
15397
16115
|
workspacePath: `${homedir4()}/workspace`
|
|
15398
16116
|
};
|
|
15399
16117
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
15400
|
-
return {
|
|
16118
|
+
return attachMutationPlanDigest({
|
|
15401
16119
|
machineId: target.id,
|
|
15402
16120
|
mode: "plan",
|
|
15403
16121
|
steps,
|
|
15404
16122
|
executed: 0
|
|
15405
|
-
};
|
|
16123
|
+
});
|
|
15406
16124
|
}
|
|
15407
16125
|
function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
15408
16126
|
const plan = buildSetupPlan(machineId);
|
|
16127
|
+
return runSetupPlan(plan, options, runner);
|
|
16128
|
+
}
|
|
16129
|
+
function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
|
|
16130
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
15409
16131
|
if (!options.apply) {
|
|
15410
|
-
return plan;
|
|
16132
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
15411
16133
|
}
|
|
15412
16134
|
if (!options.yes) {
|
|
15413
16135
|
throw new Error("Setup execution requires --yes.");
|
|
@@ -15428,12 +16150,12 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
15428
16150
|
}
|
|
15429
16151
|
executed += 1;
|
|
15430
16152
|
}
|
|
15431
|
-
const summary = {
|
|
16153
|
+
const summary = attachMutationPlanDigest({
|
|
15432
16154
|
machineId: plan.machineId,
|
|
15433
16155
|
mode: "apply",
|
|
15434
16156
|
steps: plan.steps,
|
|
15435
16157
|
executed
|
|
15436
|
-
};
|
|
16158
|
+
});
|
|
15437
16159
|
recordSetupRun(plan.machineId, "completed", summary);
|
|
15438
16160
|
return summary;
|
|
15439
16161
|
}
|
|
@@ -15552,16 +16274,22 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
15552
16274
|
}
|
|
15553
16275
|
const secretsCommand = options.secretsCommand || "secrets";
|
|
15554
16276
|
const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
|
|
16277
|
+
const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
|
|
16278
|
+
const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
|
|
15555
16279
|
return {
|
|
15556
16280
|
machineId: credentials.machineId,
|
|
15557
16281
|
user: credentials.user,
|
|
15558
16282
|
passwordSecretKey: credentials.passwordSecretKey,
|
|
15559
16283
|
remoteCommand,
|
|
15560
|
-
|
|
16284
|
+
secretsCommand,
|
|
16285
|
+
secretsCommandArgs,
|
|
16286
|
+
sshCommand: sshPlan.command,
|
|
16287
|
+
sshCommandArgs: sshPlan.args,
|
|
16288
|
+
command: `${shellCommand2(secretsCommandArgs)} | ${sshPlan.shellCommand}`
|
|
15561
16289
|
};
|
|
15562
16290
|
}
|
|
15563
16291
|
// src/commands/sync.ts
|
|
15564
|
-
import { existsSync as
|
|
16292
|
+
import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, symlinkSync, copyFileSync } from "fs";
|
|
15565
16293
|
import { homedir as homedir5 } from "os";
|
|
15566
16294
|
function quote4(value) {
|
|
15567
16295
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -15614,15 +16342,15 @@ function detectFileActions(machine) {
|
|
|
15614
16342
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
15615
16343
|
}
|
|
15616
16344
|
return (machine.files || []).map((file, index) => {
|
|
15617
|
-
const sourceExists =
|
|
15618
|
-
const targetExists =
|
|
16345
|
+
const sourceExists = existsSync9(file.source);
|
|
16346
|
+
const targetExists = existsSync9(file.target);
|
|
15619
16347
|
let status = "missing";
|
|
15620
16348
|
if (sourceExists && targetExists) {
|
|
15621
16349
|
if (file.mode === "symlink") {
|
|
15622
16350
|
status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
|
|
15623
16351
|
} else {
|
|
15624
|
-
const source =
|
|
15625
|
-
const target =
|
|
16352
|
+
const source = readFileSync7(file.source, "utf8");
|
|
16353
|
+
const target = readFileSync7(file.target, "utf8");
|
|
15626
16354
|
status = source === target ? "ok" : "drifted";
|
|
15627
16355
|
}
|
|
15628
16356
|
}
|
|
@@ -15652,12 +16380,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
15652
16380
|
...detectPackageActions(target, runner),
|
|
15653
16381
|
...detectFileActions(target)
|
|
15654
16382
|
];
|
|
15655
|
-
return {
|
|
16383
|
+
return attachMutationPlanDigest({
|
|
15656
16384
|
machineId: target.id,
|
|
15657
16385
|
mode: "plan",
|
|
15658
16386
|
actions,
|
|
15659
16387
|
executed: 0
|
|
15660
|
-
};
|
|
16388
|
+
});
|
|
15661
16389
|
}
|
|
15662
16390
|
function applyFileAction(command2) {
|
|
15663
16391
|
const [verb, source, target] = command2.split(" ");
|
|
@@ -15681,8 +16409,12 @@ function applyFileAction(command2) {
|
|
|
15681
16409
|
}
|
|
15682
16410
|
function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
15683
16411
|
const plan = buildSyncPlan(machineId, runner);
|
|
16412
|
+
return runSyncPlan(plan, options, runner);
|
|
16413
|
+
}
|
|
16414
|
+
function runSyncPlan(plan, options = {}, runner = runMachineCommand) {
|
|
16415
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
15684
16416
|
if (!options.apply) {
|
|
15685
|
-
return plan;
|
|
16417
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
15686
16418
|
}
|
|
15687
16419
|
if (!options.yes) {
|
|
15688
16420
|
throw new Error("Sync execution requires --yes.");
|
|
@@ -15709,12 +16441,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
|
15709
16441
|
}
|
|
15710
16442
|
executed += 1;
|
|
15711
16443
|
}
|
|
15712
|
-
const summary = {
|
|
16444
|
+
const summary = attachMutationPlanDigest({
|
|
15713
16445
|
machineId: plan.machineId,
|
|
15714
16446
|
mode: "apply",
|
|
15715
16447
|
actions: plan.actions,
|
|
15716
16448
|
executed
|
|
15717
|
-
};
|
|
16449
|
+
});
|
|
15718
16450
|
recordSyncRun(plan.machineId, "completed", summary);
|
|
15719
16451
|
return summary;
|
|
15720
16452
|
}
|
|
@@ -23099,7 +23831,7 @@ class Protocol {
|
|
|
23099
23831
|
return;
|
|
23100
23832
|
}
|
|
23101
23833
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
|
|
23102
|
-
await new Promise((
|
|
23834
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
23103
23835
|
options?.signal?.throwIfAborted();
|
|
23104
23836
|
}
|
|
23105
23837
|
} catch (error2) {
|
|
@@ -23111,7 +23843,7 @@ class Protocol {
|
|
|
23111
23843
|
}
|
|
23112
23844
|
request(request, resultSchema, options) {
|
|
23113
23845
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
23114
|
-
return new Promise((
|
|
23846
|
+
return new Promise((resolve4, reject) => {
|
|
23115
23847
|
const earlyReject = (error2) => {
|
|
23116
23848
|
reject(error2);
|
|
23117
23849
|
};
|
|
@@ -23189,7 +23921,7 @@ class Protocol {
|
|
|
23189
23921
|
if (!parseResult.success) {
|
|
23190
23922
|
reject(parseResult.error);
|
|
23191
23923
|
} else {
|
|
23192
|
-
|
|
23924
|
+
resolve4(parseResult.data);
|
|
23193
23925
|
}
|
|
23194
23926
|
} catch (error2) {
|
|
23195
23927
|
reject(error2);
|
|
@@ -23380,12 +24112,12 @@ class Protocol {
|
|
|
23380
24112
|
interval = task.pollInterval;
|
|
23381
24113
|
}
|
|
23382
24114
|
} catch {}
|
|
23383
|
-
return new Promise((
|
|
24115
|
+
return new Promise((resolve4, reject) => {
|
|
23384
24116
|
if (signal.aborted) {
|
|
23385
24117
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
23386
24118
|
return;
|
|
23387
24119
|
}
|
|
23388
|
-
const timeoutId = setTimeout(
|
|
24120
|
+
const timeoutId = setTimeout(resolve4, interval);
|
|
23389
24121
|
signal.addEventListener("abort", () => {
|
|
23390
24122
|
clearTimeout(timeoutId);
|
|
23391
24123
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -24239,7 +24971,7 @@ class McpServer {
|
|
|
24239
24971
|
let task = createTaskResult.task;
|
|
24240
24972
|
const pollInterval = task.pollInterval ?? 5000;
|
|
24241
24973
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
24242
|
-
await new Promise((
|
|
24974
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
24243
24975
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
24244
24976
|
if (!updatedTask) {
|
|
24245
24977
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -24838,8 +25570,8 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
24838
25570
|
"storage_pull",
|
|
24839
25571
|
"storage_sync"
|
|
24840
25572
|
];
|
|
24841
|
-
function buildServer(version2 = getPackageVersion()) {
|
|
24842
|
-
return createMcpServer(version2);
|
|
25573
|
+
function buildServer(version2 = getPackageVersion(), options = {}) {
|
|
25574
|
+
return createMcpServer(version2, options);
|
|
24843
25575
|
}
|
|
24844
25576
|
function privateMetadataAllowed(requested) {
|
|
24845
25577
|
return requested === true && isPrivateOutputEnabled();
|
|
@@ -24853,9 +25585,50 @@ function appendWarnings2(payload, warnings) {
|
|
|
24853
25585
|
const currentWarnings = typeof payload === "object" && payload && "warnings" in payload && Array.isArray(payload.warnings) ? payload.warnings : [];
|
|
24854
25586
|
return { ...payload, warnings: [...currentWarnings, ...warnings] };
|
|
24855
25587
|
}
|
|
24856
|
-
|
|
25588
|
+
var approvalTokenSchema = exports_external.string().optional().describe("Operator mutation approval token");
|
|
25589
|
+
function mutationMachineId(machineId) {
|
|
25590
|
+
return machineId?.trim() || "local";
|
|
25591
|
+
}
|
|
25592
|
+
function mutationResourceId(kind, ...parts) {
|
|
25593
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
25594
|
+
return values ? `${kind}:${values}` : kind;
|
|
25595
|
+
}
|
|
25596
|
+
function mutationCallerId() {
|
|
25597
|
+
return process.env["HASNA_MACHINES_MUTATION_CALLER_ID"]?.trim() || "mcp";
|
|
25598
|
+
}
|
|
25599
|
+
function mutationRunId() {
|
|
25600
|
+
return process.env["HASNA_MACHINES_MUTATION_RUN_ID"]?.trim() || "mcp";
|
|
25601
|
+
}
|
|
25602
|
+
function assertScopedMcpMutation(operation, approvalToken, scope = {}, transport) {
|
|
25603
|
+
assertMutationApproved({
|
|
25604
|
+
surface: "mcp",
|
|
25605
|
+
operation,
|
|
25606
|
+
transport,
|
|
25607
|
+
callerId: mutationCallerId(),
|
|
25608
|
+
runId: mutationRunId(),
|
|
25609
|
+
machineId: scope.machineId === undefined ? undefined : mutationMachineId(scope.machineId),
|
|
25610
|
+
resourceId: scope.resourceId === undefined || scope.resourceId === null ? undefined : scope.resourceId,
|
|
25611
|
+
args: scope.args,
|
|
25612
|
+
approvalToken
|
|
25613
|
+
});
|
|
25614
|
+
}
|
|
25615
|
+
function mcpPlanApprovalArgs(args, plan) {
|
|
25616
|
+
return {
|
|
25617
|
+
...args,
|
|
25618
|
+
plan_digest: mutationPlanDigest(plan)
|
|
25619
|
+
};
|
|
25620
|
+
}
|
|
25621
|
+
function mcpPlanResourceId(operation, machineId, plan) {
|
|
25622
|
+
return mutationResourceId("plan", operation, machineId, mutationPlanDigest(plan));
|
|
25623
|
+
}
|
|
25624
|
+
function createMcpServer(version2, options = {}) {
|
|
24857
25625
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
24858
25626
|
const events = new EventsClient3;
|
|
25627
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
25628
|
+
const mutationTransport = options.mutationTransport ?? "mcp:stdio";
|
|
25629
|
+
function requireMcpMutation(operation, approvalToken, scope = {}) {
|
|
25630
|
+
assertScopedMcpMutation(operation, approvalToken, scope, mutationTransport);
|
|
25631
|
+
}
|
|
24859
25632
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", { private_metadata: exports_external.boolean().optional().describe("Include private local paths and machine identifiers") }, async ({ private_metadata }) => {
|
|
24860
25633
|
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24861
25634
|
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
@@ -24869,18 +25642,31 @@ function createMcpServer(version2) {
|
|
|
24869
25642
|
server.tool("machines_apps_status", "Check installed state for manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(getAppsStatus(machine_id), null, 2) }] }));
|
|
24870
25643
|
server.tool("machines_apps_diff", "Show missing and installed manifest-managed apps.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(diffApps(machine_id), null, 2) }] }));
|
|
24871
25644
|
server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }] }));
|
|
24872
|
-
server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25645
|
+
server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25646
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25647
|
+
const plan = buildAppsPlan(machine_id);
|
|
25648
|
+
requireMcpMutation("machines_apps_apply", approval_token, {
|
|
25649
|
+
machineId: resolvedMachineId,
|
|
25650
|
+
resourceId: mcpPlanResourceId("machines_apps_apply", resolvedMachineId, plan),
|
|
25651
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25652
|
+
});
|
|
25653
|
+
return { content: [{ type: "text", text: JSON.stringify(runAppsPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25654
|
+
});
|
|
24873
25655
|
server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
|
|
24874
25656
|
content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
|
|
24875
25657
|
}));
|
|
24876
25658
|
server.tool("machines_manifest_validate", "Validate the current fleet manifest.", {}, async () => ({
|
|
24877
25659
|
content: [{ type: "text", text: JSON.stringify(manifestValidate(), null, 2) }]
|
|
24878
25660
|
}));
|
|
24879
|
-
server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () =>
|
|
24880
|
-
|
|
24881
|
-
|
|
25661
|
+
server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", { approval_token: approvalTokenSchema }, async ({ approval_token }) => {
|
|
25662
|
+
requireMcpMutation("machines_manifest_bootstrap", approval_token, { resourceId: "manifest:bootstrap", args: {} });
|
|
25663
|
+
return { content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }] };
|
|
25664
|
+
});
|
|
24882
25665
|
server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
|
|
24883
|
-
server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) =>
|
|
25666
|
+
server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier"), approval_token: approvalTokenSchema }, async ({ machine_id, approval_token }) => {
|
|
25667
|
+
requireMcpMutation("machines_manifest_remove", approval_token, { machineId: machine_id, args: { machine_id } });
|
|
25668
|
+
return { content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] };
|
|
25669
|
+
});
|
|
24884
25670
|
server.tool("machines_agent_status", "List current machine agent heartbeats.", { private_metadata: exports_external.boolean().optional().describe("Include private heartbeat metadata") }, async ({ private_metadata }) => {
|
|
24885
25671
|
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24886
25672
|
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
@@ -24932,9 +25718,27 @@ function createMcpServer(version2) {
|
|
|
24932
25718
|
}]
|
|
24933
25719
|
}));
|
|
24934
25720
|
server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
|
|
24935
|
-
server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25721
|
+
server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25722
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25723
|
+
const plan = buildSetupPlan(machine_id);
|
|
25724
|
+
requireMcpMutation("machines_setup_apply", approval_token, {
|
|
25725
|
+
machineId: resolvedMachineId,
|
|
25726
|
+
resourceId: mcpPlanResourceId("machines_setup_apply", resolvedMachineId, plan),
|
|
25727
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25728
|
+
});
|
|
25729
|
+
return { content: [{ type: "text", text: JSON.stringify(runSetupPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25730
|
+
});
|
|
24936
25731
|
server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
|
|
24937
|
-
server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25732
|
+
server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25733
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25734
|
+
const plan = buildSyncPlan(machine_id);
|
|
25735
|
+
requireMcpMutation("machines_sync_apply", approval_token, {
|
|
25736
|
+
machineId: resolvedMachineId,
|
|
25737
|
+
resourceId: mcpPlanResourceId("machines_sync_apply", resolvedMachineId, plan),
|
|
25738
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25739
|
+
});
|
|
25740
|
+
return { content: [{ type: "text", text: JSON.stringify(runSyncPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25741
|
+
});
|
|
24938
25742
|
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
|
|
24939
25743
|
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
24940
25744
|
private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
|
|
@@ -24989,12 +25793,31 @@ function createMcpServer(version2) {
|
|
|
24989
25793
|
server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
|
|
24990
25794
|
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
24991
25795
|
tools: exports_external.array(exports_external.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
|
|
24992
|
-
yes: exports_external.boolean().describe("Confirmation flag for execution")
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
25796
|
+
yes: exports_external.boolean().describe("Confirmation flag for execution"),
|
|
25797
|
+
approval_token: approvalTokenSchema
|
|
25798
|
+
}, async ({ machine_id, tools, yes, approval_token }) => {
|
|
25799
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25800
|
+
const plan = buildClaudeInstallPlan(machine_id, tools);
|
|
25801
|
+
requireMcpMutation("machines_install_claude_apply", approval_token, {
|
|
25802
|
+
machineId: resolvedMachineId,
|
|
25803
|
+
resourceId: mcpPlanResourceId("machines_install_claude_apply", resolvedMachineId, plan),
|
|
25804
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, tools, yes }, plan)
|
|
25805
|
+
});
|
|
25806
|
+
return {
|
|
25807
|
+
content: [{ type: "text", text: JSON.stringify(runClaudeInstallPlan(plan, { apply: true, yes }), null, 2) }]
|
|
25808
|
+
};
|
|
25809
|
+
});
|
|
24996
25810
|
server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
|
|
24997
|
-
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) =>
|
|
25811
|
+
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ machine_id, yes, approval_token }) => {
|
|
25812
|
+
const resolvedMachineId = mutationMachineId(machine_id);
|
|
25813
|
+
const plan = buildTailscaleInstallPlan(machine_id);
|
|
25814
|
+
requireMcpMutation("machines_install_tailscale_apply", approval_token, {
|
|
25815
|
+
machineId: resolvedMachineId,
|
|
25816
|
+
resourceId: mcpPlanResourceId("machines_install_tailscale_apply", resolvedMachineId, plan),
|
|
25817
|
+
args: mcpPlanApprovalArgs({ machine_id: resolvedMachineId, yes }, plan)
|
|
25818
|
+
});
|
|
25819
|
+
return { content: [{ type: "text", text: JSON.stringify(runTailscaleInstallPlan(plan, { apply: true, yes }), null, 2) }] };
|
|
25820
|
+
});
|
|
24998
25821
|
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
|
|
24999
25822
|
machine_id: exports_external.string().describe("Machine identifier"),
|
|
25000
25823
|
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
@@ -25062,41 +25885,72 @@ function createMcpServer(version2) {
|
|
|
25062
25885
|
content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
|
|
25063
25886
|
}));
|
|
25064
25887
|
server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
|
|
25065
|
-
server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) =>
|
|
25888
|
+
server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ bucket, prefix, yes, approval_token }) => {
|
|
25889
|
+
requireMcpMutation("machines_backup_apply", approval_token, { resourceId: mutationResourceId("backup", bucket, prefix), args: { bucket, prefix, yes } });
|
|
25890
|
+
return { content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] };
|
|
25891
|
+
});
|
|
25066
25892
|
server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
|
|
25067
|
-
server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) =>
|
|
25068
|
-
|
|
25893
|
+
server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution"), approval_token: approvalTokenSchema }, async ({ domains, yes, approval_token }) => {
|
|
25894
|
+
requireMcpMutation("machines_cert_apply", approval_token, { resourceId: mutationResourceId("cert", domains.join(",")), args: { domains, yes } });
|
|
25895
|
+
return { content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] };
|
|
25896
|
+
});
|
|
25897
|
+
server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host"), approval_token: approvalTokenSchema }, async ({ domain, port, target_host, approval_token }) => {
|
|
25898
|
+
const resolvedTargetHost = target_host ?? "127.0.0.1";
|
|
25899
|
+
requireMcpMutation("machines_dns_add", approval_token, { resourceId: mutationResourceId("dns", domain), args: { domain, port, target_host: resolvedTargetHost } });
|
|
25900
|
+
return { content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, resolvedTargetHost), null, 2) }] };
|
|
25901
|
+
});
|
|
25069
25902
|
server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
|
|
25070
25903
|
server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", { domain: exports_external.string().describe("Domain name") }, async ({ domain }) => ({ content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }] }));
|
|
25071
25904
|
server.tool("machines_notifications_add", "Add or replace a notification channel.", {
|
|
25072
25905
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
25073
25906
|
type: exports_external.enum(["email", "webhook", "command"]).describe("Notification transport"),
|
|
25074
|
-
target: exports_external.string().describe("Email, webhook URL, or
|
|
25907
|
+
target: exports_external.string().describe("Email, webhook URL, or command executable"),
|
|
25908
|
+
command_args: exports_external.array(exports_external.string()).optional().describe("Arguments for command transports"),
|
|
25075
25909
|
events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
|
|
25076
|
-
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
25077
|
-
|
|
25078
|
-
|
|
25079
|
-
|
|
25910
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
|
|
25911
|
+
approval_token: approvalTokenSchema
|
|
25912
|
+
}, async ({ channel_id, type, target, command_args, events: events2, enabled, approval_token }) => {
|
|
25913
|
+
const resolvedEnabled = enabled ?? true;
|
|
25914
|
+
const resolvedEvents = [...new Set(events2)];
|
|
25915
|
+
const commandArgs = command_args ?? [];
|
|
25916
|
+
requireMcpMutation("machines_notifications_add", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id, type, target, command_args: commandArgs, events: resolvedEvents, enabled: resolvedEnabled } });
|
|
25917
|
+
return {
|
|
25918
|
+
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, commandArgs: type === "command" && commandArgs.length > 0 ? commandArgs : undefined, events: resolvedEvents, enabled: resolvedEnabled }, { trustedApproval: trustedNotificationApproval2 }), null, 2) }]
|
|
25919
|
+
};
|
|
25920
|
+
});
|
|
25080
25921
|
server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
|
|
25081
25922
|
content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
|
|
25082
25923
|
}));
|
|
25083
|
-
server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true") }, async ({ channel_id, event, message, yes }) =>
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
|
|
25087
|
-
|
|
25924
|
+
server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external.string().describe("Channel identifier"), event: exports_external.string().optional().describe("Event name"), message: exports_external.string().optional().describe("Message body"), yes: exports_external.boolean().optional().describe("Execute the test when true"), approval_token: approvalTokenSchema }, async ({ channel_id, event, message, yes, approval_token }) => {
|
|
25925
|
+
if (yes === true)
|
|
25926
|
+
requireMcpMutation("machines_notifications_test", approval_token, { resourceId: mutationResourceId("notification-test", channel_id, event), args: { channel_id, event, message, yes: true } });
|
|
25927
|
+
return {
|
|
25928
|
+
content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes, trustedApproval: yes === true ? trustedNotificationApproval2 : undefined }), null, 2) }]
|
|
25929
|
+
};
|
|
25930
|
+
});
|
|
25931
|
+
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel"), approval_token: approvalTokenSchema }, async ({ event, message, channel_id, approval_token }) => {
|
|
25932
|
+
requireMcpMutation("machines_notifications_dispatch", approval_token, { resourceId: mutationResourceId("notification-dispatch", channel_id, event), args: { event, message, channel_id } });
|
|
25933
|
+
return { content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id, trustedApproval: trustedNotificationApproval2 }), null, 2) }] };
|
|
25934
|
+
});
|
|
25935
|
+
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
|
|
25936
|
+
requireMcpMutation("machines_notifications_remove", approval_token, { resourceId: mutationResourceId("notification", channel_id), args: { channel_id } });
|
|
25937
|
+
return { content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] };
|
|
25938
|
+
});
|
|
25088
25939
|
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
25089
25940
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
25090
25941
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
25091
25942
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
25092
25943
|
source: exports_external.string().optional().describe("Optional source filter"),
|
|
25093
25944
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
25094
|
-
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
25095
|
-
|
|
25945
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled"),
|
|
25946
|
+
approval_token: approvalTokenSchema
|
|
25947
|
+
}, async ({ channel_id, url, event_type, source, secret, enabled, approval_token }) => {
|
|
25948
|
+
const resolvedEnabled = enabled ?? true;
|
|
25949
|
+
requireMcpMutation("machines_webhooks_add", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id, url, event_type, source, secret, enabled: resolvedEnabled } });
|
|
25096
25950
|
const now = new Date().toISOString();
|
|
25097
25951
|
const channel = await events.addChannel({
|
|
25098
25952
|
id: channel_id,
|
|
25099
|
-
enabled:
|
|
25953
|
+
enabled: resolvedEnabled,
|
|
25100
25954
|
transport: "webhook",
|
|
25101
25955
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
25102
25956
|
webhook: { url, secret },
|
|
@@ -25108,10 +25962,16 @@ function createMcpServer(version2) {
|
|
|
25108
25962
|
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
25109
25963
|
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
25110
25964
|
}));
|
|
25111
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) =>
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
|
|
25965
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body"), approval_token: approvalTokenSchema }, async ({ channel_id, event_type, message, approval_token }) => {
|
|
25966
|
+
requireMcpMutation("machines_webhooks_test", approval_token, { resourceId: mutationResourceId("webhook-test", channel_id, event_type), args: { channel_id, event_type, message } });
|
|
25967
|
+
return {
|
|
25968
|
+
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
25969
|
+
};
|
|
25970
|
+
});
|
|
25971
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), approval_token: approvalTokenSchema }, async ({ channel_id, approval_token }) => {
|
|
25972
|
+
requireMcpMutation("machines_webhooks_remove", approval_token, { resourceId: mutationResourceId("webhook", channel_id), args: { channel_id } });
|
|
25973
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] };
|
|
25974
|
+
});
|
|
25115
25975
|
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
25116
25976
|
event_type: exports_external.string().describe("Event type"),
|
|
25117
25977
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
@@ -25120,25 +25980,36 @@ function createMcpServer(version2) {
|
|
|
25120
25980
|
data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
|
|
25121
25981
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
|
|
25122
25982
|
dedupe_key: exports_external.string().optional().describe("Dedupe key"),
|
|
25123
|
-
deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
|
|
25124
|
-
|
|
25125
|
-
|
|
25126
|
-
|
|
25127
|
-
|
|
25128
|
-
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
|
|
25983
|
+
deliver: exports_external.boolean().optional().describe("Deliver to matching channels"),
|
|
25984
|
+
approval_token: approvalTokenSchema
|
|
25985
|
+
}, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver, approval_token }) => {
|
|
25986
|
+
const resolvedData = data ?? {};
|
|
25987
|
+
const resolvedMetadata = metadata ?? {};
|
|
25988
|
+
const resolvedDeliver = deliver !== false;
|
|
25989
|
+
requireMcpMutation("machines_events_emit", approval_token, { resourceId: mutationResourceId("event", event_type, subject, dedupe_key), args: { event_type, subject, severity, message, data: resolvedData, metadata: resolvedMetadata, dedupe_key, deliver: resolvedDeliver } });
|
|
25990
|
+
return {
|
|
25991
|
+
content: [{ type: "text", text: JSON.stringify(await events.emit({
|
|
25992
|
+
source: "machines",
|
|
25993
|
+
type: event_type,
|
|
25994
|
+
subject,
|
|
25995
|
+
severity,
|
|
25996
|
+
message,
|
|
25997
|
+
data: resolvedData,
|
|
25998
|
+
metadata: resolvedMetadata,
|
|
25999
|
+
dedupeKey: dedupe_key
|
|
26000
|
+
}, { deliver: resolvedDeliver }), null, 2) }]
|
|
26001
|
+
};
|
|
26002
|
+
});
|
|
25136
26003
|
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
25137
26004
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
25138
26005
|
}));
|
|
25139
|
-
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) =>
|
|
25140
|
-
|
|
25141
|
-
|
|
26006
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery"), approval_token: approvalTokenSchema }, async ({ event_id, source, event_type, dry_run, approval_token }) => {
|
|
26007
|
+
if (dry_run !== true)
|
|
26008
|
+
requireMcpMutation("machines_events_replay", approval_token, { resourceId: mutationResourceId("event-replay", event_id, source, event_type), args: { event_id, source, event_type, dry_run: false } });
|
|
26009
|
+
return {
|
|
26010
|
+
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
26011
|
+
};
|
|
26012
|
+
});
|
|
25142
26013
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|
|
25143
26014
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
25144
26015
|
content: [{ type: "text", text: renderDashboardHtml() }]
|
|
@@ -25146,9 +26017,21 @@ function createMcpServer(version2) {
|
|
|
25146
26017
|
server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
|
|
25147
26018
|
content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
|
|
25148
26019
|
}));
|
|
25149
|
-
server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push")
|
|
25150
|
-
|
|
25151
|
-
|
|
26020
|
+
server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26021
|
+
const resolvedTables = resolveTables(tables);
|
|
26022
|
+
requireMcpMutation("storage_push", approval_token, { resourceId: mutationResourceId("storage-push", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26023
|
+
return { content: [{ type: "text", text: JSON.stringify(await storagePush({ tables: resolvedTables }), null, 2) }] };
|
|
26024
|
+
});
|
|
26025
|
+
server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26026
|
+
const resolvedTables = resolveTables(tables);
|
|
26027
|
+
requireMcpMutation("storage_pull", approval_token, { resourceId: mutationResourceId("storage-pull", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26028
|
+
return { content: [{ type: "text", text: JSON.stringify(await storagePull({ tables: resolvedTables }), null, 2) }] };
|
|
26029
|
+
});
|
|
26030
|
+
server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync"), approval_token: approvalTokenSchema }, async ({ tables, approval_token }) => {
|
|
26031
|
+
const resolvedTables = resolveTables(tables);
|
|
26032
|
+
requireMcpMutation("storage_sync", approval_token, { resourceId: mutationResourceId("storage-sync", resolvedTables.join(",")), args: { tables: resolvedTables } });
|
|
26033
|
+
return { content: [{ type: "text", text: JSON.stringify(await storageSync({ tables: resolvedTables }), null, 2) }] };
|
|
26034
|
+
});
|
|
25152
26035
|
return server;
|
|
25153
26036
|
}
|
|
25154
26037
|
export {
|
|
@@ -25157,6 +26040,8 @@ export {
|
|
|
25157
26040
|
writeHeartbeatTick,
|
|
25158
26041
|
writeHeartbeat,
|
|
25159
26042
|
watchTmuxPane,
|
|
26043
|
+
verifyMutationApprovalToken,
|
|
26044
|
+
validateSshTarget,
|
|
25160
26045
|
validateManifest,
|
|
25161
26046
|
upsertHeartbeat,
|
|
25162
26047
|
testNotificationChannel,
|
|
@@ -25166,16 +26051,21 @@ export {
|
|
|
25166
26051
|
startDashboardServer,
|
|
25167
26052
|
setHeartbeatStatus,
|
|
25168
26053
|
sanitizePublicString,
|
|
26054
|
+
runTailscaleInstallPlan,
|
|
25169
26055
|
runTailscaleInstall,
|
|
26056
|
+
runSyncPlan,
|
|
25170
26057
|
runSync,
|
|
25171
26058
|
runStorageMigrations,
|
|
26059
|
+
runSetupPlan,
|
|
25172
26060
|
runSetup,
|
|
25173
26061
|
runSelfTest,
|
|
25174
26062
|
runDoctor,
|
|
25175
26063
|
runDaemonServicePlan,
|
|
26064
|
+
runClaudeInstallPlan,
|
|
25176
26065
|
runClaudeInstall,
|
|
25177
26066
|
runCertPlan,
|
|
25178
26067
|
runBackup,
|
|
26068
|
+
runAppsPlan,
|
|
25179
26069
|
runAppsInstall,
|
|
25180
26070
|
resolveTables,
|
|
25181
26071
|
resolveSshTarget,
|
|
@@ -25209,6 +26099,8 @@ export {
|
|
|
25209
26099
|
probeTmuxPane,
|
|
25210
26100
|
parseStorageTables,
|
|
25211
26101
|
parsePortOutput,
|
|
26102
|
+
mutationPlanDigest,
|
|
26103
|
+
mutationArgsSha256,
|
|
25212
26104
|
markOffline,
|
|
25213
26105
|
manifestValidate,
|
|
25214
26106
|
manifestRemove,
|
|
@@ -25227,6 +26119,7 @@ export {
|
|
|
25227
26119
|
isSensitiveKey,
|
|
25228
26120
|
isPrivateOutputEnabled,
|
|
25229
26121
|
isPrivateMetadataEnabled,
|
|
26122
|
+
isMutationApproved,
|
|
25230
26123
|
getSyncMetaAll,
|
|
25231
26124
|
getStorageStatus,
|
|
25232
26125
|
getStoragePg,
|
|
@@ -25265,13 +26158,18 @@ export {
|
|
|
25265
26158
|
diffApps,
|
|
25266
26159
|
detectCurrentMachineManifest,
|
|
25267
26160
|
defaultScreenPasswordSecretKey,
|
|
26161
|
+
createMutationApprovalToken,
|
|
25268
26162
|
createMcpServer,
|
|
25269
26163
|
createMachineResolverSnapshot,
|
|
25270
26164
|
countRuns,
|
|
25271
26165
|
closeDb,
|
|
25272
26166
|
checkMachineCompatibility,
|
|
26167
|
+
canonicalMutationArgs,
|
|
26168
|
+
buildTmuxPaneDiedHookPlan,
|
|
25273
26169
|
buildTailscaleInstallPlan,
|
|
25274
26170
|
buildSyncPlan,
|
|
26171
|
+
buildSshCommandPlan,
|
|
26172
|
+
buildSshCommandArgs,
|
|
25275
26173
|
buildSshCommand,
|
|
25276
26174
|
buildSetupPlan,
|
|
25277
26175
|
buildServer,
|
|
@@ -25289,6 +26187,9 @@ export {
|
|
|
25289
26187
|
buildCertPlan,
|
|
25290
26188
|
buildBackupPlan,
|
|
25291
26189
|
buildAppsPlan,
|
|
26190
|
+
attachMutationPlanDigest,
|
|
26191
|
+
assertMutationPlanDigest,
|
|
26192
|
+
assertMutationApproved,
|
|
25292
26193
|
addNotificationChannel,
|
|
25293
26194
|
addDomainMapping,
|
|
25294
26195
|
SqliteAdapter,
|
|
@@ -25304,6 +26205,11 @@ export {
|
|
|
25304
26205
|
PRIVATE_METADATA_ENV,
|
|
25305
26206
|
PRIVATE_MANIFEST_REF_ENV,
|
|
25306
26207
|
PRIVATE_MANIFEST_BACKEND_ENV,
|
|
26208
|
+
MUTATION_APPROVAL_TOKEN_ENV,
|
|
26209
|
+
MUTATION_APPROVAL_RUN_ENV,
|
|
26210
|
+
MUTATION_APPROVAL_REPLAY_PATH_ENV,
|
|
26211
|
+
MUTATION_APPROVAL_FLAG_ENV,
|
|
26212
|
+
MUTATION_APPROVAL_CALLER_ENV,
|
|
25307
26213
|
MACHINE_MCP_TOOL_NAMES,
|
|
25308
26214
|
MACHINES_STORAGE_TABLES,
|
|
25309
26215
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|
|
@@ -25322,6 +26228,7 @@ export {
|
|
|
25322
26228
|
MACHINES_BACKUP_PREFIX_ENV,
|
|
25323
26229
|
MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
|
|
25324
26230
|
MACHINES_BACKUP_BUCKET_ENV,
|
|
26231
|
+
LEGACY_MUTATION_APPROVAL_FLAG_ENV,
|
|
25325
26232
|
DOCTOR_OPTIONAL_ADAPTER_DOMAINS,
|
|
25326
26233
|
DEFAULT_SCREEN_SECRET_NAMESPACE,
|
|
25327
26234
|
DEFAULT_MACHINE_RESOLVER_TTL_MS,
|