@lifeaitools/clauth 1.30.26 → 1.31.1

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.
Files changed (39) hide show
  1. package/.clauth-skill/SKILL.md +0 -31
  2. package/.clauth-skill/references/keys-guide.md +270 -270
  3. package/.clauth-skill/references/operator-guide.md +0 -27
  4. package/README.md +2 -48
  5. package/cli/api.js +238 -238
  6. package/cli/commands/agent-pool.js +51 -15
  7. package/cli/commands/install.js +396 -396
  8. package/cli/commands/login.js +135 -0
  9. package/cli/commands/login.test.js +73 -0
  10. package/cli/commands/serve.js +16 -846
  11. package/cli/commands/uninstall.js +164 -164
  12. package/cli/commands/watchdog.js +1 -1
  13. package/cli/index.js +29 -20
  14. package/cli/supervisor-registry.js +1 -6
  15. package/cli/watchdog-registry.js +2 -30
  16. package/cli/watchdog-registry.test.js +5 -28
  17. package/cli/webdav-service.js +339 -339
  18. package/install.ps1 +102 -102
  19. package/install.sh +49 -49
  20. package/package.json +4 -6
  21. package/scripts/bin/bootstrap-linux +0 -0
  22. package/scripts/bin/bootstrap-macos +0 -0
  23. package/scripts/bin/bootstrap-win.exe +0 -0
  24. package/scripts/bootstrap.cjs +121 -121
  25. package/scripts/build.mjs +66 -0
  26. package/scripts/build.sh +5 -45
  27. package/scripts/postinstall.js +189 -189
  28. package/supabase/functions/auth-vault/index.ts +350 -350
  29. package/supabase/migrations/001_clauth_schema.sql +94 -94
  30. package/supabase/migrations/002_vault_helpers.sql +90 -90
  31. package/supabase/migrations/20260317_lockout.sql +26 -26
  32. package/cli/commands/ops-install.js +0 -211
  33. package/cli/commands/ops.js +0 -69
  34. package/cli/ops/coolify-adapter.js +0 -80
  35. package/cli/ops/deployment-adapter.js +0 -63
  36. package/cli/ops/job-store.js +0 -116
  37. package/cli/ops/operation-policy.js +0 -51
  38. package/cli/ops/pm2-adapter.js +0 -128
  39. package/cli/ops/serialized-executor.js +0 -9
@@ -14,7 +14,7 @@ import { getMachineHash, deriveToken, deriveSeedHash } from "../fingerprint.js";
14
14
  import * as api from "../api.js";
15
15
  import chalk from "chalk";
16
16
  import ora from "ora";
17
- import { execFileSync, execSync as execSyncTop, spawn } from "child_process";
17
+ import { execFileSync, execSync as execSyncTop } from "child_process";
18
18
  import Conf from "conf";
19
19
  import { getConfOptions } from "../conf-path.js";
20
20
  import { appendFile, readdir, readFile, writeFile, rm, mkdir, stat, rename, cp } from "node:fs/promises";
@@ -30,24 +30,6 @@ import {
30
30
  readWatchdogEvents,
31
31
  restartWatchdogService,
32
32
  } from "../watchdog-registry.js";
33
- import {
34
- discoverPlugins,
35
- getSupervisorDir,
36
- getSupervisorPort,
37
- loadSupervisorState,
38
- listPlugins,
39
- listRoutes,
40
- listSurfaces,
41
- listTunnels,
42
- readSupervisorEvents,
43
- addTunnelRoute,
44
- removeTunnelRoute,
45
- reconcileSurfaceHealth,
46
- runPluginAction,
47
- runSurfaceAction,
48
- setPluginEnabled,
49
- supervisorHealth,
50
- } from "../supervisor-registry.js";
51
33
  import {
52
34
  AgentPool,
53
35
  DelegationLane,
@@ -58,14 +40,6 @@ import {
58
40
  DEFAULT_BOOTSTRAP,
59
41
  } from "./agent-pool.js";
60
42
  import { AgentCron, cronEnabled, nextRun } from "./agent-cron.js";
61
- import pm2 from "pm2";
62
- import { createPm2Adapter, PM2_OPERATION_CATALOG } from "../ops/pm2-adapter.js";
63
- import { createOperationPolicy } from "../ops/operation-policy.js";
64
- import { createJobStore } from "../ops/job-store.js";
65
- import { createCoolifyAdapter, deploymentUuidFrom } from "../ops/coolify-adapter.js";
66
- import { createDeploymentAdapter, parseDeploymentRegistry } from "../ops/deployment-adapter.js";
67
- import { createSerializedExecutor } from "../ops/serialized-executor.js";
68
- import { requestOps } from "./ops.js";
69
43
 
70
44
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
71
45
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -489,7 +463,6 @@ function createRotationEngine(password, machineHash, logFile) {
489
463
 
490
464
  const PID_FILE = path.join(os.tmpdir(), "clauth-serve.pid");
491
465
  const STAGED_PID_FILE = path.join(os.tmpdir(), "clauth-serve-staged.pid");
492
- const SUPERVISOR_PID_FILE = path.join(os.tmpdir(), "clauth-supervisor.pid");
493
466
  const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
494
467
  const LIVE_PORT = 52437;
495
468
  const STAGED_PORT = 52438;
@@ -514,74 +487,6 @@ function validateWriteToken(req, writeSession) {
514
487
  return a.length === b.length && crypto.timingSafeEqual(a, b);
515
488
  }
516
489
 
517
- export function isLoopbackAddress(remote) {
518
- return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
519
- }
520
-
521
- function summarizeSupervisorTarget(target) {
522
- if (!target || typeof target !== "object") return null;
523
- return {
524
- plugin_id: typeof target.plugin_id === "string" ? target.plugin_id : undefined,
525
- surface_id: typeof target.surface_id === "string" ? target.surface_id : undefined,
526
- tunnel_id: typeof target.tunnel_id === "string" ? target.tunnel_id : undefined,
527
- route_id: typeof target.route_id === "string" ? target.route_id : undefined,
528
- };
529
- }
530
-
531
- function summarizeSupervisorState(value) {
532
- if (!value || typeof value !== "object") return null;
533
- return {
534
- ok: typeof value.ok === "boolean" ? value.ok : undefined,
535
- state: typeof value.state === "string" ? value.state : undefined,
536
- reason: typeof value.reason === "string" ? value.reason : undefined,
537
- status: typeof value.status === "number" ? value.status : undefined,
538
- private: typeof value.private === "boolean" ? value.private : undefined,
539
- public_route: typeof value.public_route === "boolean" ? value.public_route : undefined,
540
- evidence: Array.isArray(value.evidence) ? value.evidence.map((item) => String(item).slice(0, 200)).slice(0, 5) : [],
541
- };
542
- }
543
-
544
- export function supervisorLogDto(event) {
545
- if (!event || typeof event !== "object") return { kind: "unknown" };
546
- if (event.kind === "operation") {
547
- return {
548
- ts: event.ts || event.created_at,
549
- kind: "operation",
550
- operationId: event.operationId,
551
- actor: event.actor,
552
- action: event.action,
553
- target: summarizeSupervisorTarget(event.target),
554
- resulting_state: summarizeSupervisorState(event.resulting_state),
555
- completed_at: event.completed_at,
556
- };
557
- }
558
- return {
559
- ts: event.ts,
560
- kind: event.kind,
561
- plugin_id: event.plugin_id,
562
- source: event.source,
563
- state: event.state,
564
- };
565
- }
566
-
567
- function supervisorOperationDto(receipt) {
568
- return {
569
- operationId: receipt.operationId,
570
- actor: receipt.actor,
571
- action: receipt.action,
572
- target: summarizeSupervisorTarget(receipt.target),
573
- resulting_state: summarizeSupervisorState(receipt.resulting_state),
574
- evidence: Array.isArray(receipt.evidence) ? receipt.evidence.map((item) => String(item).slice(0, 200)).slice(0, 5) : [],
575
- created_at: receipt.created_at,
576
- completed_at: receipt.completed_at,
577
- };
578
- }
579
-
580
- export function supervisorRequiresWriteToken(port = getSupervisorPort(), env = process.env) {
581
- if (port !== getSupervisorPort()) return true;
582
- return env.CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN === "1";
583
- }
584
-
585
490
  // ── PID helpers ──────────────────────────────────────────────
586
491
  function readPid() {
587
492
  try {
@@ -607,18 +512,6 @@ function writeStagedPid(pid, port) {
607
512
  fs.writeFileSync(STAGED_PID_FILE, `${pid}:${port}`, "utf8");
608
513
  }
609
514
 
610
- function readSupervisorPid() {
611
- try {
612
- const raw = fs.readFileSync(SUPERVISOR_PID_FILE, "utf8").trim();
613
- const [pid, port] = raw.split(":");
614
- return { pid: parseInt(pid, 10), port: parseInt(port, 10) };
615
- } catch { return null; }
616
- }
617
-
618
- function writeSupervisorPid(pid, port) {
619
- fs.writeFileSync(SUPERVISOR_PID_FILE, `${pid}:${port}`, "utf8");
620
- }
621
-
622
515
  function removeStagedPid() {
623
516
  try { fs.unlinkSync(STAGED_PID_FILE); } catch {}
624
517
  }
@@ -627,10 +520,6 @@ function removePid() {
627
520
  try { fs.unlinkSync(PID_FILE); } catch {}
628
521
  }
629
522
 
630
- function removeSupervisorPid() {
631
- try { fs.unlinkSync(SUPERVISOR_PID_FILE); } catch {}
632
- }
633
-
634
523
  function isProcessAlive(pid) {
635
524
  try { process.kill(pid, 0); return true; } catch { return false; }
636
525
  }
@@ -685,9 +574,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
685
574
  .btn-unlock:hover{background:#2563eb}
686
575
  .btn-unlock:disabled{background:#1e3a5f;color:#4a6fa5;cursor:not-allowed}
687
576
  .lock-err{color:#f87171;font-size:.82rem;margin-top:.75rem;min-height:1.2em}
688
- #write-unlock-overlay{display:none;position:fixed;inset:0;background:rgba(2,6,23,.72);z-index:9999;align-items:center;justify-content:center;padding:2rem}
689
- .write-unlock-cancel{width:100%;background:transparent;color:#94a3b8;border:1px solid #334155;border-radius:8px;padding:8px;font-size:.85rem;cursor:pointer;margin-top:.6rem;transition:border-color .15s}
690
- .write-unlock-cancel:hover{border-color:#64748b;color:#cbd5e1}
691
577
  /* ── Main view ── */
692
578
  #main-view{display:none;padding:2rem}
693
579
  .header{display:flex;align-items:center;gap:10px;margin-bottom:1.5rem;flex-wrap:wrap}
@@ -919,30 +805,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
919
805
  .wiz-test-result{padding:12px 14px;border-radius:8px;font-size:.85rem;margin-top:10px;display:none}
920
806
  .wiz-test-result.ok{background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.2);color:#4ade80}
921
807
  .wiz-test-result.fail{background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.2);color:#f87171}
922
- .supervisor-panel{margin:14px 16px;border:1px solid #233249;background:linear-gradient(135deg,rgba(15,23,42,.96),rgba(10,15,26,.98));border-radius:12px;padding:14px}
923
- .supervisor-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}
924
- .supervisor-title{font-size:.95rem;font-weight:700;color:#e2e8f0}
925
- .supervisor-sub{font-size:.76rem;color:#94a3b8;margin-top:3px;line-height:1.35}
926
- .supervisor-actions{display:flex;gap:8px;flex-wrap:wrap}
927
- .supervisor-action{background:#0f172a;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:6px 10px;font-size:.75rem;cursor:pointer}
928
- .supervisor-action:hover{border-color:#38bdf8;color:#e0f2fe}
929
- .supervisor-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
930
- .supervisor-card{border:1px solid #1e293b;background:rgba(2,6,23,.52);border-radius:10px;padding:10px;min-width:0}
931
- .supervisor-card h4{margin:0 0 8px;color:#cbd5e1;font-size:.75rem;letter-spacing:.08em;text-transform:uppercase}
932
- .supervisor-kpi{font-size:1.35rem;font-weight:700;color:#f8fafc}
933
- .supervisor-meta{font-size:.72rem;color:#94a3b8;font-family:'Courier New',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
934
- .supervisor-pill{display:inline-flex;align-items:center;border-radius:999px;padding:2px 7px;margin:2px 4px 2px 0;font-size:.68rem;border:1px solid #334155;color:#cbd5e1;background:#0f172a}
935
- .supervisor-pill.ok{border-color:rgba(74,222,128,.35);color:#86efac;background:rgba(34,197,94,.08)}
936
- .supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:#fde68a;background:rgba(250,204,21,.08)}
937
- .supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:#fecaca;background:rgba(248,113,113,.08)}
938
- .supervisor-list{display:flex;flex-direction:column;gap:8px;max-height:320px;overflow:auto}
939
- .supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:8px;background:rgba(15,23,42,.45)}
940
- .supervisor-row-top{display:flex;justify-content:space-between;gap:8px;align-items:center}
941
- .supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
942
- .supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
943
- .supervisor-log{font-family:'Courier New',monospace;font-size:.7rem;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow:auto}
944
- .supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:#86efac;background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
945
- .supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
946
808
  </style>
947
809
  </head>
948
810
  <body>
@@ -962,24 +824,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
962
824
  </div>
963
825
  </div>
964
826
 
965
- <!-- Write-unlock modal (replaces window.prompt(), which silently no-ops in
966
- embedded/webview browser contexts and after a browser suppresses repeated
967
- native dialogs) -->
968
- <div id="write-unlock-overlay">
969
- <div class="lock-card">
970
- <div class="lock-icon">🔒</div>
971
- <div class="lock-title">Enable writes</div>
972
- <div class="lock-sub">Enter your vault password to enable saving changes (30-minute write session)</div>
973
- <form onsubmit="submitWriteUnlock();return false;" autocomplete="on">
974
- <input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
975
- <input class="lock-input" id="write-unlock-input" type="password" placeholder="••••••••••••" autocomplete="current-password">
976
- <button class="btn-unlock" id="write-unlock-btn" type="submit">Unlock Writes</button>
977
- </form>
978
- <div class="lock-err" id="write-unlock-err"></div>
979
- <button type="button" class="write-unlock-cancel" onclick="closeWriteUnlockModal()">Cancel</button>
980
- </div>
981
- </div>
982
-
983
827
  <!-- ── Main view (shown after unlock) ──────── -->
984
828
  <div id="main-view">
985
829
  <div id="upgrade-banner" style="display:none" class="upgrade-banner">
@@ -1181,35 +1025,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1181
1025
  </div>
1182
1026
  </div>
1183
1027
 
1184
- <section class="supervisor-panel" id="supervisor-panel">
1185
- <div class="supervisor-head">
1186
- <div>
1187
- <div class="supervisor-title">Local Software Factory Supervisor</div>
1188
- <div class="supervisor-sub">clauth owns local LIFEAI plugin discovery, candidate tests, clauth-owned surface operations, receipts, and supervisor event logs. Dev Center should call this surface instead of PM2 directly.</div>
1189
- </div>
1190
- <div class="supervisor-actions">
1191
- <button class="supervisor-action" onclick="loadSupervisorCockpit()">Refresh</button>
1192
- <button class="supervisor-action" onclick="rescanSupervisorPlugins()">Rescan plugins</button>
1193
- </div>
1194
- </div>
1195
- <div id="supervisor-status" class="supervisor-sub">Loading supervisor state…</div>
1196
- <div id="supervisor-feedback" class="supervisor-feedback" role="status"></div>
1197
- <div class="supervisor-grid" style="margin-top:10px">
1198
- <div class="supervisor-card">
1199
- <h4>Surfaces</h4>
1200
- <div class="supervisor-kpi" id="supervisor-surface-count">—</div>
1201
- <div class="supervisor-meta" id="supervisor-surface-meta">destination + owner matrix</div>
1202
- <div class="supervisor-list" id="supervisor-surfaces" style="margin-top:8px"></div>
1203
- </div>
1204
- <div class="supervisor-card">
1205
- <h4>Operations + log</h4>
1206
- <div class="supervisor-kpi" id="supervisor-operation-count">—</div>
1207
- <div class="supervisor-meta" id="supervisor-log-path">events.jsonl</div>
1208
- <div class="supervisor-log" id="supervisor-events" style="margin-top:8px">No events loaded.</div>
1209
- </div>
1210
- </div>
1211
- </section>
1212
-
1213
1028
 
1214
1029
  <div class="tunnel-panel" id="webdav-panel" style="flex-direction:column;align-items:stretch;gap:8px">
1215
1030
  <div style="display:flex;align-items:center;gap:10px;width:100%">
@@ -1254,16 +1069,11 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1254
1069
  <span id="service-search-count" class="service-search-count"></span>
1255
1070
  </div>
1256
1071
  <div id="grid" class="grid"><p class="loading">Loading services…</p></div>
1257
- <div class="footer" id="originFooter">checking origin… · 10-strike lockout</div>
1072
+ <div class="footer">localhost:${port} · 127.0.0.1 only · 10-strike lockout</div>
1258
1073
  </div>
1259
1074
 
1260
1075
  <script>
1261
- const BASE = location.origin;
1262
- (function reportOrigin() {
1263
- const isLocal = /^(127\\.0\\.0\\.1|localhost|\\[::1\\])$/.test(location.hostname);
1264
- const el = document.getElementById("originFooter");
1265
- if (el) el.textContent = (isLocal ? "LOCAL" : "REMOTE") + " · " + BASE + " · 10-strike lockout";
1266
- })();
1076
+ const BASE = "http://127.0.0.1:${port}";
1267
1077
 
1268
1078
  const SERVICE_HINTS = {
1269
1079
  "neo4j": "neo4j+s://username:password@instance.databases.neo4j.io",
@@ -1495,81 +1305,9 @@ function showMain(ping) {
1495
1305
  pollTunnel();
1496
1306
  loadWebdavMounts();
1497
1307
  updateBuildStatus();
1498
- loadSupervisorCockpit();
1499
1308
  refreshWriteLockUi();
1500
1309
  }
1501
1310
 
1502
- function supervisorBadge(text, kind) {
1503
- return '<span class="supervisor-pill ' + (kind || '') + '">' + htmlEscape(text) + '</span>';
1504
- }
1505
-
1506
- async function supervisorJson(path, options) {
1507
- const response = await fetch(BASE + path, { cache: "no-store", ...(options || {}) });
1508
- const data = await response.json().catch(() => ({}));
1509
- if (!response.ok || data.error) throw new Error(data.error || ("HTTP " + response.status));
1510
- return data;
1511
- }
1512
-
1513
- function supervisorFeedback(text, bad) {
1514
- const el = document.getElementById("supervisor-feedback");
1515
- if (!el) return;
1516
- el.textContent = text || "";
1517
- el.className = "supervisor-feedback" + (bad ? " bad" : "");
1518
- el.style.display = text ? "block" : "none";
1519
- }
1520
-
1521
- async function loadSupervisorCockpit() {
1522
- const status = document.getElementById("supervisor-status");
1523
- if (status) status.textContent = "Loading supervisor state…";
1524
- try {
1525
- const [health, surfaces, logs] = await Promise.all([
1526
- supervisorJson("/health"),
1527
- supervisorJson("/v1/surfaces"),
1528
- supervisorJson("/v1/logs?limit=40"),
1529
- ]);
1530
- const surfaceRows = surfaces.surfaces || [];
1531
- document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length);
1532
- document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
1533
- document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
1534
- document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
1535
- document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.length ? surfaceRows.map(renderSupervisorSurface).join("") : '<div class="supervisor-sub">No surfaces declared.</div>';
1536
- document.getElementById("supervisor-events").textContent = (logs.events || []).slice(-20).reverse().map(e => {
1537
- const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1538
- return (e.ts || e.created_at || "") + " " + label;
1539
- }).join("\\n") || "No events yet.";
1540
- if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
1541
- } catch (err) {
1542
- if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
1543
- supervisorFeedback("Supervisor unavailable — actions are disabled until the local control plane returns.", true);
1544
- }
1545
- }
1546
-
1547
- function renderSupervisorSurface(surface) {
1548
- const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
1549
- const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
1550
- return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) + '</div>' +
1551
- '<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
1552
- '<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
1553
- '<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
1554
- }
1555
-
1556
- async function rescanSupervisorPlugins() {
1557
- try {
1558
- await supervisorJson("/v1/plugins/rescan", { method: "POST", headers: writeHeaders() });
1559
- supervisorFeedback("Plugin rescan completed.", false);
1560
- await loadSupervisorCockpit();
1561
- } catch (err) { supervisorFeedback("Plugin rescan failed: " + (err.message || err), true); }
1562
- }
1563
-
1564
- async function runSupervisorSurface(id, action) {
1565
- try {
1566
- const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
1567
- const result = receipt.resulting_state || {};
1568
- supervisorFeedback("Surface " + action + " receipt " + (receipt.operationId || "recorded") + " · " + (result.state || "completed"), result.ok === false);
1569
- await loadSupervisorCockpit();
1570
- } catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
1571
- }
1572
-
1573
1311
  // ── Unlock ──────────────────────────────────
1574
1312
  async function unlock() {
1575
1313
  const input = document.getElementById("lock-input");
@@ -1651,51 +1389,21 @@ async function lockVault() {
1651
1389
  // ── Unlock writes (re-establish write scope without locking) ──
1652
1390
  // Needed when the daemon auto-unlocks via --pw/boot.key: the page never sees the
1653
1391
  // unlock screen, so it holds no write token. POST /auth mints one (10-min TTL).
1654
- function unlockWrites() {
1655
- openWriteUnlockModal();
1656
- }
1657
-
1658
- function openWriteUnlockModal() {
1659
- const overlay = document.getElementById("write-unlock-overlay");
1660
- const input = document.getElementById("write-unlock-input");
1661
- const err = document.getElementById("write-unlock-err");
1662
- if (err) err.textContent = "";
1663
- if (input) { input.value = ""; input.className = "lock-input"; }
1664
- if (overlay) overlay.style.display = "flex";
1665
- if (input) setTimeout(() => input.focus(), 50);
1666
- }
1667
-
1668
- function closeWriteUnlockModal() {
1669
- const overlay = document.getElementById("write-unlock-overlay");
1670
- if (overlay) overlay.style.display = "none";
1671
- }
1672
-
1673
- async function submitWriteUnlock() {
1674
- const input = document.getElementById("write-unlock-input");
1675
- const btn = document.getElementById("write-unlock-btn");
1676
- const err = document.getElementById("write-unlock-err");
1677
- const pw = input ? input.value : "";
1678
- if (!pw) { if (err) err.textContent = "Password is required."; return; }
1679
- if (btn) { btn.disabled = true; btn.textContent = "Verifying..."; }
1392
+ async function unlockWrites() {
1393
+ if (writeToken && !confirm("Writes are already unlocked this session. Re-unlock?")) return;
1394
+ const pw = prompt("Enter your vault password to enable saving changes (10-minute write session):");
1395
+ if (!pw) return;
1680
1396
  try {
1681
1397
  const r = await fetch(BASE + "/auth", {
1682
1398
  method: "POST",
1683
1399
  headers: { "Content-Type": "application/json" },
1684
1400
  body: JSON.stringify({ password: pw }),
1685
1401
  }).then(r => r.json());
1686
- if (r.error) {
1687
- if (input) { input.className = "lock-input error"; setTimeout(() => input.className = "lock-input", 600); }
1688
- if (err) err.textContent = "Invalid: " + (r.error || "Invalid password");
1689
- return;
1690
- }
1402
+ if (r.error) { alert("Unlock failed: " + r.error); return; }
1691
1403
  writeToken = r.write_token || null;
1692
1404
  refreshWriteLockUi();
1693
- closeWriteUnlockModal();
1694
- } catch (e) {
1695
- if (err) err.textContent = "Unlock error: " + (e.message || e);
1696
- } finally {
1697
- if (btn) { btn.disabled = false; btn.textContent = "Unlock Writes"; }
1698
- }
1405
+ alert(writeToken ? "Writes unlocked for 10 minutes." : "Unlock did not return a write token.");
1406
+ } catch (e) { alert("Unlock error: " + (e.message || e)); }
1699
1407
  }
1700
1408
 
1701
1409
  // Reflect write-lock state on the button so it is obvious when a save will fail.
@@ -4062,187 +3770,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4062
3770
  });
4063
3771
  },
4064
3772
  });
4065
- const isSupervisorPort = port === getSupervisorPort();
4066
- const supervisorTestNoToken = process.env.CLAUTH_SUPERVISOR_TEST_NO_TOKEN === "1";
4067
- const opsAdapter = createPm2Adapter(pm2);
4068
- const executePm2 = createSerializedExecutor();
4069
- const opsPolicy = createOperationPolicy({
4070
- enabled: String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).filter(Boolean),
4071
- applications: (() => { try { return JSON.parse(process.env.CLAUTH_OPS_APPLICATIONS || "{}"); } catch { return {}; } })(),
4072
- adminEnabled: String(process.env.CLAUTH_OPS_ADMIN_ENABLED || "").split(",").map((item) => item.trim()).filter(Boolean),
4073
- adminApplications: (() => { try { return JSON.parse(process.env.CLAUTH_OPS_ADMIN_APPLICATIONS || "{}"); } catch { return {}; } })(),
4074
- allowHostWide: process.env.CLAUTH_OPS_ALLOW_HOST_WIDE === "1",
4075
- });
4076
- const opsJobs = createJobStore({
4077
- filePath: process.env.CLAUTH_OPS_JOB_STORE_PATH || path.join(getSupervisorDir(), "ops-jobs.json"),
4078
- });
4079
-
4080
- async function getLoopbackSecret(service) {
4081
- const response = await fetch(`http://127.0.0.1:${LIVE_PORT}/v/${encodeURIComponent(service)}`, { signal: AbortSignal.timeout(5000) });
4082
- if (!response.ok) throw new Error(`${service} unavailable from local clauth`);
4083
- const value = (await response.text()).trim();
4084
- if (!value) throw new Error(`${service} is empty`);
4085
- return value;
4086
- }
4087
- const coolify = createCoolifyAdapter({
4088
- baseUrl: process.env.CLAUTH_COOLIFY_BASE_URL || "https://deploy.regendevcorp.com",
4089
- getToken: () => getLoopbackSecret("coolify-api"),
4090
- });
4091
- const deployments = (() => { try { return parseDeploymentRegistry(process.env.CLAUTH_OPS_DEPLOYMENTS || "{}"); } catch { return {}; } })();
4092
- const deploymentAdapter = createDeploymentAdapter({
4093
- deployments,
4094
- reload: async (target) => {
4095
- await executePm2(async () => {
4096
- await opsAdapter.connect();
4097
- try { await opsAdapter.execute("reload", { target, options: { updateEnv: true } }); } finally { await opsAdapter.disconnect(); }
4098
- });
4099
- },
4100
- });
4101
-
4102
- /**
4103
- * Record an ops failure's upstream message to the LOCAL log only.
4104
- *
4105
- * job-store's sanitizer deliberately drops free-form `error` text so an
4106
- * upstream message cannot carry a credential into the persisted job file or
4107
- * the API response. That protection left every failure with an empty detail,
4108
- * so jobs reported `failed` with no reason at all. Jobs now carry an
4109
- * enumerated `code`; the underlying message goes here, to the same
4110
- * operator-only log as the rest of the daemon's diagnostics.
4111
- */
4112
- function logOpsFailure(kind, operation, error) {
4113
- const message = String(error?.message || error || "unknown");
4114
- try {
4115
- fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] [OPS ${kind}/${operation}] ${message}\n`);
4116
- } catch {}
4117
- }
4118
-
4119
- async function opsBearerRole(req) {
4120
- const header = req.headers.authorization;
4121
- const supplied = Array.isArray(header) ? header[0] : header;
4122
- if (!supplied || !String(supplied).startsWith("Bearer ")) return null;
4123
- const actual = String(supplied).slice(7).trim();
4124
- let admin; let agent;
4125
- try { admin = await getLoopbackSecret(process.env.CLAUTH_OPS_ADMIN_TOKEN_SERVICE || "vultr-ops-admin-token"); } catch {}
4126
- try { agent = await getLoopbackSecret(process.env.CLAUTH_OPS_AGENT_TOKEN_SERVICE || "vultr-ops-api-token"); } catch {}
4127
- if (admin && agent && admin === agent) return null;
4128
- for (const [role, expected] of [["admin", admin], ["agent", agent]]) {
4129
- if (!expected) continue;
4130
- const a = Buffer.from(actual); const b = Buffer.from(expected);
4131
- if (a.length === b.length && crypto.timingSafeEqual(a, b)) return role;
4132
- }
4133
- return null;
4134
- }
4135
-
4136
- async function requireOpsBearer(req, res) {
4137
- const role = await opsBearerRole(req);
4138
- if (role) { req._opsRole = role; return true; }
4139
- res.writeHead(401, { "Content-Type": "application/json", ...CORS });
4140
- res.end(JSON.stringify({ error: "ops_bearer_required" }));
4141
- return false;
4142
- }
4143
-
4144
- function submitOpsJob(operation, input, role = "agent") {
4145
- const authorization = opsPolicy.authorize(operation, input, role);
4146
- const job = opsJobs.create({ kind: "pm2", operation, target: input.target || input.name || null });
4147
- if (!authorization.ok) {
4148
- return opsJobs.event(job.id, "rejected", { code: authorization.code });
4149
- }
4150
- void (async () => {
4151
- opsJobs.event(job.id, "running");
4152
- try {
4153
- const result = await executePm2(async () => {
4154
- await opsAdapter.connect();
4155
- try { return await opsAdapter.execute(operation, input); } finally { await opsAdapter.disconnect(); }
4156
- });
4157
- opsJobs.event(job.id, "succeeded", operationReceipt(operation, result, authorization.allowed_targets || []));
4158
- } catch (error) {
4159
- // `error` alone is dropped by job-store's sanitizer (it refuses
4160
- // free-form upstream text so a credential cannot ride along), which
4161
- // left every failure with an empty detail. Emit an enumerated code so
4162
- // the failure has a reason; keep the message for the local log only.
4163
- logOpsFailure("pm2", operation, error);
4164
- opsJobs.event(job.id, "failed", { code: "pm2_operation_failed" });
4165
- }
4166
- })();
4167
- return opsJobs.get(job.id);
4168
- }
4169
-
4170
- function operationReceipt(operation, result, allowedTargets) {
4171
- if (["list", "describe", "logs"].includes(operation)) {
4172
- const processes = Array.isArray(result)
4173
- ? result.filter((process) => allowedTargets.includes("*") || allowedTargets.includes(process?.name))
4174
- : [];
4175
- return { processes };
4176
- }
4177
- if (operation === "ping") return { status: "connected" };
4178
- return { status: "completed" };
4179
- }
4180
-
4181
- function submitPromotionJob(applicationUuid) {
4182
- const job = opsJobs.create({ kind: "coolify", operation: "promote", target: applicationUuid });
4183
- const enabled = String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).includes("coolify_promote");
4184
- const allowlist = (() => { try { return JSON.parse(process.env.CLAUTH_COOLIFY_PROMOTE_UUIDS || "[]"); } catch { return []; } })();
4185
- if (!enabled || !Array.isArray(allowlist) || !allowlist.includes(applicationUuid)) {
4186
- return opsJobs.event(job.id, "rejected", { code: "service_not_available" });
4187
- }
4188
- void (async () => {
4189
- opsJobs.event(job.id, "running");
4190
- try {
4191
- const deployment = await coolify.promote(applicationUuid);
4192
- // Coolify answers with a `deployments` ARRAY, not a flat object — see
4193
- // deploymentUuidFrom. Reading the flat field alone marked the job failed
4194
- // while the deployment was actually running.
4195
- const deploymentUuid = deploymentUuidFrom(deployment);
4196
- if (!deploymentUuid) {
4197
- // The deploy request itself SUCCEEDED (no throw); only the UUID was
4198
- // unreadable, so the deployment may well be RUNNING. The code says so
4199
- // explicitly rather than a bare "failed", because a plain failure
4200
- // invites a retry and a duplicate production deploy.
4201
- //
4202
- // An enumerated code, not a free-form error: job-store's sanitizer
4203
- // drops `error` on purpose to keep upstream text (and any credential
4204
- // inside it) out of the persisted job.
4205
- return opsJobs.event(job.id, "failed", { code: "coolify_deploy_accepted_uuid_unreadable" });
4206
- }
4207
- opsJobs.event(job.id, "waiting", { deployment_uuid: deploymentUuid });
4208
- const terminal = await coolify.poll(deploymentUuid, { attempts: Number(process.env.CLAUTH_COOLIFY_POLL_ATTEMPTS || 60), delay: () => new Promise((resolve) => setTimeout(resolve, 5000)) });
4209
- opsJobs.event(job.id, terminal.state === "succeeded" ? "succeeded" : terminal.state, { deployment_uuid: deploymentUuid, status: terminal.deployment?.status || null });
4210
- } catch (error) {
4211
- // The throw may have happened AFTER Coolify accepted the deploy (e.g.
4212
- // the poll lost the network), so this is not proof nothing shipped.
4213
- logOpsFailure("coolify", "promote", error);
4214
- opsJobs.event(job.id, "failed", { code: "coolify_promote_failed" });
4215
- }
4216
- })();
4217
- return opsJobs.get(job.id);
4218
- }
4219
-
4220
- function submitDeploymentJob(application, ref) {
4221
- const job = opsJobs.create({ kind: "deployment", operation: "deploy", target: application });
4222
- const enabled = String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).includes("deploy");
4223
- if (!enabled || !deployments[application]) return opsJobs.event(job.id, "rejected", { code: "service_not_available" });
4224
- void (async () => {
4225
- opsJobs.event(job.id, "running");
4226
- try { opsJobs.event(job.id, "building"); opsJobs.event(job.id, "succeeded", { result: await deploymentAdapter.deploy({ application, ref }) }); }
4227
- catch (error) { logOpsFailure("deployment", "deploy", error); opsJobs.event(job.id, "failed", { code: "deployment_failed" }); }
4228
- })();
4229
- return opsJobs.get(job.id);
4230
- }
4231
-
4232
- function hasSupervisorWrite(req) {
4233
- if (validateWriteToken(req, writeSession)) return true;
4234
- if (!isSupervisorPort) return false;
4235
- if (!isLoopbackAddress(req.socket?.remoteAddress)) return false;
4236
- return supervisorTestNoToken || !supervisorRequiresWriteToken(port);
4237
- }
4238
-
4239
- function rejectSupervisorWrite(res) {
4240
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
4241
- return res.end(JSON.stringify({
4242
- error: "write_token_required",
4243
- hint: "Unlock clauth writes, unset CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN for localhost supervisor operations, or set CLAUTH_SUPERVISOR_TEST_NO_TOKEN=1 for temporary localhost-only supervisor tests.",
4244
- }));
4245
- }
4246
3773
 
4247
3774
  // ── MCP SSE session tracking ──────────────────────────────
4248
3775
  const sseSessions = new Map(); // sessionId → { res, initialized }
@@ -4783,7 +4310,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4783
4310
 
4784
4311
  const server = http.createServer(async (req, res) => {
4785
4312
  const remote = req.socket.remoteAddress;
4786
- const isLocal = isLoopbackAddress(remote);
4313
+ const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
4787
4314
 
4788
4315
  const url = new URL(req.url, `http://127.0.0.1:${port}`);
4789
4316
  const reqPath = url.pathname;
@@ -4823,205 +4350,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4823
4350
  return res.end(JSON.stringify(result));
4824
4351
  }
4825
4352
 
4826
- if (method === "GET" && reqPath === "/health") {
4827
- return ok(res, { ...supervisorHealth(), listening_port: port, process_id: process.pid, vault_locked: !password, clauth_version: VERSION });
4828
- }
4829
-
4830
- // Bearer-gated remote operations surface. It remains loopback-only at this
4831
- // layer; ingress/tunnel policy decides whether it is reachable remotely.
4832
- if (method === "GET" && reqPath === "/v1/ops/catalog") {
4833
- if (!await requireOpsBearer(req, res)) return;
4834
- return ok(res, { schema: "clauth.ops.v1", operations: PM2_OPERATION_CATALOG });
4835
- }
4836
-
4837
- if (method === "GET" && reqPath === "/v1/ops/processes") {
4838
- if (!await requireOpsBearer(req, res)) return;
4839
- const job = submitOpsJob("list", {}, req._opsRole);
4840
- res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
4841
- return res.end(JSON.stringify(job));
4842
- }
4843
-
4844
- const opsProcessMatch = reqPath.match(/^\/v1\/ops\/processes\/([^/]+)$/);
4845
- if (method === "GET" && opsProcessMatch) {
4846
- if (!await requireOpsBearer(req, res)) return;
4847
- const job = submitOpsJob("describe", { target: decodeURIComponent(opsProcessMatch[1]) }, req._opsRole);
4848
- res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
4849
- return res.end(JSON.stringify(job));
4850
- }
4851
-
4852
- if (method === "POST" && reqPath === "/v1/ops/operations") {
4853
- if (!await requireOpsBearer(req, res)) return;
4854
- let body;
4855
- try { body = await readBody(req); } catch {
4856
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4857
- return res.end(JSON.stringify({ error: "invalid_json" }));
4858
- }
4859
- const operation = String(body?.operation || "");
4860
- if (!PM2_OPERATION_CATALOG[operation]) {
4861
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4862
- return res.end(JSON.stringify({ error: "unknown_operation" }));
4863
- }
4864
- const job = submitOpsJob(operation, body?.input && typeof body.input === "object" ? body.input : {}, req._opsRole);
4865
- res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
4866
- return res.end(JSON.stringify(job));
4867
- }
4868
-
4869
- if (method === "POST" && reqPath === "/v1/ops/promotions") {
4870
- if (!await requireOpsBearer(req, res)) return;
4871
- let body;
4872
- try { body = await readBody(req); } catch {
4873
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4874
- return res.end(JSON.stringify({ error: "invalid_json" }));
4875
- }
4876
- const applicationUuid = String(body?.application_uuid || "").trim();
4877
- if (!applicationUuid) {
4878
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4879
- return res.end(JSON.stringify({ error: "application_uuid_required" }));
4880
- }
4881
- const job = submitPromotionJob(applicationUuid);
4882
- res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
4883
- return res.end(JSON.stringify(job));
4884
- }
4885
-
4886
- if (method === "POST" && reqPath === "/v1/ops/deployments") {
4887
- if (!await requireOpsBearer(req, res)) return;
4888
- let body;
4889
- try { body = await readBody(req); } catch {
4890
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4891
- return res.end(JSON.stringify({ error: "invalid_json" }));
4892
- }
4893
- const application = String(body?.application || "").trim();
4894
- if (!application) {
4895
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4896
- return res.end(JSON.stringify({ error: "application_required" }));
4897
- }
4898
- const job = submitDeploymentJob(application, body?.ref ? String(body.ref) : undefined);
4899
- res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
4900
- return res.end(JSON.stringify(job));
4901
- }
4902
-
4903
- const opsJobMatch = reqPath.match(/^\/v1\/ops\/jobs\/([^/]+)$/);
4904
- if (method === "GET" && opsJobMatch) {
4905
- if (!await requireOpsBearer(req, res)) return;
4906
- const job = opsJobs.get(decodeURIComponent(opsJobMatch[1]));
4907
- res.writeHead(job ? 200 : 404, { "Content-Type": "application/json", ...CORS });
4908
- return res.end(JSON.stringify(job || { error: "job_not_found" }));
4909
- }
4910
-
4911
- const opsJobEventsMatch = reqPath.match(/^\/v1\/ops\/jobs\/([^/]+)\/events$/);
4912
- if (method === "GET" && opsJobEventsMatch) {
4913
- if (!await requireOpsBearer(req, res)) return;
4914
- const jobId = decodeURIComponent(opsJobEventsMatch[1]);
4915
- if (!opsJobs.get(jobId)) {
4916
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
4917
- return res.end(JSON.stringify({ error: "job_not_found" }));
4918
- }
4919
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", ...CORS });
4920
- const unsubscribe = opsJobs.subscribe(jobId, (job) => {
4921
- if (!res.writableEnded) res.write(`event: job\ndata: ${JSON.stringify(job)}\n\n`);
4922
- });
4923
- req.on("close", unsubscribe);
4924
- return;
4925
- }
4926
-
4927
- if (method === "GET" && reqPath === "/v1/plugins") {
4928
- return ok(res, { plugins: listPlugins() });
4929
- }
4930
-
4931
- if (method === "POST" && reqPath === "/v1/plugins/rescan") {
4932
- if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
4933
- return ok(res, discoverPlugins());
4934
- }
4935
-
4936
- const pluginEnableMatch = reqPath.match(/^\/v1\/plugins\/([^/]+)\/(enable|disable|test|promote)$/);
4937
- if (method === "POST" && pluginEnableMatch) {
4938
- if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
4939
- const pluginId = decodeURIComponent(pluginEnableMatch[1]);
4940
- const op = pluginEnableMatch[2];
4941
- const result = op === "enable"
4942
- ? setPluginEnabled(pluginId, true)
4943
- : op === "disable"
4944
- ? setPluginEnabled(pluginId, false)
4945
- : runPluginAction(pluginId, op);
4946
- res.writeHead(result.error ? 400 : 200, { "Content-Type": "application/json", ...CORS });
4947
- return res.end(JSON.stringify(result));
4948
- }
4949
-
4950
- if (method === "GET" && reqPath === "/v1/surfaces") {
4951
- return ok(res, { surfaces: listSurfaces() });
4952
- }
4953
-
4954
- const surfaceActionMatch = reqPath.match(/^\/v1\/surfaces\/([^/]+)\/actions$/);
4955
- if (method === "POST" && surfaceActionMatch) {
4956
- if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
4957
- let body;
4958
- try { body = await readBody(req); } catch {
4959
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4960
- return res.end(JSON.stringify({ error: "Invalid JSON" }));
4961
- }
4962
- const result = runSurfaceAction(decodeURIComponent(surfaceActionMatch[1]), body?.action || "reconcile");
4963
- res.writeHead(result.error ? 400 : 200, { "Content-Type": "application/json", ...CORS });
4964
- return res.end(JSON.stringify(result));
4965
- }
4966
-
4967
- if (method === "GET" && reqPath === "/v1/routes") {
4968
- return ok(res, { routes: listRoutes() });
4969
- }
4970
-
4971
- if (method === "GET" && reqPath === "/v1/tunnels") {
4972
- return ok(res, { tunnels: listTunnels() });
4973
- }
4974
-
4975
- if (method === "GET" && reqPath === "/v1/logs") {
4976
- const limit = Number(url.searchParams.get("limit") || 100);
4977
- const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 100;
4978
- const state = loadSupervisorState();
4979
- return ok(res, {
4980
- schema: "clauth.supervisor.logs.v1",
4981
- log_path: path.join(getSupervisorDir(), "events.jsonl"),
4982
- events: readSupervisorEvents(boundedLimit).map(supervisorLogDto),
4983
- operations: (state.operations || []).slice(0, boundedLimit).map(supervisorOperationDto),
4984
- });
4985
- }
4986
-
4987
- const tunnelRoutesMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes$/);
4988
- if (method === "POST" && tunnelRoutesMatch) {
4989
- if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
4990
- let body;
4991
- try { body = await readBody(req); } catch {
4992
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4993
- return res.end(JSON.stringify({ error: "Invalid JSON" }));
4994
- }
4995
- return ok(res, addTunnelRoute(decodeURIComponent(tunnelRoutesMatch[1]), body, "localhost"));
4996
- }
4997
-
4998
- const tunnelRouteDeleteMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes\/([^/]+)$/);
4999
- if (method === "DELETE" && tunnelRouteDeleteMatch) {
5000
- if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
5001
- return ok(res, removeTunnelRoute(decodeURIComponent(tunnelRouteDeleteMatch[1]), decodeURIComponent(tunnelRouteDeleteMatch[2]), "localhost"));
5002
- }
5003
-
5004
- if (method === "GET" && reqPath.startsWith("/v1/operations/")) {
5005
- const id = decodeURIComponent(reqPath.split("/").pop());
5006
- const operation = (supervisorHealth(), readSupervisorEvents(500)).find((event) => event.operationId === id);
5007
- res.writeHead(operation ? 200 : 404, { "Content-Type": "application/json", ...CORS });
5008
- return res.end(JSON.stringify(operation ? supervisorLogDto(operation) : { error: "operation_not_found" }));
5009
- }
5010
-
5011
- if (method === "GET" && reqPath === "/v1/events") {
5012
- res.writeHead(200, {
5013
- "Content-Type": "text/event-stream",
5014
- "Cache-Control": "no-cache",
5015
- "Connection": "keep-alive",
5016
- ...CORS,
5017
- });
5018
- for (const event of readSupervisorEvents(Number(url.searchParams.get("limit") || 100))) {
5019
- res.write(`event: supervisor\ndata: ${JSON.stringify(supervisorLogDto(event))}\n\n`);
5020
- }
5021
- res.end();
5022
- return;
5023
- }
5024
-
5025
4353
  // ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
5026
4354
  const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
5027
4355
  const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
@@ -8137,27 +7465,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
8137
7465
  });
8138
7466
  }
8139
7467
 
8140
- // The localhost supervisor is the only process allowed to repair clauth-owned
8141
- // local surfaces. Keep this loop out of the vault/staged instances and make
8142
- // the cadence configurable for deterministic tests.
8143
- if (port === getSupervisorPort() && process.env.CLAUTH_SUPERVISOR_HEALTH_RECONCILE !== "0") {
8144
- const configuredInterval = Number(process.env.CLAUTH_SUPERVISOR_HEALTH_INTERVAL_MS || 10000);
8145
- const intervalMs = Number.isFinite(configuredInterval) ? Math.max(1000, Math.min(configuredInterval, 300000)) : 10000;
8146
- let healthReconcileInFlight = false;
8147
- const runHealthReconcile = () => {
8148
- if (healthReconcileInFlight) return;
8149
- healthReconcileInFlight = true;
8150
- reconcileSurfaceHealth().catch((err) => {
8151
- try { fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] supervisor health reconcile failed: ${err.message}\n`); } catch {}
8152
- }).finally(() => { healthReconcileInFlight = false; });
8153
- };
8154
- const healthTimer = setInterval(runHealthReconcile, intervalMs);
8155
- healthTimer.unref?.();
8156
- server.__supervisorHealthTimer = healthTimer;
8157
- server.on("close", () => clearInterval(healthTimer));
8158
- setImmediate(runHealthReconcile);
8159
- }
8160
-
8161
7468
  return server;
8162
7469
  }
8163
7470
 
@@ -8173,61 +7480,6 @@ async function verifyAuth(password) {
8173
7480
  }
8174
7481
  }
8175
7482
 
8176
- async function supervisorResponds(port = getSupervisorPort()) {
8177
- try {
8178
- const resp = await fetch(`http://127.0.0.1:${port}/health`);
8179
- return resp.ok;
8180
- } catch {
8181
- return false;
8182
- }
8183
- }
8184
-
8185
- async function ensureSupervisorStarted(cliEntry) {
8186
- const port = getSupervisorPort();
8187
- const existing = readSupervisorPid();
8188
- if (existing && isProcessAlive(existing.pid) && await supervisorResponds(existing.port)) {
8189
- return { started: false, pid: existing.pid, port: existing.port, state: "already_running" };
8190
- }
8191
- if (existing && !isProcessAlive(existing.pid)) removeSupervisorPid();
8192
- if (await supervisorResponds(port)) {
8193
- return { started: false, pid: existing?.pid || null, port, state: "port_already_live" };
8194
- }
8195
-
8196
- const out = fs.openSync(LOG_FILE, "a");
8197
- const child = spawn(process.execPath, [cliEntry, "serve", "supervisor", "--port", String(port)], {
8198
- detached: true,
8199
- stdio: ["ignore", out, out],
8200
- env: { ...process.env, __CLAUTH_SUPERVISOR_DAEMON: "1" },
8201
- });
8202
- child.unref();
8203
- writeSupervisorPid(child.pid, port);
8204
-
8205
- for (let attempt = 0; attempt < 5; attempt++) {
8206
- await new Promise(r => setTimeout(r, 500));
8207
- if (await supervisorResponds(port)) {
8208
- return { started: true, pid: child.pid, port, state: "started" };
8209
- }
8210
- }
8211
- return { started: true, pid: child.pid, port, state: "start_unverified" };
8212
- }
8213
-
8214
- async function stopSupervisorSibling() {
8215
- const info = readSupervisorPid();
8216
- if (!info) return null;
8217
- if (!isProcessAlive(info.pid)) {
8218
- removeSupervisorPid();
8219
- return { stopped: false, pid: info.pid, port: info.port, state: "stale" };
8220
- }
8221
- try {
8222
- process.kill(info.pid, "SIGTERM");
8223
- await new Promise(r => setTimeout(r, 300));
8224
- removeSupervisorPid();
8225
- return { stopped: true, pid: info.pid, port: info.port, state: "stopped" };
8226
- } catch (err) {
8227
- return { stopped: false, pid: info.pid, port: info.port, state: "stop_failed", error: err.message };
8228
- }
8229
- }
8230
-
8231
7483
  async function actionStart(opts) {
8232
7484
  if (opts.isolated) {
8233
7485
  return actionForeground(opts);
@@ -8429,11 +7681,6 @@ async function actionStart(opts) {
8429
7681
  console.log(chalk.gray(` Port: 127.0.0.1:${info.port}`));
8430
7682
  console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
8431
7683
  console.log(chalk.gray(` Log: ${LOG_FILE}`));
8432
- if (!isStaged) {
8433
- const supervisor = await ensureSupervisorStarted(cliEntry);
8434
- const verb = supervisor.started ? "started" : "available";
8435
- console.log(chalk.gray(` Supervisor: ${verb} on 127.0.0.1:${supervisor.port}${supervisor.pid ? ` (PID ${supervisor.pid})` : ""}`));
8436
- }
8437
7684
  if (isStaged) {
8438
7685
  console.log(chalk.yellow(`\n ⚡ Staged on port ${port} — open dashboard to verify, then click "Make Live"`));
8439
7686
  } else if (!password) {
@@ -8451,7 +7698,6 @@ async function actionStart(opts) {
8451
7698
  async function actionStop() {
8452
7699
  const info = readPid();
8453
7700
  if (!info) {
8454
- await stopSupervisorSibling();
8455
7701
  console.log(chalk.yellow("\n No clauth serve PID file found — not running.\n"));
8456
7702
  return;
8457
7703
  }
@@ -8459,7 +7705,6 @@ async function actionStop() {
8459
7705
  if (!isProcessAlive(info.pid)) {
8460
7706
  console.log(chalk.yellow(`\n PID ${info.pid} is not running (stale PID file). Cleaning up.\n`));
8461
7707
  removePid();
8462
- await stopSupervisorSibling();
8463
7708
  return;
8464
7709
  }
8465
7710
 
@@ -8470,7 +7715,6 @@ async function actionStop() {
8470
7715
  await new Promise(r => setTimeout(r, 300));
8471
7716
  console.log(chalk.green(`\n 🛑 clauth serve stopped (was PID ${info.pid}, port ${info.port})\n`));
8472
7717
  removePid();
8473
- await stopSupervisorSibling();
8474
7718
  return;
8475
7719
  }
8476
7720
  } catch {}
@@ -8484,7 +7728,6 @@ async function actionStop() {
8484
7728
  console.log(chalk.yellow(`\n Could not kill PID ${info.pid}: ${err.message}\n`));
8485
7729
  }
8486
7730
  removePid();
8487
- await stopSupervisorSibling();
8488
7731
  }
8489
7732
 
8490
7733
  async function actionPing() {
@@ -8529,9 +7772,7 @@ async function actionRestart(opts) {
8529
7772
  async function actionForeground(opts) {
8530
7773
  const port = parseInt(opts.port || "52437", 10);
8531
7774
  const isolated = !!opts.isolated;
8532
- const containerPassword = process.env.CLAUTH_MASTER_PASSWORD || process.env["clauth-master-password"] || null;
8533
- const password = isolated ? null : (opts.pw || containerPassword);
8534
- const bindHost = process.env.CLAUTH_BIND_HOST || "127.0.0.1";
7775
+ const password = isolated ? null : (opts.pw || null);
8535
7776
  const tunnelHostname = opts.tunnel || null;
8536
7777
  const whitelist = opts.services
8537
7778
  ? opts.services.split(",").map(s => s.trim().toLowerCase())
@@ -8557,14 +7798,14 @@ async function actionForeground(opts) {
8557
7798
  console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
8558
7799
  }
8559
7800
 
8560
- console.log(chalk.gray(` Port: ${bindHost}:${port}`));
7801
+ console.log(chalk.gray(` Port: 127.0.0.1:${port}`));
8561
7802
  console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
8562
7803
  console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
8563
7804
 
8564
7805
  const server = createServer(password, whitelist, port, tunnelHostname);
8565
- server.listen(port, bindHost, () => {
7806
+ server.listen(port, "127.0.0.1", () => {
8566
7807
  if (!isolated) writePid(process.pid, port);
8567
- console.log(chalk.green(` clauth serve → http://${bindHost}:${port}`));
7808
+ console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
8568
7809
  if (tunnelHostname) {
8569
7810
  console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
8570
7811
  console.log("");
@@ -11641,41 +10882,6 @@ const MCP_TOOLS = [
11641
10882
  additionalProperties: false,
11642
10883
  },
11643
10884
  },
11644
- {
11645
- name: "clauth_ops_catalog",
11646
- description: "Return the Vultr deployment-control API catalog. Availability is still decided on Vultr.",
11647
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
11648
- },
11649
- {
11650
- name: "clauth_ops_processes",
11651
- description: "Submit a scoped PM2 process-status query to the Vultr control plane. Only server-approved applications are returned.",
11652
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
11653
- },
11654
- {
11655
- name: "clauth_ops_describe",
11656
- description: "Submit a scoped PM2 status query for one server-approved application.",
11657
- inputSchema: { type: "object", properties: { application: { type: "string" } }, required: ["application"], additionalProperties: false },
11658
- },
11659
- {
11660
- name: "clauth_ops_deploy",
11661
- description: "Submit one manifest-scoped development deployment to Vultr. The server chooses the repository, build, PM2 process, and allowed ref.",
11662
- inputSchema: { type: "object", properties: { application: { type: "string" }, ref: { type: "string" } }, required: ["application"], additionalProperties: false },
11663
- },
11664
- {
11665
- name: "clauth_ops_promote",
11666
- description: "Submit one server-allowlisted Coolify promotion and return its asynchronous job receipt.",
11667
- inputSchema: { type: "object", properties: { application_uuid: { type: "string" } }, required: ["application_uuid"], additionalProperties: false },
11668
- },
11669
- {
11670
- name: "clauth_ops_job",
11671
- description: "Read the terminal or in-progress receipt for a deployment-control job.",
11672
- inputSchema: { type: "object", properties: { job_id: { type: "string" } }, required: ["job_id"], additionalProperties: false },
11673
- },
11674
- {
11675
- name: "clauth_ops_run",
11676
- description: "Submit one PM2 operation through the Vultr control plane. The remote agent/admin profile decides whether the operation and target are available.",
11677
- inputSchema: { type: "object", properties: { operation: { type: "string" }, input: { type: "object" } }, required: ["operation"], additionalProperties: false },
11678
- },
11679
10885
  ];
11680
10886
 
11681
10887
  const MCP_WRITE_TOOL_NAMES = new Set([
@@ -11683,9 +10889,6 @@ const MCP_WRITE_TOOL_NAMES = new Set([
11683
10889
  "clauth_disable",
11684
10890
  "clauth_set_project",
11685
10891
  "clauth_generate_token",
11686
- "clauth_ops_deploy",
11687
- "clauth_ops_promote",
11688
- "clauth_ops_run",
11689
10892
  ]);
11690
10893
 
11691
10894
  function filterMcpToolsForWriteMode(tools) {
@@ -11719,24 +10922,6 @@ function mcpError(text) {
11719
10922
  return { content: [{ type: "text", text }], isError: true };
11720
10923
  }
11721
10924
 
11722
- async function callOpsFromMcp(vault, method, requestPath, body, { write = false } = {}) {
11723
- if (write && !vault.writeEnabled) return mcpError("MCP write tools are disabled by default. Launch clauth with CLAUTH_MCP_WRITE=1 for an explicit write-capable session.");
11724
- if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
11725
- const endpoint = process.env.CLAUTH_OPS_APPROVED_ORIGIN;
11726
- if (!endpoint) return mcpError("service_not_available");
11727
- try {
11728
- const url = new URL(endpoint);
11729
- if (url.protocol !== "https:") return mcpError("service_not_available");
11730
- const service = process.env.CLAUTH_OPS_TOKEN_SERVICE || "vultr-ops-api-token";
11731
- const credential = await vaultRetrieveValue(vault, service);
11732
- if (credential.error || !credential.value) return mcpError("service_not_available");
11733
- const payload = await requestOps({ endpoint: url.toString().replace(/\/$/, ""), token: String(credential.value), method, path: requestPath, body });
11734
- return mcpResult(JSON.stringify(payload, null, 2));
11735
- } catch {
11736
- return mcpError("service_not_available");
11737
- }
11738
- }
11739
-
11740
10925
  // Windows cmd.exe doesn't support single quotes — use bash for gws JSON args
11741
10926
  const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
11742
10927
 
@@ -11747,13 +10932,6 @@ async function handleMcpTool(vault, name, args) {
11747
10932
  };
11748
10933
 
11749
10934
  switch (name) {
11750
- case "clauth_ops_catalog": return callOpsFromMcp(vault, "GET", "/v1/ops/catalog");
11751
- case "clauth_ops_processes": return callOpsFromMcp(vault, "GET", "/v1/ops/processes");
11752
- case "clauth_ops_describe": return callOpsFromMcp(vault, "GET", `/v1/ops/processes/${encodeURIComponent(args.application || "")}`);
11753
- case "clauth_ops_deploy": return callOpsFromMcp(vault, "POST", "/v1/ops/deployments", { application: args.application, ...(args.ref ? { ref: args.ref } : {}) }, { write: true });
11754
- case "clauth_ops_promote": return callOpsFromMcp(vault, "POST", "/v1/ops/promotions", { application_uuid: args.application_uuid }, { write: true });
11755
- case "clauth_ops_job": return callOpsFromMcp(vault, "GET", `/v1/ops/jobs/${encodeURIComponent(args.job_id || "")}`);
11756
- case "clauth_ops_run": return callOpsFromMcp(vault, "POST", "/v1/ops/operations", { operation: args.operation, input: args.input && typeof args.input === "object" ? args.input : {} }, { write: true });
11757
10935
  case "clauth_ping": {
11758
10936
  return mcpResult(
11759
10937
  vault.password
@@ -13954,13 +13132,6 @@ async function actionUpgrade(opts) {
13954
13132
  return actionStart(opts);
13955
13133
  }
13956
13134
 
13957
- async function actionSupervisor(opts) {
13958
- opts.isolated = true;
13959
- opts.port = String(opts.port || getSupervisorPort());
13960
- discoverPlugins();
13961
- return actionForeground(opts);
13962
- }
13963
-
13964
13135
  export async function runServe(opts) {
13965
13136
  const action = opts.action || "foreground";
13966
13137
 
@@ -13971,13 +13142,12 @@ export async function runServe(opts) {
13971
13142
  case "ping": return actionPing();
13972
13143
  case "foreground": return actionForeground(opts);
13973
13144
  case "mcp": return actionMcp(opts);
13974
- case "supervisor": return actionSupervisor(opts);
13975
13145
  case "install": return actionInstall(opts);
13976
13146
  case "uninstall": return actionUninstall();
13977
13147
  case "upgrade": return actionUpgrade(opts);
13978
13148
  default:
13979
13149
  console.log(chalk.red(`\n Unknown serve action: ${action}`));
13980
- console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground | mcp | supervisor | install | uninstall | upgrade\n"));
13150
+ console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground | mcp | install | uninstall | upgrade\n"));
13981
13151
  process.exit(1);
13982
13152
  }
13983
13153
  }