@hasna/machines 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
@@ -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
+ });
12970
+ }
12971
+ function sanitizeAdapterCheck(check, domain, adapterId) {
12972
+ const safeAdapterId = redactIdentifier(adapterId);
12973
+ return makeCheck2(check.id.startsWith(`${domain}-`) || check.id.startsWith(`${domain}:`) ? check.id : `${domain}:${check.id}`, check.status, check.summary, String(redactSensitiveValue(check.detail)), {
12974
+ ...check,
12975
+ optional: check.optional ?? true,
12976
+ source: check.source ? String(redactSensitiveValue(check.source)) : `adapter:${safeAdapterId}`,
12977
+ data: check.data ? redactSensitiveValue(check.data) : undefined
12978
+ });
12664
12979
  }
12665
- function getAgentStatus(machineId = getLocalMachineId()) {
12666
- return listHeartbeats(machineId).map((heartbeat) => ({
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,183 +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)] : [];
13733
+ return {
13734
+ platform: resolved.platform,
13735
+ mode: resolved.mode,
13736
+ action: resolved.action,
13737
+ serviceName: resolved.serviceName,
13738
+ serviceId: resolved.serviceId,
13739
+ executable: resolved.executable,
13740
+ intervalMs: resolved.intervalMs,
13741
+ commands: buildActionCommands(resolved),
13742
+ files,
13743
+ warnings: resolved.warnings,
13744
+ manualSteps: buildManualSteps(resolved, files)
13745
+ };
13746
+ }
13747
+ function runDaemonServicePlan(plan, options = {}) {
13748
+ const apply = options.apply === true;
13749
+ const allowed = apply && options.yes === true;
13750
+ const warnings = [...plan.warnings];
13751
+ const filesWritten = [];
13752
+ const commands = [];
13753
+ if (apply && !allowed) {
13754
+ warnings.push("apply_requires_yes");
13755
+ }
13756
+ if (allowed) {
13757
+ for (const file of plan.files) {
13758
+ const path = expandShellPath(file.path);
13759
+ let content;
13760
+ try {
13761
+ content = materializePlaceholders(file.content);
13762
+ } catch (error) {
13763
+ warnings.push(error instanceof Error ? error.message : String(error));
13764
+ return {
13765
+ mode: "plan",
13766
+ applied: false,
13767
+ plan,
13768
+ filesWritten,
13769
+ commands: plan.commands.map((commandSpec) => ({
13770
+ id: commandSpec.id,
13771
+ command: renderCommand(commandSpec),
13772
+ skipped: true,
13773
+ exitCode: null,
13774
+ stdout: "",
13775
+ stderr: ""
13776
+ })),
13777
+ warnings
13778
+ };
13779
+ }
13780
+ mkdirSync2(dirname4(path), { recursive: true });
13781
+ writeFileSync3(path, content, "utf8");
13782
+ chmodSync(path, Number.parseInt(file.mode, 8));
13783
+ filesWritten.push(path);
13784
+ }
13785
+ }
13786
+ for (const commandSpec of plan.commands) {
13787
+ const commandLine = renderCommand(commandSpec);
13788
+ if (!allowed) {
13789
+ commands.push({
13790
+ id: commandSpec.id,
13791
+ command: commandLine,
13792
+ skipped: true,
13793
+ exitCode: null,
13794
+ stdout: "",
13795
+ stderr: ""
13796
+ });
13797
+ continue;
13798
+ }
13799
+ const program = commandLine[0];
13800
+ const args = commandLine.slice(1);
13801
+ try {
13802
+ const result = execFileSync2(program, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13803
+ commands.push({
13804
+ id: commandSpec.id,
13805
+ command: commandLine,
13806
+ skipped: false,
13807
+ exitCode: 0,
13808
+ stdout: result,
13809
+ stderr: ""
13810
+ });
13811
+ } catch (error) {
13812
+ const maybe = error;
13813
+ if (commandSpec.allowFailure) {
13814
+ commands.push({
13815
+ id: commandSpec.id,
13816
+ command: commandLine,
13817
+ skipped: false,
13818
+ exitCode: typeof maybe.status === "number" ? maybe.status : 1,
13819
+ stdout: String(maybe.stdout ?? ""),
13820
+ stderr: String(maybe.stderr ?? ""),
13821
+ error: maybe.message ?? String(error)
13822
+ });
13823
+ continue;
13824
+ }
13825
+ commands.push({
13826
+ id: commandSpec.id,
13827
+ command: commandLine,
13828
+ skipped: false,
13829
+ exitCode: typeof maybe.status === "number" ? maybe.status : 1,
13830
+ stdout: String(maybe.stdout ?? ""),
13831
+ stderr: String(maybe.stderr ?? ""),
13832
+ error: maybe.message ?? String(error)
13833
+ });
13834
+ break;
13835
+ }
13836
+ }
13837
+ return {
13838
+ mode: allowed ? "apply" : "plan",
13839
+ applied: allowed,
13840
+ plan,
13841
+ filesWritten,
13842
+ commands,
13843
+ warnings
13844
+ };
13845
+ }
13846
+ function buildDaemonInstallPlan(options = {}) {
13847
+ return buildDaemonServicePlan({ ...options, action: "install" });
13848
+ }
13849
+ function buildDaemonUninstallPlan(options = {}) {
13850
+ return buildDaemonServicePlan({ ...options, action: "uninstall" });
13851
+ }
13852
+ function buildDaemonRestartPlan(options = {}) {
13853
+ return buildDaemonServicePlan({ ...options, action: "restart" });
13854
+ }
13855
+ function buildDaemonStatusPlan(options = {}) {
13856
+ return buildDaemonServicePlan({ ...options, action: "status" });
13857
+ }
13858
+ function buildDaemonLogsPlan(options = {}) {
13859
+ return buildDaemonServicePlan({ ...options, action: "logs" });
13860
+ }
13861
+ function renderLaunchdPlist(options = {}) {
13862
+ const resolved = resolveDaemonServiceOptions({ ...options, action: "install", platform: "macos" });
13863
+ return launchdPlist(resolved);
13864
+ }
13865
+ function renderSystemdUnit(options = {}) {
13866
+ const resolved = resolveDaemonServiceOptions({ ...options, action: "install", platform: "linux" });
13867
+ return systemdUnit(resolved);
13868
+ }
13869
+ function resolveDaemonServiceOptions(options) {
13870
+ const warnings = [];
13871
+ const serviceName = normalizeServiceName(options.serviceName, warnings);
13872
+ const platform5 = normalizePlatform3(options.platform, warnings);
13873
+ const mode = options.mode ?? "user";
13874
+ const intervalMs = normalizeIntervalMs(options.intervalMs, warnings);
13875
+ const executable = options.executable?.trim() || DEFAULT_EXECUTABLE;
13876
+ if (platform5 === "linux" && !executable.startsWith("/")) {
13877
+ warnings.push("systemd units should use an absolute executable path; install plan keeps the provided path unchanged.");
13878
+ }
13044
13879
  return {
13045
- ...rest,
13046
- id,
13047
- status,
13048
- summary,
13049
- detail,
13050
- data: data ? redactSensitiveValue(data) : undefined
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 = getLocalMachineId(), options = {}) {
13131
- const now = options.now ?? new Date;
13132
- const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
13133
- const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
13134
- const details = parseKeyValueOutput(commandChecks.stdout);
13135
- const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
13136
- const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
13137
- machineId,
13138
- manifest,
13139
- manifestSource,
13140
- commandDetails: details,
13141
- now
13142
- }, options.adapters ?? []);
13143
- const checks = [
13144
- makeCheck2("manifest-source", manifestSource.warnings.length > 0 ? "warn" : "ok", "Manifest source boundary", `${manifestSource.source.kind}:${manifestSource.source.ref} loaded from ${manifestSource.loadedFrom}`, {
13145
- data: {
13146
- source: manifestSource.source,
13147
- loadedFrom: manifestSource.loadedFrom,
13148
- fallbackSource: manifestSource.fallbackSource,
13149
- warnings: manifestSource.warnings
13150
- },
13151
- remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
13152
- }),
13153
- makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(redactManifestForDiagnostics(machineInManifest)) : `No manifest entry for ${machineId}`, {
13154
- data: {
13155
- declared: Boolean(machineInManifest),
13156
- machine: machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null
13157
- }
13158
- }),
13159
- 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"}`, {
13160
- data: {
13161
- path: redactPath(details["data_dir"] || "unknown"),
13162
- exists: details["data_dir_exists"] === "yes"
13163
- }
13164
- }),
13165
- makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${redactPath(details["manifest_path"] || "unknown")} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`, {
13166
- data: {
13167
- path: redactPath(details["manifest_path"] || "unknown"),
13168
- exists: details["manifest_exists"] === "yes"
13169
- }
13170
- }),
13171
- makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${redactPath(details["db_path"] || "unknown")} ${details["db_exists"] === "yes" ? "exists" : "missing"}`, {
13172
- data: {
13173
- path: redactPath(details["db_path"] || "unknown"),
13174
- exists: details["db_exists"] === "yes"
13175
- }
13176
- }),
13177
- makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${redactPath(details["notifications_path"] || "unknown")} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`, {
13178
- data: {
13179
- path: redactPath(details["notifications_path"] || "unknown"),
13180
- exists: details["notifications_exists"] === "yes"
13181
- }
13182
- }),
13183
- makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
13184
- makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
13185
- makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
13186
- makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
13187
- makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing"),
13188
- 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.", {
13189
- data: { available: details["sudo_noninteractive"] === "ok" },
13190
- remediation: details["sudo_noninteractive"] === "ok" ? undefined : ["Configure explicit sudo policy or run setup commands manually; do not store sudo passwords in public manifests."]
13191
- }),
13192
- 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.", {
13193
- data: { supported: details["ssh_cert_support"] === "ok" },
13194
- remediation: details["ssh_cert_support"] === "ok" ? undefined : ["Install or update OpenSSH before adopting SSH certificate auth for this machine."]
13195
- }),
13196
- 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.", {
13197
- data: {
13198
- gh_cli: details["gh_cli"] && details["gh_cli"] !== "missing",
13199
- gh_auth: details["gh_auth"] === "ok",
13200
- app_ref_configured: details["github_app_ref"] === "configured"
13201
- },
13202
- 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."]
13203
- }),
13204
- ...optionalAdapterChecks
13205
- ];
13206
- return {
13207
- machineId,
13208
- source: commandChecks.source,
13209
- schemaVersion: 1,
13210
- generatedAt: now.toISOString(),
13211
- manifestSource,
13212
- manifestPath: details["manifest_path"] ? redactPath(details["manifest_path"]) : undefined,
13213
- dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
13214
- notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
13215
- checks
13216
- };
14073
+ function escapeSystemdEnvironmentValue(value) {
14074
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%");
14075
+ }
14076
+ function launchdPlist(options) {
14077
+ const env = Object.entries(options.env).map(([name, value]) => ` <key>${xmlEscape(name)}</key>
14078
+ <string>${xmlEscape(value)}</string>`).join(`
14079
+ `);
14080
+ return `<?xml version="1.0" encoding="UTF-8"?>
14081
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
14082
+ <plist version="1.0">
14083
+ <dict>
14084
+ <key>Label</key>
14085
+ <string>${xmlEscape(options.serviceId)}</string>
14086
+ <key>ProgramArguments</key>
14087
+ <array>
14088
+ <string>${xmlEscape(options.executable)}</string>
14089
+ <string>--interval-ms</string>
14090
+ <string>${options.intervalMs}</string>
14091
+ </array>
14092
+ <key>EnvironmentVariables</key>
14093
+ <dict>
14094
+ ${env}
14095
+ </dict>
14096
+ <key>KeepAlive</key>
14097
+ <true/>
14098
+ <key>RunAtLoad</key>
14099
+ <true/>
14100
+ <key>StandardOutPath</key>
14101
+ <string>${xmlEscape(launchdLogPath(options, "out"))}</string>
14102
+ <key>StandardErrorPath</key>
14103
+ <string>${xmlEscape(launchdLogPath(options, "err"))}</string>
14104
+ </dict>
14105
+ </plist>
14106
+ `;
14107
+ }
14108
+ function systemdUnit(options) {
14109
+ const env = Object.entries(options.env).map(([name, value]) => `Environment=${quoteSystemdEnvironment(name, value)}`).join(`
14110
+ `);
14111
+ return `[Unit]
14112
+ Description=Hasna machines agent
14113
+ After=network-online.target
14114
+ Wants=network-online.target
14115
+
14116
+ [Service]
14117
+ Type=simple
14118
+ ExecStart=${quoteSystemdExecArg(options.executable)} --interval-ms ${options.intervalMs}
14119
+ Restart=always
14120
+ RestartSec=10
14121
+ ${env}
14122
+
14123
+ [Install]
14124
+ WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
14125
+ `;
14126
+ }
14127
+ function launchdDomain(options) {
14128
+ return options.mode === "system" ? "system" : "gui/$UID";
14129
+ }
14130
+ function launchdPlistPath(options) {
14131
+ if (options.mode === "system")
14132
+ return `/Library/LaunchDaemons/${options.serviceId}.plist`;
14133
+ return `$HOME/Library/LaunchAgents/${options.serviceId}.plist`;
14134
+ }
14135
+ function launchdLogPath(options, stream) {
14136
+ const fileName = `${options.serviceId}.${stream}.log`;
14137
+ if (options.mode === "system")
14138
+ return `/var/log/${fileName}`;
14139
+ return `$HOME/Library/Logs/${fileName}`;
14140
+ }
14141
+ function systemdUnitName(options) {
14142
+ return `${options.serviceId}.service`;
14143
+ }
14144
+ function systemdUnitPath(options) {
14145
+ if (options.mode === "system")
14146
+ return `/etc/systemd/system/${systemdUnitName(options)}`;
14147
+ return `$HOME/.config/systemd/user/${systemdUnitName(options)}`;
14148
+ }
14149
+ function xmlEscape(value) {
14150
+ return String(value).replace(/&/g, "&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;
13217
14162
  }
13218
14163
  // src/commands/manifest.ts
13219
14164
  function manifestInit() {
@@ -13459,7 +14404,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
13459
14404
  };
13460
14405
  }
13461
14406
  // src/commands/notifications.ts
13462
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
14407
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
13463
14408
  var notificationChannelSchema = exports_external.object({
13464
14409
  id: exports_external.string(),
13465
14410
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -13522,8 +14467,8 @@ ${message}
13522
14467
  };
13523
14468
  }
13524
14469
  if (hasCommand2("mail")) {
13525
- const command = `printf %s ${shellQuote6(message)} | mail -s ${shellQuote6(subject)} ${shellQuote6(channel.target)}`;
13526
- 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], {
13527
14472
  stdout: "pipe",
13528
14473
  stderr: "pipe",
13529
14474
  env: process.env
@@ -13627,7 +14572,7 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
13627
14572
  updatedAt: new Date().toISOString(),
13628
14573
  channels: sortChannels(config.channels)
13629
14574
  };
13630
- writeFileSync3(path, `${JSON.stringify(nextConfig, null, 2)}
14575
+ writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
13631
14576
  `, "utf8");
13632
14577
  return nextConfig;
13633
14578
  }
@@ -13746,8 +14691,8 @@ function listPorts(machineId) {
13746
14691
  const targetMachineId = machineId || getLocalMachineId();
13747
14692
  const isLocal = targetMachineId === getLocalMachineId();
13748
14693
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
13749
- const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
13750
- 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" });
13751
14696
  if (result.status !== 0) {
13752
14697
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
13753
14698
  }
@@ -13759,7 +14704,7 @@ function listPorts(machineId) {
13759
14704
  }
13760
14705
  // src/commands/runtime.ts
13761
14706
  import { spawnSync as spawnSync4 } from "child_process";
13762
- import { setTimeout as sleep } from "timers/promises";
14707
+ import { setTimeout as sleep2 } from "timers/promises";
13763
14708
  import { EventsClient } from "@hasna/events";
13764
14709
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
13765
14710
  const checkedAt = new Date().toISOString();
@@ -13788,7 +14733,7 @@ async function watchTmuxPane(options) {
13788
14733
  const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
13789
14734
  const client = options.client ?? new EventsClient;
13790
14735
  const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
13791
- const wait = options.sleep ?? sleep;
14736
+ const wait = options.sleep ?? sleep2;
13792
14737
  let lastPresent;
13793
14738
  let lastProbe;
13794
14739
  for (let checks = 1;checks <= maxChecks; checks += 1) {
@@ -13849,6 +14794,16 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
13849
14794
  import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
13850
14795
 
13851
14796
  // src/commands/status.ts
14797
+ function parseJsonObject2(value) {
14798
+ if (!value)
14799
+ return null;
14800
+ try {
14801
+ const parsed = JSON.parse(value);
14802
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
14803
+ } catch {
14804
+ return null;
14805
+ }
14806
+ }
13852
14807
  function getStatus() {
13853
14808
  const manifest = readManifest();
13854
14809
  const heartbeats = listHeartbeats();
@@ -13872,7 +14827,12 @@ function getStatus() {
13872
14827
  platform: declared?.platform,
13873
14828
  manifestDeclared: Boolean(declared),
13874
14829
  heartbeatStatus: heartbeat?.status || "unknown",
13875
- lastHeartbeatAt: heartbeat?.updated_at
14830
+ lastHeartbeatAt: heartbeat?.updated_at,
14831
+ daemonVersion: heartbeat?.daemon_version ?? null,
14832
+ agentMode: heartbeat?.agent_mode ?? null,
14833
+ storageSyncStatus: heartbeat?.storage_sync_status ?? null,
14834
+ doctorSummary: parseJsonObject2(heartbeat?.doctor_summary_json),
14835
+ privateMetadata: Boolean(heartbeat?.private_metadata)
13876
14836
  };
13877
14837
  }),
13878
14838
  recentSetupRuns: countRuns("setup_runs"),
@@ -13895,6 +14855,9 @@ function getServeInfo(options = {}) {
13895
14855
  "/",
13896
14856
  "/health",
13897
14857
  "/api/status",
14858
+ "/api/topology",
14859
+ "/api/routes",
14860
+ "/api/daemon/status",
13898
14861
  "/api/manifest",
13899
14862
  "/api/notifications",
13900
14863
  "/api/webhooks",
@@ -13912,6 +14875,7 @@ function getServeInfo(options = {}) {
13912
14875
  }
13913
14876
  function renderDashboardHtml() {
13914
14877
  const status = getStatus();
14878
+ const topology = discoverMachineTopology();
13915
14879
  const manifest = manifestList();
13916
14880
  const notifications = listNotificationChannels();
13917
14881
  const doctor = runDoctor();
@@ -13950,17 +14914,20 @@ function renderDashboardHtml() {
13950
14914
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
13951
14915
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
13952
14916
  <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
14917
+ <section class="card"><div>Tailscale routes</div><div class="stat">${topology.machines.filter((machine) => machine.ssh.route === "tailscale").length}</div></section>
13953
14918
  </div>
13954
14919
 
13955
14920
  <section class="card" style="margin-top:16px">
13956
14921
  <h2>Machines</h2>
13957
14922
  <table>
13958
- <thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Last heartbeat</th></tr></thead>
14923
+ <thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Agent</th><th>Storage</th><th>Last heartbeat</th></tr></thead>
13959
14924
  <tbody>
13960
14925
  ${status.machines.map((machine) => `<tr>
13961
14926
  <td><code>${escapeHtml(machine.machineId)}</code></td>
13962
14927
  <td>${escapeHtml(machine.platform || "unknown")}</td>
13963
14928
  <td><span class="badge ${escapeHtml(machine.heartbeatStatus)}">${escapeHtml(machine.heartbeatStatus)}</span></td>
14929
+ <td>${escapeHtml(machine.agentMode || "unknown")} ${escapeHtml(machine.daemonVersion || "")}</td>
14930
+ <td>${escapeHtml(machine.storageSyncStatus || "unknown")}</td>
13964
14931
  <td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
13965
14932
  </tr>`).join("")}
13966
14933
  </tbody>
@@ -14004,6 +14971,14 @@ function renderDashboardHtml() {
14004
14971
  <script>
14005
14972
  // Auto-refresh dashboard data every 15s
14006
14973
  const REFRESH_INTERVAL = 15000;
14974
+ function escapeHtml(value) {
14975
+ return String(value ?? "")
14976
+ .replaceAll("&", "&amp;")
14977
+ .replaceAll("<", "&lt;")
14978
+ .replaceAll(">", "&gt;")
14979
+ .replaceAll('"', "&quot;")
14980
+ .replaceAll("'", "&#39;");
14981
+ }
14007
14982
  async function refreshData() {
14008
14983
  try {
14009
14984
  const [statusRes, doctorRes] = await Promise.all([
@@ -14024,10 +14999,12 @@ function renderDashboardHtml() {
14024
14999
  tbody.innerHTML = status.machines
14025
15000
  .map((m) =>
14026
15001
  "<tr>" +
14027
- "<td><code>" + m.machineId + "</code></td>" +
14028
- "<td>" + (m.platform || "unknown") + "</td>" +
14029
- '<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
14030
- "<td>" + (m.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>" +
14031
15008
  "</tr>"
14032
15009
  )
14033
15010
  .join("");
@@ -14039,9 +15016,9 @@ function renderDashboardHtml() {
14039
15016
  doctorTbody.innerHTML = doctor.checks
14040
15017
  .map((c) =>
14041
15018
  "<tr>" +
14042
- "<td>" + c.summary + "</td>" +
14043
- '<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
14044
- '<td class="muted">' + c.detail + "</td>" +
15019
+ "<td>" + escapeHtml(c.summary) + "</td>" +
15020
+ '<td><span class="badge ' + escapeHtml(c.status) + '">' + escapeHtml(c.status) + '</span></td>' +
15021
+ '<td class="muted">' + escapeHtml(c.detail) + "</td>" +
14045
15022
  "</tr>"
14046
15023
  )
14047
15024
  .join("");
@@ -14071,6 +15048,14 @@ async function parseJsonBody(request) {
14071
15048
  function jsonError(message, status = 400) {
14072
15049
  return Response.json({ error: message }, { status });
14073
15050
  }
15051
+ function privateOutputWarnings(requested, allowed) {
15052
+ return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
15053
+ }
15054
+ function appendWarnings(payload, warnings) {
15055
+ if (warnings.length === 0)
15056
+ return payload;
15057
+ return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
15058
+ }
14074
15059
  function startDashboardServer(options = {}) {
14075
15060
  const info = getServeInfo(options);
14076
15061
  const events = new EventsClient2;
@@ -14081,12 +15066,34 @@ function startDashboardServer(options = {}) {
14081
15066
  const url = new URL(request.url);
14082
15067
  const machineId = url.searchParams.get("machine") || undefined;
14083
15068
  const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
15069
+ const privateMetadataRequested = url.searchParams.get("privateMetadata") === "true" || url.searchParams.get("private_metadata") === "true";
15070
+ const privateMetadata = privateMetadataRequested && isPrivateOutputEnabled();
15071
+ const privateWarnings = privateOutputWarnings(privateMetadataRequested, privateMetadata);
14084
15072
  if (url.pathname === "/health") {
14085
15073
  return Response.json({ ok: true, ...getServeInfo(options) });
14086
15074
  }
14087
15075
  if (url.pathname === "/api/status") {
14088
15076
  return Response.json(getStatus());
14089
15077
  }
15078
+ if (url.pathname === "/api/topology") {
15079
+ const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
15080
+ return Response.json(appendWarnings(redactTopologyForOutput(topology, { privateMetadata }), privateWarnings));
15081
+ }
15082
+ if (url.pathname === "/api/routes") {
15083
+ const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
15084
+ return Response.json({
15085
+ generated_at: topology.generated_at,
15086
+ routes: topology.machines.map((machine) => redactRouteForOutput(resolveMachineRoute(machine.machine_id, { topology }), { privateMetadata })),
15087
+ ...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
15088
+ });
15089
+ }
15090
+ if (url.pathname === "/api/daemon/status") {
15091
+ return Response.json({
15092
+ generated_at: new Date().toISOString(),
15093
+ agents: getAgentStatus(machineId, { privateMetadata }),
15094
+ ...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
15095
+ });
15096
+ }
14090
15097
  if (url.pathname === "/api/manifest") {
14091
15098
  return Response.json(manifestList());
14092
15099
  }
@@ -14299,18 +15306,18 @@ function buildBaseSteps(machine) {
14299
15306
  function buildPackageSteps(machine) {
14300
15307
  return (machine.packages || []).map((pkg, index) => {
14301
15308
  const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
14302
- let command = pkg.name;
15309
+ let command2 = pkg.name;
14303
15310
  if (manager === "bun") {
14304
- command = `bun install -g ${quote3(pkg.name)}`;
15311
+ command2 = `bun install -g ${quote3(pkg.name)}`;
14305
15312
  } else if (manager === "brew") {
14306
- command = `brew install ${quote3(pkg.name)}`;
15313
+ command2 = `brew install ${quote3(pkg.name)}`;
14307
15314
  } else if (manager === "apt") {
14308
- command = `sudo apt-get install -y ${quote3(pkg.name)}`;
15315
+ command2 = `sudo apt-get install -y ${quote3(pkg.name)}`;
14309
15316
  }
14310
15317
  return {
14311
15318
  id: `package-${index + 1}`,
14312
15319
  title: `Install package ${pkg.name}`,
14313
- command,
15320
+ command: command2,
14314
15321
  manager,
14315
15322
  privileged: manager === "apt"
14316
15323
  };
@@ -14375,8 +15382,8 @@ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
14375
15382
  function shellQuote7(value) {
14376
15383
  return `'${value.replace(/'/g, "'\\''")}'`;
14377
15384
  }
14378
- function shellCommand2(command) {
14379
- return command.map(shellQuote7).join(" ");
15385
+ function shellCommand2(command2) {
15386
+ return command2.map(shellQuote7).join(" ");
14380
15387
  }
14381
15388
  function metadataString2(metadata, keys) {
14382
15389
  if (!metadata)
@@ -14558,11 +15565,11 @@ function detectFileActions(machine) {
14558
15565
  status = source === target ? "ok" : "drifted";
14559
15566
  }
14560
15567
  }
14561
- const 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)}`;
14562
15569
  return {
14563
15570
  id: `file-${index + 1}`,
14564
15571
  title: `${status === "ok" ? "File in sync" : "Reconcile file"} ${file.target}`,
14565
- command,
15572
+ command: command2,
14566
15573
  status,
14567
15574
  kind: "file"
14568
15575
  };
@@ -14591,18 +15598,18 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
14591
15598
  executed: 0
14592
15599
  };
14593
15600
  }
14594
- function applyFileAction(command) {
14595
- const [verb, source, target] = command.split(" ");
15601
+ function applyFileAction(command2) {
15602
+ const [verb, source, target] = command2.split(" ");
14596
15603
  if (verb === "cp" && source && target) {
14597
15604
  ensureParentDir(target);
14598
15605
  copyFileSync(source.slice(1, -1), target.slice(1, -1));
14599
15606
  return;
14600
15607
  }
14601
15608
  if (verb === "ln" && source && target) {
14602
- const sourcePath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
14603
- const targetPath = command.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
15609
+ const sourcePath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[1];
15610
+ const targetPath = command2.match(/ln -sfn '(.+)' '(.+)'/)?.[2];
14604
15611
  if (!sourcePath || !targetPath) {
14605
- throw new Error(`Unable to parse symlink command: ${command}`);
15612
+ throw new Error(`Unable to parse symlink command: ${command2}`);
14606
15613
  }
14607
15614
  ensureParentDir(targetPath);
14608
15615
  try {
@@ -15525,7 +16532,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
15525
16532
  var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
15526
16533
  var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
15527
16534
  var base64url = /^[A-Za-z0-9_-]*$/;
15528
- var hostname6 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
16535
+ var hostname7 = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/;
15529
16536
  var e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
15530
16537
  var dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`;
15531
16538
  var date = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
@@ -16137,7 +17144,7 @@ var $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
16137
17144
  code: "invalid_format",
16138
17145
  format: "url",
16139
17146
  note: "Invalid hostname",
16140
- pattern: hostname6.source,
17147
+ pattern: hostname7.source,
16141
17148
  input: payload.value,
16142
17149
  inst,
16143
17150
  continue: !def.abort
@@ -23725,6 +24732,8 @@ var MACHINE_MCP_TOOL_NAMES = [
23725
24732
  "machines_manifest_get",
23726
24733
  "machines_manifest_remove",
23727
24734
  "machines_agent_status",
24735
+ "machines_daemon_status",
24736
+ "machines_daemon_service_plan",
23728
24737
  "machines_setup_preview",
23729
24738
  "machines_setup_apply",
23730
24739
  "machines_sync_preview",
@@ -23771,6 +24780,17 @@ var MACHINE_MCP_TOOL_NAMES = [
23771
24780
  function buildServer(version2 = getPackageVersion()) {
23772
24781
  return createMcpServer(version2);
23773
24782
  }
24783
+ function privateMetadataAllowed(requested) {
24784
+ return requested === true && isPrivateOutputEnabled();
24785
+ }
24786
+ function privateOutputWarnings2(requested, allowed) {
24787
+ return requested === true && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
24788
+ }
24789
+ function appendWarnings2(payload, warnings) {
24790
+ if (warnings.length === 0)
24791
+ return payload;
24792
+ return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
24793
+ }
23774
24794
  function createMcpServer(version2) {
23775
24795
  const server = new McpServer({ name: "machines", version: version2 });
23776
24796
  const events = new EventsClient3;
@@ -23797,16 +24817,69 @@ function createMcpServer(version2) {
23797
24817
  }));
23798
24818
  server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
23799
24819
  server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] }));
23800
- server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () => ({
23801
- 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
+ }]
23802
24869
  }));
23803
24870
  server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
23804
24871
  server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
23805
24872
  server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
23806
24873
  server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
23807
- server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", { include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ include_tailscale }) => ({
23808
- content: [{ type: "text", text: JSON.stringify(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), null, 2) }]
23809
- }));
24874
+ server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", {
24875
+ include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
24876
+ private_metadata: exports_external.boolean().optional().describe("Include private host/network route fields")
24877
+ }, async ({ include_tailscale, private_metadata }) => {
24878
+ const privateMetadata = privateMetadataAllowed(private_metadata);
24879
+ const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
24880
+ const topology = redactTopologyForOutput(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), { privateMetadata });
24881
+ return { content: [{ type: "text", text: JSON.stringify(appendWarnings2(topology, warnings), null, 2) }] };
24882
+ });
23810
24883
  server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
23811
24884
  machine_id: exports_external.string().optional().describe("Machine identifier"),
23812
24885
  commands: exports_external.array(exports_external.object({
@@ -23858,9 +24931,16 @@ function createMcpServer(version2) {
23858
24931
  }));
23859
24932
  server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
23860
24933
  server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
23861
- server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", { 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 }) => ({
23862
- content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
23863
- }));
24934
+ server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", {
24935
+ machine_id: exports_external.string().describe("Machine identifier"),
24936
+ include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json"),
24937
+ private_metadata: exports_external.boolean().optional().describe("Include private route targets")
24938
+ }, async ({ machine_id, include_tailscale, private_metadata }) => {
24939
+ const privateMetadata = privateMetadataAllowed(private_metadata);
24940
+ const warnings = privateOutputWarnings2(private_metadata, privateMetadata);
24941
+ const route = redactRouteForOutput(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), { privateMetadata });
24942
+ return { content: [{ type: "text", text: JSON.stringify(appendWarnings2(route, warnings), null, 2) }] };
24943
+ });
23864
24944
  server.tool("machines_workspace_resolve", "Resolve sync-safe repo and open-files roots for an open-* consumer.", {
23865
24945
  machine_id: exports_external.string().describe("Machine identifier"),
23866
24946
  project_id: exports_external.string().describe("Canonical project id"),
@@ -23996,6 +25076,7 @@ function createMcpServer(version2) {
23996
25076
  export {
23997
25077
  writeNotificationConfig,
23998
25078
  writeManifest,
25079
+ writeHeartbeatTick,
23999
25080
  writeHeartbeat,
24000
25081
  watchTmuxPane,
24001
25082
  validateManifest,
@@ -24006,12 +25087,14 @@ export {
24006
25087
  storagePull,
24007
25088
  startDashboardServer,
24008
25089
  setHeartbeatStatus,
25090
+ sanitizePublicString,
24009
25091
  runTailscaleInstall,
24010
25092
  runSync,
24011
25093
  runStorageMigrations,
24012
25094
  runSetup,
24013
25095
  runSelfTest,
24014
25096
  runDoctor,
25097
+ runDaemonServicePlan,
24015
25098
  runClaudeInstall,
24016
25099
  runCertPlan,
24017
25100
  runBackup,
@@ -24024,15 +25107,21 @@ export {
24024
25107
  resolveMachineRoute,
24025
25108
  resolveBackupTarget,
24026
25109
  repairWorkspaceManifestMappings,
25110
+ renderSystemdUnit,
25111
+ renderLaunchdPlist,
24027
25112
  renderDomainMapping,
24028
25113
  renderDashboardHtml,
24029
25114
  removeNotificationChannel,
25115
+ redactTopologyForOutput,
24030
25116
  redactSensitiveValue,
25117
+ redactRouteForOutput,
24031
25118
  redactPrivateRef,
24032
25119
  redactPath,
25120
+ redactNetworkValue,
24033
25121
  redactMetadata,
24034
25122
  redactManifestForDiagnostics,
24035
25123
  redactIdentifier,
25124
+ redactErrorMessage,
24036
25125
  recordSyncRun,
24037
25126
  recordSetupRun,
24038
25127
  readNotificationConfig,
@@ -24057,6 +25146,8 @@ export {
24057
25146
  listDomainMappings,
24058
25147
  listApps,
24059
25148
  isSensitiveKey,
25149
+ isPrivateOutputEnabled,
25150
+ isPrivateMetadataEnabled,
24060
25151
  getSyncMetaAll,
24061
25152
  getStorageStatus,
24062
25153
  getStoragePg,
@@ -24109,6 +25200,12 @@ export {
24109
25200
  buildScreenEnableRemoteCommand,
24110
25201
  buildScreenEnableCommand,
24111
25202
  buildScreenCommand,
25203
+ buildDaemonUninstallPlan,
25204
+ buildDaemonStatusPlan,
25205
+ buildDaemonServicePlan,
25206
+ buildDaemonRestartPlan,
25207
+ buildDaemonLogsPlan,
25208
+ buildDaemonInstallPlan,
24112
25209
  buildClaudeInstallPlan,
24113
25210
  buildCertPlan,
24114
25211
  buildBackupPlan,
@@ -24121,6 +25218,11 @@ export {
24121
25218
  STORAGE_DATABASE_ENV,
24122
25219
  SCREEN_SECRET_NAMESPACE_ENV,
24123
25220
  REDACTED_VALUE,
25221
+ PRIVATE_OUTPUT_FALLBACK_ENV,
25222
+ PRIVATE_OUTPUT_ENV,
25223
+ PRIVATE_OUTPUT_DENIED_WARNING,
25224
+ PRIVATE_METADATA_FALLBACK_ENV,
25225
+ PRIVATE_METADATA_ENV,
24124
25226
  PRIVATE_MANIFEST_REF_ENV,
24125
25227
  PRIVATE_MANIFEST_BACKEND_ENV,
24126
25228
  MACHINE_MCP_TOOL_NAMES,