@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.
- package/.clauth-skill/SKILL.md +0 -31
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +0 -27
- package/README.md +2 -48
- package/cli/api.js +238 -238
- package/cli/commands/agent-pool.js +51 -15
- package/cli/commands/install.js +396 -396
- package/cli/commands/login.js +135 -0
- package/cli/commands/login.test.js +73 -0
- package/cli/commands/serve.js +16 -846
- package/cli/commands/uninstall.js +164 -164
- package/cli/commands/watchdog.js +1 -1
- package/cli/index.js +29 -20
- package/cli/supervisor-registry.js +1 -6
- package/cli/watchdog-registry.js +2 -30
- package/cli/watchdog-registry.test.js +5 -28
- package/cli/webdav-service.js +339 -339
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -6
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/bootstrap.cjs +121 -121
- package/scripts/build.mjs +66 -0
- package/scripts/build.sh +5 -45
- package/scripts/postinstall.js +189 -189
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
- package/cli/commands/ops-install.js +0 -211
- package/cli/commands/ops.js +0 -69
- package/cli/ops/coolify-adapter.js +0 -80
- package/cli/ops/deployment-adapter.js +0 -63
- package/cli/ops/job-store.js +0 -116
- package/cli/ops/operation-policy.js +0 -51
- package/cli/ops/pm2-adapter.js +0 -128
- package/cli/ops/serialized-executor.js +0 -9
package/cli/commands/serve.js
CHANGED
|
@@ -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
|
|
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"
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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,
|
|
7806
|
+
server.listen(port, "127.0.0.1", () => {
|
|
8566
7807
|
if (!isolated) writePid(process.pid, port);
|
|
8567
|
-
console.log(chalk.green(` clauth serve → http
|
|
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 |
|
|
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
|
}
|