@hasna/machines 0.0.36 → 0.0.38
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 +886 -58
- package/dist/commands/daemon.d.ts +76 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/consumer.js +225 -6
- 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 +1330 -228
- package/dist/manifests.d.ts +6 -6
- package/dist/mcp/index.js +1755 -1083
- 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);
|
|
@@ -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
|
+
});
|
|
12970
|
+
}
|
|
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
|
+
});
|
|
12664
12979
|
}
|
|
12665
|
-
function
|
|
12666
|
-
|
|
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,183 +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)] : [];
|
|
13733
|
+
return {
|
|
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
|
+
}
|
|
13044
13879
|
return {
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
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
|
-
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
|
|
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;
|
|
13217
14162
|
}
|
|
13218
14163
|
// src/commands/manifest.ts
|
|
13219
14164
|
function manifestInit() {
|
|
@@ -13459,7 +14404,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
13459
14404
|
};
|
|
13460
14405
|
}
|
|
13461
14406
|
// src/commands/notifications.ts
|
|
13462
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as
|
|
14407
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
13463
14408
|
var notificationChannelSchema = exports_external.object({
|
|
13464
14409
|
id: exports_external.string(),
|
|
13465
14410
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -13522,8 +14467,8 @@ ${message}
|
|
|
13522
14467
|
};
|
|
13523
14468
|
}
|
|
13524
14469
|
if (hasCommand2("mail")) {
|
|
13525
|
-
const
|
|
13526
|
-
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], {
|
|
13527
14472
|
stdout: "pipe",
|
|
13528
14473
|
stderr: "pipe",
|
|
13529
14474
|
env: process.env
|
|
@@ -13627,7 +14572,7 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
|
13627
14572
|
updatedAt: new Date().toISOString(),
|
|
13628
14573
|
channels: sortChannels(config.channels)
|
|
13629
14574
|
};
|
|
13630
|
-
|
|
14575
|
+
writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
|
|
13631
14576
|
`, "utf8");
|
|
13632
14577
|
return nextConfig;
|
|
13633
14578
|
}
|
|
@@ -13746,8 +14691,8 @@ function listPorts(machineId) {
|
|
|
13746
14691
|
const targetMachineId = machineId || getLocalMachineId();
|
|
13747
14692
|
const isLocal = targetMachineId === getLocalMachineId();
|
|
13748
14693
|
const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
|
|
13749
|
-
const
|
|
13750
|
-
const result = spawnSync3("bash", ["-lc",
|
|
14694
|
+
const command2 = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
|
|
14695
|
+
const result = spawnSync3("bash", ["-lc", command2], { encoding: "utf8" });
|
|
13751
14696
|
if (result.status !== 0) {
|
|
13752
14697
|
throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
|
|
13753
14698
|
}
|
|
@@ -13759,7 +14704,7 @@ function listPorts(machineId) {
|
|
|
13759
14704
|
}
|
|
13760
14705
|
// src/commands/runtime.ts
|
|
13761
14706
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13762
|
-
import { setTimeout as
|
|
14707
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
13763
14708
|
import { EventsClient } from "@hasna/events";
|
|
13764
14709
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
13765
14710
|
const checkedAt = new Date().toISOString();
|
|
@@ -13788,7 +14733,7 @@ async function watchTmuxPane(options) {
|
|
|
13788
14733
|
const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
|
|
13789
14734
|
const client = options.client ?? new EventsClient;
|
|
13790
14735
|
const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
|
|
13791
|
-
const wait = options.sleep ??
|
|
14736
|
+
const wait = options.sleep ?? sleep2;
|
|
13792
14737
|
let lastPresent;
|
|
13793
14738
|
let lastProbe;
|
|
13794
14739
|
for (let checks = 1;checks <= maxChecks; checks += 1) {
|
|
@@ -13849,6 +14794,16 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
13849
14794
|
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
13850
14795
|
|
|
13851
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
|
+
}
|
|
13852
14807
|
function getStatus() {
|
|
13853
14808
|
const manifest = readManifest();
|
|
13854
14809
|
const heartbeats = listHeartbeats();
|
|
@@ -13872,7 +14827,12 @@ function getStatus() {
|
|
|
13872
14827
|
platform: declared?.platform,
|
|
13873
14828
|
manifestDeclared: Boolean(declared),
|
|
13874
14829
|
heartbeatStatus: heartbeat?.status || "unknown",
|
|
13875
|
-
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)
|
|
13876
14836
|
};
|
|
13877
14837
|
}),
|
|
13878
14838
|
recentSetupRuns: countRuns("setup_runs"),
|
|
@@ -13895,6 +14855,9 @@ function getServeInfo(options = {}) {
|
|
|
13895
14855
|
"/",
|
|
13896
14856
|
"/health",
|
|
13897
14857
|
"/api/status",
|
|
14858
|
+
"/api/topology",
|
|
14859
|
+
"/api/routes",
|
|
14860
|
+
"/api/daemon/status",
|
|
13898
14861
|
"/api/manifest",
|
|
13899
14862
|
"/api/notifications",
|
|
13900
14863
|
"/api/webhooks",
|
|
@@ -13912,6 +14875,7 @@ function getServeInfo(options = {}) {
|
|
|
13912
14875
|
}
|
|
13913
14876
|
function renderDashboardHtml() {
|
|
13914
14877
|
const status = getStatus();
|
|
14878
|
+
const topology = discoverMachineTopology();
|
|
13915
14879
|
const manifest = manifestList();
|
|
13916
14880
|
const notifications = listNotificationChannels();
|
|
13917
14881
|
const doctor = runDoctor();
|
|
@@ -13950,17 +14914,20 @@ function renderDashboardHtml() {
|
|
|
13950
14914
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
13951
14915
|
<section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
|
|
13952
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>
|
|
13953
14918
|
</div>
|
|
13954
14919
|
|
|
13955
14920
|
<section class="card" style="margin-top:16px">
|
|
13956
14921
|
<h2>Machines</h2>
|
|
13957
14922
|
<table>
|
|
13958
|
-
<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>
|
|
13959
14924
|
<tbody>
|
|
13960
14925
|
${status.machines.map((machine) => `<tr>
|
|
13961
14926
|
<td><code>${escapeHtml(machine.machineId)}</code></td>
|
|
13962
14927
|
<td>${escapeHtml(machine.platform || "unknown")}</td>
|
|
13963
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>
|
|
13964
14931
|
<td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
|
|
13965
14932
|
</tr>`).join("")}
|
|
13966
14933
|
</tbody>
|
|
@@ -14004,6 +14971,14 @@ function renderDashboardHtml() {
|
|
|
14004
14971
|
<script>
|
|
14005
14972
|
// Auto-refresh dashboard data every 15s
|
|
14006
14973
|
const REFRESH_INTERVAL = 15000;
|
|
14974
|
+
function escapeHtml(value) {
|
|
14975
|
+
return String(value ?? "")
|
|
14976
|
+
.replaceAll("&", "&")
|
|
14977
|
+
.replaceAll("<", "<")
|
|
14978
|
+
.replaceAll(">", ">")
|
|
14979
|
+
.replaceAll('"', """)
|
|
14980
|
+
.replaceAll("'", "'");
|
|
14981
|
+
}
|
|
14007
14982
|
async function refreshData() {
|
|
14008
14983
|
try {
|
|
14009
14984
|
const [statusRes, doctorRes] = await Promise.all([
|
|
@@ -14024,10 +14999,12 @@ function renderDashboardHtml() {
|
|
|
14024
14999
|
tbody.innerHTML = status.machines
|
|
14025
15000
|
.map((m) =>
|
|
14026
15001
|
"<tr>" +
|
|
14027
|
-
"<td><code>" + m.machineId + "</code></td>" +
|
|
14028
|
-
"<td>" + (m.platform || "unknown") + "</td>" +
|
|
14029
|
-
'<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
|
|
14030
|
-
"<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>" +
|
|
14031
15008
|
"</tr>"
|
|
14032
15009
|
)
|
|
14033
15010
|
.join("");
|
|
@@ -14039,9 +15016,9 @@ function renderDashboardHtml() {
|
|
|
14039
15016
|
doctorTbody.innerHTML = doctor.checks
|
|
14040
15017
|
.map((c) =>
|
|
14041
15018
|
"<tr>" +
|
|
14042
|
-
"<td>" + c.summary + "</td>" +
|
|
14043
|
-
'<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
|
|
14044
|
-
'<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>" +
|
|
14045
15022
|
"</tr>"
|
|
14046
15023
|
)
|
|
14047
15024
|
.join("");
|
|
@@ -14071,6 +15048,14 @@ async function parseJsonBody(request) {
|
|
|
14071
15048
|
function jsonError(message, status = 400) {
|
|
14072
15049
|
return Response.json({ error: message }, { status });
|
|
14073
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
|
+
}
|
|
14074
15059
|
function startDashboardServer(options = {}) {
|
|
14075
15060
|
const info = getServeInfo(options);
|
|
14076
15061
|
const events = new EventsClient2;
|
|
@@ -14081,12 +15066,34 @@ function startDashboardServer(options = {}) {
|
|
|
14081
15066
|
const url = new URL(request.url);
|
|
14082
15067
|
const machineId = url.searchParams.get("machine") || undefined;
|
|
14083
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);
|
|
14084
15072
|
if (url.pathname === "/health") {
|
|
14085
15073
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
14086
15074
|
}
|
|
14087
15075
|
if (url.pathname === "/api/status") {
|
|
14088
15076
|
return Response.json(getStatus());
|
|
14089
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
|
+
}
|
|
14090
15097
|
if (url.pathname === "/api/manifest") {
|
|
14091
15098
|
return Response.json(manifestList());
|
|
14092
15099
|
}
|
|
@@ -14299,18 +15306,18 @@ function buildBaseSteps(machine) {
|
|
|
14299
15306
|
function buildPackageSteps(machine) {
|
|
14300
15307
|
return (machine.packages || []).map((pkg, index) => {
|
|
14301
15308
|
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
14302
|
-
let
|
|
15309
|
+
let command2 = pkg.name;
|
|
14303
15310
|
if (manager === "bun") {
|
|
14304
|
-
|
|
15311
|
+
command2 = `bun install -g ${quote3(pkg.name)}`;
|
|
14305
15312
|
} else if (manager === "brew") {
|
|
14306
|
-
|
|
15313
|
+
command2 = `brew install ${quote3(pkg.name)}`;
|
|
14307
15314
|
} else if (manager === "apt") {
|
|
14308
|
-
|
|
15315
|
+
command2 = `sudo apt-get install -y ${quote3(pkg.name)}`;
|
|
14309
15316
|
}
|
|
14310
15317
|
return {
|
|
14311
15318
|
id: `package-${index + 1}`,
|
|
14312
15319
|
title: `Install package ${pkg.name}`,
|
|
14313
|
-
command,
|
|
15320
|
+
command: command2,
|
|
14314
15321
|
manager,
|
|
14315
15322
|
privileged: manager === "apt"
|
|
14316
15323
|
};
|
|
@@ -14375,8 +15382,8 @@ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
|
14375
15382
|
function shellQuote7(value) {
|
|
14376
15383
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14377
15384
|
}
|
|
14378
|
-
function shellCommand2(
|
|
14379
|
-
return
|
|
15385
|
+
function shellCommand2(command2) {
|
|
15386
|
+
return command2.map(shellQuote7).join(" ");
|
|
14380
15387
|
}
|
|
14381
15388
|
function metadataString2(metadata, keys) {
|
|
14382
15389
|
if (!metadata)
|
|
@@ -14558,11 +15565,11 @@ function detectFileActions(machine) {
|
|
|
14558
15565
|
status = source === target ? "ok" : "drifted";
|
|
14559
15566
|
}
|
|
14560
15567
|
}
|
|
14561
|
-
const
|
|
15568
|
+
const command2 = file.mode === "symlink" ? `ln -sfn ${quote4(file.source)} ${quote4(file.target)}` : `cp ${quote4(file.source)} ${quote4(file.target)}`;
|
|
14562
15569
|
return {
|
|
14563
15570
|
id: `file-${index + 1}`,
|
|
14564
15571
|
title: `${status === "ok" ? "File in sync" : "Reconcile file"} ${file.target}`,
|
|
14565
|
-
command,
|
|
15572
|
+
command: command2,
|
|
14566
15573
|
status,
|
|
14567
15574
|
kind: "file"
|
|
14568
15575
|
};
|
|
@@ -14591,18 +15598,18 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
14591
15598
|
executed: 0
|
|
14592
15599
|
};
|
|
14593
15600
|
}
|
|
14594
|
-
function applyFileAction(
|
|
14595
|
-
const [verb, source, target] =
|
|
15601
|
+
function applyFileAction(command2) {
|
|
15602
|
+
const [verb, source, target] = command2.split(" ");
|
|
14596
15603
|
if (verb === "cp" && source && target) {
|
|
14597
15604
|
ensureParentDir(target);
|
|
14598
15605
|
copyFileSync(source.slice(1, -1), target.slice(1, -1));
|
|
14599
15606
|
return;
|
|
14600
15607
|
}
|
|
14601
15608
|
if (verb === "ln" && source && target) {
|
|
14602
|
-
const sourcePath =
|
|
14603
|
-
const targetPath =
|
|
15609
|
+
const sourcePath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
|
|
15610
|
+
const targetPath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
|
|
14604
15611
|
if (!sourcePath || !targetPath) {
|
|
14605
|
-
throw new Error(`Unable to parse symlink command: ${
|
|
15612
|
+
throw new Error(`Unable to parse symlink command: ${command2}`);
|
|
14606
15613
|
}
|
|
14607
15614
|
ensureParentDir(targetPath);
|
|
14608
15615
|
try {
|
|
@@ -15525,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]
|
|
|
15525
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])$/;
|
|
15526
16533
|
var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
|
|
15527
16534
|
var base64url = /^[A-Za-z0-9_-]*$/;
|
|
15528
|
-
var
|
|
16535
|
+
var hostname7 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
|
|
15529
16536
|
var e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
|
|
15530
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])))`;
|
|
15531
16538
|
var date = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
|
|
@@ -16137,7 +17144,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
16137
17144
|
code: "invalid_format",
|
|
16138
17145
|
format: "url",
|
|
16139
17146
|
note: "Invalid hostname",
|
|
16140
|
-
pattern:
|
|
17147
|
+
pattern: hostname7.source,
|
|
16141
17148
|
input: payload.value,
|
|
16142
17149
|
inst,
|
|
16143
17150
|
continue: !def.abort
|
|
@@ -23725,6 +24732,8 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
23725
24732
|
"machines_manifest_get",
|
|
23726
24733
|
"machines_manifest_remove",
|
|
23727
24734
|
"machines_agent_status",
|
|
24735
|
+
"machines_daemon_status",
|
|
24736
|
+
"machines_daemon_service_plan",
|
|
23728
24737
|
"machines_setup_preview",
|
|
23729
24738
|
"machines_setup_apply",
|
|
23730
24739
|
"machines_sync_preview",
|
|
@@ -23771,6 +24780,17 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
23771
24780
|
function buildServer(version2 = getPackageVersion()) {
|
|
23772
24781
|
return createMcpServer(version2);
|
|
23773
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
|
+
}
|
|
23774
24794
|
function createMcpServer(version2) {
|
|
23775
24795
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23776
24796
|
const events = new EventsClient3;
|
|
@@ -23797,16 +24817,69 @@ function createMcpServer(version2) {
|
|
|
23797
24817
|
}));
|
|
23798
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) }] }));
|
|
23799
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) }] }));
|
|
23800
|
-
server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () =>
|
|
23801
|
-
|
|
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
|
+
}]
|
|
23802
24869
|
}));
|
|
23803
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) }] }));
|
|
23804
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) }] }));
|
|
23805
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) }] }));
|
|
23806
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) }] }));
|
|
23807
|
-
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
|
|
23808
|
-
|
|
23809
|
-
|
|
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
|
+
});
|
|
23810
24883
|
server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
|
|
23811
24884
|
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
23812
24885
|
commands: exports_external.array(exports_external.object({
|
|
@@ -23858,9 +24931,16 @@ function createMcpServer(version2) {
|
|
|
23858
24931
|
}));
|
|
23859
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) }] }));
|
|
23860
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) }] }));
|
|
23861
|
-
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
|
|
23862
|
-
|
|
23863
|
-
|
|
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
|
+
});
|
|
23864
24944
|
server.tool("machines_workspace_resolve", "Resolve sync-safe repo and open-files roots for an open-* consumer.", {
|
|
23865
24945
|
machine_id: exports_external.string().describe("Machine identifier"),
|
|
23866
24946
|
project_id: exports_external.string().describe("Canonical project id"),
|
|
@@ -23996,6 +25076,7 @@ function createMcpServer(version2) {
|
|
|
23996
25076
|
export {
|
|
23997
25077
|
writeNotificationConfig,
|
|
23998
25078
|
writeManifest,
|
|
25079
|
+
writeHeartbeatTick,
|
|
23999
25080
|
writeHeartbeat,
|
|
24000
25081
|
watchTmuxPane,
|
|
24001
25082
|
validateManifest,
|
|
@@ -24006,12 +25087,14 @@ export {
|
|
|
24006
25087
|
storagePull,
|
|
24007
25088
|
startDashboardServer,
|
|
24008
25089
|
setHeartbeatStatus,
|
|
25090
|
+
sanitizePublicString,
|
|
24009
25091
|
runTailscaleInstall,
|
|
24010
25092
|
runSync,
|
|
24011
25093
|
runStorageMigrations,
|
|
24012
25094
|
runSetup,
|
|
24013
25095
|
runSelfTest,
|
|
24014
25096
|
runDoctor,
|
|
25097
|
+
runDaemonServicePlan,
|
|
24015
25098
|
runClaudeInstall,
|
|
24016
25099
|
runCertPlan,
|
|
24017
25100
|
runBackup,
|
|
@@ -24024,15 +25107,21 @@ export {
|
|
|
24024
25107
|
resolveMachineRoute,
|
|
24025
25108
|
resolveBackupTarget,
|
|
24026
25109
|
repairWorkspaceManifestMappings,
|
|
25110
|
+
renderSystemdUnit,
|
|
25111
|
+
renderLaunchdPlist,
|
|
24027
25112
|
renderDomainMapping,
|
|
24028
25113
|
renderDashboardHtml,
|
|
24029
25114
|
removeNotificationChannel,
|
|
25115
|
+
redactTopologyForOutput,
|
|
24030
25116
|
redactSensitiveValue,
|
|
25117
|
+
redactRouteForOutput,
|
|
24031
25118
|
redactPrivateRef,
|
|
24032
25119
|
redactPath,
|
|
25120
|
+
redactNetworkValue,
|
|
24033
25121
|
redactMetadata,
|
|
24034
25122
|
redactManifestForDiagnostics,
|
|
24035
25123
|
redactIdentifier,
|
|
25124
|
+
redactErrorMessage,
|
|
24036
25125
|
recordSyncRun,
|
|
24037
25126
|
recordSetupRun,
|
|
24038
25127
|
readNotificationConfig,
|
|
@@ -24057,6 +25146,8 @@ export {
|
|
|
24057
25146
|
listDomainMappings,
|
|
24058
25147
|
listApps,
|
|
24059
25148
|
isSensitiveKey,
|
|
25149
|
+
isPrivateOutputEnabled,
|
|
25150
|
+
isPrivateMetadataEnabled,
|
|
24060
25151
|
getSyncMetaAll,
|
|
24061
25152
|
getStorageStatus,
|
|
24062
25153
|
getStoragePg,
|
|
@@ -24109,6 +25200,12 @@ export {
|
|
|
24109
25200
|
buildScreenEnableRemoteCommand,
|
|
24110
25201
|
buildScreenEnableCommand,
|
|
24111
25202
|
buildScreenCommand,
|
|
25203
|
+
buildDaemonUninstallPlan,
|
|
25204
|
+
buildDaemonStatusPlan,
|
|
25205
|
+
buildDaemonServicePlan,
|
|
25206
|
+
buildDaemonRestartPlan,
|
|
25207
|
+
buildDaemonLogsPlan,
|
|
25208
|
+
buildDaemonInstallPlan,
|
|
24112
25209
|
buildClaudeInstallPlan,
|
|
24113
25210
|
buildCertPlan,
|
|
24114
25211
|
buildBackupPlan,
|
|
@@ -24121,6 +25218,11 @@ export {
|
|
|
24121
25218
|
STORAGE_DATABASE_ENV,
|
|
24122
25219
|
SCREEN_SECRET_NAMESPACE_ENV,
|
|
24123
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,
|
|
24124
25226
|
PRIVATE_MANIFEST_REF_ENV,
|
|
24125
25227
|
PRIVATE_MANIFEST_BACKEND_ENV,
|
|
24126
25228
|
MACHINE_MCP_TOOL_NAMES,
|