@hasna/machines 0.0.37 → 0.0.39
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 +54 -0
- package/dist/agent/index.js +10554 -166
- package/dist/agent/runtime.d.ts +33 -3
- package/dist/agent/runtime.d.ts.map +1 -1
- package/dist/cli/index.js +875 -53
- package/dist/commands/daemon.d.ts +76 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/consumer.js +227 -8
- package/dist/db.d.ts +29 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1332 -236
- package/dist/manifests.d.ts +6 -6
- package/dist/mcp/index.js +1744 -1078
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/pg-migrations.d.ts.map +1 -1
- package/dist/redaction.d.ts +9 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/remote-storage.d.ts +3 -0
- package/dist/remote-storage.d.ts.map +1 -1
- package/dist/storage.js +116 -7
- package/dist/topology.d.ts +21 -0
- package/dist/topology.d.ts.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6569,6 +6569,21 @@ class SqliteAdapter {
|
|
|
6569
6569
|
}
|
|
6570
6570
|
}
|
|
6571
6571
|
var adapter = null;
|
|
6572
|
+
var AGENT_HEARTBEAT_COLUMNS = [
|
|
6573
|
+
{ name: "daemon_version", definition: "TEXT" },
|
|
6574
|
+
{ name: "agent_mode", definition: "TEXT" },
|
|
6575
|
+
{ name: "platform", definition: "TEXT" },
|
|
6576
|
+
{ name: "os_version", definition: "TEXT" },
|
|
6577
|
+
{ name: "os_build", definition: "TEXT" },
|
|
6578
|
+
{ name: "arch", definition: "TEXT" },
|
|
6579
|
+
{ name: "uptime_seconds", definition: "INTEGER" },
|
|
6580
|
+
{ name: "tool_versions_json", definition: "TEXT" },
|
|
6581
|
+
{ name: "tailscale_json", definition: "TEXT" },
|
|
6582
|
+
{ name: "storage_sync_status", definition: "TEXT" },
|
|
6583
|
+
{ name: "storage_sync_last_error", definition: "TEXT" },
|
|
6584
|
+
{ name: "doctor_summary_json", definition: "TEXT" },
|
|
6585
|
+
{ name: "private_metadata", definition: "INTEGER NOT NULL DEFAULT 0" }
|
|
6586
|
+
];
|
|
6572
6587
|
function createTables(db) {
|
|
6573
6588
|
db.exec(`
|
|
6574
6589
|
CREATE TABLE IF NOT EXISTS agent_heartbeats (
|
|
@@ -6576,9 +6591,23 @@ function createTables(db) {
|
|
|
6576
6591
|
pid INTEGER NOT NULL,
|
|
6577
6592
|
status TEXT NOT NULL,
|
|
6578
6593
|
updated_at TEXT NOT NULL,
|
|
6594
|
+
daemon_version TEXT,
|
|
6595
|
+
agent_mode TEXT,
|
|
6596
|
+
platform TEXT,
|
|
6597
|
+
os_version TEXT,
|
|
6598
|
+
os_build TEXT,
|
|
6599
|
+
arch TEXT,
|
|
6600
|
+
uptime_seconds INTEGER,
|
|
6601
|
+
tool_versions_json TEXT,
|
|
6602
|
+
tailscale_json TEXT,
|
|
6603
|
+
storage_sync_status TEXT,
|
|
6604
|
+
storage_sync_last_error TEXT,
|
|
6605
|
+
doctor_summary_json TEXT,
|
|
6606
|
+
private_metadata INTEGER NOT NULL DEFAULT 0,
|
|
6579
6607
|
PRIMARY KEY (machine_id, pid)
|
|
6580
6608
|
)
|
|
6581
6609
|
`);
|
|
6610
|
+
migrateAgentHeartbeats(db);
|
|
6582
6611
|
db.exec(`
|
|
6583
6612
|
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
6584
6613
|
id TEXT PRIMARY KEY,
|
|
@@ -6600,6 +6629,15 @@ function createTables(db) {
|
|
|
6600
6629
|
)
|
|
6601
6630
|
`);
|
|
6602
6631
|
}
|
|
6632
|
+
function migrateAgentHeartbeats(db) {
|
|
6633
|
+
const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
|
|
6634
|
+
const existing = new Set(columns.map((column) => column.name));
|
|
6635
|
+
for (const column of AGENT_HEARTBEAT_COLUMNS) {
|
|
6636
|
+
if (existing.has(column.name))
|
|
6637
|
+
continue;
|
|
6638
|
+
db.exec(`ALTER TABLE agent_heartbeats ADD COLUMN ${column.name} ${column.definition}`);
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6603
6641
|
function getAdapter(path = getDbPath()) {
|
|
6604
6642
|
if (path === ":memory:") {
|
|
6605
6643
|
const memoryAdapter = new SqliteAdapter(path);
|
|
@@ -6628,13 +6666,44 @@ function closeDb() {
|
|
|
6628
6666
|
adapter = null;
|
|
6629
6667
|
}
|
|
6630
6668
|
}
|
|
6631
|
-
function upsertHeartbeat(machineId, pid = process.pid, status = "online") {
|
|
6669
|
+
function upsertHeartbeat(machineId, pid = process.pid, status = "online", metadata = {}) {
|
|
6632
6670
|
const db = getDb();
|
|
6633
|
-
db.query(`INSERT INTO agent_heartbeats (
|
|
6634
|
-
|
|
6671
|
+
db.query(`INSERT INTO agent_heartbeats (
|
|
6672
|
+
machine_id,
|
|
6673
|
+
pid,
|
|
6674
|
+
status,
|
|
6675
|
+
updated_at,
|
|
6676
|
+
daemon_version,
|
|
6677
|
+
agent_mode,
|
|
6678
|
+
platform,
|
|
6679
|
+
os_version,
|
|
6680
|
+
os_build,
|
|
6681
|
+
arch,
|
|
6682
|
+
uptime_seconds,
|
|
6683
|
+
tool_versions_json,
|
|
6684
|
+
tailscale_json,
|
|
6685
|
+
storage_sync_status,
|
|
6686
|
+
storage_sync_last_error,
|
|
6687
|
+
doctor_summary_json,
|
|
6688
|
+
private_metadata
|
|
6689
|
+
)
|
|
6690
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6635
6691
|
ON CONFLICT(machine_id, pid) DO UPDATE SET
|
|
6636
6692
|
status = excluded.status,
|
|
6637
|
-
updated_at = excluded.updated_at
|
|
6693
|
+
updated_at = excluded.updated_at,
|
|
6694
|
+
daemon_version = excluded.daemon_version,
|
|
6695
|
+
agent_mode = excluded.agent_mode,
|
|
6696
|
+
platform = excluded.platform,
|
|
6697
|
+
os_version = excluded.os_version,
|
|
6698
|
+
os_build = excluded.os_build,
|
|
6699
|
+
arch = excluded.arch,
|
|
6700
|
+
uptime_seconds = excluded.uptime_seconds,
|
|
6701
|
+
tool_versions_json = excluded.tool_versions_json,
|
|
6702
|
+
tailscale_json = excluded.tailscale_json,
|
|
6703
|
+
storage_sync_status = excluded.storage_sync_status,
|
|
6704
|
+
storage_sync_last_error = excluded.storage_sync_last_error,
|
|
6705
|
+
doctor_summary_json = excluded.doctor_summary_json,
|
|
6706
|
+
private_metadata = excluded.private_metadata`).run(machineId, pid, status, new Date().toISOString(), metadata.daemonVersion ?? null, metadata.agentMode ?? null, metadata.platform ?? null, metadata.osVersion ?? null, metadata.osBuild ?? null, metadata.arch ?? null, metadata.uptimeSeconds == null ? null : Math.max(0, Math.floor(metadata.uptimeSeconds)), metadata.toolVersions ? JSON.stringify(metadata.toolVersions) : null, metadata.tailscale ? JSON.stringify(metadata.tailscale) : null, metadata.storageSyncStatus ?? null, metadata.storageSyncLastError ?? null, metadata.doctorSummary ? JSON.stringify(metadata.doctorSummary) : null, metadata.privateMetadata ? 1 : 0);
|
|
6638
6707
|
}
|
|
6639
6708
|
function getLocalMachineId() {
|
|
6640
6709
|
return process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
|
|
@@ -6642,12 +6711,12 @@ function getLocalMachineId() {
|
|
|
6642
6711
|
function listHeartbeats(machineId) {
|
|
6643
6712
|
const db = getDb();
|
|
6644
6713
|
if (machineId) {
|
|
6645
|
-
return db.query(`SELECT
|
|
6714
|
+
return db.query(`SELECT *
|
|
6646
6715
|
FROM agent_heartbeats
|
|
6647
6716
|
WHERE machine_id = ?
|
|
6648
6717
|
ORDER BY updated_at DESC`).all(machineId);
|
|
6649
6718
|
}
|
|
6650
|
-
return db.query(`SELECT
|
|
6719
|
+
return db.query(`SELECT *
|
|
6651
6720
|
FROM agent_heartbeats
|
|
6652
6721
|
ORDER BY updated_at DESC`).all();
|
|
6653
6722
|
}
|
|
@@ -6683,9 +6752,36 @@ var PG_MIGRATIONS = [
|
|
|
6683
6752
|
pid INTEGER NOT NULL,
|
|
6684
6753
|
status TEXT NOT NULL,
|
|
6685
6754
|
updated_at TIMESTAMPTZ NOT NULL,
|
|
6755
|
+
daemon_version TEXT,
|
|
6756
|
+
agent_mode TEXT,
|
|
6757
|
+
platform TEXT,
|
|
6758
|
+
os_version TEXT,
|
|
6759
|
+
os_build TEXT,
|
|
6760
|
+
arch TEXT,
|
|
6761
|
+
uptime_seconds INTEGER,
|
|
6762
|
+
tool_versions_json TEXT,
|
|
6763
|
+
tailscale_json TEXT,
|
|
6764
|
+
storage_sync_status TEXT,
|
|
6765
|
+
storage_sync_last_error TEXT,
|
|
6766
|
+
doctor_summary_json TEXT,
|
|
6767
|
+
private_metadata INTEGER NOT NULL DEFAULT 0,
|
|
6686
6768
|
PRIMARY KEY (machine_id, pid)
|
|
6687
6769
|
);
|
|
6688
6770
|
|
|
6771
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS daemon_version TEXT;
|
|
6772
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS agent_mode TEXT;
|
|
6773
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS platform TEXT;
|
|
6774
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_version TEXT;
|
|
6775
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_build TEXT;
|
|
6776
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS arch TEXT;
|
|
6777
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS uptime_seconds INTEGER;
|
|
6778
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tool_versions_json TEXT;
|
|
6779
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tailscale_json TEXT;
|
|
6780
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_status TEXT;
|
|
6781
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_last_error TEXT;
|
|
6782
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS doctor_summary_json TEXT;
|
|
6783
|
+
ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS private_metadata INTEGER NOT NULL DEFAULT 0;
|
|
6784
|
+
|
|
6689
6785
|
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
6690
6786
|
id TEXT PRIMARY KEY,
|
|
6691
6787
|
machine_id TEXT NOT NULL,
|
|
@@ -6717,7 +6813,20 @@ function normalizeParams(params) {
|
|
|
6717
6813
|
return flat.map((value) => value === undefined ? null : value);
|
|
6718
6814
|
}
|
|
6719
6815
|
function sslConfigFor(connectionString) {
|
|
6720
|
-
|
|
6816
|
+
let url;
|
|
6817
|
+
try {
|
|
6818
|
+
url = new URL(connectionString);
|
|
6819
|
+
} catch {
|
|
6820
|
+
return;
|
|
6821
|
+
}
|
|
6822
|
+
const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
|
|
6823
|
+
const ssl = url.searchParams.get("ssl")?.toLowerCase();
|
|
6824
|
+
if (sslMode === "disable" || ssl === "false")
|
|
6825
|
+
return;
|
|
6826
|
+
if (sslMode === "no-verify" || process.env["HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED"] === "0") {
|
|
6827
|
+
return { rejectUnauthorized: false };
|
|
6828
|
+
}
|
|
6829
|
+
return sslMode || ssl === "true" ? { rejectUnauthorized: true } : undefined;
|
|
6721
6830
|
}
|
|
6722
6831
|
|
|
6723
6832
|
class PgAdapterAsync {
|
|
@@ -10984,6 +11093,11 @@ var coerce = {
|
|
|
10984
11093
|
var NEVER = INVALID;
|
|
10985
11094
|
// src/redaction.ts
|
|
10986
11095
|
var REDACTED_VALUE = "[redacted]";
|
|
11096
|
+
var PRIVATE_METADATA_ENV = "HASNA_MACHINES_PRIVATE_METADATA";
|
|
11097
|
+
var PRIVATE_METADATA_FALLBACK_ENV = "MACHINES_PRIVATE_METADATA";
|
|
11098
|
+
var PRIVATE_OUTPUT_ENV = "HASNA_MACHINES_ALLOW_PRIVATE_OUTPUT";
|
|
11099
|
+
var PRIVATE_OUTPUT_FALLBACK_ENV = "MACHINES_ALLOW_PRIVATE_OUTPUT";
|
|
11100
|
+
var PRIVATE_OUTPUT_DENIED_WARNING = `private_output_denied:set ${PRIVATE_OUTPUT_ENV}=1 to allow private metadata output`;
|
|
10987
11101
|
var SENSITIVE_KEY_PATTERN = /(password|passwd|token|credential|private[_-]?key|privateKey|api[_-]?key|github.*key|pem|secret)/i;
|
|
10988
11102
|
var SECRET_REFERENCE_KEY_PATTERN = /(secret(ref(erence)?|key)?|secretRef|secretKey)$/i;
|
|
10989
11103
|
var SENSITIVE_VALUE_PATTERNS = [
|
|
@@ -10994,9 +11108,21 @@ var SENSITIVE_VALUE_PATTERNS = [
|
|
|
10994
11108
|
/\bAKIA[0-9A-Z]{16}\b/,
|
|
10995
11109
|
/\bsk-[A-Za-z0-9_-]{20,}\b/
|
|
10996
11110
|
];
|
|
11111
|
+
var IPV4_PATTERN = /\b(?:10|127|169\.254|172\.(?:1[6-9]|2\d|3[0-1])|192\.168|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7]))(?:\.\d{1,3}){2}\b/g;
|
|
11112
|
+
var IPV6_PATTERN = /\b(?:fc|fd|fe80)[0-9a-f:]*:[0-9a-f:]+\b/gi;
|
|
11113
|
+
var DATABASE_URL_PATTERN = /\b(?:postgres(?:ql)?|mysql|mariadb|redis|mongodb|s3):\/\/[^\s"'<>]+/gi;
|
|
11114
|
+
var PRIVATE_HOST_PATTERN = /\b(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.tailnet(?:\.[A-Za-z0-9-]+)*|\.ts\.net|\.private(?:\.[A-Za-z0-9-]+)*|\.internal|\.local)\b/gi;
|
|
10997
11115
|
function isSensitiveKey(key) {
|
|
10998
11116
|
return SENSITIVE_KEY_PATTERN.test(key);
|
|
10999
11117
|
}
|
|
11118
|
+
function isPrivateMetadataEnabled(env = process.env) {
|
|
11119
|
+
const value = env[PRIVATE_METADATA_ENV] ?? env[PRIVATE_METADATA_FALLBACK_ENV];
|
|
11120
|
+
return ["1", "true", "yes", "on", "private"].includes(String(value ?? "").trim().toLowerCase());
|
|
11121
|
+
}
|
|
11122
|
+
function isPrivateOutputEnabled(env = process.env) {
|
|
11123
|
+
const value = env[PRIVATE_OUTPUT_ENV] ?? env[PRIVATE_OUTPUT_FALLBACK_ENV];
|
|
11124
|
+
return ["1", "true", "yes", "on", "private"].includes(String(value ?? "").trim().toLowerCase());
|
|
11125
|
+
}
|
|
11000
11126
|
function isSecretReferenceKey(key) {
|
|
11001
11127
|
return SECRET_REFERENCE_KEY_PATTERN.test(key);
|
|
11002
11128
|
}
|
|
@@ -11009,6 +11135,17 @@ function isRecord(value) {
|
|
|
11009
11135
|
function redactPath(value) {
|
|
11010
11136
|
return value.replace(/\/home\/[^/\s]+/g, "/home/<user>").replace(/\/Users\/[^/\s]+/g, "/Users/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, "C:\\Users\\<user>");
|
|
11011
11137
|
}
|
|
11138
|
+
function redactNetworkValue(value) {
|
|
11139
|
+
if (!value.trim())
|
|
11140
|
+
return value;
|
|
11141
|
+
return REDACTED_VALUE;
|
|
11142
|
+
}
|
|
11143
|
+
function redactErrorMessage(value) {
|
|
11144
|
+
return redactPath(value).replace(DATABASE_URL_PATTERN, (match) => {
|
|
11145
|
+
const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
|
|
11146
|
+
return `${scheme}${REDACTED_VALUE}`;
|
|
11147
|
+
}).replace(IPV4_PATTERN, REDACTED_VALUE).replace(IPV6_PATTERN, REDACTED_VALUE).replace(PRIVATE_HOST_PATTERN, REDACTED_VALUE);
|
|
11148
|
+
}
|
|
11012
11149
|
function redactPrivateRef(value) {
|
|
11013
11150
|
const trimmed = value.trim();
|
|
11014
11151
|
const scheme = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)/i);
|
|
@@ -11271,12 +11408,12 @@ import { arch as arch2, hostname as hostname3, platform as platform2, userInfo a
|
|
|
11271
11408
|
import { spawnSync } from "child_process";
|
|
11272
11409
|
|
|
11273
11410
|
// src/version.ts
|
|
11274
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
11411
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
11275
11412
|
import { dirname as dirname3, join as join2 } from "path";
|
|
11276
11413
|
import { fileURLToPath } from "url";
|
|
11277
11414
|
function getPackageVersion() {
|
|
11278
11415
|
try {
|
|
11279
|
-
const here = dirname3(fileURLToPath(import.meta.url));
|
|
11416
|
+
const here = dirname3(realpathSync(fileURLToPath(import.meta.url)));
|
|
11280
11417
|
const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
|
|
11281
11418
|
const pkgPath = candidates.find((candidate) => existsSync3(candidate));
|
|
11282
11419
|
if (!pkgPath) {
|
|
@@ -11518,6 +11655,16 @@ function routeRank(hint) {
|
|
|
11518
11655
|
function selectRouteHint(hints) {
|
|
11519
11656
|
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
11520
11657
|
}
|
|
11658
|
+
function parseHeartbeatJson(value) {
|
|
11659
|
+
if (!value)
|
|
11660
|
+
return null;
|
|
11661
|
+
try {
|
|
11662
|
+
const parsed = JSON.parse(value);
|
|
11663
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
11664
|
+
} catch {
|
|
11665
|
+
return null;
|
|
11666
|
+
}
|
|
11667
|
+
}
|
|
11521
11668
|
function buildEntry(input) {
|
|
11522
11669
|
const manifest = input.manifest;
|
|
11523
11670
|
const peer = input.peer;
|
|
@@ -11540,6 +11687,22 @@ function buildEntry(input) {
|
|
|
11540
11687
|
manifest_declared: Boolean(manifest),
|
|
11541
11688
|
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
11542
11689
|
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
11690
|
+
agent: {
|
|
11691
|
+
pid: input.heartbeat?.pid ?? null,
|
|
11692
|
+
daemon_version: input.heartbeat?.daemon_version ?? null,
|
|
11693
|
+
mode: input.heartbeat?.agent_mode ?? null,
|
|
11694
|
+
private_metadata: Boolean(input.heartbeat?.private_metadata),
|
|
11695
|
+
platform: input.heartbeat?.platform ?? null,
|
|
11696
|
+
os_version: input.heartbeat?.os_version ?? null,
|
|
11697
|
+
os_build: input.heartbeat?.os_build ?? null,
|
|
11698
|
+
arch: input.heartbeat?.arch ?? null,
|
|
11699
|
+
uptime_seconds: input.heartbeat?.uptime_seconds ?? null,
|
|
11700
|
+
tool_versions: parseHeartbeatJson(input.heartbeat?.tool_versions_json),
|
|
11701
|
+
tailscale: parseHeartbeatJson(input.heartbeat?.tailscale_json),
|
|
11702
|
+
storage_sync_status: input.heartbeat?.storage_sync_status ?? null,
|
|
11703
|
+
storage_sync_last_error: input.heartbeat?.storage_sync_last_error ?? null,
|
|
11704
|
+
doctor_summary: parseHeartbeatJson(input.heartbeat?.doctor_summary_json)
|
|
11705
|
+
},
|
|
11543
11706
|
tailscale: {
|
|
11544
11707
|
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
11545
11708
|
ips: peer?.TailscaleIPs ?? [],
|
|
@@ -11599,6 +11762,72 @@ function discoverMachineTopology(options = {}) {
|
|
|
11599
11762
|
warnings
|
|
11600
11763
|
};
|
|
11601
11764
|
}
|
|
11765
|
+
function redactFleetString(value) {
|
|
11766
|
+
if (!value)
|
|
11767
|
+
return value;
|
|
11768
|
+
return redactErrorMessage(value);
|
|
11769
|
+
}
|
|
11770
|
+
function redactPublicRecord(value) {
|
|
11771
|
+
if (!value)
|
|
11772
|
+
return null;
|
|
11773
|
+
const redacted = {};
|
|
11774
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
11775
|
+
if (/(host|hostname|dns|ip|ips|user|username|serial|address|target|url|token|secret|password|credential)/i.test(key)) {
|
|
11776
|
+
redacted[key] = REDACTED_VALUE;
|
|
11777
|
+
continue;
|
|
11778
|
+
}
|
|
11779
|
+
if (typeof entry === "string") {
|
|
11780
|
+
redacted[key] = redactFleetString(entry);
|
|
11781
|
+
} else if (Array.isArray(entry)) {
|
|
11782
|
+
redacted[key] = entry.map((item) => {
|
|
11783
|
+
if (typeof item === "string")
|
|
11784
|
+
return redactFleetString(item);
|
|
11785
|
+
if (item && typeof item === "object")
|
|
11786
|
+
return redactPublicRecord(item);
|
|
11787
|
+
return item;
|
|
11788
|
+
});
|
|
11789
|
+
} else if (entry && typeof entry === "object") {
|
|
11790
|
+
redacted[key] = redactPublicRecord(entry);
|
|
11791
|
+
} else {
|
|
11792
|
+
redacted[key] = entry;
|
|
11793
|
+
}
|
|
11794
|
+
}
|
|
11795
|
+
return redactSensitiveValue(redacted);
|
|
11796
|
+
}
|
|
11797
|
+
function redactTopologyForOutput(topology, options = {}) {
|
|
11798
|
+
if (options.privateMetadata)
|
|
11799
|
+
return topology;
|
|
11800
|
+
return {
|
|
11801
|
+
...topology,
|
|
11802
|
+
local_hostname: REDACTED_VALUE,
|
|
11803
|
+
warnings: topology.warnings.map(redactFleetString),
|
|
11804
|
+
machines: topology.machines.map((machine) => ({
|
|
11805
|
+
...machine,
|
|
11806
|
+
hostname: machine.hostname ? REDACTED_VALUE : null,
|
|
11807
|
+
user: machine.user ? REDACTED_VALUE : null,
|
|
11808
|
+
tailscale: {
|
|
11809
|
+
...machine.tailscale,
|
|
11810
|
+
dns_name: machine.tailscale.dns_name ? REDACTED_VALUE : null,
|
|
11811
|
+
ips: machine.tailscale.ips.map(() => REDACTED_VALUE)
|
|
11812
|
+
},
|
|
11813
|
+
ssh: {
|
|
11814
|
+
...machine.ssh,
|
|
11815
|
+
address: machine.ssh.address ? REDACTED_VALUE : null,
|
|
11816
|
+
command_target: machine.ssh.command_target ? REDACTED_VALUE : null
|
|
11817
|
+
},
|
|
11818
|
+
route_hints: machine.route_hints.map((hint) => ({
|
|
11819
|
+
...hint,
|
|
11820
|
+
target: REDACTED_VALUE
|
|
11821
|
+
})),
|
|
11822
|
+
agent: {
|
|
11823
|
+
...machine.agent,
|
|
11824
|
+
tailscale: redactPublicRecord(machine.agent.tailscale),
|
|
11825
|
+
storage_sync_last_error: machine.agent.storage_sync_last_error ? redactFleetString(machine.agent.storage_sync_last_error) : null,
|
|
11826
|
+
doctor_summary: redactPublicRecord(machine.agent.doctor_summary)
|
|
11827
|
+
}
|
|
11828
|
+
}))
|
|
11829
|
+
};
|
|
11830
|
+
}
|
|
11602
11831
|
function normalizeMachineAlias(value) {
|
|
11603
11832
|
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
11604
11833
|
}
|
|
@@ -11823,6 +12052,20 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
11823
12052
|
warnings
|
|
11824
12053
|
};
|
|
11825
12054
|
}
|
|
12055
|
+
function redactRouteForOutput(route, options = {}) {
|
|
12056
|
+
if (options.privateMetadata)
|
|
12057
|
+
return route;
|
|
12058
|
+
return {
|
|
12059
|
+
...route,
|
|
12060
|
+
target: route.target ? REDACTED_VALUE : null,
|
|
12061
|
+
command_target: route.command_target ? REDACTED_VALUE : null,
|
|
12062
|
+
warnings: route.warnings.map(redactFleetString),
|
|
12063
|
+
evidence: {
|
|
12064
|
+
...route.evidence,
|
|
12065
|
+
selected_hint: route.evidence.selected_hint ? { ...route.evidence.selected_hint, target: REDACTED_VALUE } : null
|
|
12066
|
+
}
|
|
12067
|
+
};
|
|
12068
|
+
}
|
|
11826
12069
|
function isRecord2(value) {
|
|
11827
12070
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11828
12071
|
}
|
|
@@ -12322,6 +12565,22 @@ function getLocalMachineTopology(options = {}) {
|
|
|
12322
12565
|
manifest_declared: false,
|
|
12323
12566
|
heartbeat_status: "unknown",
|
|
12324
12567
|
last_heartbeat_at: null,
|
|
12568
|
+
agent: {
|
|
12569
|
+
pid: null,
|
|
12570
|
+
daemon_version: null,
|
|
12571
|
+
mode: null,
|
|
12572
|
+
private_metadata: false,
|
|
12573
|
+
platform: null,
|
|
12574
|
+
os_version: null,
|
|
12575
|
+
os_build: null,
|
|
12576
|
+
arch: null,
|
|
12577
|
+
uptime_seconds: null,
|
|
12578
|
+
tool_versions: null,
|
|
12579
|
+
tailscale: null,
|
|
12580
|
+
storage_sync_status: null,
|
|
12581
|
+
storage_sync_last_error: null,
|
|
12582
|
+
doctor_summary: null
|
|
12583
|
+
},
|
|
12325
12584
|
tailscale: { dns_name: null, ips: [], online: null, active: null, last_seen: null },
|
|
12326
12585
|
ssh: { address: null, route: "local", command_target: "localhost" },
|
|
12327
12586
|
route_hints: [{ kind: "local", target: "localhost", reachable: true }],
|
|
@@ -12649,29 +12908,450 @@ function checkMachineCompatibility(options = {}) {
|
|
|
12649
12908
|
};
|
|
12650
12909
|
}
|
|
12651
12910
|
// src/agent/runtime.ts
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12911
|
+
import { execFileSync } from "child_process";
|
|
12912
|
+
import { arch as arch3, hostname as hostname5, platform as platform3, release, uptime, version as osVersion } from "os";
|
|
12913
|
+
|
|
12914
|
+
// src/commands/doctor.ts
|
|
12915
|
+
var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
|
|
12916
|
+
function makeCheck2(id, status, summary, detail, extra = {}) {
|
|
12917
|
+
const { data, ...rest } = extra;
|
|
12655
12918
|
return {
|
|
12656
|
-
|
|
12657
|
-
|
|
12919
|
+
...rest,
|
|
12920
|
+
id,
|
|
12658
12921
|
status,
|
|
12659
|
-
|
|
12922
|
+
summary,
|
|
12923
|
+
detail,
|
|
12924
|
+
data: data ? redactSensitiveValue(data) : undefined
|
|
12660
12925
|
};
|
|
12661
12926
|
}
|
|
12662
|
-
function
|
|
12663
|
-
|
|
12927
|
+
function parseKeyValueOutput(stdout) {
|
|
12928
|
+
const result = {};
|
|
12929
|
+
for (const line of stdout.trim().split(`
|
|
12930
|
+
`)) {
|
|
12931
|
+
const index = line.indexOf("=");
|
|
12932
|
+
if (index <= 0)
|
|
12933
|
+
continue;
|
|
12934
|
+
result[line.slice(0, index)] = line.slice(index + 1);
|
|
12935
|
+
}
|
|
12936
|
+
return result;
|
|
12937
|
+
}
|
|
12938
|
+
function buildDoctorCommand() {
|
|
12939
|
+
return [
|
|
12940
|
+
'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
|
|
12941
|
+
'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
|
|
12942
|
+
'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
|
|
12943
|
+
'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
|
|
12944
|
+
`printf 'data_dir=%s\\n' "$data_dir"`,
|
|
12945
|
+
`printf 'manifest_path=%s\\n' "$manifest_path"`,
|
|
12946
|
+
`printf 'db_path=%s\\n' "$db_path"`,
|
|
12947
|
+
`printf 'notifications_path=%s\\n' "$notifications_path"`,
|
|
12948
|
+
`printf 'data_dir_exists=%s\\n' "$(test -d "$data_dir" && printf yes || printf no)"`,
|
|
12949
|
+
`printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
|
|
12950
|
+
`printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
|
|
12951
|
+
`printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
|
|
12952
|
+
`printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
|
|
12953
|
+
`printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
|
|
12954
|
+
`printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
|
|
12955
|
+
`printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
|
|
12956
|
+
`printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`,
|
|
12957
|
+
`printf 'sudo_noninteractive=%s\\n' "$(sudo -n true >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
12958
|
+
`printf 'ssh_cert_support=%s\\n' "$(ssh -Q key-cert 2>/dev/null | grep -q 'ssh-ed25519-cert-v01@openssh.com' && printf ok || printf unavailable)"`,
|
|
12959
|
+
`printf 'gh_cli=%s\\n' "$(command -v gh 2>/dev/null || printf missing)"`,
|
|
12960
|
+
`printf 'gh_auth=%s\\n' "$(gh auth status >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
12961
|
+
"printf 'github_app_ref=%s\\n' \"$(test -n \\\"${HASNA_GITHUB_APP_ID:-}\\\" -a -n \\\"${HASNA_GITHUB_APP_PRIVATE_KEY_REF:-}\\\" && printf configured || printf missing)\""
|
|
12962
|
+
].join("; ");
|
|
12963
|
+
}
|
|
12964
|
+
function fallbackAdapterCheck(domain) {
|
|
12965
|
+
return makeCheck2(`${domain}-adapter`, "ok", `Optional ${domain} adapter`, `No ${domain} adapter configured; skipped optional private integration check.`, {
|
|
12966
|
+
optional: true,
|
|
12967
|
+
source: "open-machines",
|
|
12968
|
+
data: { configured: false, fallback: true }
|
|
12969
|
+
});
|
|
12664
12970
|
}
|
|
12665
|
-
function
|
|
12666
|
-
|
|
12971
|
+
function sanitizeAdapterCheck(check, domain, adapterId) {
|
|
12972
|
+
const safeAdapterId = redactIdentifier(adapterId);
|
|
12973
|
+
return makeCheck2(check.id.startsWith(`${domain}-`) || check.id.startsWith(`${domain}:`) ? check.id : `${domain}:${check.id}`, check.status, check.summary, String(redactSensitiveValue(check.detail)), {
|
|
12974
|
+
...check,
|
|
12975
|
+
optional: check.optional ?? true,
|
|
12976
|
+
source: check.source ? String(redactSensitiveValue(check.source)) : `adapter:${safeAdapterId}`,
|
|
12977
|
+
data: check.data ? redactSensitiveValue(check.data) : undefined
|
|
12978
|
+
});
|
|
12979
|
+
}
|
|
12980
|
+
function runOptionalAdapterChecks(context, adapters) {
|
|
12981
|
+
const checks = [];
|
|
12982
|
+
for (const domain of DOCTOR_OPTIONAL_ADAPTER_DOMAINS) {
|
|
12983
|
+
const adapter2 = adapters.find((candidate) => candidate.checks?.[domain]);
|
|
12984
|
+
const hook = adapter2?.checks?.[domain];
|
|
12985
|
+
if (!adapter2 || !hook) {
|
|
12986
|
+
checks.push(fallbackAdapterCheck(domain));
|
|
12987
|
+
continue;
|
|
12988
|
+
}
|
|
12989
|
+
try {
|
|
12990
|
+
const result = hook(context);
|
|
12991
|
+
const domainChecks = Array.isArray(result) ? result : result ? [result] : [fallbackAdapterCheck(domain)];
|
|
12992
|
+
checks.push(...domainChecks.map((check) => sanitizeAdapterCheck(check, domain, adapter2.id)));
|
|
12993
|
+
} catch {
|
|
12994
|
+
const safeAdapterId = redactIdentifier(adapter2.id);
|
|
12995
|
+
checks.push(makeCheck2(`${domain}-adapter`, "warn", `Optional ${domain} adapter failed`, "Adapter failed; details are intentionally hidden to avoid leaking private refs or credentials.", {
|
|
12996
|
+
optional: true,
|
|
12997
|
+
source: `adapter:${safeAdapterId}`,
|
|
12998
|
+
data: { adapter: safeAdapterId, fallback: true }
|
|
12999
|
+
}));
|
|
13000
|
+
}
|
|
13001
|
+
}
|
|
13002
|
+
return checks;
|
|
13003
|
+
}
|
|
13004
|
+
function runDoctor(machineId, options = {}) {
|
|
13005
|
+
const implicitLocalMachine = !machineId;
|
|
13006
|
+
const requestedMachineId = machineId ?? getLocalMachineId();
|
|
13007
|
+
const reportedMachineId = implicitLocalMachine ? "local" : requestedMachineId;
|
|
13008
|
+
const now = options.now ?? new Date;
|
|
13009
|
+
const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
|
|
13010
|
+
const commandChecks = runMachineCommand(requestedMachineId, buildDoctorCommand());
|
|
13011
|
+
const details = parseKeyValueOutput(commandChecks.stdout);
|
|
13012
|
+
const machineInManifest = manifest.machines.find((machine) => machine.id === requestedMachineId);
|
|
13013
|
+
const diagnosticMachine = machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null;
|
|
13014
|
+
if (implicitLocalMachine && diagnosticMachine)
|
|
13015
|
+
diagnosticMachine.id = reportedMachineId;
|
|
13016
|
+
const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
|
|
13017
|
+
machineId: requestedMachineId,
|
|
13018
|
+
manifest,
|
|
13019
|
+
manifestSource,
|
|
13020
|
+
commandDetails: details,
|
|
13021
|
+
now
|
|
13022
|
+
}, options.adapters ?? []);
|
|
13023
|
+
const checks = [
|
|
13024
|
+
makeCheck2("manifest-source", manifestSource.warnings.length > 0 ? "warn" : "ok", "Manifest source boundary", `${manifestSource.source.kind}:${manifestSource.source.ref} loaded from ${manifestSource.loadedFrom}`, {
|
|
13025
|
+
data: {
|
|
13026
|
+
source: manifestSource.source,
|
|
13027
|
+
loadedFrom: manifestSource.loadedFrom,
|
|
13028
|
+
fallbackSource: manifestSource.fallbackSource,
|
|
13029
|
+
warnings: manifestSource.warnings
|
|
13030
|
+
},
|
|
13031
|
+
remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
|
|
13032
|
+
}),
|
|
13033
|
+
makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", diagnosticMachine ? JSON.stringify(diagnosticMachine) : `No manifest entry for ${reportedMachineId}`, {
|
|
13034
|
+
data: {
|
|
13035
|
+
declared: Boolean(machineInManifest),
|
|
13036
|
+
machine: diagnosticMachine
|
|
13037
|
+
}
|
|
13038
|
+
}),
|
|
13039
|
+
makeCheck2("data-dir", details["data_dir_exists"] === "yes" ? "ok" : "warn", "Data directory check", `${redactPath(details["data_dir"] || "unknown")} ${details["data_dir_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
13040
|
+
data: {
|
|
13041
|
+
path: redactPath(details["data_dir"] || "unknown"),
|
|
13042
|
+
exists: details["data_dir_exists"] === "yes"
|
|
13043
|
+
}
|
|
13044
|
+
}),
|
|
13045
|
+
makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${redactPath(details["manifest_path"] || "unknown")} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
13046
|
+
data: {
|
|
13047
|
+
path: redactPath(details["manifest_path"] || "unknown"),
|
|
13048
|
+
exists: details["manifest_exists"] === "yes"
|
|
13049
|
+
}
|
|
13050
|
+
}),
|
|
13051
|
+
makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${redactPath(details["db_path"] || "unknown")} ${details["db_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
13052
|
+
data: {
|
|
13053
|
+
path: redactPath(details["db_path"] || "unknown"),
|
|
13054
|
+
exists: details["db_exists"] === "yes"
|
|
13055
|
+
}
|
|
13056
|
+
}),
|
|
13057
|
+
makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${redactPath(details["notifications_path"] || "unknown")} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`, {
|
|
13058
|
+
data: {
|
|
13059
|
+
path: redactPath(details["notifications_path"] || "unknown"),
|
|
13060
|
+
exists: details["notifications_exists"] === "yes"
|
|
13061
|
+
}
|
|
13062
|
+
}),
|
|
13063
|
+
makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
|
|
13064
|
+
makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
|
|
13065
|
+
makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
|
|
13066
|
+
makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
|
|
13067
|
+
makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing"),
|
|
13068
|
+
makeCheck2("sudo-noninteractive", details["sudo_noninteractive"] === "ok" ? "ok" : "warn", "Noninteractive sudo availability", details["sudo_noninteractive"] === "ok" ? "sudo -n is available" : "sudo -n unavailable; setup may require user-provided approval or password handling.", {
|
|
13069
|
+
data: { available: details["sudo_noninteractive"] === "ok" },
|
|
13070
|
+
remediation: details["sudo_noninteractive"] === "ok" ? undefined : ["Configure explicit sudo policy or run setup commands manually; do not store sudo passwords in public manifests."]
|
|
13071
|
+
}),
|
|
13072
|
+
makeCheck2("ssh-cert-support", details["ssh_cert_support"] === "ok" ? "ok" : "warn", "SSH certificate support", details["ssh_cert_support"] === "ok" ? "OpenSSH reports ed25519 certificate support" : "OpenSSH certificate support not detected.", {
|
|
13073
|
+
data: { supported: details["ssh_cert_support"] === "ok" },
|
|
13074
|
+
remediation: details["ssh_cert_support"] === "ok" ? undefined : ["Install or update OpenSSH before adopting SSH certificate auth for this machine."]
|
|
13075
|
+
}),
|
|
13076
|
+
makeCheck2("github-app-auth", details["github_app_ref"] === "configured" ? "ok" : "warn", "GitHub App auth references", details["github_app_ref"] === "configured" ? "GitHub App id and private-key reference are configured" : "GitHub App id/private-key reference missing; use secret references, not user tokens or raw private keys.", {
|
|
13077
|
+
data: {
|
|
13078
|
+
gh_cli: details["gh_cli"] && details["gh_cli"] !== "missing",
|
|
13079
|
+
gh_auth: details["gh_auth"] === "ok",
|
|
13080
|
+
app_ref_configured: details["github_app_ref"] === "configured"
|
|
13081
|
+
},
|
|
13082
|
+
remediation: details["github_app_ref"] === "configured" ? undefined : ["Set HASNA_GITHUB_APP_ID plus HASNA_GITHUB_APP_PRIVATE_KEY_REF or provide an equivalent open-secrets adapter."]
|
|
13083
|
+
}),
|
|
13084
|
+
...optionalAdapterChecks
|
|
13085
|
+
];
|
|
13086
|
+
return {
|
|
13087
|
+
machineId: reportedMachineId,
|
|
13088
|
+
source: commandChecks.source,
|
|
13089
|
+
schemaVersion: 1,
|
|
13090
|
+
generatedAt: now.toISOString(),
|
|
13091
|
+
manifestSource,
|
|
13092
|
+
manifestPath: details["manifest_path"] ? redactPath(details["manifest_path"]) : undefined,
|
|
13093
|
+
dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
|
|
13094
|
+
notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
|
|
13095
|
+
checks
|
|
13096
|
+
};
|
|
13097
|
+
}
|
|
13098
|
+
|
|
13099
|
+
// src/agent/runtime.ts
|
|
13100
|
+
function resolvePrivateMetadata(value) {
|
|
13101
|
+
return value ?? isPrivateMetadataEnabled();
|
|
13102
|
+
}
|
|
13103
|
+
function readToolVersion(command, args) {
|
|
13104
|
+
try {
|
|
13105
|
+
const output = execFileSync(command, args, {
|
|
13106
|
+
encoding: "utf8",
|
|
13107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13108
|
+
timeout: 1000
|
|
13109
|
+
});
|
|
13110
|
+
const firstLine2 = output.split(/\r?\n/).find((line) => line.trim());
|
|
13111
|
+
return firstLine2 ? sanitizePublicString(firstLine2.trim()) : null;
|
|
13112
|
+
} catch {
|
|
13113
|
+
return null;
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
function selectedToolVersions() {
|
|
13117
|
+
const versions = {};
|
|
13118
|
+
if (process.versions.bun)
|
|
13119
|
+
versions.bun = process.versions.bun;
|
|
13120
|
+
if (process.version)
|
|
13121
|
+
versions.node = process.version;
|
|
13122
|
+
const git = readToolVersion("git", ["--version"]);
|
|
13123
|
+
if (git)
|
|
13124
|
+
versions.git = git;
|
|
13125
|
+
const tailscale = readToolVersion("tailscale", ["version"]);
|
|
13126
|
+
if (tailscale)
|
|
13127
|
+
versions.tailscale = tailscale;
|
|
13128
|
+
return versions;
|
|
13129
|
+
}
|
|
13130
|
+
function summarizeTailscale(privateMetadata) {
|
|
13131
|
+
let parsed;
|
|
13132
|
+
try {
|
|
13133
|
+
const output = execFileSync("tailscale", ["status", "--json"], {
|
|
13134
|
+
encoding: "utf8",
|
|
13135
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13136
|
+
timeout: 1500
|
|
13137
|
+
});
|
|
13138
|
+
parsed = JSON.parse(output);
|
|
13139
|
+
} catch {
|
|
13140
|
+
return { available: false };
|
|
13141
|
+
}
|
|
13142
|
+
const peers = parsed["Peer"] && typeof parsed["Peer"] === "object" ? Object.values(parsed["Peer"]) : [];
|
|
13143
|
+
const self = parsed["Self"] && typeof parsed["Self"] === "object" ? parsed["Self"] : {};
|
|
13144
|
+
const summary = {
|
|
13145
|
+
available: true,
|
|
13146
|
+
backendState: typeof parsed["BackendState"] === "string" ? parsed["BackendState"] : undefined,
|
|
13147
|
+
selfOnline: typeof self["Online"] === "boolean" ? self["Online"] : undefined,
|
|
13148
|
+
peerCount: peers.length
|
|
13149
|
+
};
|
|
13150
|
+
if (privateMetadata) {
|
|
13151
|
+
summary.selfDnsName = typeof self["DNSName"] === "string" ? self["DNSName"] : undefined;
|
|
13152
|
+
summary.selfTailscaleIps = Array.isArray(self["TailscaleIPs"]) ? self["TailscaleIPs"] : undefined;
|
|
13153
|
+
}
|
|
13154
|
+
return sanitizeRecord(summary, privateMetadata);
|
|
13155
|
+
}
|
|
13156
|
+
function envFlag(name) {
|
|
13157
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
13158
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
13159
|
+
}
|
|
13160
|
+
function shouldCollectDoctorSummary(value) {
|
|
13161
|
+
return value ?? envFlag("HASNA_MACHINES_AGENT_DOCTOR_SUMMARY");
|
|
13162
|
+
}
|
|
13163
|
+
function collectDoctorSummary(machineId, enabled) {
|
|
13164
|
+
if (!enabled)
|
|
13165
|
+
return null;
|
|
13166
|
+
try {
|
|
13167
|
+
const report = runDoctor(machineId, { includeOptionalAdapters: false });
|
|
13168
|
+
const summary = report.checks.reduce((counts, check) => {
|
|
13169
|
+
counts[check.status] += 1;
|
|
13170
|
+
return counts;
|
|
13171
|
+
}, { ok: 0, warn: 0, fail: 0 });
|
|
13172
|
+
return {
|
|
13173
|
+
generated_at: report.generatedAt,
|
|
13174
|
+
source: report.source,
|
|
13175
|
+
summary,
|
|
13176
|
+
blockers: report.checks.filter((check) => check.status !== "ok").slice(0, 20).map((check) => ({
|
|
13177
|
+
id: check.id,
|
|
13178
|
+
status: check.status,
|
|
13179
|
+
summary: check.summary,
|
|
13180
|
+
detail: check.detail,
|
|
13181
|
+
remediation: check.remediation ?? []
|
|
13182
|
+
}))
|
|
13183
|
+
};
|
|
13184
|
+
} catch (error) {
|
|
13185
|
+
return {
|
|
13186
|
+
generated_at: new Date().toISOString(),
|
|
13187
|
+
source: "doctor",
|
|
13188
|
+
summary: { ok: 0, warn: 1, fail: 0 },
|
|
13189
|
+
blockers: [{
|
|
13190
|
+
id: "doctor-summary",
|
|
13191
|
+
status: "warn",
|
|
13192
|
+
summary: "Doctor summary unavailable",
|
|
13193
|
+
detail: redactErrorMessage(error instanceof Error ? error.message : String(error)),
|
|
13194
|
+
remediation: []
|
|
13195
|
+
}]
|
|
13196
|
+
};
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
function collectHeartbeatMetadata(machineId, options = {}) {
|
|
13200
|
+
const privateMetadata = resolvePrivateMetadata(options.privateMetadata);
|
|
13201
|
+
return {
|
|
13202
|
+
daemonVersion: getPackageVersion(),
|
|
13203
|
+
agentMode: sanitizePublicString(options.mode?.trim() || "daemon", privateMetadata),
|
|
13204
|
+
platform: platform3(),
|
|
13205
|
+
osVersion: sanitizePublicString(osVersion(), privateMetadata),
|
|
13206
|
+
osBuild: sanitizePublicString(release(), privateMetadata),
|
|
13207
|
+
arch: arch3(),
|
|
13208
|
+
uptimeSeconds: uptime(),
|
|
13209
|
+
toolVersions: sanitizeRecord(selectedToolVersions(), privateMetadata),
|
|
13210
|
+
tailscale: summarizeTailscale(privateMetadata),
|
|
13211
|
+
storageSyncStatus: options.storageSyncStatus ?? null,
|
|
13212
|
+
storageSyncLastError: options.storageSyncLastError ? sanitizePublicString(options.storageSyncLastError, privateMetadata) : null,
|
|
13213
|
+
doctorSummary: collectDoctorSummary(machineId, shouldCollectDoctorSummary(options.doctorSummary)),
|
|
13214
|
+
privateMetadata
|
|
13215
|
+
};
|
|
13216
|
+
}
|
|
13217
|
+
function parseJsonObject(value) {
|
|
13218
|
+
if (!value)
|
|
13219
|
+
return null;
|
|
13220
|
+
try {
|
|
13221
|
+
const parsed = JSON.parse(value);
|
|
13222
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
13223
|
+
} catch {
|
|
13224
|
+
return null;
|
|
13225
|
+
}
|
|
13226
|
+
}
|
|
13227
|
+
function heartbeatToStatus(heartbeat, options = {}) {
|
|
13228
|
+
const privateMetadata = options.privateMetadata === true;
|
|
13229
|
+
const tailscale = parseJsonObject(heartbeat.tailscale_json);
|
|
13230
|
+
const doctorSummary = parseJsonObject(heartbeat.doctor_summary_json);
|
|
13231
|
+
return {
|
|
12667
13232
|
machineId: heartbeat.machine_id,
|
|
12668
13233
|
pid: heartbeat.pid,
|
|
12669
13234
|
status: heartbeat.status,
|
|
12670
|
-
updatedAt: heartbeat.updated_at
|
|
12671
|
-
|
|
13235
|
+
updatedAt: heartbeat.updated_at,
|
|
13236
|
+
daemonVersion: heartbeat.daemon_version,
|
|
13237
|
+
agentMode: heartbeat.agent_mode,
|
|
13238
|
+
platform: heartbeat.platform,
|
|
13239
|
+
osVersion: heartbeat.os_version,
|
|
13240
|
+
osBuild: heartbeat.os_build,
|
|
13241
|
+
arch: heartbeat.arch,
|
|
13242
|
+
uptimeSeconds: heartbeat.uptime_seconds,
|
|
13243
|
+
toolVersions: sanitizeRecord(parseJsonObject(heartbeat.tool_versions_json) ?? {}, privateMetadata),
|
|
13244
|
+
tailscale: tailscale ? sanitizeRecord(tailscale, privateMetadata) : null,
|
|
13245
|
+
storageSyncStatus: heartbeat.storage_sync_status,
|
|
13246
|
+
storageSyncLastError: heartbeat.storage_sync_last_error ? sanitizeStorageError(heartbeat.storage_sync_last_error, privateMetadata) : null,
|
|
13247
|
+
doctorSummary: doctorSummary ? sanitizeRecord(doctorSummary, privateMetadata) : null,
|
|
13248
|
+
privateMetadata: Boolean(heartbeat.private_metadata)
|
|
13249
|
+
};
|
|
13250
|
+
}
|
|
13251
|
+
function sanitizePublicString(value, privateMetadata = false) {
|
|
13252
|
+
if (privateMetadata)
|
|
13253
|
+
return value;
|
|
13254
|
+
let redacted = value;
|
|
13255
|
+
const localHostname = hostname5();
|
|
13256
|
+
const localUser = process.env["USER"] || process.env["LOGNAME"] || process.env["USERNAME"];
|
|
13257
|
+
if (localHostname)
|
|
13258
|
+
redacted = redacted.replaceAll(localHostname, "[redacted-host]");
|
|
13259
|
+
if (localUser)
|
|
13260
|
+
redacted = redacted.replaceAll(localUser, "[redacted-user]");
|
|
13261
|
+
return redactErrorMessage(redacted.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s'")]+/gi, (match) => {
|
|
13262
|
+
const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
|
|
13263
|
+
return `${scheme}[redacted]`;
|
|
13264
|
+
}).replace(/\b10(?:\.\d{1,3}){3}\b/g, "[redacted-ip]").replace(/\b172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b192\.168(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b(password|passwd|token|secret|api[_-]?key)=([^&\s]+)/gi, "$1=[redacted]"));
|
|
13265
|
+
}
|
|
13266
|
+
function sanitizeValue(value, privateMetadata) {
|
|
13267
|
+
if (typeof value === "string")
|
|
13268
|
+
return sanitizePublicString(value, privateMetadata);
|
|
13269
|
+
if (Array.isArray(value))
|
|
13270
|
+
return value.map((entry) => sanitizeValue(entry, privateMetadata));
|
|
13271
|
+
if (value && typeof value === "object")
|
|
13272
|
+
return sanitizeRecord(value, privateMetadata);
|
|
13273
|
+
return value;
|
|
13274
|
+
}
|
|
13275
|
+
function sanitizeRecord(value, privateMetadata) {
|
|
13276
|
+
const sanitized = {};
|
|
13277
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
13278
|
+
if (!privateMetadata && /(hostname|hostName|user|username|serial|dnsName|ip|ips|databaseUrl|url|token|secret|password|credential)/i.test(key)) {
|
|
13279
|
+
sanitized[key] = "[redacted]";
|
|
13280
|
+
continue;
|
|
13281
|
+
}
|
|
13282
|
+
sanitized[key] = sanitizeValue(entry, privateMetadata);
|
|
13283
|
+
}
|
|
13284
|
+
return sanitized;
|
|
13285
|
+
}
|
|
13286
|
+
function writeHeartbeat(status = "online", options = {}) {
|
|
13287
|
+
const machineId = getLocalMachineId();
|
|
13288
|
+
upsertHeartbeat(machineId, process.pid, status, collectHeartbeatMetadata(machineId, options));
|
|
13289
|
+
return getAgentStatus(machineId, { privateMetadata: resolvePrivateMetadata(options.privateMetadata) }).find((heartbeat) => heartbeat.pid === process.pid) ?? {
|
|
13290
|
+
machineId,
|
|
13291
|
+
pid: process.pid,
|
|
13292
|
+
status,
|
|
13293
|
+
updatedAt: new Date().toISOString()
|
|
13294
|
+
};
|
|
13295
|
+
}
|
|
13296
|
+
function sleep(ms) {
|
|
13297
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
13298
|
+
}
|
|
13299
|
+
function sanitizeStorageError(message, privateMetadata) {
|
|
13300
|
+
return privateMetadata ? message : redactErrorMessage(message);
|
|
13301
|
+
}
|
|
13302
|
+
async function pushHeartbeatRowsWithRetry(options, privateMetadata) {
|
|
13303
|
+
const retries = Number.isInteger(options.storagePushRetries) && options.storagePushRetries >= 0 ? options.storagePushRetries : 2;
|
|
13304
|
+
const backoffMs = Number.isInteger(options.storagePushBackoffMs) && options.storagePushBackoffMs >= 0 ? options.storagePushBackoffMs : 250;
|
|
13305
|
+
const attempts = retries + 1;
|
|
13306
|
+
let lastError = null;
|
|
13307
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
13308
|
+
try {
|
|
13309
|
+
const results = await storagePush({ tables: ["agent_heartbeats"] });
|
|
13310
|
+
const errors2 = results.flatMap((result) => result.errors);
|
|
13311
|
+
if (errors2.length === 0)
|
|
13312
|
+
return { ok: true, error: null, attempts: attempt };
|
|
13313
|
+
lastError = errors2.map((error) => sanitizeStorageError(error, privateMetadata)).join("; ");
|
|
13314
|
+
} catch (error) {
|
|
13315
|
+
lastError = sanitizeStorageError(error instanceof Error ? error.message : String(error), privateMetadata);
|
|
13316
|
+
}
|
|
13317
|
+
if (attempt < attempts && backoffMs > 0) {
|
|
13318
|
+
await sleep(backoffMs * attempt);
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13321
|
+
return { ok: false, error: lastError, attempts };
|
|
13322
|
+
}
|
|
13323
|
+
async function writeHeartbeatTick(status = "online", options = {}) {
|
|
13324
|
+
const privateMetadata = resolvePrivateMetadata(options.privateMetadata);
|
|
13325
|
+
if (!options.storagePush)
|
|
13326
|
+
return writeHeartbeat(status, { ...options, storageSyncStatus: options.storageSyncStatus ?? "disabled" });
|
|
13327
|
+
let heartbeat = writeHeartbeat(status, { ...options, doctorSummary: false, storageSyncStatus: "pending", storageSyncLastError: null });
|
|
13328
|
+
const pushed = await pushHeartbeatRowsWithRetry(options, privateMetadata);
|
|
13329
|
+
heartbeat = writeHeartbeat(status, {
|
|
13330
|
+
...options,
|
|
13331
|
+
storageSyncStatus: pushed.ok ? "ok" : "error",
|
|
13332
|
+
storageSyncLastError: pushed.ok ? null : `${pushed.error ?? "storage push failed"} (attempts=${pushed.attempts})`
|
|
13333
|
+
});
|
|
13334
|
+
if (pushed.ok) {
|
|
13335
|
+
const finalPush = await pushHeartbeatRowsWithRetry({ ...options, storagePushRetries: 0 }, privateMetadata);
|
|
13336
|
+
if (!finalPush.ok) {
|
|
13337
|
+
heartbeat = writeHeartbeat(status, {
|
|
13338
|
+
...options,
|
|
13339
|
+
storageSyncStatus: "error",
|
|
13340
|
+
storageSyncLastError: `${finalPush.error ?? "final storage push failed"} (attempts=${finalPush.attempts})`
|
|
13341
|
+
});
|
|
13342
|
+
await pushHeartbeatRowsWithRetry({ ...options, storagePushRetries: 0 }, privateMetadata);
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
return heartbeat;
|
|
13346
|
+
}
|
|
13347
|
+
function markOffline(options = {}) {
|
|
13348
|
+
return writeHeartbeat("offline", options);
|
|
13349
|
+
}
|
|
13350
|
+
function getAgentStatus(machineId, options = {}) {
|
|
13351
|
+
return listHeartbeats(machineId).map((heartbeat) => heartbeatToStatus(heartbeat, options));
|
|
12672
13352
|
}
|
|
12673
13353
|
// src/commands/backup.ts
|
|
12674
|
-
import { homedir as homedir2, hostname as
|
|
13354
|
+
import { homedir as homedir2, hostname as hostname6 } from "os";
|
|
12675
13355
|
import { join as join3 } from "path";
|
|
12676
13356
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
12677
13357
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
@@ -12741,7 +13421,7 @@ function buildBackupPlan(bucket, prefix) {
|
|
|
12741
13421
|
{
|
|
12742
13422
|
id: "backup-upload",
|
|
12743
13423
|
title: "Upload archive to S3",
|
|
12744
|
-
command: `aws s3 cp ${quote(archivePath)} ${quote(`s3://${target.bucket}/${target.prefix}/${
|
|
13424
|
+
command: `aws s3 cp ${quote(archivePath)} ${quote(`s3://${target.bucket}/${target.prefix}/${hostname6()}-backup.tgz`)}`,
|
|
12745
13425
|
manager: "custom"
|
|
12746
13426
|
}
|
|
12747
13427
|
];
|
|
@@ -12918,7 +13598,7 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
|
12918
13598
|
};
|
|
12919
13599
|
}
|
|
12920
13600
|
// src/commands/cert.ts
|
|
12921
|
-
import { homedir as homedir3, platform as
|
|
13601
|
+
import { homedir as homedir3, platform as platform4 } from "os";
|
|
12922
13602
|
import { join as join4 } from "path";
|
|
12923
13603
|
function quote2(value) {
|
|
12924
13604
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -12934,7 +13614,7 @@ function buildCertPlan(domains) {
|
|
|
12934
13614
|
const certPath = join4(certDir(), `${primary}.pem`);
|
|
12935
13615
|
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
12936
13616
|
const steps = [];
|
|
12937
|
-
if (
|
|
13617
|
+
if (platform4() === "darwin") {
|
|
12938
13618
|
steps.push({
|
|
12939
13619
|
id: "mkcert-install-macos",
|
|
12940
13620
|
title: "Install mkcert on macOS",
|
|
@@ -13037,189 +13717,448 @@ function renderDomainMapping(domain) {
|
|
|
13037
13717
|
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
13038
13718
|
};
|
|
13039
13719
|
}
|
|
13040
|
-
// src/commands/
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13720
|
+
// src/commands/daemon.ts
|
|
13721
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
13722
|
+
import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
13723
|
+
import { dirname as dirname4 } from "path";
|
|
13724
|
+
import { platform as osPlatform } from "os";
|
|
13725
|
+
var DEFAULT_SERVICE_NAME = "machines-agent";
|
|
13726
|
+
var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
|
|
13727
|
+
var DEFAULT_INTERVAL_MS = 30000;
|
|
13728
|
+
var ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
13729
|
+
var SERVICE_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
13730
|
+
function buildDaemonServicePlan(options) {
|
|
13731
|
+
const resolved = resolveDaemonServiceOptions(options);
|
|
13732
|
+
const files = resolved.action === "install" ? [buildServiceFile(resolved)] : [];
|
|
13044
13733
|
return {
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13734
|
+
platform: resolved.platform,
|
|
13735
|
+
mode: resolved.mode,
|
|
13736
|
+
action: resolved.action,
|
|
13737
|
+
serviceName: resolved.serviceName,
|
|
13738
|
+
serviceId: resolved.serviceId,
|
|
13739
|
+
executable: resolved.executable,
|
|
13740
|
+
intervalMs: resolved.intervalMs,
|
|
13741
|
+
commands: buildActionCommands(resolved),
|
|
13742
|
+
files,
|
|
13743
|
+
warnings: resolved.warnings,
|
|
13744
|
+
manualSteps: buildManualSteps(resolved, files)
|
|
13745
|
+
};
|
|
13746
|
+
}
|
|
13747
|
+
function runDaemonServicePlan(plan, options = {}) {
|
|
13748
|
+
const apply = options.apply === true;
|
|
13749
|
+
const allowed = apply && options.yes === true;
|
|
13750
|
+
const warnings = [...plan.warnings];
|
|
13751
|
+
const filesWritten = [];
|
|
13752
|
+
const commands = [];
|
|
13753
|
+
if (apply && !allowed) {
|
|
13754
|
+
warnings.push("apply_requires_yes");
|
|
13755
|
+
}
|
|
13756
|
+
if (allowed) {
|
|
13757
|
+
for (const file of plan.files) {
|
|
13758
|
+
const path = expandShellPath(file.path);
|
|
13759
|
+
let content;
|
|
13760
|
+
try {
|
|
13761
|
+
content = materializePlaceholders(file.content);
|
|
13762
|
+
} catch (error) {
|
|
13763
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
13764
|
+
return {
|
|
13765
|
+
mode: "plan",
|
|
13766
|
+
applied: false,
|
|
13767
|
+
plan,
|
|
13768
|
+
filesWritten,
|
|
13769
|
+
commands: plan.commands.map((commandSpec) => ({
|
|
13770
|
+
id: commandSpec.id,
|
|
13771
|
+
command: renderCommand(commandSpec),
|
|
13772
|
+
skipped: true,
|
|
13773
|
+
exitCode: null,
|
|
13774
|
+
stdout: "",
|
|
13775
|
+
stderr: ""
|
|
13776
|
+
})),
|
|
13777
|
+
warnings
|
|
13778
|
+
};
|
|
13779
|
+
}
|
|
13780
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
13781
|
+
writeFileSync3(path, content, "utf8");
|
|
13782
|
+
chmodSync(path, Number.parseInt(file.mode, 8));
|
|
13783
|
+
filesWritten.push(path);
|
|
13784
|
+
}
|
|
13785
|
+
}
|
|
13786
|
+
for (const commandSpec of plan.commands) {
|
|
13787
|
+
const commandLine = renderCommand(commandSpec);
|
|
13788
|
+
if (!allowed) {
|
|
13789
|
+
commands.push({
|
|
13790
|
+
id: commandSpec.id,
|
|
13791
|
+
command: commandLine,
|
|
13792
|
+
skipped: true,
|
|
13793
|
+
exitCode: null,
|
|
13794
|
+
stdout: "",
|
|
13795
|
+
stderr: ""
|
|
13796
|
+
});
|
|
13797
|
+
continue;
|
|
13798
|
+
}
|
|
13799
|
+
const program = commandLine[0];
|
|
13800
|
+
const args = commandLine.slice(1);
|
|
13801
|
+
try {
|
|
13802
|
+
const result = execFileSync2(program, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13803
|
+
commands.push({
|
|
13804
|
+
id: commandSpec.id,
|
|
13805
|
+
command: commandLine,
|
|
13806
|
+
skipped: false,
|
|
13807
|
+
exitCode: 0,
|
|
13808
|
+
stdout: result,
|
|
13809
|
+
stderr: ""
|
|
13810
|
+
});
|
|
13811
|
+
} catch (error) {
|
|
13812
|
+
const maybe = error;
|
|
13813
|
+
if (commandSpec.allowFailure) {
|
|
13814
|
+
commands.push({
|
|
13815
|
+
id: commandSpec.id,
|
|
13816
|
+
command: commandLine,
|
|
13817
|
+
skipped: false,
|
|
13818
|
+
exitCode: typeof maybe.status === "number" ? maybe.status : 1,
|
|
13819
|
+
stdout: String(maybe.stdout ?? ""),
|
|
13820
|
+
stderr: String(maybe.stderr ?? ""),
|
|
13821
|
+
error: maybe.message ?? String(error)
|
|
13822
|
+
});
|
|
13823
|
+
continue;
|
|
13824
|
+
}
|
|
13825
|
+
commands.push({
|
|
13826
|
+
id: commandSpec.id,
|
|
13827
|
+
command: commandLine,
|
|
13828
|
+
skipped: false,
|
|
13829
|
+
exitCode: typeof maybe.status === "number" ? maybe.status : 1,
|
|
13830
|
+
stdout: String(maybe.stdout ?? ""),
|
|
13831
|
+
stderr: String(maybe.stderr ?? ""),
|
|
13832
|
+
error: maybe.message ?? String(error)
|
|
13833
|
+
});
|
|
13834
|
+
break;
|
|
13835
|
+
}
|
|
13836
|
+
}
|
|
13837
|
+
return {
|
|
13838
|
+
mode: allowed ? "apply" : "plan",
|
|
13839
|
+
applied: allowed,
|
|
13840
|
+
plan,
|
|
13841
|
+
filesWritten,
|
|
13842
|
+
commands,
|
|
13843
|
+
warnings
|
|
13844
|
+
};
|
|
13845
|
+
}
|
|
13846
|
+
function buildDaemonInstallPlan(options = {}) {
|
|
13847
|
+
return buildDaemonServicePlan({ ...options, action: "install" });
|
|
13848
|
+
}
|
|
13849
|
+
function buildDaemonUninstallPlan(options = {}) {
|
|
13850
|
+
return buildDaemonServicePlan({ ...options, action: "uninstall" });
|
|
13851
|
+
}
|
|
13852
|
+
function buildDaemonRestartPlan(options = {}) {
|
|
13853
|
+
return buildDaemonServicePlan({ ...options, action: "restart" });
|
|
13854
|
+
}
|
|
13855
|
+
function buildDaemonStatusPlan(options = {}) {
|
|
13856
|
+
return buildDaemonServicePlan({ ...options, action: "status" });
|
|
13857
|
+
}
|
|
13858
|
+
function buildDaemonLogsPlan(options = {}) {
|
|
13859
|
+
return buildDaemonServicePlan({ ...options, action: "logs" });
|
|
13860
|
+
}
|
|
13861
|
+
function renderLaunchdPlist(options = {}) {
|
|
13862
|
+
const resolved = resolveDaemonServiceOptions({ ...options, action: "install", platform: "macos" });
|
|
13863
|
+
return launchdPlist(resolved);
|
|
13864
|
+
}
|
|
13865
|
+
function renderSystemdUnit(options = {}) {
|
|
13866
|
+
const resolved = resolveDaemonServiceOptions({ ...options, action: "install", platform: "linux" });
|
|
13867
|
+
return systemdUnit(resolved);
|
|
13868
|
+
}
|
|
13869
|
+
function resolveDaemonServiceOptions(options) {
|
|
13870
|
+
const warnings = [];
|
|
13871
|
+
const serviceName = normalizeServiceName(options.serviceName, warnings);
|
|
13872
|
+
const platform5 = normalizePlatform3(options.platform, warnings);
|
|
13873
|
+
const mode = options.mode ?? "user";
|
|
13874
|
+
const intervalMs = normalizeIntervalMs(options.intervalMs, warnings);
|
|
13875
|
+
const executable = options.executable?.trim() || DEFAULT_EXECUTABLE;
|
|
13876
|
+
if (platform5 === "linux" && !executable.startsWith("/")) {
|
|
13877
|
+
warnings.push("systemd units should use an absolute executable path; install plan keeps the provided path unchanged.");
|
|
13878
|
+
}
|
|
13879
|
+
return {
|
|
13880
|
+
action: options.action,
|
|
13881
|
+
platform: platform5,
|
|
13882
|
+
mode,
|
|
13883
|
+
serviceName,
|
|
13884
|
+
serviceId: serviceName,
|
|
13885
|
+
executable,
|
|
13886
|
+
intervalMs,
|
|
13887
|
+
env: buildEnvironment(serviceName, options, warnings),
|
|
13888
|
+
warnings
|
|
13051
13889
|
};
|
|
13052
13890
|
}
|
|
13053
|
-
function
|
|
13054
|
-
const
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13891
|
+
function normalizeServiceName(value, warnings) {
|
|
13892
|
+
const serviceName = value?.trim() || DEFAULT_SERVICE_NAME;
|
|
13893
|
+
if (SERVICE_NAME_PATTERN.test(serviceName))
|
|
13894
|
+
return serviceName;
|
|
13895
|
+
warnings.push(`Invalid serviceName "${serviceName}"; using ${DEFAULT_SERVICE_NAME}.`);
|
|
13896
|
+
return DEFAULT_SERVICE_NAME;
|
|
13897
|
+
}
|
|
13898
|
+
function normalizePlatform3(value, warnings) {
|
|
13899
|
+
const raw = value ?? osPlatform();
|
|
13900
|
+
if (raw === "darwin" || raw === "macos")
|
|
13901
|
+
return "macos";
|
|
13902
|
+
if (raw === "linux")
|
|
13903
|
+
return "linux";
|
|
13904
|
+
warnings.push(`Unsupported platform "${raw}"; using linux service planning.`);
|
|
13905
|
+
return "linux";
|
|
13906
|
+
}
|
|
13907
|
+
function normalizeIntervalMs(value, warnings) {
|
|
13908
|
+
if (value === undefined)
|
|
13909
|
+
return DEFAULT_INTERVAL_MS;
|
|
13910
|
+
if (Number.isInteger(value) && value > 0)
|
|
13911
|
+
return value;
|
|
13912
|
+
warnings.push(`Invalid intervalMs "${String(value)}"; using ${DEFAULT_INTERVAL_MS}.`);
|
|
13913
|
+
return DEFAULT_INTERVAL_MS;
|
|
13914
|
+
}
|
|
13915
|
+
function buildEnvironment(serviceName, options, warnings) {
|
|
13916
|
+
const env = {
|
|
13917
|
+
HASNA_MACHINES_AGENT_MODE: "daemon",
|
|
13918
|
+
HASNA_MACHINES_AGENT_SERVICE: serviceName
|
|
13919
|
+
};
|
|
13920
|
+
if (options.storagePush) {
|
|
13921
|
+
env["HASNA_MACHINES_AGENT_STORAGE_PUSH"] = "1";
|
|
13922
|
+
env["HASNA_MACHINES_AGENT_STORAGE_PUSH_BACKOFF_MS"] = "250";
|
|
13923
|
+
env["HASNA_MACHINES_AGENT_STORAGE_PUSH_RETRIES"] = "2";
|
|
13924
|
+
env["HASNA_MACHINES_STORAGE_MODE"] = "hybrid";
|
|
13925
|
+
env["HASNA_MACHINES_DATABASE_URL"] = placeholderForEnv("HASNA_MACHINES_DATABASE_URL");
|
|
13926
|
+
warnings.push("storagePush is represented with env placeholders; no database URL is embedded in the plan.");
|
|
13927
|
+
}
|
|
13928
|
+
if (options.doctorSummary) {
|
|
13929
|
+
env["HASNA_MACHINES_AGENT_DOCTOR_SUMMARY"] = "1";
|
|
13930
|
+
}
|
|
13931
|
+
if (options.privateMetadata === true) {
|
|
13932
|
+
env["HASNA_MACHINES_PRIVATE_METADATA"] = "1";
|
|
13933
|
+
warnings.push("privateMetadata=true enables private host/network facts in heartbeat rows; do not share private-mode output publicly.");
|
|
13934
|
+
} else if (Array.isArray(options.privateMetadata)) {
|
|
13935
|
+
addEnvPlaceholders(env, options.privateMetadata, warnings);
|
|
13936
|
+
}
|
|
13937
|
+
addEnvPlaceholders(env, options.env ?? [], warnings);
|
|
13938
|
+
return Object.fromEntries(Object.entries(env).sort(([left], [right]) => left.localeCompare(right)));
|
|
13939
|
+
}
|
|
13940
|
+
function addEnvPlaceholders(env, names, warnings) {
|
|
13941
|
+
for (const rawName of names) {
|
|
13942
|
+
const name = rawName.trim();
|
|
13943
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
13944
|
+
warnings.push(`Invalid environment variable name "${rawName}"; skipped.`);
|
|
13059
13945
|
continue;
|
|
13060
|
-
|
|
13946
|
+
}
|
|
13947
|
+
env[name] = placeholderForEnv(name);
|
|
13061
13948
|
}
|
|
13062
|
-
return result;
|
|
13063
13949
|
}
|
|
13064
|
-
function
|
|
13950
|
+
function placeholderForEnv(name) {
|
|
13951
|
+
return `<set:${name}>`;
|
|
13952
|
+
}
|
|
13953
|
+
function buildServiceFile(options) {
|
|
13954
|
+
if (options.platform === "macos") {
|
|
13955
|
+
return {
|
|
13956
|
+
id: "launchd-plist",
|
|
13957
|
+
description: "launchd property list for machines-agent",
|
|
13958
|
+
path: launchdPlistPath(options),
|
|
13959
|
+
mode: "0644",
|
|
13960
|
+
content: launchdPlist(options)
|
|
13961
|
+
};
|
|
13962
|
+
}
|
|
13963
|
+
return {
|
|
13964
|
+
id: "systemd-unit",
|
|
13965
|
+
description: "systemd unit for machines-agent",
|
|
13966
|
+
path: systemdUnitPath(options),
|
|
13967
|
+
mode: "0644",
|
|
13968
|
+
content: systemdUnit(options)
|
|
13969
|
+
};
|
|
13970
|
+
}
|
|
13971
|
+
function buildActionCommands(options) {
|
|
13972
|
+
if (options.platform === "macos")
|
|
13973
|
+
return buildLaunchdCommands(options);
|
|
13974
|
+
return buildSystemdCommands(options);
|
|
13975
|
+
}
|
|
13976
|
+
function buildLaunchdCommands(options) {
|
|
13977
|
+
const domain = launchdDomain(options);
|
|
13978
|
+
const serviceTarget = `${domain}/${options.serviceId}`;
|
|
13979
|
+
const plistPath = launchdPlistPath(options);
|
|
13980
|
+
const sudo = options.mode === "system";
|
|
13981
|
+
if (options.action === "install") {
|
|
13982
|
+
return [
|
|
13983
|
+
command("launchd-bootout-existing", "Unload any existing launchd job before bootstrap.", "launchctl", ["bootout", domain, plistPath], sudo, true, true),
|
|
13984
|
+
command("launchd-bootstrap", "Load the planned launchd plist.", "launchctl", ["bootstrap", domain, plistPath], sudo, true),
|
|
13985
|
+
command("launchd-enable", "Enable the launchd service.", "launchctl", ["enable", serviceTarget], sudo, true),
|
|
13986
|
+
command("launchd-kickstart", "Start or restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)
|
|
13987
|
+
];
|
|
13988
|
+
}
|
|
13989
|
+
if (options.action === "uninstall") {
|
|
13990
|
+
return [
|
|
13991
|
+
command("launchd-bootout", "Unload the launchd job.", "launchctl", ["bootout", domain, plistPath], sudo, true),
|
|
13992
|
+
command("remove-launchd-plist", "Remove the planned launchd plist file.", "rm", ["-f", plistPath], sudo, true)
|
|
13993
|
+
];
|
|
13994
|
+
}
|
|
13995
|
+
if (options.action === "restart") {
|
|
13996
|
+
return [command("launchd-kickstart", "Restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)];
|
|
13997
|
+
}
|
|
13998
|
+
if (options.action === "status") {
|
|
13999
|
+
return [command("launchd-print", "Print launchd service status.", "launchctl", ["print", serviceTarget], sudo, false)];
|
|
14000
|
+
}
|
|
13065
14001
|
return [
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
|
|
13069
|
-
'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
|
|
13070
|
-
`printf 'data_dir=%s\\n' "$data_dir"`,
|
|
13071
|
-
`printf 'manifest_path=%s\\n' "$manifest_path"`,
|
|
13072
|
-
`printf 'db_path=%s\\n' "$db_path"`,
|
|
13073
|
-
`printf 'notifications_path=%s\\n' "$notifications_path"`,
|
|
13074
|
-
`printf 'data_dir_exists=%s\\n' "$(test -d "$data_dir" && printf yes || printf no)"`,
|
|
13075
|
-
`printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
|
|
13076
|
-
`printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
|
|
13077
|
-
`printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
|
|
13078
|
-
`printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
|
|
13079
|
-
`printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
|
|
13080
|
-
`printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
|
|
13081
|
-
`printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
|
|
13082
|
-
`printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`,
|
|
13083
|
-
`printf 'sudo_noninteractive=%s\\n' "$(sudo -n true >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
13084
|
-
`printf 'ssh_cert_support=%s\\n' "$(ssh -Q key-cert 2>/dev/null | grep -q 'ssh-ed25519-cert-v01@openssh.com' && printf ok || printf unavailable)"`,
|
|
13085
|
-
`printf 'gh_cli=%s\\n' "$(command -v gh 2>/dev/null || printf missing)"`,
|
|
13086
|
-
`printf 'gh_auth=%s\\n' "$(gh auth status >/dev/null 2>&1 && printf ok || printf unavailable)"`,
|
|
13087
|
-
"printf 'github_app_ref=%s\\n' \"$(test -n \\\"${HASNA_GITHUB_APP_ID:-}\\\" -a -n \\\"${HASNA_GITHUB_APP_PRIVATE_KEY_REF:-}\\\" && printf configured || printf missing)\""
|
|
13088
|
-
].join("; ");
|
|
14002
|
+
command("launchd-logs", "Stream logs for machines-agent.", "log", ["stream", "--style", "compact", "--predicate", `process == "${basename(options.executable)}" OR eventMessage CONTAINS "${options.serviceId}"`], false, false)
|
|
14003
|
+
];
|
|
13089
14004
|
}
|
|
13090
|
-
function
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
14005
|
+
function buildSystemdCommands(options) {
|
|
14006
|
+
const userFlag = options.mode === "user" ? ["--user"] : [];
|
|
14007
|
+
const sudo = options.mode === "system";
|
|
14008
|
+
const unitName = systemdUnitName(options);
|
|
14009
|
+
const daemonReload = command("systemd-daemon-reload", "Reload systemd unit metadata.", "systemctl", [...userFlag, "daemon-reload"], sudo, true);
|
|
14010
|
+
if (options.action === "install") {
|
|
14011
|
+
return [
|
|
14012
|
+
daemonReload,
|
|
14013
|
+
command("systemd-enable-now", "Enable and start the systemd service.", "systemctl", [...userFlag, "enable", "--now", unitName], sudo, true)
|
|
14014
|
+
];
|
|
14015
|
+
}
|
|
14016
|
+
if (options.action === "uninstall") {
|
|
14017
|
+
return [
|
|
14018
|
+
command("systemd-disable-now", "Stop and disable the systemd service.", "systemctl", [...userFlag, "disable", "--now", unitName], sudo, true),
|
|
14019
|
+
command("remove-systemd-unit", "Remove the planned systemd unit file.", "rm", ["-f", systemdUnitPath(options)], sudo, true),
|
|
14020
|
+
daemonReload
|
|
14021
|
+
];
|
|
14022
|
+
}
|
|
14023
|
+
if (options.action === "restart") {
|
|
14024
|
+
return [command("systemd-restart", "Restart the systemd service.", "systemctl", [...userFlag, "restart", unitName], sudo, true)];
|
|
14025
|
+
}
|
|
14026
|
+
if (options.action === "status") {
|
|
14027
|
+
return [command("systemd-status", "Show systemd service status.", "systemctl", [...userFlag, "status", unitName, "--no-pager"], sudo, false)];
|
|
14028
|
+
}
|
|
14029
|
+
return [
|
|
14030
|
+
command("systemd-logs", "Follow journal logs for the service.", "journalctl", [...userFlag, "-u", unitName, "-f", "--no-pager"], sudo, false)
|
|
14031
|
+
];
|
|
13096
14032
|
}
|
|
13097
|
-
function
|
|
13098
|
-
const
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
14033
|
+
function buildManualSteps(options, files) {
|
|
14034
|
+
const steps = [];
|
|
14035
|
+
if (files[0])
|
|
14036
|
+
steps.push(`Write ${files[0].id} content to ${files[0].path} with mode ${files[0].mode}.`);
|
|
14037
|
+
if (options.mode === "system")
|
|
14038
|
+
steps.push("Run commands marked sudo with root privileges.");
|
|
14039
|
+
if (options.platform === "linux" && options.mode === "user") {
|
|
14040
|
+
steps.push("Run commands as the target user; enable lingering separately if the service must survive logout.");
|
|
14041
|
+
}
|
|
14042
|
+
if (options.action === "logs")
|
|
14043
|
+
steps.push("Stop the log command manually when finished.");
|
|
14044
|
+
return steps;
|
|
13105
14045
|
}
|
|
13106
|
-
function
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
14046
|
+
function command(id, description, program, args, sudo, mutates, allowFailure = false) {
|
|
14047
|
+
return { id, description, program, args, sudo, mutates, ...allowFailure ? { allowFailure: true } : {} };
|
|
14048
|
+
}
|
|
14049
|
+
function renderCommand(commandSpec) {
|
|
14050
|
+
const expanded = [commandSpec.program, ...commandSpec.args].map(expandShellPath);
|
|
14051
|
+
if (commandSpec.sudo)
|
|
14052
|
+
return ["sudo", ...expanded];
|
|
14053
|
+
return expanded;
|
|
14054
|
+
}
|
|
14055
|
+
function expandShellPath(path) {
|
|
14056
|
+
if (path.startsWith("$HOME/"))
|
|
14057
|
+
return `${process.env["HOME"] ?? ""}/${path.slice("$HOME/".length)}`;
|
|
14058
|
+
return path.replaceAll("$HOME", process.env["HOME"] ?? "").replaceAll("$UID", String(process.getuid ? process.getuid() : ""));
|
|
14059
|
+
}
|
|
14060
|
+
function materializePlaceholders(content) {
|
|
14061
|
+
return content.replace(/(?:<set:([A-Z_][A-Z0-9_]*)>|<set:([A-Z_][A-Z0-9_]*)>)/g, (_match, rawName, escapedName) => {
|
|
14062
|
+
const name = rawName ?? escapedName ?? "";
|
|
14063
|
+
const value = process.env[name];
|
|
14064
|
+
if (value === undefined || value === "") {
|
|
14065
|
+
throw new Error(`Missing environment variable required for service apply: ${name}`);
|
|
13114
14066
|
}
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
const domainChecks = Array.isArray(result) ? result : result ? [result] : [fallbackAdapterCheck(domain)];
|
|
13118
|
-
checks.push(...domainChecks.map((check) => sanitizeAdapterCheck(check, domain, adapter2.id)));
|
|
13119
|
-
} catch {
|
|
13120
|
-
const safeAdapterId = redactIdentifier(adapter2.id);
|
|
13121
|
-
checks.push(makeCheck2(`${domain}-adapter`, "warn", `Optional ${domain} adapter failed`, "Adapter failed; details are intentionally hidden to avoid leaking private refs or credentials.", {
|
|
13122
|
-
optional: true,
|
|
13123
|
-
source: `adapter:${safeAdapterId}`,
|
|
13124
|
-
data: { adapter: safeAdapterId, fallback: true }
|
|
13125
|
-
}));
|
|
14067
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
14068
|
+
throw new Error(`Environment variable ${name} contains control characters; refusing to write service file.`);
|
|
13126
14069
|
}
|
|
13127
|
-
|
|
13128
|
-
|
|
14070
|
+
return escapedName ? xmlEscape(value) : escapeSystemdEnvironmentValue(value);
|
|
14071
|
+
});
|
|
13129
14072
|
}
|
|
13130
|
-
function
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
const
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
|
|
13146
|
-
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
13155
|
-
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13202
|
-
|
|
13203
|
-
|
|
13204
|
-
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
return {
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
|
|
13220
|
-
notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
|
|
13221
|
-
checks
|
|
13222
|
-
};
|
|
14073
|
+
function escapeSystemdEnvironmentValue(value) {
|
|
14074
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%");
|
|
14075
|
+
}
|
|
14076
|
+
function launchdPlist(options) {
|
|
14077
|
+
const env = Object.entries(options.env).map(([name, value]) => ` <key>${xmlEscape(name)}</key>
|
|
14078
|
+
<string>${xmlEscape(value)}</string>`).join(`
|
|
14079
|
+
`);
|
|
14080
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
14081
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
14082
|
+
<plist version="1.0">
|
|
14083
|
+
<dict>
|
|
14084
|
+
<key>Label</key>
|
|
14085
|
+
<string>${xmlEscape(options.serviceId)}</string>
|
|
14086
|
+
<key>ProgramArguments</key>
|
|
14087
|
+
<array>
|
|
14088
|
+
<string>${xmlEscape(options.executable)}</string>
|
|
14089
|
+
<string>--interval-ms</string>
|
|
14090
|
+
<string>${options.intervalMs}</string>
|
|
14091
|
+
</array>
|
|
14092
|
+
<key>EnvironmentVariables</key>
|
|
14093
|
+
<dict>
|
|
14094
|
+
${env}
|
|
14095
|
+
</dict>
|
|
14096
|
+
<key>KeepAlive</key>
|
|
14097
|
+
<true/>
|
|
14098
|
+
<key>RunAtLoad</key>
|
|
14099
|
+
<true/>
|
|
14100
|
+
<key>StandardOutPath</key>
|
|
14101
|
+
<string>${xmlEscape(launchdLogPath(options, "out"))}</string>
|
|
14102
|
+
<key>StandardErrorPath</key>
|
|
14103
|
+
<string>${xmlEscape(launchdLogPath(options, "err"))}</string>
|
|
14104
|
+
</dict>
|
|
14105
|
+
</plist>
|
|
14106
|
+
`;
|
|
14107
|
+
}
|
|
14108
|
+
function systemdUnit(options) {
|
|
14109
|
+
const env = Object.entries(options.env).map(([name, value]) => `Environment=${quoteSystemdEnvironment(name, value)}`).join(`
|
|
14110
|
+
`);
|
|
14111
|
+
return `[Unit]
|
|
14112
|
+
Description=Hasna machines agent
|
|
14113
|
+
After=network-online.target
|
|
14114
|
+
Wants=network-online.target
|
|
14115
|
+
|
|
14116
|
+
[Service]
|
|
14117
|
+
Type=simple
|
|
14118
|
+
ExecStart=${quoteSystemdExecArg(options.executable)} --interval-ms ${options.intervalMs}
|
|
14119
|
+
Restart=always
|
|
14120
|
+
RestartSec=10
|
|
14121
|
+
${env}
|
|
14122
|
+
|
|
14123
|
+
[Install]
|
|
14124
|
+
WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
|
|
14125
|
+
`;
|
|
14126
|
+
}
|
|
14127
|
+
function launchdDomain(options) {
|
|
14128
|
+
return options.mode === "system" ? "system" : "gui/$UID";
|
|
14129
|
+
}
|
|
14130
|
+
function launchdPlistPath(options) {
|
|
14131
|
+
if (options.mode === "system")
|
|
14132
|
+
return `/Library/LaunchDaemons/${options.serviceId}.plist`;
|
|
14133
|
+
return `$HOME/Library/LaunchAgents/${options.serviceId}.plist`;
|
|
14134
|
+
}
|
|
14135
|
+
function launchdLogPath(options, stream) {
|
|
14136
|
+
const fileName = `${options.serviceId}.${stream}.log`;
|
|
14137
|
+
if (options.mode === "system")
|
|
14138
|
+
return `/var/log/${fileName}`;
|
|
14139
|
+
return `$HOME/Library/Logs/${fileName}`;
|
|
14140
|
+
}
|
|
14141
|
+
function systemdUnitName(options) {
|
|
14142
|
+
return `${options.serviceId}.service`;
|
|
14143
|
+
}
|
|
14144
|
+
function systemdUnitPath(options) {
|
|
14145
|
+
if (options.mode === "system")
|
|
14146
|
+
return `/etc/systemd/system/${systemdUnitName(options)}`;
|
|
14147
|
+
return `$HOME/.config/systemd/user/${systemdUnitName(options)}`;
|
|
14148
|
+
}
|
|
14149
|
+
function xmlEscape(value) {
|
|
14150
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14151
|
+
}
|
|
14152
|
+
function quoteSystemdExecArg(value) {
|
|
14153
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value) && !value.includes("%"))
|
|
14154
|
+
return value;
|
|
14155
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
|
|
14156
|
+
}
|
|
14157
|
+
function quoteSystemdEnvironment(name, value) {
|
|
14158
|
+
return `"${name}=${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
|
|
14159
|
+
}
|
|
14160
|
+
function basename(path) {
|
|
14161
|
+
return path.split("/").filter(Boolean).at(-1) || path;
|
|
13223
14162
|
}
|
|
13224
14163
|
// src/commands/manifest.ts
|
|
13225
14164
|
function manifestInit() {
|
|
@@ -13465,7 +14404,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
13465
14404
|
};
|
|
13466
14405
|
}
|
|
13467
14406
|
// src/commands/notifications.ts
|
|
13468
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as
|
|
14407
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
13469
14408
|
var notificationChannelSchema = exports_external.object({
|
|
13470
14409
|
id: exports_external.string(),
|
|
13471
14410
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -13528,8 +14467,8 @@ ${message}
|
|
|
13528
14467
|
};
|
|
13529
14468
|
}
|
|
13530
14469
|
if (hasCommand2("mail")) {
|
|
13531
|
-
const
|
|
13532
|
-
const result = Bun.spawnSync(["bash", "-lc",
|
|
14470
|
+
const command2 = `printf %s ${shellQuote6(message)} | mail -s ${shellQuote6(subject)} ${shellQuote6(channel.target)}`;
|
|
14471
|
+
const result = Bun.spawnSync(["bash", "-lc", command2], {
|
|
13533
14472
|
stdout: "pipe",
|
|
13534
14473
|
stderr: "pipe",
|
|
13535
14474
|
env: process.env
|
|
@@ -13633,7 +14572,7 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
|
13633
14572
|
updatedAt: new Date().toISOString(),
|
|
13634
14573
|
channels: sortChannels(config.channels)
|
|
13635
14574
|
};
|
|
13636
|
-
|
|
14575
|
+
writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
|
|
13637
14576
|
`, "utf8");
|
|
13638
14577
|
return nextConfig;
|
|
13639
14578
|
}
|
|
@@ -13752,8 +14691,8 @@ function listPorts(machineId) {
|
|
|
13752
14691
|
const targetMachineId = machineId || getLocalMachineId();
|
|
13753
14692
|
const isLocal = targetMachineId === getLocalMachineId();
|
|
13754
14693
|
const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
|
|
13755
|
-
const
|
|
13756
|
-
const result = spawnSync3("bash", ["-lc",
|
|
14694
|
+
const command2 = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
|
|
14695
|
+
const result = spawnSync3("bash", ["-lc", command2], { encoding: "utf8" });
|
|
13757
14696
|
if (result.status !== 0) {
|
|
13758
14697
|
throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
|
|
13759
14698
|
}
|
|
@@ -13765,7 +14704,7 @@ function listPorts(machineId) {
|
|
|
13765
14704
|
}
|
|
13766
14705
|
// src/commands/runtime.ts
|
|
13767
14706
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13768
|
-
import { setTimeout as
|
|
14707
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
13769
14708
|
import { EventsClient } from "@hasna/events";
|
|
13770
14709
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
13771
14710
|
const checkedAt = new Date().toISOString();
|
|
@@ -13794,7 +14733,7 @@ async function watchTmuxPane(options) {
|
|
|
13794
14733
|
const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
|
|
13795
14734
|
const client = options.client ?? new EventsClient;
|
|
13796
14735
|
const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
|
|
13797
|
-
const wait = options.sleep ??
|
|
14736
|
+
const wait = options.sleep ?? sleep2;
|
|
13798
14737
|
let lastPresent;
|
|
13799
14738
|
let lastProbe;
|
|
13800
14739
|
for (let checks = 1;checks <= maxChecks; checks += 1) {
|
|
@@ -13855,6 +14794,16 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
13855
14794
|
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
13856
14795
|
|
|
13857
14796
|
// src/commands/status.ts
|
|
14797
|
+
function parseJsonObject2(value) {
|
|
14798
|
+
if (!value)
|
|
14799
|
+
return null;
|
|
14800
|
+
try {
|
|
14801
|
+
const parsed = JSON.parse(value);
|
|
14802
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
14803
|
+
} catch {
|
|
14804
|
+
return null;
|
|
14805
|
+
}
|
|
14806
|
+
}
|
|
13858
14807
|
function getStatus() {
|
|
13859
14808
|
const manifest = readManifest();
|
|
13860
14809
|
const heartbeats = listHeartbeats();
|
|
@@ -13878,7 +14827,12 @@ function getStatus() {
|
|
|
13878
14827
|
platform: declared?.platform,
|
|
13879
14828
|
manifestDeclared: Boolean(declared),
|
|
13880
14829
|
heartbeatStatus: heartbeat?.status || "unknown",
|
|
13881
|
-
lastHeartbeatAt: heartbeat?.updated_at
|
|
14830
|
+
lastHeartbeatAt: heartbeat?.updated_at,
|
|
14831
|
+
daemonVersion: heartbeat?.daemon_version ?? null,
|
|
14832
|
+
agentMode: heartbeat?.agent_mode ?? null,
|
|
14833
|
+
storageSyncStatus: heartbeat?.storage_sync_status ?? null,
|
|
14834
|
+
doctorSummary: parseJsonObject2(heartbeat?.doctor_summary_json),
|
|
14835
|
+
privateMetadata: Boolean(heartbeat?.private_metadata)
|
|
13882
14836
|
};
|
|
13883
14837
|
}),
|
|
13884
14838
|
recentSetupRuns: countRuns("setup_runs"),
|
|
@@ -13901,6 +14855,9 @@ function getServeInfo(options = {}) {
|
|
|
13901
14855
|
"/",
|
|
13902
14856
|
"/health",
|
|
13903
14857
|
"/api/status",
|
|
14858
|
+
"/api/topology",
|
|
14859
|
+
"/api/routes",
|
|
14860
|
+
"/api/daemon/status",
|
|
13904
14861
|
"/api/manifest",
|
|
13905
14862
|
"/api/notifications",
|
|
13906
14863
|
"/api/webhooks",
|
|
@@ -13918,6 +14875,7 @@ function getServeInfo(options = {}) {
|
|
|
13918
14875
|
}
|
|
13919
14876
|
function renderDashboardHtml() {
|
|
13920
14877
|
const status = getStatus();
|
|
14878
|
+
const topology = discoverMachineTopology();
|
|
13921
14879
|
const manifest = manifestList();
|
|
13922
14880
|
const notifications = listNotificationChannels();
|
|
13923
14881
|
const doctor = runDoctor();
|
|
@@ -13956,17 +14914,20 @@ function renderDashboardHtml() {
|
|
|
13956
14914
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
13957
14915
|
<section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
|
|
13958
14916
|
<section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
|
|
14917
|
+
<section class="card"><div>Tailscale routes</div><div class="stat">${topology.machines.filter((machine) => machine.ssh.route === "tailscale").length}</div></section>
|
|
13959
14918
|
</div>
|
|
13960
14919
|
|
|
13961
14920
|
<section class="card" style="margin-top:16px">
|
|
13962
14921
|
<h2>Machines</h2>
|
|
13963
14922
|
<table>
|
|
13964
|
-
<thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Last heartbeat</th></tr></thead>
|
|
14923
|
+
<thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Agent</th><th>Storage</th><th>Last heartbeat</th></tr></thead>
|
|
13965
14924
|
<tbody>
|
|
13966
14925
|
${status.machines.map((machine) => `<tr>
|
|
13967
14926
|
<td><code>${escapeHtml(machine.machineId)}</code></td>
|
|
13968
14927
|
<td>${escapeHtml(machine.platform || "unknown")}</td>
|
|
13969
14928
|
<td><span class="badge ${escapeHtml(machine.heartbeatStatus)}">${escapeHtml(machine.heartbeatStatus)}</span></td>
|
|
14929
|
+
<td>${escapeHtml(machine.agentMode || "unknown")} ${escapeHtml(machine.daemonVersion || "")}</td>
|
|
14930
|
+
<td>${escapeHtml(machine.storageSyncStatus || "unknown")}</td>
|
|
13970
14931
|
<td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
|
|
13971
14932
|
</tr>`).join("")}
|
|
13972
14933
|
</tbody>
|
|
@@ -14010,6 +14971,14 @@ function renderDashboardHtml() {
|
|
|
14010
14971
|
<script>
|
|
14011
14972
|
// Auto-refresh dashboard data every 15s
|
|
14012
14973
|
const REFRESH_INTERVAL = 15000;
|
|
14974
|
+
function escapeHtml(value) {
|
|
14975
|
+
return String(value ?? "")
|
|
14976
|
+
.replaceAll("&", "&")
|
|
14977
|
+
.replaceAll("<", "<")
|
|
14978
|
+
.replaceAll(">", ">")
|
|
14979
|
+
.replaceAll('"', """)
|
|
14980
|
+
.replaceAll("'", "'");
|
|
14981
|
+
}
|
|
14013
14982
|
async function refreshData() {
|
|
14014
14983
|
try {
|
|
14015
14984
|
const [statusRes, doctorRes] = await Promise.all([
|
|
@@ -14030,10 +14999,12 @@ function renderDashboardHtml() {
|
|
|
14030
14999
|
tbody.innerHTML = status.machines
|
|
14031
15000
|
.map((m) =>
|
|
14032
15001
|
"<tr>" +
|
|
14033
|
-
"<td><code>" + m.machineId + "</code></td>" +
|
|
14034
|
-
"<td>" + (m.platform || "unknown") + "</td>" +
|
|
14035
|
-
'<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
|
|
14036
|
-
"<td>" + (m.
|
|
15002
|
+
"<td><code>" + escapeHtml(m.machineId) + "</code></td>" +
|
|
15003
|
+
"<td>" + escapeHtml(m.platform || "unknown") + "</td>" +
|
|
15004
|
+
'<td><span class="badge ' + escapeHtml(m.heartbeatStatus) + '">' + escapeHtml(m.heartbeatStatus) + '</span></td>' +
|
|
15005
|
+
"<td>" + escapeHtml(m.agentMode || "unknown") + " " + escapeHtml(m.daemonVersion || "") + "</td>" +
|
|
15006
|
+
"<td>" + escapeHtml(m.storageSyncStatus || "unknown") + "</td>" +
|
|
15007
|
+
"<td>" + escapeHtml(m.lastHeartbeatAt || "\\u2014") + "</td>" +
|
|
14037
15008
|
"</tr>"
|
|
14038
15009
|
)
|
|
14039
15010
|
.join("");
|
|
@@ -14045,9 +15016,9 @@ function renderDashboardHtml() {
|
|
|
14045
15016
|
doctorTbody.innerHTML = doctor.checks
|
|
14046
15017
|
.map((c) =>
|
|
14047
15018
|
"<tr>" +
|
|
14048
|
-
"<td>" + c.summary + "</td>" +
|
|
14049
|
-
'<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
|
|
14050
|
-
'<td class="muted">' + c.detail + "</td>" +
|
|
15019
|
+
"<td>" + escapeHtml(c.summary) + "</td>" +
|
|
15020
|
+
'<td><span class="badge ' + escapeHtml(c.status) + '">' + escapeHtml(c.status) + '</span></td>' +
|
|
15021
|
+
'<td class="muted">' + escapeHtml(c.detail) + "</td>" +
|
|
14051
15022
|
"</tr>"
|
|
14052
15023
|
)
|
|
14053
15024
|
.join("");
|
|
@@ -14077,6 +15048,14 @@ async function parseJsonBody(request) {
|
|
|
14077
15048
|
function jsonError(message, status = 400) {
|
|
14078
15049
|
return Response.json({ error: message }, { status });
|
|
14079
15050
|
}
|
|
15051
|
+
function privateOutputWarnings(requested, allowed) {
|
|
15052
|
+
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
15053
|
+
}
|
|
15054
|
+
function appendWarnings(payload, warnings) {
|
|
15055
|
+
if (warnings.length === 0)
|
|
15056
|
+
return payload;
|
|
15057
|
+
return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
|
|
15058
|
+
}
|
|
14080
15059
|
function startDashboardServer(options = {}) {
|
|
14081
15060
|
const info = getServeInfo(options);
|
|
14082
15061
|
const events = new EventsClient2;
|
|
@@ -14087,12 +15066,34 @@ function startDashboardServer(options = {}) {
|
|
|
14087
15066
|
const url = new URL(request.url);
|
|
14088
15067
|
const machineId = url.searchParams.get("machine") || undefined;
|
|
14089
15068
|
const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
|
|
15069
|
+
const privateMetadataRequested = url.searchParams.get("privateMetadata") === "true" || url.searchParams.get("private_metadata") === "true";
|
|
15070
|
+
const privateMetadata = privateMetadataRequested && isPrivateOutputEnabled();
|
|
15071
|
+
const privateWarnings = privateOutputWarnings(privateMetadataRequested, privateMetadata);
|
|
14090
15072
|
if (url.pathname === "/health") {
|
|
14091
15073
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
14092
15074
|
}
|
|
14093
15075
|
if (url.pathname === "/api/status") {
|
|
14094
15076
|
return Response.json(getStatus());
|
|
14095
15077
|
}
|
|
15078
|
+
if (url.pathname === "/api/topology") {
|
|
15079
|
+
const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
|
|
15080
|
+
return Response.json(appendWarnings(redactTopologyForOutput(topology, { privateMetadata }), privateWarnings));
|
|
15081
|
+
}
|
|
15082
|
+
if (url.pathname === "/api/routes") {
|
|
15083
|
+
const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
|
|
15084
|
+
return Response.json({
|
|
15085
|
+
generated_at: topology.generated_at,
|
|
15086
|
+
routes: topology.machines.map((machine) => redactRouteForOutput(resolveMachineRoute(machine.machine_id, { topology }), { privateMetadata })),
|
|
15087
|
+
...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
|
|
15088
|
+
});
|
|
15089
|
+
}
|
|
15090
|
+
if (url.pathname === "/api/daemon/status") {
|
|
15091
|
+
return Response.json({
|
|
15092
|
+
generated_at: new Date().toISOString(),
|
|
15093
|
+
agents: getAgentStatus(machineId, { privateMetadata }),
|
|
15094
|
+
...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
|
|
15095
|
+
});
|
|
15096
|
+
}
|
|
14096
15097
|
if (url.pathname === "/api/manifest") {
|
|
14097
15098
|
return Response.json(manifestList());
|
|
14098
15099
|
}
|
|
@@ -14305,18 +15306,18 @@ function buildBaseSteps(machine) {
|
|
|
14305
15306
|
function buildPackageSteps(machine) {
|
|
14306
15307
|
return (machine.packages || []).map((pkg, index) => {
|
|
14307
15308
|
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
14308
|
-
let
|
|
15309
|
+
let command2 = pkg.name;
|
|
14309
15310
|
if (manager === "bun") {
|
|
14310
|
-
|
|
15311
|
+
command2 = `bun install -g ${quote3(pkg.name)}`;
|
|
14311
15312
|
} else if (manager === "brew") {
|
|
14312
|
-
|
|
15313
|
+
command2 = `brew install ${quote3(pkg.name)}`;
|
|
14313
15314
|
} else if (manager === "apt") {
|
|
14314
|
-
|
|
15315
|
+
command2 = `sudo apt-get install -y ${quote3(pkg.name)}`;
|
|
14315
15316
|
}
|
|
14316
15317
|
return {
|
|
14317
15318
|
id: `package-${index + 1}`,
|
|
14318
15319
|
title: `Install package ${pkg.name}`,
|
|
14319
|
-
command,
|
|
15320
|
+
command: command2,
|
|
14320
15321
|
manager,
|
|
14321
15322
|
privileged: manager === "apt"
|
|
14322
15323
|
};
|
|
@@ -14381,8 +15382,8 @@ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
|
14381
15382
|
function shellQuote7(value) {
|
|
14382
15383
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14383
15384
|
}
|
|
14384
|
-
function shellCommand2(
|
|
14385
|
-
return
|
|
15385
|
+
function shellCommand2(command2) {
|
|
15386
|
+
return command2.map(shellQuote7).join(" ");
|
|
14386
15387
|
}
|
|
14387
15388
|
function metadataString2(metadata, keys) {
|
|
14388
15389
|
if (!metadata)
|
|
@@ -14564,11 +15565,11 @@ function detectFileActions(machine) {
|
|
|
14564
15565
|
status = source === target ? "ok" : "drifted";
|
|
14565
15566
|
}
|
|
14566
15567
|
}
|
|
14567
|
-
const
|
|
15568
|
+
const command2 = file.mode === "symlink" ? `ln -sfn ${quote4(file.source)} ${quote4(file.target)}` : `cp ${quote4(file.source)} ${quote4(file.target)}`;
|
|
14568
15569
|
return {
|
|
14569
15570
|
id: `file-${index + 1}`,
|
|
14570
15571
|
title: `${status === "ok" ? "File in sync" : "Reconcile file"} ${file.target}`,
|
|
14571
|
-
command,
|
|
15572
|
+
command: command2,
|
|
14572
15573
|
status,
|
|
14573
15574
|
kind: "file"
|
|
14574
15575
|
};
|
|
@@ -14597,18 +15598,18 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
14597
15598
|
executed: 0
|
|
14598
15599
|
};
|
|
14599
15600
|
}
|
|
14600
|
-
function applyFileAction(
|
|
14601
|
-
const [verb, source, target] =
|
|
15601
|
+
function applyFileAction(command2) {
|
|
15602
|
+
const [verb, source, target] = command2.split(" ");
|
|
14602
15603
|
if (verb === "cp" && source && target) {
|
|
14603
15604
|
ensureParentDir(target);
|
|
14604
15605
|
copyFileSync(source.slice(1, -1), target.slice(1, -1));
|
|
14605
15606
|
return;
|
|
14606
15607
|
}
|
|
14607
15608
|
if (verb === "ln" && source && target) {
|
|
14608
|
-
const sourcePath =
|
|
14609
|
-
const targetPath =
|
|
15609
|
+
const sourcePath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
|
|
15610
|
+
const targetPath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
|
|
14610
15611
|
if (!sourcePath || !targetPath) {
|
|
14611
|
-
throw new Error(`Unable to parse symlink command: ${
|
|
15612
|
+
throw new Error(`Unable to parse symlink command: ${command2}`);
|
|
14612
15613
|
}
|
|
14613
15614
|
ensureParentDir(targetPath);
|
|
14614
15615
|
try {
|
|
@@ -15531,7 +16532,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
|
|
|
15531
16532
|
var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
|
|
15532
16533
|
var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
|
|
15533
16534
|
var base64url = /^[A-Za-z0-9_-]*$/;
|
|
15534
|
-
var
|
|
16535
|
+
var hostname7 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
|
|
15535
16536
|
var e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
|
|
15536
16537
|
var dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`;
|
|
15537
16538
|
var date = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
|
|
@@ -16143,7 +17144,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
16143
17144
|
code: "invalid_format",
|
|
16144
17145
|
format: "url",
|
|
16145
17146
|
note: "Invalid hostname",
|
|
16146
|
-
pattern:
|
|
17147
|
+
pattern: hostname7.source,
|
|
16147
17148
|
input: payload.value,
|
|
16148
17149
|
inst,
|
|
16149
17150
|
continue: !def.abort
|
|
@@ -23731,6 +24732,8 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
23731
24732
|
"machines_manifest_get",
|
|
23732
24733
|
"machines_manifest_remove",
|
|
23733
24734
|
"machines_agent_status",
|
|
24735
|
+
"machines_daemon_status",
|
|
24736
|
+
"machines_daemon_service_plan",
|
|
23734
24737
|
"machines_setup_preview",
|
|
23735
24738
|
"machines_setup_apply",
|
|
23736
24739
|
"machines_sync_preview",
|
|
@@ -23777,6 +24780,17 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
23777
24780
|
function buildServer(version2 = getPackageVersion()) {
|
|
23778
24781
|
return createMcpServer(version2);
|
|
23779
24782
|
}
|
|
24783
|
+
function privateMetadataAllowed(requested) {
|
|
24784
|
+
return requested === true && isPrivateOutputEnabled();
|
|
24785
|
+
}
|
|
24786
|
+
function privateOutputWarnings2(requested, allowed) {
|
|
24787
|
+
return requested === true && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
24788
|
+
}
|
|
24789
|
+
function appendWarnings2(payload, warnings) {
|
|
24790
|
+
if (warnings.length === 0)
|
|
24791
|
+
return payload;
|
|
24792
|
+
return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
|
|
24793
|
+
}
|
|
23780
24794
|
function createMcpServer(version2) {
|
|
23781
24795
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23782
24796
|
const events = new EventsClient3;
|
|
@@ -23803,16 +24817,69 @@ function createMcpServer(version2) {
|
|
|
23803
24817
|
}));
|
|
23804
24818
|
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) }] }));
|
|
23805
24819
|
server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] }));
|
|
23806
|
-
server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () =>
|
|
23807
|
-
|
|
24820
|
+
server.tool("machines_agent_status", "List current machine agent heartbeats.", { private_metadata: exports_external.boolean().optional().describe("Include private heartbeat metadata") }, async ({ private_metadata }) => {
|
|
24821
|
+
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24822
|
+
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
24823
|
+
const agents = getAgentStatus(undefined, { privateMetadata });
|
|
24824
|
+
return {
|
|
24825
|
+
content: [{ type: "text", text: JSON.stringify(warnings.length > 0 ? { agents, warnings } : agents, null, 2) }]
|
|
24826
|
+
};
|
|
24827
|
+
});
|
|
24828
|
+
server.tool("machines_daemon_status", "List fleet daemon heartbeat status rows.", { private_metadata: exports_external.boolean().optional().describe("Include private heartbeat metadata") }, async ({ private_metadata }) => {
|
|
24829
|
+
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24830
|
+
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
24831
|
+
return {
|
|
24832
|
+
content: [{
|
|
24833
|
+
type: "text",
|
|
24834
|
+
text: JSON.stringify({
|
|
24835
|
+
generated_at: new Date().toISOString(),
|
|
24836
|
+
agents: getAgentStatus(undefined, { privateMetadata }),
|
|
24837
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
24838
|
+
}, null, 2)
|
|
24839
|
+
}]
|
|
24840
|
+
};
|
|
24841
|
+
});
|
|
24842
|
+
server.tool("machines_daemon_service_plan", "Plan launchd/systemd lifecycle commands for the machines-agent daemon.", {
|
|
24843
|
+
action: exports_external.enum(["install", "uninstall", "restart", "status", "logs"]).describe("Daemon lifecycle action"),
|
|
24844
|
+
platform: exports_external.enum(["macos", "linux"]).optional().describe("Target service platform"),
|
|
24845
|
+
mode: exports_external.enum(["user", "system"]).optional().describe("Service mode"),
|
|
24846
|
+
service_name: exports_external.string().optional().describe("Service name/label"),
|
|
24847
|
+
executable: exports_external.string().optional().describe("machines-agent executable path"),
|
|
24848
|
+
interval_ms: exports_external.number().optional().describe("Heartbeat interval in milliseconds"),
|
|
24849
|
+
storage_push: exports_external.boolean().optional().describe("Configure heartbeat storage push"),
|
|
24850
|
+
doctor_summary: exports_external.boolean().optional().describe("Configure lightweight doctor summaries in heartbeat metadata"),
|
|
24851
|
+
private_metadata: exports_external.boolean().optional().describe("Opt in to private heartbeat metadata"),
|
|
24852
|
+
env: exports_external.array(exports_external.string()).optional().describe("Environment variable names to include as placeholders")
|
|
24853
|
+
}, async ({ action, platform: platform5, mode, service_name, executable, interval_ms, storage_push, doctor_summary, private_metadata, env }) => ({
|
|
24854
|
+
content: [{
|
|
24855
|
+
type: "text",
|
|
24856
|
+
text: JSON.stringify(buildDaemonServicePlan({
|
|
24857
|
+
action,
|
|
24858
|
+
platform: platform5,
|
|
24859
|
+
mode,
|
|
24860
|
+
serviceName: service_name,
|
|
24861
|
+
executable,
|
|
24862
|
+
intervalMs: interval_ms,
|
|
24863
|
+
storagePush: storage_push,
|
|
24864
|
+
doctorSummary: doctor_summary,
|
|
24865
|
+
privateMetadata: private_metadata,
|
|
24866
|
+
env
|
|
24867
|
+
}), null, 2)
|
|
24868
|
+
}]
|
|
23808
24869
|
}));
|
|
23809
24870
|
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) }] }));
|
|
23810
24871
|
server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
23811
24872
|
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) }] }));
|
|
23812
24873
|
server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
23813
|
-
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
|
|
23814
|
-
|
|
23815
|
-
|
|
24874
|
+
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
|
|
24875
|
+
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
24876
|
+
private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
|
|
24877
|
+
}, async ({ include_tailscale, private_metadata }) => {
|
|
24878
|
+
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24879
|
+
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
24880
|
+
const topology = redactTopologyForOutput(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), { privateMetadata });
|
|
24881
|
+
return { content: [{ type: "text", text: JSON.stringify(appendWarnings2(topology, warnings), null, 2) }] };
|
|
24882
|
+
});
|
|
23816
24883
|
server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
|
|
23817
24884
|
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
23818
24885
|
commands: exports_external.array(exports_external.object({
|
|
@@ -23864,9 +24931,16 @@ function createMcpServer(version2) {
|
|
|
23864
24931
|
}));
|
|
23865
24932
|
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) }] }));
|
|
23866
24933
|
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
23867
|
-
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
|
|
23868
|
-
|
|
23869
|
-
|
|
24934
|
+
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
|
|
24935
|
+
machine_id: exports_external.string().describe("Machine identifier"),
|
|
24936
|
+
include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
|
|
24937
|
+
private_metadata: exports_external.boolean().optional().describe("Include private route targets")
|
|
24938
|
+
}, async ({ machine_id, include_tailscale, private_metadata }) => {
|
|
24939
|
+
const privateMetadata = privateMetadataAllowed(private_metadata);
|
|
24940
|
+
const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
|
|
24941
|
+
const route = redactRouteForOutput(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), { privateMetadata });
|
|
24942
|
+
return { content: [{ type: "text", text: JSON.stringify(appendWarnings2(route, warnings), null, 2) }] };
|
|
24943
|
+
});
|
|
23870
24944
|
server.tool("machines_workspace_resolve", "Resolve sync-safe repo and open-files roots for an open-* consumer.", {
|
|
23871
24945
|
machine_id: exports_external.string().describe("Machine identifier"),
|
|
23872
24946
|
project_id: exports_external.string().describe("Canonical project id"),
|
|
@@ -24002,6 +25076,7 @@ function createMcpServer(version2) {
|
|
|
24002
25076
|
export {
|
|
24003
25077
|
writeNotificationConfig,
|
|
24004
25078
|
writeManifest,
|
|
25079
|
+
writeHeartbeatTick,
|
|
24005
25080
|
writeHeartbeat,
|
|
24006
25081
|
watchTmuxPane,
|
|
24007
25082
|
validateManifest,
|
|
@@ -24012,12 +25087,14 @@ export {
|
|
|
24012
25087
|
storagePull,
|
|
24013
25088
|
startDashboardServer,
|
|
24014
25089
|
setHeartbeatStatus,
|
|
25090
|
+
sanitizePublicString,
|
|
24015
25091
|
runTailscaleInstall,
|
|
24016
25092
|
runSync,
|
|
24017
25093
|
runStorageMigrations,
|
|
24018
25094
|
runSetup,
|
|
24019
25095
|
runSelfTest,
|
|
24020
25096
|
runDoctor,
|
|
25097
|
+
runDaemonServicePlan,
|
|
24021
25098
|
runClaudeInstall,
|
|
24022
25099
|
runCertPlan,
|
|
24023
25100
|
runBackup,
|
|
@@ -24030,15 +25107,21 @@ export {
|
|
|
24030
25107
|
resolveMachineRoute,
|
|
24031
25108
|
resolveBackupTarget,
|
|
24032
25109
|
repairWorkspaceManifestMappings,
|
|
25110
|
+
renderSystemdUnit,
|
|
25111
|
+
renderLaunchdPlist,
|
|
24033
25112
|
renderDomainMapping,
|
|
24034
25113
|
renderDashboardHtml,
|
|
24035
25114
|
removeNotificationChannel,
|
|
25115
|
+
redactTopologyForOutput,
|
|
24036
25116
|
redactSensitiveValue,
|
|
25117
|
+
redactRouteForOutput,
|
|
24037
25118
|
redactPrivateRef,
|
|
24038
25119
|
redactPath,
|
|
25120
|
+
redactNetworkValue,
|
|
24039
25121
|
redactMetadata,
|
|
24040
25122
|
redactManifestForDiagnostics,
|
|
24041
25123
|
redactIdentifier,
|
|
25124
|
+
redactErrorMessage,
|
|
24042
25125
|
recordSyncRun,
|
|
24043
25126
|
recordSetupRun,
|
|
24044
25127
|
readNotificationConfig,
|
|
@@ -24063,6 +25146,8 @@ export {
|
|
|
24063
25146
|
listDomainMappings,
|
|
24064
25147
|
listApps,
|
|
24065
25148
|
isSensitiveKey,
|
|
25149
|
+
isPrivateOutputEnabled,
|
|
25150
|
+
isPrivateMetadataEnabled,
|
|
24066
25151
|
getSyncMetaAll,
|
|
24067
25152
|
getStorageStatus,
|
|
24068
25153
|
getStoragePg,
|
|
@@ -24115,6 +25200,12 @@ export {
|
|
|
24115
25200
|
buildScreenEnableRemoteCommand,
|
|
24116
25201
|
buildScreenEnableCommand,
|
|
24117
25202
|
buildScreenCommand,
|
|
25203
|
+
buildDaemonUninstallPlan,
|
|
25204
|
+
buildDaemonStatusPlan,
|
|
25205
|
+
buildDaemonServicePlan,
|
|
25206
|
+
buildDaemonRestartPlan,
|
|
25207
|
+
buildDaemonLogsPlan,
|
|
25208
|
+
buildDaemonInstallPlan,
|
|
24118
25209
|
buildClaudeInstallPlan,
|
|
24119
25210
|
buildCertPlan,
|
|
24120
25211
|
buildBackupPlan,
|
|
@@ -24127,6 +25218,11 @@ export {
|
|
|
24127
25218
|
STORAGE_DATABASE_ENV,
|
|
24128
25219
|
SCREEN_SECRET_NAMESPACE_ENV,
|
|
24129
25220
|
REDACTED_VALUE,
|
|
25221
|
+
PRIVATE_OUTPUT_FALLBACK_ENV,
|
|
25222
|
+
PRIVATE_OUTPUT_ENV,
|
|
25223
|
+
PRIVATE_OUTPUT_DENIED_WARNING,
|
|
25224
|
+
PRIVATE_METADATA_FALLBACK_ENV,
|
|
25225
|
+
PRIVATE_METADATA_ENV,
|
|
24130
25226
|
PRIVATE_MANIFEST_REF_ENV,
|
|
24131
25227
|
PRIVATE_MANIFEST_BACKEND_ENV,
|
|
24132
25228
|
MACHINE_MCP_TOOL_NAMES,
|