@hasna/machines 0.0.37 → 0.0.39

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