@lifeaitools/clauth 1.30.25 → 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 +9 -815
- 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
|
}
|
|
@@ -916,30 +805,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
916
805
|
.wiz-test-result{padding:12px 14px;border-radius:8px;font-size:.85rem;margin-top:10px;display:none}
|
|
917
806
|
.wiz-test-result.ok{background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.2);color:#4ade80}
|
|
918
807
|
.wiz-test-result.fail{background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.2);color:#f87171}
|
|
919
|
-
.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}
|
|
920
|
-
.supervisor-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}
|
|
921
|
-
.supervisor-title{font-size:.95rem;font-weight:700;color:#e2e8f0}
|
|
922
|
-
.supervisor-sub{font-size:.76rem;color:#94a3b8;margin-top:3px;line-height:1.35}
|
|
923
|
-
.supervisor-actions{display:flex;gap:8px;flex-wrap:wrap}
|
|
924
|
-
.supervisor-action{background:#0f172a;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:6px 10px;font-size:.75rem;cursor:pointer}
|
|
925
|
-
.supervisor-action:hover{border-color:#38bdf8;color:#e0f2fe}
|
|
926
|
-
.supervisor-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
|
927
|
-
.supervisor-card{border:1px solid #1e293b;background:rgba(2,6,23,.52);border-radius:10px;padding:10px;min-width:0}
|
|
928
|
-
.supervisor-card h4{margin:0 0 8px;color:#cbd5e1;font-size:.75rem;letter-spacing:.08em;text-transform:uppercase}
|
|
929
|
-
.supervisor-kpi{font-size:1.35rem;font-weight:700;color:#f8fafc}
|
|
930
|
-
.supervisor-meta{font-size:.72rem;color:#94a3b8;font-family:'Courier New',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
931
|
-
.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}
|
|
932
|
-
.supervisor-pill.ok{border-color:rgba(74,222,128,.35);color:#86efac;background:rgba(34,197,94,.08)}
|
|
933
|
-
.supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:#fde68a;background:rgba(250,204,21,.08)}
|
|
934
|
-
.supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:#fecaca;background:rgba(248,113,113,.08)}
|
|
935
|
-
.supervisor-list{display:flex;flex-direction:column;gap:8px;max-height:320px;overflow:auto}
|
|
936
|
-
.supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:8px;background:rgba(15,23,42,.45)}
|
|
937
|
-
.supervisor-row-top{display:flex;justify-content:space-between;gap:8px;align-items:center}
|
|
938
|
-
.supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
939
|
-
.supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
|
|
940
|
-
.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}
|
|
941
|
-
.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}
|
|
942
|
-
.supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
|
|
943
808
|
</style>
|
|
944
809
|
</head>
|
|
945
810
|
<body>
|
|
@@ -1160,41 +1025,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1160
1025
|
</div>
|
|
1161
1026
|
</div>
|
|
1162
1027
|
|
|
1163
|
-
<section class="supervisor-panel" id="supervisor-panel">
|
|
1164
|
-
<div class="supervisor-head">
|
|
1165
|
-
<div>
|
|
1166
|
-
<div class="supervisor-title">Local Software Factory Supervisor</div>
|
|
1167
|
-
<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>
|
|
1168
|
-
</div>
|
|
1169
|
-
<div class="supervisor-actions">
|
|
1170
|
-
<button class="supervisor-action" onclick="loadSupervisorCockpit()">Refresh</button>
|
|
1171
|
-
<button class="supervisor-action" onclick="rescanSupervisorPlugins()">Rescan plugins</button>
|
|
1172
|
-
</div>
|
|
1173
|
-
</div>
|
|
1174
|
-
<div id="supervisor-status" class="supervisor-sub">Loading supervisor state…</div>
|
|
1175
|
-
<div id="supervisor-feedback" class="supervisor-feedback" role="status"></div>
|
|
1176
|
-
<div class="supervisor-grid" style="margin-top:10px">
|
|
1177
|
-
<div class="supervisor-card">
|
|
1178
|
-
<h4>Plugins</h4>
|
|
1179
|
-
<div class="supervisor-kpi" id="supervisor-plugin-count">—</div>
|
|
1180
|
-
<div class="supervisor-meta" id="supervisor-plugin-meta">awaiting data</div>
|
|
1181
|
-
<div class="supervisor-list" id="supervisor-plugins" style="margin-top:8px"></div>
|
|
1182
|
-
</div>
|
|
1183
|
-
<div class="supervisor-card">
|
|
1184
|
-
<h4>Surfaces</h4>
|
|
1185
|
-
<div class="supervisor-kpi" id="supervisor-surface-count">—</div>
|
|
1186
|
-
<div class="supervisor-meta" id="supervisor-surface-meta">destination + owner matrix</div>
|
|
1187
|
-
<div class="supervisor-list" id="supervisor-surfaces" style="margin-top:8px"></div>
|
|
1188
|
-
</div>
|
|
1189
|
-
<div class="supervisor-card">
|
|
1190
|
-
<h4>Operations + log</h4>
|
|
1191
|
-
<div class="supervisor-kpi" id="supervisor-operation-count">—</div>
|
|
1192
|
-
<div class="supervisor-meta" id="supervisor-log-path">events.jsonl</div>
|
|
1193
|
-
<div class="supervisor-log" id="supervisor-events" style="margin-top:8px">No events loaded.</div>
|
|
1194
|
-
</div>
|
|
1195
|
-
</div>
|
|
1196
|
-
</section>
|
|
1197
|
-
|
|
1198
1028
|
|
|
1199
1029
|
<div class="tunnel-panel" id="webdav-panel" style="flex-direction:column;align-items:stretch;gap:8px">
|
|
1200
1030
|
<div style="display:flex;align-items:center;gap:10px;width:100%">
|
|
@@ -1239,16 +1069,11 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1239
1069
|
<span id="service-search-count" class="service-search-count"></span>
|
|
1240
1070
|
</div>
|
|
1241
1071
|
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
1242
|
-
<div class="footer"
|
|
1072
|
+
<div class="footer">localhost:${port} · 127.0.0.1 only · 10-strike lockout</div>
|
|
1243
1073
|
</div>
|
|
1244
1074
|
|
|
1245
1075
|
<script>
|
|
1246
|
-
const BASE =
|
|
1247
|
-
(function reportOrigin() {
|
|
1248
|
-
const isLocal = /^(127\\.0\\.0\\.1|localhost|\\[::1\\])$/.test(location.hostname);
|
|
1249
|
-
const el = document.getElementById("originFooter");
|
|
1250
|
-
if (el) el.textContent = (isLocal ? "LOCAL" : "REMOTE") + " · " + BASE + " · 10-strike lockout";
|
|
1251
|
-
})();
|
|
1076
|
+
const BASE = "http://127.0.0.1:${port}";
|
|
1252
1077
|
|
|
1253
1078
|
const SERVICE_HINTS = {
|
|
1254
1079
|
"neo4j": "neo4j+s://username:password@instance.databases.neo4j.io",
|
|
@@ -1480,102 +1305,9 @@ function showMain(ping) {
|
|
|
1480
1305
|
pollTunnel();
|
|
1481
1306
|
loadWebdavMounts();
|
|
1482
1307
|
updateBuildStatus();
|
|
1483
|
-
loadSupervisorCockpit();
|
|
1484
1308
|
refreshWriteLockUi();
|
|
1485
1309
|
}
|
|
1486
1310
|
|
|
1487
|
-
function supervisorBadge(text, kind) {
|
|
1488
|
-
return '<span class="supervisor-pill ' + (kind || '') + '">' + htmlEscape(text) + '</span>';
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
async function supervisorJson(path, options) {
|
|
1492
|
-
const response = await fetch(BASE + path, { cache: "no-store", ...(options || {}) });
|
|
1493
|
-
const data = await response.json().catch(() => ({}));
|
|
1494
|
-
if (!response.ok || data.error) throw new Error(data.error || ("HTTP " + response.status));
|
|
1495
|
-
return data;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
function supervisorFeedback(text, bad) {
|
|
1499
|
-
const el = document.getElementById("supervisor-feedback");
|
|
1500
|
-
if (!el) return;
|
|
1501
|
-
el.textContent = text || "";
|
|
1502
|
-
el.className = "supervisor-feedback" + (bad ? " bad" : "");
|
|
1503
|
-
el.style.display = text ? "block" : "none";
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
async function loadSupervisorCockpit() {
|
|
1507
|
-
const status = document.getElementById("supervisor-status");
|
|
1508
|
-
if (status) status.textContent = "Loading supervisor state…";
|
|
1509
|
-
try {
|
|
1510
|
-
const [health, plugins, surfaces, logs] = await Promise.all([
|
|
1511
|
-
supervisorJson("/health"),
|
|
1512
|
-
supervisorJson("/v1/plugins"),
|
|
1513
|
-
supervisorJson("/v1/surfaces"),
|
|
1514
|
-
supervisorJson("/v1/logs?limit=40"),
|
|
1515
|
-
]);
|
|
1516
|
-
const pluginRows = plugins.plugins || [];
|
|
1517
|
-
const surfaceRows = surfaces.surfaces || [];
|
|
1518
|
-
document.getElementById("supervisor-plugin-count").textContent = String(pluginRows.length);
|
|
1519
|
-
document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length);
|
|
1520
|
-
document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
|
|
1521
|
-
document.getElementById("supervisor-plugin-meta").textContent = "current " + (pluginRows.filter(p => p.state === "current").length) + " · awaiting " + (pluginRows.filter(p => p.state === "awaiting_enable").length);
|
|
1522
|
-
document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
|
|
1523
|
-
document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
|
|
1524
|
-
document.getElementById("supervisor-plugins").innerHTML = pluginRows.length ? pluginRows.map(renderSupervisorPlugin).join("") : '<div class="supervisor-sub">No plugins discovered.</div>';
|
|
1525
|
-
document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.length ? surfaceRows.map(renderSupervisorSurface).join("") : '<div class="supervisor-sub">No surfaces declared.</div>';
|
|
1526
|
-
document.getElementById("supervisor-events").textContent = (logs.events || []).slice(-20).reverse().map(e => {
|
|
1527
|
-
const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
|
|
1528
|
-
return (e.ts || e.created_at || "") + " " + label;
|
|
1529
|
-
}).join("\\n") || "No events yet.";
|
|
1530
|
-
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}"), "");
|
|
1531
|
-
} catch (err) {
|
|
1532
|
-
if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
|
|
1533
|
-
supervisorFeedback("Supervisor unavailable — actions are disabled until the local control plane returns.", true);
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function renderSupervisorPlugin(plugin) {
|
|
1538
|
-
const stateKind = plugin.state === "current" ? "ok" : (plugin.state === "manifest_invalid" || plugin.state === "missing" ? "bad" : "warn");
|
|
1539
|
-
return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(plugin.id) + '</span>' + supervisorBadge(plugin.state || "unknown", stateKind) + '</div>' +
|
|
1540
|
-
'<div class="supervisor-meta">' + htmlEscape(plugin.publisher || "unknown") + ' · ' + htmlEscape(plugin.version || "—") + ' · ' + htmlEscape(plugin.source || "—") + '</div>' +
|
|
1541
|
-
'<div class="supervisor-meta">' + htmlEscape((plugin.manifest_hash || "").slice(0, 12)) + '</div>' +
|
|
1542
|
-
'<div class="supervisor-row-actions"><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg(plugin.enabled ? "disable" : "enable") + ')">' + (plugin.enabled ? "Disable" : "Enable") + '</button><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg("test") + ')">Test</button><button class="supervisor-action" onclick="runSupervisorPlugin(' + jsArg(plugin.id) + ',' + jsArg("promote") + ')">Promote</button></div></div>';
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function renderSupervisorSurface(surface) {
|
|
1546
|
-
const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
|
|
1547
|
-
const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
|
|
1548
|
-
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>' +
|
|
1549
|
-
'<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
|
|
1550
|
-
'<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
|
|
1551
|
-
'<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>';
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
async function rescanSupervisorPlugins() {
|
|
1555
|
-
try {
|
|
1556
|
-
await supervisorJson("/v1/plugins/rescan", { method: "POST", headers: writeHeaders() });
|
|
1557
|
-
supervisorFeedback("Plugin rescan completed.", false);
|
|
1558
|
-
await loadSupervisorCockpit();
|
|
1559
|
-
} catch (err) { supervisorFeedback("Plugin rescan failed: " + (err.message || err), true); }
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
async function runSupervisorPlugin(id, action) {
|
|
1563
|
-
try {
|
|
1564
|
-
const receipt = await supervisorJson("/v1/plugins/" + encodeURIComponent(id) + "/" + encodeURIComponent(action), { method: "POST", headers: writeHeaders() });
|
|
1565
|
-
supervisorFeedback("Plugin " + action + " receipt " + (receipt.operationId || "recorded") + " · " + ((receipt.resulting_state && receipt.resulting_state.state) || "completed"), false);
|
|
1566
|
-
await loadSupervisorCockpit();
|
|
1567
|
-
} catch (err) { supervisorFeedback("Plugin " + action + " failed: " + (err.message || err), true); }
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
async function runSupervisorSurface(id, action) {
|
|
1571
|
-
try {
|
|
1572
|
-
const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
|
|
1573
|
-
const result = receipt.resulting_state || {};
|
|
1574
|
-
supervisorFeedback("Surface " + action + " receipt " + (receipt.operationId || "recorded") + " · " + (result.state || "completed"), result.ok === false);
|
|
1575
|
-
await loadSupervisorCockpit();
|
|
1576
|
-
} catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
1311
|
// ── Unlock ──────────────────────────────────
|
|
1580
1312
|
async function unlock() {
|
|
1581
1313
|
const input = document.getElementById("lock-input");
|
|
@@ -4038,187 +3770,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4038
3770
|
});
|
|
4039
3771
|
},
|
|
4040
3772
|
});
|
|
4041
|
-
const isSupervisorPort = port === getSupervisorPort();
|
|
4042
|
-
const supervisorTestNoToken = process.env.CLAUTH_SUPERVISOR_TEST_NO_TOKEN === "1";
|
|
4043
|
-
const opsAdapter = createPm2Adapter(pm2);
|
|
4044
|
-
const executePm2 = createSerializedExecutor();
|
|
4045
|
-
const opsPolicy = createOperationPolicy({
|
|
4046
|
-
enabled: String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).filter(Boolean),
|
|
4047
|
-
applications: (() => { try { return JSON.parse(process.env.CLAUTH_OPS_APPLICATIONS || "{}"); } catch { return {}; } })(),
|
|
4048
|
-
adminEnabled: String(process.env.CLAUTH_OPS_ADMIN_ENABLED || "").split(",").map((item) => item.trim()).filter(Boolean),
|
|
4049
|
-
adminApplications: (() => { try { return JSON.parse(process.env.CLAUTH_OPS_ADMIN_APPLICATIONS || "{}"); } catch { return {}; } })(),
|
|
4050
|
-
allowHostWide: process.env.CLAUTH_OPS_ALLOW_HOST_WIDE === "1",
|
|
4051
|
-
});
|
|
4052
|
-
const opsJobs = createJobStore({
|
|
4053
|
-
filePath: process.env.CLAUTH_OPS_JOB_STORE_PATH || path.join(getSupervisorDir(), "ops-jobs.json"),
|
|
4054
|
-
});
|
|
4055
|
-
|
|
4056
|
-
async function getLoopbackSecret(service) {
|
|
4057
|
-
const response = await fetch(`http://127.0.0.1:${LIVE_PORT}/v/${encodeURIComponent(service)}`, { signal: AbortSignal.timeout(5000) });
|
|
4058
|
-
if (!response.ok) throw new Error(`${service} unavailable from local clauth`);
|
|
4059
|
-
const value = (await response.text()).trim();
|
|
4060
|
-
if (!value) throw new Error(`${service} is empty`);
|
|
4061
|
-
return value;
|
|
4062
|
-
}
|
|
4063
|
-
const coolify = createCoolifyAdapter({
|
|
4064
|
-
baseUrl: process.env.CLAUTH_COOLIFY_BASE_URL || "https://deploy.regendevcorp.com",
|
|
4065
|
-
getToken: () => getLoopbackSecret("coolify-api"),
|
|
4066
|
-
});
|
|
4067
|
-
const deployments = (() => { try { return parseDeploymentRegistry(process.env.CLAUTH_OPS_DEPLOYMENTS || "{}"); } catch { return {}; } })();
|
|
4068
|
-
const deploymentAdapter = createDeploymentAdapter({
|
|
4069
|
-
deployments,
|
|
4070
|
-
reload: async (target) => {
|
|
4071
|
-
await executePm2(async () => {
|
|
4072
|
-
await opsAdapter.connect();
|
|
4073
|
-
try { await opsAdapter.execute("reload", { target, options: { updateEnv: true } }); } finally { await opsAdapter.disconnect(); }
|
|
4074
|
-
});
|
|
4075
|
-
},
|
|
4076
|
-
});
|
|
4077
|
-
|
|
4078
|
-
/**
|
|
4079
|
-
* Record an ops failure's upstream message to the LOCAL log only.
|
|
4080
|
-
*
|
|
4081
|
-
* job-store's sanitizer deliberately drops free-form `error` text so an
|
|
4082
|
-
* upstream message cannot carry a credential into the persisted job file or
|
|
4083
|
-
* the API response. That protection left every failure with an empty detail,
|
|
4084
|
-
* so jobs reported `failed` with no reason at all. Jobs now carry an
|
|
4085
|
-
* enumerated `code`; the underlying message goes here, to the same
|
|
4086
|
-
* operator-only log as the rest of the daemon's diagnostics.
|
|
4087
|
-
*/
|
|
4088
|
-
function logOpsFailure(kind, operation, error) {
|
|
4089
|
-
const message = String(error?.message || error || "unknown");
|
|
4090
|
-
try {
|
|
4091
|
-
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] [OPS ${kind}/${operation}] ${message}\n`);
|
|
4092
|
-
} catch {}
|
|
4093
|
-
}
|
|
4094
|
-
|
|
4095
|
-
async function opsBearerRole(req) {
|
|
4096
|
-
const header = req.headers.authorization;
|
|
4097
|
-
const supplied = Array.isArray(header) ? header[0] : header;
|
|
4098
|
-
if (!supplied || !String(supplied).startsWith("Bearer ")) return null;
|
|
4099
|
-
const actual = String(supplied).slice(7).trim();
|
|
4100
|
-
let admin; let agent;
|
|
4101
|
-
try { admin = await getLoopbackSecret(process.env.CLAUTH_OPS_ADMIN_TOKEN_SERVICE || "vultr-ops-admin-token"); } catch {}
|
|
4102
|
-
try { agent = await getLoopbackSecret(process.env.CLAUTH_OPS_AGENT_TOKEN_SERVICE || "vultr-ops-api-token"); } catch {}
|
|
4103
|
-
if (admin && agent && admin === agent) return null;
|
|
4104
|
-
for (const [role, expected] of [["admin", admin], ["agent", agent]]) {
|
|
4105
|
-
if (!expected) continue;
|
|
4106
|
-
const a = Buffer.from(actual); const b = Buffer.from(expected);
|
|
4107
|
-
if (a.length === b.length && crypto.timingSafeEqual(a, b)) return role;
|
|
4108
|
-
}
|
|
4109
|
-
return null;
|
|
4110
|
-
}
|
|
4111
|
-
|
|
4112
|
-
async function requireOpsBearer(req, res) {
|
|
4113
|
-
const role = await opsBearerRole(req);
|
|
4114
|
-
if (role) { req._opsRole = role; return true; }
|
|
4115
|
-
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
4116
|
-
res.end(JSON.stringify({ error: "ops_bearer_required" }));
|
|
4117
|
-
return false;
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
function submitOpsJob(operation, input, role = "agent") {
|
|
4121
|
-
const authorization = opsPolicy.authorize(operation, input, role);
|
|
4122
|
-
const job = opsJobs.create({ kind: "pm2", operation, target: input.target || input.name || null });
|
|
4123
|
-
if (!authorization.ok) {
|
|
4124
|
-
return opsJobs.event(job.id, "rejected", { code: authorization.code });
|
|
4125
|
-
}
|
|
4126
|
-
void (async () => {
|
|
4127
|
-
opsJobs.event(job.id, "running");
|
|
4128
|
-
try {
|
|
4129
|
-
const result = await executePm2(async () => {
|
|
4130
|
-
await opsAdapter.connect();
|
|
4131
|
-
try { return await opsAdapter.execute(operation, input); } finally { await opsAdapter.disconnect(); }
|
|
4132
|
-
});
|
|
4133
|
-
opsJobs.event(job.id, "succeeded", operationReceipt(operation, result, authorization.allowed_targets || []));
|
|
4134
|
-
} catch (error) {
|
|
4135
|
-
// `error` alone is dropped by job-store's sanitizer (it refuses
|
|
4136
|
-
// free-form upstream text so a credential cannot ride along), which
|
|
4137
|
-
// left every failure with an empty detail. Emit an enumerated code so
|
|
4138
|
-
// the failure has a reason; keep the message for the local log only.
|
|
4139
|
-
logOpsFailure("pm2", operation, error);
|
|
4140
|
-
opsJobs.event(job.id, "failed", { code: "pm2_operation_failed" });
|
|
4141
|
-
}
|
|
4142
|
-
})();
|
|
4143
|
-
return opsJobs.get(job.id);
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
function operationReceipt(operation, result, allowedTargets) {
|
|
4147
|
-
if (["list", "describe", "logs"].includes(operation)) {
|
|
4148
|
-
const processes = Array.isArray(result)
|
|
4149
|
-
? result.filter((process) => allowedTargets.includes("*") || allowedTargets.includes(process?.name))
|
|
4150
|
-
: [];
|
|
4151
|
-
return { processes };
|
|
4152
|
-
}
|
|
4153
|
-
if (operation === "ping") return { status: "connected" };
|
|
4154
|
-
return { status: "completed" };
|
|
4155
|
-
}
|
|
4156
|
-
|
|
4157
|
-
function submitPromotionJob(applicationUuid) {
|
|
4158
|
-
const job = opsJobs.create({ kind: "coolify", operation: "promote", target: applicationUuid });
|
|
4159
|
-
const enabled = String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).includes("coolify_promote");
|
|
4160
|
-
const allowlist = (() => { try { return JSON.parse(process.env.CLAUTH_COOLIFY_PROMOTE_UUIDS || "[]"); } catch { return []; } })();
|
|
4161
|
-
if (!enabled || !Array.isArray(allowlist) || !allowlist.includes(applicationUuid)) {
|
|
4162
|
-
return opsJobs.event(job.id, "rejected", { code: "service_not_available" });
|
|
4163
|
-
}
|
|
4164
|
-
void (async () => {
|
|
4165
|
-
opsJobs.event(job.id, "running");
|
|
4166
|
-
try {
|
|
4167
|
-
const deployment = await coolify.promote(applicationUuid);
|
|
4168
|
-
// Coolify answers with a `deployments` ARRAY, not a flat object — see
|
|
4169
|
-
// deploymentUuidFrom. Reading the flat field alone marked the job failed
|
|
4170
|
-
// while the deployment was actually running.
|
|
4171
|
-
const deploymentUuid = deploymentUuidFrom(deployment);
|
|
4172
|
-
if (!deploymentUuid) {
|
|
4173
|
-
// The deploy request itself SUCCEEDED (no throw); only the UUID was
|
|
4174
|
-
// unreadable, so the deployment may well be RUNNING. The code says so
|
|
4175
|
-
// explicitly rather than a bare "failed", because a plain failure
|
|
4176
|
-
// invites a retry and a duplicate production deploy.
|
|
4177
|
-
//
|
|
4178
|
-
// An enumerated code, not a free-form error: job-store's sanitizer
|
|
4179
|
-
// drops `error` on purpose to keep upstream text (and any credential
|
|
4180
|
-
// inside it) out of the persisted job.
|
|
4181
|
-
return opsJobs.event(job.id, "failed", { code: "coolify_deploy_accepted_uuid_unreadable" });
|
|
4182
|
-
}
|
|
4183
|
-
opsJobs.event(job.id, "waiting", { deployment_uuid: deploymentUuid });
|
|
4184
|
-
const terminal = await coolify.poll(deploymentUuid, { attempts: Number(process.env.CLAUTH_COOLIFY_POLL_ATTEMPTS || 60), delay: () => new Promise((resolve) => setTimeout(resolve, 5000)) });
|
|
4185
|
-
opsJobs.event(job.id, terminal.state === "succeeded" ? "succeeded" : terminal.state, { deployment_uuid: deploymentUuid, status: terminal.deployment?.status || null });
|
|
4186
|
-
} catch (error) {
|
|
4187
|
-
// The throw may have happened AFTER Coolify accepted the deploy (e.g.
|
|
4188
|
-
// the poll lost the network), so this is not proof nothing shipped.
|
|
4189
|
-
logOpsFailure("coolify", "promote", error);
|
|
4190
|
-
opsJobs.event(job.id, "failed", { code: "coolify_promote_failed" });
|
|
4191
|
-
}
|
|
4192
|
-
})();
|
|
4193
|
-
return opsJobs.get(job.id);
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
|
-
function submitDeploymentJob(application, ref) {
|
|
4197
|
-
const job = opsJobs.create({ kind: "deployment", operation: "deploy", target: application });
|
|
4198
|
-
const enabled = String(process.env.CLAUTH_OPS_ENABLED || "").split(",").map((item) => item.trim()).includes("deploy");
|
|
4199
|
-
if (!enabled || !deployments[application]) return opsJobs.event(job.id, "rejected", { code: "service_not_available" });
|
|
4200
|
-
void (async () => {
|
|
4201
|
-
opsJobs.event(job.id, "running");
|
|
4202
|
-
try { opsJobs.event(job.id, "building"); opsJobs.event(job.id, "succeeded", { result: await deploymentAdapter.deploy({ application, ref }) }); }
|
|
4203
|
-
catch (error) { logOpsFailure("deployment", "deploy", error); opsJobs.event(job.id, "failed", { code: "deployment_failed" }); }
|
|
4204
|
-
})();
|
|
4205
|
-
return opsJobs.get(job.id);
|
|
4206
|
-
}
|
|
4207
|
-
|
|
4208
|
-
function hasSupervisorWrite(req) {
|
|
4209
|
-
if (validateWriteToken(req, writeSession)) return true;
|
|
4210
|
-
if (!isSupervisorPort) return false;
|
|
4211
|
-
if (!isLoopbackAddress(req.socket?.remoteAddress)) return false;
|
|
4212
|
-
return supervisorTestNoToken || !supervisorRequiresWriteToken(port);
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
function rejectSupervisorWrite(res) {
|
|
4216
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
4217
|
-
return res.end(JSON.stringify({
|
|
4218
|
-
error: "write_token_required",
|
|
4219
|
-
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.",
|
|
4220
|
-
}));
|
|
4221
|
-
}
|
|
4222
3773
|
|
|
4223
3774
|
// ── MCP SSE session tracking ──────────────────────────────
|
|
4224
3775
|
const sseSessions = new Map(); // sessionId → { res, initialized }
|
|
@@ -4759,7 +4310,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4759
4310
|
|
|
4760
4311
|
const server = http.createServer(async (req, res) => {
|
|
4761
4312
|
const remote = req.socket.remoteAddress;
|
|
4762
|
-
const isLocal =
|
|
4313
|
+
const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
|
|
4763
4314
|
|
|
4764
4315
|
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
4765
4316
|
const reqPath = url.pathname;
|
|
@@ -4799,205 +4350,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4799
4350
|
return res.end(JSON.stringify(result));
|
|
4800
4351
|
}
|
|
4801
4352
|
|
|
4802
|
-
if (method === "GET" && reqPath === "/health") {
|
|
4803
|
-
return ok(res, { ...supervisorHealth(), listening_port: port, process_id: process.pid, vault_locked: !password, clauth_version: VERSION });
|
|
4804
|
-
}
|
|
4805
|
-
|
|
4806
|
-
// Bearer-gated remote operations surface. It remains loopback-only at this
|
|
4807
|
-
// layer; ingress/tunnel policy decides whether it is reachable remotely.
|
|
4808
|
-
if (method === "GET" && reqPath === "/v1/ops/catalog") {
|
|
4809
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4810
|
-
return ok(res, { schema: "clauth.ops.v1", operations: PM2_OPERATION_CATALOG });
|
|
4811
|
-
}
|
|
4812
|
-
|
|
4813
|
-
if (method === "GET" && reqPath === "/v1/ops/processes") {
|
|
4814
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4815
|
-
const job = submitOpsJob("list", {}, req._opsRole);
|
|
4816
|
-
res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
|
|
4817
|
-
return res.end(JSON.stringify(job));
|
|
4818
|
-
}
|
|
4819
|
-
|
|
4820
|
-
const opsProcessMatch = reqPath.match(/^\/v1\/ops\/processes\/([^/]+)$/);
|
|
4821
|
-
if (method === "GET" && opsProcessMatch) {
|
|
4822
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4823
|
-
const job = submitOpsJob("describe", { target: decodeURIComponent(opsProcessMatch[1]) }, req._opsRole);
|
|
4824
|
-
res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
|
|
4825
|
-
return res.end(JSON.stringify(job));
|
|
4826
|
-
}
|
|
4827
|
-
|
|
4828
|
-
if (method === "POST" && reqPath === "/v1/ops/operations") {
|
|
4829
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4830
|
-
let body;
|
|
4831
|
-
try { body = await readBody(req); } catch {
|
|
4832
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4833
|
-
return res.end(JSON.stringify({ error: "invalid_json" }));
|
|
4834
|
-
}
|
|
4835
|
-
const operation = String(body?.operation || "");
|
|
4836
|
-
if (!PM2_OPERATION_CATALOG[operation]) {
|
|
4837
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4838
|
-
return res.end(JSON.stringify({ error: "unknown_operation" }));
|
|
4839
|
-
}
|
|
4840
|
-
const job = submitOpsJob(operation, body?.input && typeof body.input === "object" ? body.input : {}, req._opsRole);
|
|
4841
|
-
res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
|
|
4842
|
-
return res.end(JSON.stringify(job));
|
|
4843
|
-
}
|
|
4844
|
-
|
|
4845
|
-
if (method === "POST" && reqPath === "/v1/ops/promotions") {
|
|
4846
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4847
|
-
let body;
|
|
4848
|
-
try { body = await readBody(req); } catch {
|
|
4849
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4850
|
-
return res.end(JSON.stringify({ error: "invalid_json" }));
|
|
4851
|
-
}
|
|
4852
|
-
const applicationUuid = String(body?.application_uuid || "").trim();
|
|
4853
|
-
if (!applicationUuid) {
|
|
4854
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4855
|
-
return res.end(JSON.stringify({ error: "application_uuid_required" }));
|
|
4856
|
-
}
|
|
4857
|
-
const job = submitPromotionJob(applicationUuid);
|
|
4858
|
-
res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
|
|
4859
|
-
return res.end(JSON.stringify(job));
|
|
4860
|
-
}
|
|
4861
|
-
|
|
4862
|
-
if (method === "POST" && reqPath === "/v1/ops/deployments") {
|
|
4863
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4864
|
-
let body;
|
|
4865
|
-
try { body = await readBody(req); } catch {
|
|
4866
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4867
|
-
return res.end(JSON.stringify({ error: "invalid_json" }));
|
|
4868
|
-
}
|
|
4869
|
-
const application = String(body?.application || "").trim();
|
|
4870
|
-
if (!application) {
|
|
4871
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4872
|
-
return res.end(JSON.stringify({ error: "application_required" }));
|
|
4873
|
-
}
|
|
4874
|
-
const job = submitDeploymentJob(application, body?.ref ? String(body.ref) : undefined);
|
|
4875
|
-
res.writeHead(job.phase === "rejected" ? 403 : 202, { "Content-Type": "application/json", ...CORS });
|
|
4876
|
-
return res.end(JSON.stringify(job));
|
|
4877
|
-
}
|
|
4878
|
-
|
|
4879
|
-
const opsJobMatch = reqPath.match(/^\/v1\/ops\/jobs\/([^/]+)$/);
|
|
4880
|
-
if (method === "GET" && opsJobMatch) {
|
|
4881
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4882
|
-
const job = opsJobs.get(decodeURIComponent(opsJobMatch[1]));
|
|
4883
|
-
res.writeHead(job ? 200 : 404, { "Content-Type": "application/json", ...CORS });
|
|
4884
|
-
return res.end(JSON.stringify(job || { error: "job_not_found" }));
|
|
4885
|
-
}
|
|
4886
|
-
|
|
4887
|
-
const opsJobEventsMatch = reqPath.match(/^\/v1\/ops\/jobs\/([^/]+)\/events$/);
|
|
4888
|
-
if (method === "GET" && opsJobEventsMatch) {
|
|
4889
|
-
if (!await requireOpsBearer(req, res)) return;
|
|
4890
|
-
const jobId = decodeURIComponent(opsJobEventsMatch[1]);
|
|
4891
|
-
if (!opsJobs.get(jobId)) {
|
|
4892
|
-
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
4893
|
-
return res.end(JSON.stringify({ error: "job_not_found" }));
|
|
4894
|
-
}
|
|
4895
|
-
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", ...CORS });
|
|
4896
|
-
const unsubscribe = opsJobs.subscribe(jobId, (job) => {
|
|
4897
|
-
if (!res.writableEnded) res.write(`event: job\ndata: ${JSON.stringify(job)}\n\n`);
|
|
4898
|
-
});
|
|
4899
|
-
req.on("close", unsubscribe);
|
|
4900
|
-
return;
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
if (method === "GET" && reqPath === "/v1/plugins") {
|
|
4904
|
-
return ok(res, { plugins: listPlugins() });
|
|
4905
|
-
}
|
|
4906
|
-
|
|
4907
|
-
if (method === "POST" && reqPath === "/v1/plugins/rescan") {
|
|
4908
|
-
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
4909
|
-
return ok(res, discoverPlugins());
|
|
4910
|
-
}
|
|
4911
|
-
|
|
4912
|
-
const pluginEnableMatch = reqPath.match(/^\/v1\/plugins\/([^/]+)\/(enable|disable|test|promote)$/);
|
|
4913
|
-
if (method === "POST" && pluginEnableMatch) {
|
|
4914
|
-
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
4915
|
-
const pluginId = decodeURIComponent(pluginEnableMatch[1]);
|
|
4916
|
-
const op = pluginEnableMatch[2];
|
|
4917
|
-
const result = op === "enable"
|
|
4918
|
-
? setPluginEnabled(pluginId, true)
|
|
4919
|
-
: op === "disable"
|
|
4920
|
-
? setPluginEnabled(pluginId, false)
|
|
4921
|
-
: runPluginAction(pluginId, op);
|
|
4922
|
-
res.writeHead(result.error ? 400 : 200, { "Content-Type": "application/json", ...CORS });
|
|
4923
|
-
return res.end(JSON.stringify(result));
|
|
4924
|
-
}
|
|
4925
|
-
|
|
4926
|
-
if (method === "GET" && reqPath === "/v1/surfaces") {
|
|
4927
|
-
return ok(res, { surfaces: listSurfaces() });
|
|
4928
|
-
}
|
|
4929
|
-
|
|
4930
|
-
const surfaceActionMatch = reqPath.match(/^\/v1\/surfaces\/([^/]+)\/actions$/);
|
|
4931
|
-
if (method === "POST" && surfaceActionMatch) {
|
|
4932
|
-
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
4933
|
-
let body;
|
|
4934
|
-
try { body = await readBody(req); } catch {
|
|
4935
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4936
|
-
return res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
4937
|
-
}
|
|
4938
|
-
const result = runSurfaceAction(decodeURIComponent(surfaceActionMatch[1]), body?.action || "reconcile");
|
|
4939
|
-
res.writeHead(result.error ? 400 : 200, { "Content-Type": "application/json", ...CORS });
|
|
4940
|
-
return res.end(JSON.stringify(result));
|
|
4941
|
-
}
|
|
4942
|
-
|
|
4943
|
-
if (method === "GET" && reqPath === "/v1/routes") {
|
|
4944
|
-
return ok(res, { routes: listRoutes() });
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
if (method === "GET" && reqPath === "/v1/tunnels") {
|
|
4948
|
-
return ok(res, { tunnels: listTunnels() });
|
|
4949
|
-
}
|
|
4950
|
-
|
|
4951
|
-
if (method === "GET" && reqPath === "/v1/logs") {
|
|
4952
|
-
const limit = Number(url.searchParams.get("limit") || 100);
|
|
4953
|
-
const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 100;
|
|
4954
|
-
const state = loadSupervisorState();
|
|
4955
|
-
return ok(res, {
|
|
4956
|
-
schema: "clauth.supervisor.logs.v1",
|
|
4957
|
-
log_path: path.join(getSupervisorDir(), "events.jsonl"),
|
|
4958
|
-
events: readSupervisorEvents(boundedLimit).map(supervisorLogDto),
|
|
4959
|
-
operations: (state.operations || []).slice(0, boundedLimit).map(supervisorOperationDto),
|
|
4960
|
-
});
|
|
4961
|
-
}
|
|
4962
|
-
|
|
4963
|
-
const tunnelRoutesMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes$/);
|
|
4964
|
-
if (method === "POST" && tunnelRoutesMatch) {
|
|
4965
|
-
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
4966
|
-
let body;
|
|
4967
|
-
try { body = await readBody(req); } catch {
|
|
4968
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4969
|
-
return res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
4970
|
-
}
|
|
4971
|
-
return ok(res, addTunnelRoute(decodeURIComponent(tunnelRoutesMatch[1]), body, "localhost"));
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
|
-
const tunnelRouteDeleteMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes\/([^/]+)$/);
|
|
4975
|
-
if (method === "DELETE" && tunnelRouteDeleteMatch) {
|
|
4976
|
-
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
4977
|
-
return ok(res, removeTunnelRoute(decodeURIComponent(tunnelRouteDeleteMatch[1]), decodeURIComponent(tunnelRouteDeleteMatch[2]), "localhost"));
|
|
4978
|
-
}
|
|
4979
|
-
|
|
4980
|
-
if (method === "GET" && reqPath.startsWith("/v1/operations/")) {
|
|
4981
|
-
const id = decodeURIComponent(reqPath.split("/").pop());
|
|
4982
|
-
const operation = (supervisorHealth(), readSupervisorEvents(500)).find((event) => event.operationId === id);
|
|
4983
|
-
res.writeHead(operation ? 200 : 404, { "Content-Type": "application/json", ...CORS });
|
|
4984
|
-
return res.end(JSON.stringify(operation ? supervisorLogDto(operation) : { error: "operation_not_found" }));
|
|
4985
|
-
}
|
|
4986
|
-
|
|
4987
|
-
if (method === "GET" && reqPath === "/v1/events") {
|
|
4988
|
-
res.writeHead(200, {
|
|
4989
|
-
"Content-Type": "text/event-stream",
|
|
4990
|
-
"Cache-Control": "no-cache",
|
|
4991
|
-
"Connection": "keep-alive",
|
|
4992
|
-
...CORS,
|
|
4993
|
-
});
|
|
4994
|
-
for (const event of readSupervisorEvents(Number(url.searchParams.get("limit") || 100))) {
|
|
4995
|
-
res.write(`event: supervisor\ndata: ${JSON.stringify(supervisorLogDto(event))}\n\n`);
|
|
4996
|
-
}
|
|
4997
|
-
res.end();
|
|
4998
|
-
return;
|
|
4999
|
-
}
|
|
5000
|
-
|
|
5001
4353
|
// ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
|
|
5002
4354
|
const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
|
|
5003
4355
|
const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
|
|
@@ -8113,27 +7465,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
8113
7465
|
});
|
|
8114
7466
|
}
|
|
8115
7467
|
|
|
8116
|
-
// The localhost supervisor is the only process allowed to repair clauth-owned
|
|
8117
|
-
// local surfaces. Keep this loop out of the vault/staged instances and make
|
|
8118
|
-
// the cadence configurable for deterministic tests.
|
|
8119
|
-
if (port === getSupervisorPort() && process.env.CLAUTH_SUPERVISOR_HEALTH_RECONCILE !== "0") {
|
|
8120
|
-
const configuredInterval = Number(process.env.CLAUTH_SUPERVISOR_HEALTH_INTERVAL_MS || 10000);
|
|
8121
|
-
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(1000, Math.min(configuredInterval, 300000)) : 10000;
|
|
8122
|
-
let healthReconcileInFlight = false;
|
|
8123
|
-
const runHealthReconcile = () => {
|
|
8124
|
-
if (healthReconcileInFlight) return;
|
|
8125
|
-
healthReconcileInFlight = true;
|
|
8126
|
-
reconcileSurfaceHealth().catch((err) => {
|
|
8127
|
-
try { fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] supervisor health reconcile failed: ${err.message}\n`); } catch {}
|
|
8128
|
-
}).finally(() => { healthReconcileInFlight = false; });
|
|
8129
|
-
};
|
|
8130
|
-
const healthTimer = setInterval(runHealthReconcile, intervalMs);
|
|
8131
|
-
healthTimer.unref?.();
|
|
8132
|
-
server.__supervisorHealthTimer = healthTimer;
|
|
8133
|
-
server.on("close", () => clearInterval(healthTimer));
|
|
8134
|
-
setImmediate(runHealthReconcile);
|
|
8135
|
-
}
|
|
8136
|
-
|
|
8137
7468
|
return server;
|
|
8138
7469
|
}
|
|
8139
7470
|
|
|
@@ -8149,61 +7480,6 @@ async function verifyAuth(password) {
|
|
|
8149
7480
|
}
|
|
8150
7481
|
}
|
|
8151
7482
|
|
|
8152
|
-
async function supervisorResponds(port = getSupervisorPort()) {
|
|
8153
|
-
try {
|
|
8154
|
-
const resp = await fetch(`http://127.0.0.1:${port}/health`);
|
|
8155
|
-
return resp.ok;
|
|
8156
|
-
} catch {
|
|
8157
|
-
return false;
|
|
8158
|
-
}
|
|
8159
|
-
}
|
|
8160
|
-
|
|
8161
|
-
async function ensureSupervisorStarted(cliEntry) {
|
|
8162
|
-
const port = getSupervisorPort();
|
|
8163
|
-
const existing = readSupervisorPid();
|
|
8164
|
-
if (existing && isProcessAlive(existing.pid) && await supervisorResponds(existing.port)) {
|
|
8165
|
-
return { started: false, pid: existing.pid, port: existing.port, state: "already_running" };
|
|
8166
|
-
}
|
|
8167
|
-
if (existing && !isProcessAlive(existing.pid)) removeSupervisorPid();
|
|
8168
|
-
if (await supervisorResponds(port)) {
|
|
8169
|
-
return { started: false, pid: existing?.pid || null, port, state: "port_already_live" };
|
|
8170
|
-
}
|
|
8171
|
-
|
|
8172
|
-
const out = fs.openSync(LOG_FILE, "a");
|
|
8173
|
-
const child = spawn(process.execPath, [cliEntry, "serve", "supervisor", "--port", String(port)], {
|
|
8174
|
-
detached: true,
|
|
8175
|
-
stdio: ["ignore", out, out],
|
|
8176
|
-
env: { ...process.env, __CLAUTH_SUPERVISOR_DAEMON: "1" },
|
|
8177
|
-
});
|
|
8178
|
-
child.unref();
|
|
8179
|
-
writeSupervisorPid(child.pid, port);
|
|
8180
|
-
|
|
8181
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
8182
|
-
await new Promise(r => setTimeout(r, 500));
|
|
8183
|
-
if (await supervisorResponds(port)) {
|
|
8184
|
-
return { started: true, pid: child.pid, port, state: "started" };
|
|
8185
|
-
}
|
|
8186
|
-
}
|
|
8187
|
-
return { started: true, pid: child.pid, port, state: "start_unverified" };
|
|
8188
|
-
}
|
|
8189
|
-
|
|
8190
|
-
async function stopSupervisorSibling() {
|
|
8191
|
-
const info = readSupervisorPid();
|
|
8192
|
-
if (!info) return null;
|
|
8193
|
-
if (!isProcessAlive(info.pid)) {
|
|
8194
|
-
removeSupervisorPid();
|
|
8195
|
-
return { stopped: false, pid: info.pid, port: info.port, state: "stale" };
|
|
8196
|
-
}
|
|
8197
|
-
try {
|
|
8198
|
-
process.kill(info.pid, "SIGTERM");
|
|
8199
|
-
await new Promise(r => setTimeout(r, 300));
|
|
8200
|
-
removeSupervisorPid();
|
|
8201
|
-
return { stopped: true, pid: info.pid, port: info.port, state: "stopped" };
|
|
8202
|
-
} catch (err) {
|
|
8203
|
-
return { stopped: false, pid: info.pid, port: info.port, state: "stop_failed", error: err.message };
|
|
8204
|
-
}
|
|
8205
|
-
}
|
|
8206
|
-
|
|
8207
7483
|
async function actionStart(opts) {
|
|
8208
7484
|
if (opts.isolated) {
|
|
8209
7485
|
return actionForeground(opts);
|
|
@@ -8405,11 +7681,6 @@ async function actionStart(opts) {
|
|
|
8405
7681
|
console.log(chalk.gray(` Port: 127.0.0.1:${info.port}`));
|
|
8406
7682
|
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
8407
7683
|
console.log(chalk.gray(` Log: ${LOG_FILE}`));
|
|
8408
|
-
if (!isStaged) {
|
|
8409
|
-
const supervisor = await ensureSupervisorStarted(cliEntry);
|
|
8410
|
-
const verb = supervisor.started ? "started" : "available";
|
|
8411
|
-
console.log(chalk.gray(` Supervisor: ${verb} on 127.0.0.1:${supervisor.port}${supervisor.pid ? ` (PID ${supervisor.pid})` : ""}`));
|
|
8412
|
-
}
|
|
8413
7684
|
if (isStaged) {
|
|
8414
7685
|
console.log(chalk.yellow(`\n ⚡ Staged on port ${port} — open dashboard to verify, then click "Make Live"`));
|
|
8415
7686
|
} else if (!password) {
|
|
@@ -8427,7 +7698,6 @@ async function actionStart(opts) {
|
|
|
8427
7698
|
async function actionStop() {
|
|
8428
7699
|
const info = readPid();
|
|
8429
7700
|
if (!info) {
|
|
8430
|
-
await stopSupervisorSibling();
|
|
8431
7701
|
console.log(chalk.yellow("\n No clauth serve PID file found — not running.\n"));
|
|
8432
7702
|
return;
|
|
8433
7703
|
}
|
|
@@ -8435,7 +7705,6 @@ async function actionStop() {
|
|
|
8435
7705
|
if (!isProcessAlive(info.pid)) {
|
|
8436
7706
|
console.log(chalk.yellow(`\n PID ${info.pid} is not running (stale PID file). Cleaning up.\n`));
|
|
8437
7707
|
removePid();
|
|
8438
|
-
await stopSupervisorSibling();
|
|
8439
7708
|
return;
|
|
8440
7709
|
}
|
|
8441
7710
|
|
|
@@ -8446,7 +7715,6 @@ async function actionStop() {
|
|
|
8446
7715
|
await new Promise(r => setTimeout(r, 300));
|
|
8447
7716
|
console.log(chalk.green(`\n 🛑 clauth serve stopped (was PID ${info.pid}, port ${info.port})\n`));
|
|
8448
7717
|
removePid();
|
|
8449
|
-
await stopSupervisorSibling();
|
|
8450
7718
|
return;
|
|
8451
7719
|
}
|
|
8452
7720
|
} catch {}
|
|
@@ -8460,7 +7728,6 @@ async function actionStop() {
|
|
|
8460
7728
|
console.log(chalk.yellow(`\n Could not kill PID ${info.pid}: ${err.message}\n`));
|
|
8461
7729
|
}
|
|
8462
7730
|
removePid();
|
|
8463
|
-
await stopSupervisorSibling();
|
|
8464
7731
|
}
|
|
8465
7732
|
|
|
8466
7733
|
async function actionPing() {
|
|
@@ -8505,9 +7772,7 @@ async function actionRestart(opts) {
|
|
|
8505
7772
|
async function actionForeground(opts) {
|
|
8506
7773
|
const port = parseInt(opts.port || "52437", 10);
|
|
8507
7774
|
const isolated = !!opts.isolated;
|
|
8508
|
-
const
|
|
8509
|
-
const password = isolated ? null : (opts.pw || containerPassword);
|
|
8510
|
-
const bindHost = process.env.CLAUTH_BIND_HOST || "127.0.0.1";
|
|
7775
|
+
const password = isolated ? null : (opts.pw || null);
|
|
8511
7776
|
const tunnelHostname = opts.tunnel || null;
|
|
8512
7777
|
const whitelist = opts.services
|
|
8513
7778
|
? opts.services.split(",").map(s => s.trim().toLowerCase())
|
|
@@ -8533,14 +7798,14 @@ async function actionForeground(opts) {
|
|
|
8533
7798
|
console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
|
|
8534
7799
|
}
|
|
8535
7800
|
|
|
8536
|
-
console.log(chalk.gray(` Port:
|
|
7801
|
+
console.log(chalk.gray(` Port: 127.0.0.1:${port}`));
|
|
8537
7802
|
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
8538
7803
|
console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
|
|
8539
7804
|
|
|
8540
7805
|
const server = createServer(password, whitelist, port, tunnelHostname);
|
|
8541
|
-
server.listen(port,
|
|
7806
|
+
server.listen(port, "127.0.0.1", () => {
|
|
8542
7807
|
if (!isolated) writePid(process.pid, port);
|
|
8543
|
-
console.log(chalk.green(` clauth serve → http
|
|
7808
|
+
console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
|
|
8544
7809
|
if (tunnelHostname) {
|
|
8545
7810
|
console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
|
|
8546
7811
|
console.log("");
|
|
@@ -11617,41 +10882,6 @@ const MCP_TOOLS = [
|
|
|
11617
10882
|
additionalProperties: false,
|
|
11618
10883
|
},
|
|
11619
10884
|
},
|
|
11620
|
-
{
|
|
11621
|
-
name: "clauth_ops_catalog",
|
|
11622
|
-
description: "Return the Vultr deployment-control API catalog. Availability is still decided on Vultr.",
|
|
11623
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11624
|
-
},
|
|
11625
|
-
{
|
|
11626
|
-
name: "clauth_ops_processes",
|
|
11627
|
-
description: "Submit a scoped PM2 process-status query to the Vultr control plane. Only server-approved applications are returned.",
|
|
11628
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11629
|
-
},
|
|
11630
|
-
{
|
|
11631
|
-
name: "clauth_ops_describe",
|
|
11632
|
-
description: "Submit a scoped PM2 status query for one server-approved application.",
|
|
11633
|
-
inputSchema: { type: "object", properties: { application: { type: "string" } }, required: ["application"], additionalProperties: false },
|
|
11634
|
-
},
|
|
11635
|
-
{
|
|
11636
|
-
name: "clauth_ops_deploy",
|
|
11637
|
-
description: "Submit one manifest-scoped development deployment to Vultr. The server chooses the repository, build, PM2 process, and allowed ref.",
|
|
11638
|
-
inputSchema: { type: "object", properties: { application: { type: "string" }, ref: { type: "string" } }, required: ["application"], additionalProperties: false },
|
|
11639
|
-
},
|
|
11640
|
-
{
|
|
11641
|
-
name: "clauth_ops_promote",
|
|
11642
|
-
description: "Submit one server-allowlisted Coolify promotion and return its asynchronous job receipt.",
|
|
11643
|
-
inputSchema: { type: "object", properties: { application_uuid: { type: "string" } }, required: ["application_uuid"], additionalProperties: false },
|
|
11644
|
-
},
|
|
11645
|
-
{
|
|
11646
|
-
name: "clauth_ops_job",
|
|
11647
|
-
description: "Read the terminal or in-progress receipt for a deployment-control job.",
|
|
11648
|
-
inputSchema: { type: "object", properties: { job_id: { type: "string" } }, required: ["job_id"], additionalProperties: false },
|
|
11649
|
-
},
|
|
11650
|
-
{
|
|
11651
|
-
name: "clauth_ops_run",
|
|
11652
|
-
description: "Submit one PM2 operation through the Vultr control plane. The remote agent/admin profile decides whether the operation and target are available.",
|
|
11653
|
-
inputSchema: { type: "object", properties: { operation: { type: "string" }, input: { type: "object" } }, required: ["operation"], additionalProperties: false },
|
|
11654
|
-
},
|
|
11655
10885
|
];
|
|
11656
10886
|
|
|
11657
10887
|
const MCP_WRITE_TOOL_NAMES = new Set([
|
|
@@ -11659,9 +10889,6 @@ const MCP_WRITE_TOOL_NAMES = new Set([
|
|
|
11659
10889
|
"clauth_disable",
|
|
11660
10890
|
"clauth_set_project",
|
|
11661
10891
|
"clauth_generate_token",
|
|
11662
|
-
"clauth_ops_deploy",
|
|
11663
|
-
"clauth_ops_promote",
|
|
11664
|
-
"clauth_ops_run",
|
|
11665
10892
|
]);
|
|
11666
10893
|
|
|
11667
10894
|
function filterMcpToolsForWriteMode(tools) {
|
|
@@ -11695,24 +10922,6 @@ function mcpError(text) {
|
|
|
11695
10922
|
return { content: [{ type: "text", text }], isError: true };
|
|
11696
10923
|
}
|
|
11697
10924
|
|
|
11698
|
-
async function callOpsFromMcp(vault, method, requestPath, body, { write = false } = {}) {
|
|
11699
|
-
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.");
|
|
11700
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
11701
|
-
const endpoint = process.env.CLAUTH_OPS_APPROVED_ORIGIN;
|
|
11702
|
-
if (!endpoint) return mcpError("service_not_available");
|
|
11703
|
-
try {
|
|
11704
|
-
const url = new URL(endpoint);
|
|
11705
|
-
if (url.protocol !== "https:") return mcpError("service_not_available");
|
|
11706
|
-
const service = process.env.CLAUTH_OPS_TOKEN_SERVICE || "vultr-ops-api-token";
|
|
11707
|
-
const credential = await vaultRetrieveValue(vault, service);
|
|
11708
|
-
if (credential.error || !credential.value) return mcpError("service_not_available");
|
|
11709
|
-
const payload = await requestOps({ endpoint: url.toString().replace(/\/$/, ""), token: String(credential.value), method, path: requestPath, body });
|
|
11710
|
-
return mcpResult(JSON.stringify(payload, null, 2));
|
|
11711
|
-
} catch {
|
|
11712
|
-
return mcpError("service_not_available");
|
|
11713
|
-
}
|
|
11714
|
-
}
|
|
11715
|
-
|
|
11716
10925
|
// Windows cmd.exe doesn't support single quotes — use bash for gws JSON args
|
|
11717
10926
|
const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
|
|
11718
10927
|
|
|
@@ -11723,13 +10932,6 @@ async function handleMcpTool(vault, name, args) {
|
|
|
11723
10932
|
};
|
|
11724
10933
|
|
|
11725
10934
|
switch (name) {
|
|
11726
|
-
case "clauth_ops_catalog": return callOpsFromMcp(vault, "GET", "/v1/ops/catalog");
|
|
11727
|
-
case "clauth_ops_processes": return callOpsFromMcp(vault, "GET", "/v1/ops/processes");
|
|
11728
|
-
case "clauth_ops_describe": return callOpsFromMcp(vault, "GET", `/v1/ops/processes/${encodeURIComponent(args.application || "")}`);
|
|
11729
|
-
case "clauth_ops_deploy": return callOpsFromMcp(vault, "POST", "/v1/ops/deployments", { application: args.application, ...(args.ref ? { ref: args.ref } : {}) }, { write: true });
|
|
11730
|
-
case "clauth_ops_promote": return callOpsFromMcp(vault, "POST", "/v1/ops/promotions", { application_uuid: args.application_uuid }, { write: true });
|
|
11731
|
-
case "clauth_ops_job": return callOpsFromMcp(vault, "GET", `/v1/ops/jobs/${encodeURIComponent(args.job_id || "")}`);
|
|
11732
|
-
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 });
|
|
11733
10935
|
case "clauth_ping": {
|
|
11734
10936
|
return mcpResult(
|
|
11735
10937
|
vault.password
|
|
@@ -13930,13 +13132,6 @@ async function actionUpgrade(opts) {
|
|
|
13930
13132
|
return actionStart(opts);
|
|
13931
13133
|
}
|
|
13932
13134
|
|
|
13933
|
-
async function actionSupervisor(opts) {
|
|
13934
|
-
opts.isolated = true;
|
|
13935
|
-
opts.port = String(opts.port || getSupervisorPort());
|
|
13936
|
-
discoverPlugins();
|
|
13937
|
-
return actionForeground(opts);
|
|
13938
|
-
}
|
|
13939
|
-
|
|
13940
13135
|
export async function runServe(opts) {
|
|
13941
13136
|
const action = opts.action || "foreground";
|
|
13942
13137
|
|
|
@@ -13947,13 +13142,12 @@ export async function runServe(opts) {
|
|
|
13947
13142
|
case "ping": return actionPing();
|
|
13948
13143
|
case "foreground": return actionForeground(opts);
|
|
13949
13144
|
case "mcp": return actionMcp(opts);
|
|
13950
|
-
case "supervisor": return actionSupervisor(opts);
|
|
13951
13145
|
case "install": return actionInstall(opts);
|
|
13952
13146
|
case "uninstall": return actionUninstall();
|
|
13953
13147
|
case "upgrade": return actionUpgrade(opts);
|
|
13954
13148
|
default:
|
|
13955
13149
|
console.log(chalk.red(`\n Unknown serve action: ${action}`));
|
|
13956
|
-
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"));
|
|
13957
13151
|
process.exit(1);
|
|
13958
13152
|
}
|
|
13959
13153
|
}
|