@lifeaitools/clauth 1.6.0 → 1.7.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/cli/commands/serve.js +261 -49
- package/cli/commands/watchdog.js +79 -14
- package/cli/index.js +19 -6
- package/cli/lib/fs-git.js +217 -0
- package/cli/recovery.js +101 -0
- package/cli/watchdog-registry.js +209 -0
- package/cli/watchdog-registry.test.js +89 -0
- package/package.json +1 -1
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
package/cli/commands/serve.js
CHANGED
|
@@ -21,6 +21,13 @@ import { appendFile, readdir, readFile, writeFile, rm, mkdir, stat, rename, cp }
|
|
|
21
21
|
import fg from "fast-glob";
|
|
22
22
|
import { rgPath } from "@vscode/ripgrep";
|
|
23
23
|
import { createStudioDebugRuntime } from "../studio-debug.js";
|
|
24
|
+
import { writeCredentialWithRecovery } from "../recovery.js";
|
|
25
|
+
import * as fsGit from "../lib/fs-git.js";
|
|
26
|
+
import {
|
|
27
|
+
getWatchdogStatuses,
|
|
28
|
+
readWatchdogEvents,
|
|
29
|
+
restartWatchdogService,
|
|
30
|
+
} from "../watchdog-registry.js";
|
|
24
31
|
|
|
25
32
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
33
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
|
|
@@ -309,9 +316,14 @@ function createRotationEngine(password, machineHash, logFile) {
|
|
|
309
316
|
const result = await config.rotate(currentKey);
|
|
310
317
|
if (!result.newKey) throw new Error("Rotation returned no new key");
|
|
311
318
|
|
|
312
|
-
// Write new key to vault
|
|
313
|
-
const {
|
|
314
|
-
|
|
319
|
+
// Write new key to vault after preserving an encrypted recovery snapshot.
|
|
320
|
+
const { result: writeResult } = await writeCredentialWithRecovery({
|
|
321
|
+
password,
|
|
322
|
+
machineHash,
|
|
323
|
+
service: serviceName,
|
|
324
|
+
value: result.newKey,
|
|
325
|
+
logFile,
|
|
326
|
+
});
|
|
315
327
|
if (writeResult.error) throw new Error(`Write failed: ${writeResult.error}`);
|
|
316
328
|
|
|
317
329
|
// Update expiry in state
|
|
@@ -412,6 +424,26 @@ const STAGED_PID_FILE = path.join(os.tmpdir(), "clauth-serve-staged.pid");
|
|
|
412
424
|
const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
|
|
413
425
|
const LIVE_PORT = 52437;
|
|
414
426
|
const STAGED_PORT = 52438;
|
|
427
|
+
const WRITE_TOKEN_BYTES = 32;
|
|
428
|
+
const WRITE_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
429
|
+
|
|
430
|
+
function makeWriteToken() {
|
|
431
|
+
return {
|
|
432
|
+
token: crypto.randomBytes(WRITE_TOKEN_BYTES).toString("base64url"),
|
|
433
|
+
expiresAt: Date.now() + WRITE_TOKEN_TTL_MS,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function validateWriteToken(req, writeSession) {
|
|
438
|
+
if (!writeSession?.token || Date.now() > writeSession.expiresAt) return false;
|
|
439
|
+
const header = req.headers["x-clauth-write-token"] || req.headers.authorization;
|
|
440
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
441
|
+
if (!token) return false;
|
|
442
|
+
const supplied = String(token).startsWith("Bearer ") ? String(token).slice(7) : String(token);
|
|
443
|
+
const a = Buffer.from(supplied);
|
|
444
|
+
const b = Buffer.from(writeSession.token);
|
|
445
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
446
|
+
}
|
|
415
447
|
|
|
416
448
|
// ── PID helpers ──────────────────────────────────────────────
|
|
417
449
|
function readPid() {
|
|
@@ -968,6 +1000,13 @@ function renderSetPanel(name) {
|
|
|
968
1000
|
}
|
|
969
1001
|
|
|
970
1002
|
// ── Boot: check lock state ──────────────────
|
|
1003
|
+
let writeToken = null;
|
|
1004
|
+
|
|
1005
|
+
function writeHeaders(extra) {
|
|
1006
|
+
if (!writeToken) throw new Error("Write access requires password unlock in this browser session.");
|
|
1007
|
+
return { ...(extra || {}), "X-Clauth-Write-Token": writeToken };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
971
1010
|
async function boot() {
|
|
972
1011
|
try {
|
|
973
1012
|
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
@@ -1090,6 +1129,7 @@ async function unlock() {
|
|
|
1090
1129
|
throw new Error(r.error + rem);
|
|
1091
1130
|
}
|
|
1092
1131
|
|
|
1132
|
+
writeToken = r.write_token || null;
|
|
1093
1133
|
input.value = "";
|
|
1094
1134
|
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
1095
1135
|
showMain(ping);
|
|
@@ -1107,6 +1147,7 @@ async function unlock() {
|
|
|
1107
1147
|
// ── Lock ────────────────────────────────────
|
|
1108
1148
|
async function lockVault() {
|
|
1109
1149
|
const r = await fetch(BASE + "/lock", { method: "POST" }).then(r => r.json()).catch(() => ({}));
|
|
1150
|
+
writeToken = null;
|
|
1110
1151
|
showLockScreen(r.hard_locked || false);
|
|
1111
1152
|
}
|
|
1112
1153
|
|
|
@@ -1398,7 +1439,7 @@ async function rotateKey(name) {
|
|
|
1398
1439
|
const rotBtn = document.getElementById("rotbtn-" + name);
|
|
1399
1440
|
if (rotBtn) { rotBtn.disabled = true; rotBtn.textContent = "rotating…"; }
|
|
1400
1441
|
try {
|
|
1401
|
-
const r = await fetch(BASE + "/rotate/" + name, { method: "POST" }).then(r => r.json());
|
|
1442
|
+
const r = await fetch(BASE + "/rotate/" + name, { method: "POST", headers: writeHeaders() }).then(r => r.json());
|
|
1402
1443
|
if (r.ok) {
|
|
1403
1444
|
if (rotBtn) { rotBtn.textContent = "✓ rotated"; rotBtn.style.background = "#166534"; }
|
|
1404
1445
|
loadExpiry(); // refresh badges
|
|
@@ -1419,7 +1460,7 @@ async function setExpiry(name) {
|
|
|
1419
1460
|
try {
|
|
1420
1461
|
await fetch(BASE + "/set-expiry/" + name, {
|
|
1421
1462
|
method: "POST",
|
|
1422
|
-
headers: { "Content-Type": "application/json" },
|
|
1463
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1423
1464
|
body: JSON.stringify({ expires_at: expiresAt, rotation_days: parseInt(days) }),
|
|
1424
1465
|
});
|
|
1425
1466
|
loadExpiry();
|
|
@@ -1487,7 +1528,7 @@ async function saveProject(name) {
|
|
|
1487
1528
|
try {
|
|
1488
1529
|
const r = await fetch(BASE + "/update-service", {
|
|
1489
1530
|
method: "POST",
|
|
1490
|
-
headers: { "Content-Type": "application/json" },
|
|
1531
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1491
1532
|
body: JSON.stringify({ service: name, project: project || "" })
|
|
1492
1533
|
}).then(r => r.json());
|
|
1493
1534
|
if (r.locked) { showLockScreen(); return; }
|
|
@@ -1532,7 +1573,7 @@ async function saveLabel(name) {
|
|
|
1532
1573
|
try {
|
|
1533
1574
|
const r = await fetch(BASE + "/update-service", {
|
|
1534
1575
|
method: "POST",
|
|
1535
|
-
headers: { "Content-Type": "application/json" },
|
|
1576
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1536
1577
|
body: JSON.stringify({ service: name, label: newLabel })
|
|
1537
1578
|
}).then(r => r.json());
|
|
1538
1579
|
if (r.locked) { showLockScreen(false); return; }
|
|
@@ -1556,7 +1597,7 @@ async function saveRename(oldName) { await saveLabel(oldName); }
|
|
|
1556
1597
|
async function deleteService(name) {
|
|
1557
1598
|
if (!confirm(\`Delete service "\${name}"? This cannot be undone.\`)) return;
|
|
1558
1599
|
try {
|
|
1559
|
-
const r = await fetch(BASE + "/delete/" + name, { method: "POST" }).then(r => r.json());
|
|
1600
|
+
const r = await fetch(BASE + "/delete/" + name, { method: "POST", headers: writeHeaders() }).then(r => r.json());
|
|
1560
1601
|
if (r.locked) { showLockScreen(false); return; }
|
|
1561
1602
|
if (r.error) throw new Error(r.error);
|
|
1562
1603
|
loadServices();
|
|
@@ -1612,15 +1653,15 @@ async function saveKey(name) {
|
|
|
1612
1653
|
value = JSON.stringify(obj);
|
|
1613
1654
|
} else {
|
|
1614
1655
|
const input = document.getElementById("set-input-" + name);
|
|
1615
|
-
value = input ? input.value
|
|
1616
|
-
if (!value) { msg.className = "set-msg fail"; msg.textContent = "Value is empty."; return; }
|
|
1656
|
+
value = input ? input.value : "";
|
|
1657
|
+
if (!value.trim()) { msg.className = "set-msg fail"; msg.textContent = "Value is empty."; return; }
|
|
1617
1658
|
}
|
|
1618
1659
|
|
|
1619
1660
|
msg.className = "set-msg"; msg.textContent = "Saving…";
|
|
1620
1661
|
try {
|
|
1621
1662
|
const r = await fetch(BASE + "/set/" + name, {
|
|
1622
1663
|
method: "POST",
|
|
1623
|
-
headers: { "Content-Type": "application/json" },
|
|
1664
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1624
1665
|
body: JSON.stringify({ value })
|
|
1625
1666
|
}).then(r => r.json());
|
|
1626
1667
|
|
|
@@ -1658,7 +1699,7 @@ async function toggleService(name) {
|
|
|
1658
1699
|
try {
|
|
1659
1700
|
const r = await fetch(BASE + "/toggle/" + name, {
|
|
1660
1701
|
method: "POST",
|
|
1661
|
-
headers: { "Content-Type": "application/json" },
|
|
1702
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1662
1703
|
body: JSON.stringify({ enabled: newState })
|
|
1663
1704
|
}).then(r => r.json());
|
|
1664
1705
|
|
|
@@ -1763,12 +1804,13 @@ async function changePassword() {
|
|
|
1763
1804
|
try {
|
|
1764
1805
|
const r = await fetch(BASE + "/change-pw", {
|
|
1765
1806
|
method: "POST",
|
|
1766
|
-
headers: { "Content-Type": "application/json" },
|
|
1807
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1767
1808
|
body: JSON.stringify({ newPassword: newPw })
|
|
1768
1809
|
}).then(r => r.json());
|
|
1769
1810
|
|
|
1770
1811
|
if (r.locked) { showLockScreen(); return; }
|
|
1771
1812
|
if (r.error) throw new Error(r.error);
|
|
1813
|
+
writeToken = r.write_token || null;
|
|
1772
1814
|
|
|
1773
1815
|
msg.className = "chpw-msg ok"; msg.textContent = "✓ Password updated";
|
|
1774
1816
|
document.getElementById("chpw-new").value = "";
|
|
@@ -1838,7 +1880,7 @@ async function addService() {
|
|
|
1838
1880
|
if (project) payload.project = project;
|
|
1839
1881
|
const r = await fetch(BASE + "/add-service", {
|
|
1840
1882
|
method: "POST",
|
|
1841
|
-
headers: { "Content-Type": "application/json" },
|
|
1883
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
1842
1884
|
body: JSON.stringify(payload)
|
|
1843
1885
|
}).then(r => r.json());
|
|
1844
1886
|
|
|
@@ -2058,7 +2100,7 @@ async function wizSubmitCfToken() {
|
|
|
2058
2100
|
|
|
2059
2101
|
const r = await fetch(BASE + "/tunnel/setup/cf-token", {
|
|
2060
2102
|
method: "POST",
|
|
2061
|
-
headers: { "Content-Type": "application/json" },
|
|
2103
|
+
headers: writeHeaders({ "Content-Type": "application/json" }),
|
|
2062
2104
|
body: JSON.stringify({ token }),
|
|
2063
2105
|
}).then(r => r.json()).catch(() => null);
|
|
2064
2106
|
|
|
@@ -2496,6 +2538,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2496
2538
|
const UNKNOWN_SERVICE_THRESHOLD = 2; // misses before we return the full service list
|
|
2497
2539
|
let authHardLocked = false;
|
|
2498
2540
|
let password = initPassword || null; // null = locked; set via POST /auth
|
|
2541
|
+
let writeSession = null; // null until explicit password auth grants write scope
|
|
2499
2542
|
const machineHash = getMachineHash();
|
|
2500
2543
|
|
|
2501
2544
|
// Rotation engine — starts after unlock
|
|
@@ -2522,6 +2565,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2522
2565
|
whitelist,
|
|
2523
2566
|
failCount,
|
|
2524
2567
|
MAX_FAILS,
|
|
2568
|
+
writeEnabled: process.env.CLAUTH_MCP_WRITE === "1",
|
|
2525
2569
|
};
|
|
2526
2570
|
}
|
|
2527
2571
|
|
|
@@ -2916,6 +2960,22 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2916
2960
|
const studioDebugHandled = await studioDebugRuntime.handle(req, res, url, CORS);
|
|
2917
2961
|
if (studioDebugHandled !== false) return;
|
|
2918
2962
|
|
|
2963
|
+
if (method === "GET" && reqPath === "/watchdog/services") {
|
|
2964
|
+
return ok(res, await getWatchdogStatuses());
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
if (method === "GET" && reqPath === "/watchdog/events") {
|
|
2968
|
+
const limit = Number(url.searchParams.get("limit") || 100);
|
|
2969
|
+
return ok(res, { events: readWatchdogEvents(limit) });
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
const restartMatch = reqPath.match(/^\/watchdog\/services\/([^/]+)\/restart$/);
|
|
2973
|
+
if (method === "POST" && restartMatch) {
|
|
2974
|
+
const result = restartWatchdogService(decodeURIComponent(restartMatch[1]));
|
|
2975
|
+
res.writeHead(result.ok ? 200 : 403, { "Content-Type": "application/json", ...CORS });
|
|
2976
|
+
return res.end(JSON.stringify(result));
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2919
2979
|
// ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
|
|
2920
2980
|
const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
|
|
2921
2981
|
const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
|
|
@@ -3115,12 +3175,13 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3115
3175
|
const MCP_PATHS = ["/mcp", "/gws", "/clauth", "/fs", "/chitchat", "/codevelop"];
|
|
3116
3176
|
const isMcpPath = MCP_PATHS.includes(reqPath);
|
|
3117
3177
|
function toolsForPath(p) {
|
|
3118
|
-
|
|
3119
|
-
if (p === "/
|
|
3120
|
-
if (p === "/
|
|
3121
|
-
if (p === "/
|
|
3122
|
-
if (p === "/
|
|
3123
|
-
|
|
3178
|
+
const tools = filterMcpToolsForWriteMode(MCP_TOOLS);
|
|
3179
|
+
if (p === "/gws") return tools.filter(t => t.name.startsWith("gws_"));
|
|
3180
|
+
if (p === "/clauth") return tools.filter(t => t.name.startsWith("clauth_") || t.name === "monkey_dispatch" || t.name.startsWith("terminal_") || t.name.startsWith("channel_"));
|
|
3181
|
+
if (p === "/fs") return tools.filter(t => t.name.startsWith("fs_"));
|
|
3182
|
+
if (p === "/chitchat") return tools.filter(t => t.name.startsWith("chitchat_"));
|
|
3183
|
+
if (p === "/codevelop") return tools.filter(t => t.name.startsWith("codevelop_"));
|
|
3184
|
+
return tools; // /mcp — all tools
|
|
3124
3185
|
}
|
|
3125
3186
|
function serverNameForPath(p) {
|
|
3126
3187
|
if (p === "/gws") return "gws";
|
|
@@ -3324,7 +3385,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3324
3385
|
if (rpcMethod === "tools/list") {
|
|
3325
3386
|
sseSend(session.res, "message", {
|
|
3326
3387
|
jsonrpc: "2.0", id,
|
|
3327
|
-
result: { tools: MCP_TOOLS }
|
|
3388
|
+
result: { tools: filterMcpToolsForWriteMode(MCP_TOOLS) }
|
|
3328
3389
|
});
|
|
3329
3390
|
return;
|
|
3330
3391
|
}
|
|
@@ -4121,6 +4182,16 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4121
4182
|
return false;
|
|
4122
4183
|
}
|
|
4123
4184
|
|
|
4185
|
+
function writeGuard(req, res) {
|
|
4186
|
+
if (lockedGuard(res)) return true;
|
|
4187
|
+
if (!validateWriteToken(req, writeSession)) {
|
|
4188
|
+
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
4189
|
+
res.end(JSON.stringify({ error: "write token required", write_locked: true }));
|
|
4190
|
+
return true;
|
|
4191
|
+
}
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4124
4195
|
// GET /meta — return valid key_types from DB check constraint
|
|
4125
4196
|
if (method === "GET" && reqPath === "/meta") {
|
|
4126
4197
|
if (lockedGuard(res)) return;
|
|
@@ -4311,6 +4382,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4311
4382
|
const result = await api.test(pw, machineHash, token, timestamp);
|
|
4312
4383
|
if (result.error) throw new Error(result.error);
|
|
4313
4384
|
password = pw; // unlock — store in process memory only
|
|
4385
|
+
writeSession = makeWriteToken();
|
|
4314
4386
|
authFailCount = 0;
|
|
4315
4387
|
authHardLocked = false;
|
|
4316
4388
|
const logLine = `[${new Date().toISOString()}] Vault unlocked\n`;
|
|
@@ -4365,7 +4437,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4365
4437
|
tunnelStatus = "starting";
|
|
4366
4438
|
startTunnel().catch(() => {});
|
|
4367
4439
|
}
|
|
4368
|
-
return ok(res, { ok: true, locked: false });
|
|
4440
|
+
return ok(res, { ok: true, locked: false, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
|
|
4369
4441
|
} catch {
|
|
4370
4442
|
authFailCount++;
|
|
4371
4443
|
const authRemaining = MAX_AUTH_FAILS - authFailCount;
|
|
@@ -4386,6 +4458,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4386
4458
|
// POST /lock — clear password from memory
|
|
4387
4459
|
if (method === "POST" && reqPath === "/lock") {
|
|
4388
4460
|
password = null;
|
|
4461
|
+
writeSession = null;
|
|
4389
4462
|
stopTunnel();
|
|
4390
4463
|
const logLine = `[${new Date().toISOString()}] Vault locked\n`;
|
|
4391
4464
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
@@ -4395,7 +4468,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4395
4468
|
// POST /rename/:service — rename a service
|
|
4396
4469
|
const renameMatch = reqPath.match(/^\/rename\/([a-zA-Z0-9_-]+)$/);
|
|
4397
4470
|
if (method === "POST" && renameMatch) {
|
|
4398
|
-
if (
|
|
4471
|
+
if (writeGuard(req, res)) return;
|
|
4399
4472
|
const service = renameMatch[1].toLowerCase();
|
|
4400
4473
|
let body;
|
|
4401
4474
|
try { body = await readBody(req); } catch {
|
|
@@ -4420,7 +4493,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4420
4493
|
// POST /delete/:service — remove a service entirely
|
|
4421
4494
|
const deleteMatch = reqPath.match(/^\/delete\/([a-zA-Z0-9_-]+)$/);
|
|
4422
4495
|
if (method === "POST" && deleteMatch) {
|
|
4423
|
-
if (
|
|
4496
|
+
if (writeGuard(req, res)) return;
|
|
4424
4497
|
const service = deleteMatch[1].toLowerCase();
|
|
4425
4498
|
try {
|
|
4426
4499
|
const { token, timestamp } = deriveToken(password, machineHash);
|
|
@@ -4435,7 +4508,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4435
4508
|
// POST /toggle/:service — enable or disable a service
|
|
4436
4509
|
const toggleMatch = reqPath.match(/^\/toggle\/([a-zA-Z0-9_-]+)$/);
|
|
4437
4510
|
if (method === "POST" && toggleMatch) {
|
|
4438
|
-
if (
|
|
4511
|
+
if (writeGuard(req, res)) return;
|
|
4439
4512
|
const service = toggleMatch[1].toLowerCase();
|
|
4440
4513
|
|
|
4441
4514
|
let body;
|
|
@@ -4519,7 +4592,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4519
4592
|
|
|
4520
4593
|
// POST /rotate/:service — manually trigger rotation for a service
|
|
4521
4594
|
if (method === "POST" && reqPath.startsWith("/rotate/")) {
|
|
4522
|
-
if (
|
|
4595
|
+
if (writeGuard(req, res)) return;
|
|
4523
4596
|
const service = reqPath.slice("/rotate/".length);
|
|
4524
4597
|
if (!service) {
|
|
4525
4598
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
@@ -4531,7 +4604,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4531
4604
|
|
|
4532
4605
|
// POST /set-expiry/:service — set expiry date for a service
|
|
4533
4606
|
if (method === "POST" && reqPath.startsWith("/set-expiry/")) {
|
|
4534
|
-
if (
|
|
4607
|
+
if (writeGuard(req, res)) return;
|
|
4535
4608
|
const service = reqPath.slice("/set-expiry/".length);
|
|
4536
4609
|
let body;
|
|
4537
4610
|
try { body = await readBody(req); } catch {
|
|
@@ -4688,7 +4761,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4688
4761
|
|
|
4689
4762
|
// POST /tunnel/setup/cf-token
|
|
4690
4763
|
if (method === "POST" && reqPath === "/tunnel/setup/cf-token") {
|
|
4691
|
-
if (
|
|
4764
|
+
if (writeGuard(req, res)) return;
|
|
4692
4765
|
let body;
|
|
4693
4766
|
try { body = await readBody(req); } catch { return strike(res, 400, "Invalid JSON"); }
|
|
4694
4767
|
const { token: cfToken } = body;
|
|
@@ -4708,9 +4781,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4708
4781
|
const accountId = ad?.result?.[0]?.id;
|
|
4709
4782
|
const accountName = ad?.result?.[0]?.name;
|
|
4710
4783
|
|
|
4711
|
-
// Save token to vault using
|
|
4712
|
-
|
|
4713
|
-
await api.write(password, machineHash, t, timestamp, "cloudflare", cfToken);
|
|
4784
|
+
// Save token to vault using the same guarded recovery path as /set/:service.
|
|
4785
|
+
await writeCredentialWithRecovery({ password, machineHash, service: "cloudflare", value: cfToken, logFile: LOG_FILE });
|
|
4714
4786
|
|
|
4715
4787
|
// Save accountId to clauth_config
|
|
4716
4788
|
const sbUrl = (api.getBaseUrl() || "").replace("/functions/v1/auth-vault", "");
|
|
@@ -4991,7 +5063,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4991
5063
|
|
|
4992
5064
|
// POST /change-pw — change master password (must be unlocked)
|
|
4993
5065
|
if (method === "POST" && reqPath === "/change-pw") {
|
|
4994
|
-
if (
|
|
5066
|
+
if (writeGuard(req, res)) return;
|
|
4995
5067
|
|
|
4996
5068
|
let body;
|
|
4997
5069
|
try { body = await readBody(req); } catch {
|
|
@@ -5011,9 +5083,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5011
5083
|
const result = await api.changePassword(password, machineHash, token, timestamp, newSeedHash);
|
|
5012
5084
|
if (result.error) throw new Error(result.error);
|
|
5013
5085
|
password = newPassword; // update in-memory password to new one
|
|
5086
|
+
writeSession = makeWriteToken();
|
|
5014
5087
|
const logLine = `[${new Date().toISOString()}] Password changed\n`;
|
|
5015
5088
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
5016
|
-
return ok(res, { ok: true });
|
|
5089
|
+
return ok(res, { ok: true, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
|
|
5017
5090
|
} catch (err) {
|
|
5018
5091
|
res.writeHead(502, { "Content-Type": "application/json", ...CORS });
|
|
5019
5092
|
return res.end(JSON.stringify({ error: err.message }));
|
|
@@ -5023,7 +5096,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5023
5096
|
// POST /set/:service — write a new key value into vault
|
|
5024
5097
|
const setMatch = reqPath.match(/^\/set\/([a-zA-Z0-9_-]+)$/);
|
|
5025
5098
|
if (method === "POST" && setMatch) {
|
|
5026
|
-
if (
|
|
5099
|
+
if (writeGuard(req, res)) return;
|
|
5027
5100
|
const service = setMatch[1].toLowerCase();
|
|
5028
5101
|
|
|
5029
5102
|
if (whitelist && !whitelist.includes(service)) {
|
|
@@ -5042,10 +5115,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5042
5115
|
}
|
|
5043
5116
|
|
|
5044
5117
|
try {
|
|
5045
|
-
const {
|
|
5046
|
-
const result = await api.write(password, machineHash, token, timestamp, service, value.trim());
|
|
5118
|
+
const { result, snapshot, normalized } = await writeCredentialWithRecovery({ password, machineHash, service, value, logFile: LOG_FILE });
|
|
5047
5119
|
if (result.error) return strike(res, 502, result.error);
|
|
5048
|
-
return ok(res, { ok: true, service });
|
|
5120
|
+
return ok(res, { ok: true, service, recovery_snapshot: snapshot?.ok ? true : false, normalized });
|
|
5049
5121
|
} catch (err) {
|
|
5050
5122
|
return strike(res, 502, err.message);
|
|
5051
5123
|
}
|
|
@@ -5053,7 +5125,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5053
5125
|
|
|
5054
5126
|
// POST /generate-token — generate a cryptographically random bearer token and store it
|
|
5055
5127
|
if (method === "POST" && reqPath === "/generate-token") {
|
|
5056
|
-
if (
|
|
5128
|
+
if (writeGuard(req, res)) return;
|
|
5057
5129
|
|
|
5058
5130
|
let body;
|
|
5059
5131
|
try { body = await readBody(req); } catch {
|
|
@@ -5075,10 +5147,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5075
5147
|
try {
|
|
5076
5148
|
const randomHex = crypto.randomBytes(32).toString("hex");
|
|
5077
5149
|
const token = `${prefix}${randomHex}`;
|
|
5078
|
-
const {
|
|
5079
|
-
const result = await api.write(password, machineHash, authToken, timestamp, service, token);
|
|
5150
|
+
const { result, snapshot } = await writeCredentialWithRecovery({ password, machineHash, service, value: token, logFile: LOG_FILE, normalize: false });
|
|
5080
5151
|
if (result.error) return strike(res, 502, result.error);
|
|
5081
|
-
return ok(res, { token, service, stored: true });
|
|
5152
|
+
return ok(res, { token, service, stored: true, recovery_snapshot: snapshot?.ok ? true : false });
|
|
5082
5153
|
} catch (err) {
|
|
5083
5154
|
return strike(res, 502, err.message);
|
|
5084
5155
|
}
|
|
@@ -5086,7 +5157,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5086
5157
|
|
|
5087
5158
|
// POST /add-service — register a new service in the vault
|
|
5088
5159
|
if (method === "POST" && reqPath === "/add-service") {
|
|
5089
|
-
if (
|
|
5160
|
+
if (writeGuard(req, res)) return;
|
|
5090
5161
|
|
|
5091
5162
|
let body;
|
|
5092
5163
|
try { body = await readBody(req); } catch {
|
|
@@ -5118,7 +5189,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5118
5189
|
|
|
5119
5190
|
// POST /update-service — update service metadata (project, label, description)
|
|
5120
5191
|
if (method === "POST" && reqPath === "/update-service") {
|
|
5121
|
-
if (
|
|
5192
|
+
if (writeGuard(req, res)) return;
|
|
5122
5193
|
|
|
5123
5194
|
let body;
|
|
5124
5195
|
try { body = await readBody(req); } catch {
|
|
@@ -6316,7 +6387,7 @@ async function getFileserverMounts(vault) {
|
|
|
6316
6387
|
async function resolveInMount(requestedPath, mountName, vault) {
|
|
6317
6388
|
const { mounts, error } = await getFileserverMounts(vault);
|
|
6318
6389
|
if (error) return { error };
|
|
6319
|
-
if (!mounts || mounts.length === 0) return { error: "No fileserver services configured. Add one with key_type='fileserver' and value: {\"path\": \"C:/Dev/regen-root\", \"access\": \"
|
|
6390
|
+
if (!mounts || mounts.length === 0) return { error: "No fileserver services configured. Add one with key_type='fileserver' and value: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwdg\"} (access flags: r=read w=write d=delete g=git)" };
|
|
6320
6391
|
const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
|
|
6321
6392
|
if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
|
|
6322
6393
|
if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
|
|
@@ -6411,6 +6482,15 @@ function runGitRaw(cwd, args, opts = {}) {
|
|
|
6411
6482
|
return res.stdout || Buffer.alloc(0);
|
|
6412
6483
|
}
|
|
6413
6484
|
|
|
6485
|
+
// Read a single credential value from the vault inside an MCP handler.
|
|
6486
|
+
// (The git operations themselves live in ../lib/fs-git.js — pure + testable.)
|
|
6487
|
+
async function vaultRetrieveValue(vault, service) {
|
|
6488
|
+
if (!vault.password) return { error: "locked" };
|
|
6489
|
+
if (vault.whitelist && !vault.whitelist.includes(service.toLowerCase())) return { error: "not_in_whitelist" };
|
|
6490
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
6491
|
+
return api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6414
6494
|
function normalizeRepoPath(p) {
|
|
6415
6495
|
if (!p || typeof p !== "string") return null;
|
|
6416
6496
|
const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
@@ -7144,8 +7224,63 @@ const MCP_TOOLS = [
|
|
|
7144
7224
|
description: "List configured filesystem mounts (fileserver services from vault).",
|
|
7145
7225
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
7146
7226
|
},
|
|
7227
|
+
{
|
|
7228
|
+
name: "fs_repo_status",
|
|
7229
|
+
description: "Get the current git state of the mounted repo in ONE call: branch, HEAD sha + subject, whether the tree is clean or dirty (with changed paths), staged paths, how far ahead/behind the remote you are, and whether a merge or rebase is in progress. Call this first so you KNOW what you are working with before committing — you never have to guess whether your edits or a previous push landed. Requires 'g' (git) access on the mount.",
|
|
7230
|
+
inputSchema: {
|
|
7231
|
+
type: "object",
|
|
7232
|
+
properties: {
|
|
7233
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7234
|
+
},
|
|
7235
|
+
additionalProperties: false,
|
|
7236
|
+
},
|
|
7237
|
+
},
|
|
7238
|
+
{
|
|
7239
|
+
name: "fs_use_branch",
|
|
7240
|
+
description: "Safely make sure you are on a branch before committing. Switches to an existing branch, or creates a new one from the current HEAD when create=true. Your uncommitted edits are carried forward; if switching would overwrite local changes it refuses and leaves the tree untouched, telling you which files block the switch. Refuses while a merge/rebase is in progress. Requires 'g' (git) access on the mount.",
|
|
7241
|
+
inputSchema: {
|
|
7242
|
+
type: "object",
|
|
7243
|
+
properties: {
|
|
7244
|
+
branch: { type: "string", description: "Branch name to switch to (or create)" },
|
|
7245
|
+
create: { type: "boolean", description: "Create a new branch from current HEAD instead of switching to an existing one (default false)" },
|
|
7246
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7247
|
+
},
|
|
7248
|
+
required: ["branch"],
|
|
7249
|
+
additionalProperties: false,
|
|
7250
|
+
},
|
|
7251
|
+
},
|
|
7252
|
+
{
|
|
7253
|
+
name: "fs_commit",
|
|
7254
|
+
description: "Save the files you edited durably: stages changes, commits, and PUSHES by default — usually a single call is all you need after editing. Handles the messy cases for you: nothing-to-commit returns cleanly (no error); a merge/rebase in progress or a detached HEAD is refused with a clear message; if the branch is behind the remote it auto-rebases and retries the push, and on conflict it aborts the rebase (restoring your tree) and keeps the commit local so nothing is lost. Refuses to push protected branches (main/master/production) — those are promoted by a human. Returns the commit sha, the exact files committed, whether the push succeeded, and ahead/behind counts, so you never need a follow-up status call. Push auth uses the vault's github token directly (never exposed). Requires 'g' (git) access on the mount.",
|
|
7255
|
+
inputSchema: {
|
|
7256
|
+
type: "object",
|
|
7257
|
+
properties: {
|
|
7258
|
+
message: { type: "string", description: "Commit message (required)" },
|
|
7259
|
+
paths: { type: "array", items: { type: "string" }, description: "Repo-relative paths to commit. Omit to commit ALL current changes in the repo." },
|
|
7260
|
+
push: { type: "boolean", description: "Push to the remote after committing (default true)" },
|
|
7261
|
+
remote: { type: "string", description: "Git remote name (default: origin)" },
|
|
7262
|
+
author_name: { type: "string", description: "Commit author name (default: clauth-fs)" },
|
|
7263
|
+
author_email: { type: "string", description: "Commit author email (default: fs@clauth.local)" },
|
|
7264
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7265
|
+
},
|
|
7266
|
+
required: ["message"],
|
|
7267
|
+
additionalProperties: false,
|
|
7268
|
+
},
|
|
7269
|
+
},
|
|
7147
7270
|
];
|
|
7148
7271
|
|
|
7272
|
+
const MCP_WRITE_TOOL_NAMES = new Set([
|
|
7273
|
+
"clauth_enable",
|
|
7274
|
+
"clauth_disable",
|
|
7275
|
+
"clauth_set_project",
|
|
7276
|
+
"clauth_generate_token",
|
|
7277
|
+
]);
|
|
7278
|
+
|
|
7279
|
+
function filterMcpToolsForWriteMode(tools) {
|
|
7280
|
+
if (process.env.CLAUTH_MCP_WRITE === "1") return tools;
|
|
7281
|
+
return tools.filter(tool => !MCP_WRITE_TOOL_NAMES.has(tool.name));
|
|
7282
|
+
}
|
|
7283
|
+
|
|
7149
7284
|
function writeTempSecret(service, value) {
|
|
7150
7285
|
const filePath = path.join(os.tmpdir(), `.clauth-${service}`);
|
|
7151
7286
|
fs.writeFileSync(filePath, value, { mode: 0o600 });
|
|
@@ -7176,6 +7311,11 @@ function mcpError(text) {
|
|
|
7176
7311
|
const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
|
|
7177
7312
|
|
|
7178
7313
|
async function handleMcpTool(vault, name, args) {
|
|
7314
|
+
const requireMcpWrite = () => {
|
|
7315
|
+
if (vault.writeEnabled) return null;
|
|
7316
|
+
return mcpError("MCP write tools are disabled by default. Launch clauth with CLAUTH_MCP_WRITE=1 for an explicit write-capable session.");
|
|
7317
|
+
};
|
|
7318
|
+
|
|
7179
7319
|
switch (name) {
|
|
7180
7320
|
case "clauth_ping": {
|
|
7181
7321
|
return mcpResult(
|
|
@@ -7357,6 +7497,8 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7357
7497
|
}
|
|
7358
7498
|
|
|
7359
7499
|
case "clauth_enable": {
|
|
7500
|
+
const writeError = requireMcpWrite();
|
|
7501
|
+
if (writeError) return writeError;
|
|
7360
7502
|
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7361
7503
|
const service = (args.service || "").toLowerCase();
|
|
7362
7504
|
if (!service) return mcpError("service is required");
|
|
@@ -7371,6 +7513,8 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7371
7513
|
}
|
|
7372
7514
|
|
|
7373
7515
|
case "clauth_disable": {
|
|
7516
|
+
const writeError = requireMcpWrite();
|
|
7517
|
+
if (writeError) return writeError;
|
|
7374
7518
|
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7375
7519
|
const service = (args.service || "").toLowerCase();
|
|
7376
7520
|
if (!service) return mcpError("service is required");
|
|
@@ -7410,6 +7554,8 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7410
7554
|
}
|
|
7411
7555
|
|
|
7412
7556
|
case "clauth_set_project": {
|
|
7557
|
+
const writeError = requireMcpWrite();
|
|
7558
|
+
if (writeError) return writeError;
|
|
7413
7559
|
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7414
7560
|
const service = (args.service || "").toLowerCase();
|
|
7415
7561
|
const project = args.project;
|
|
@@ -7461,6 +7607,8 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7461
7607
|
}
|
|
7462
7608
|
|
|
7463
7609
|
case "clauth_generate_token": {
|
|
7610
|
+
const writeError = requireMcpWrite();
|
|
7611
|
+
if (writeError) return writeError;
|
|
7464
7612
|
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7465
7613
|
const service = (args.service || "").trim().toLowerCase();
|
|
7466
7614
|
const prefix = args.prefix || "";
|
|
@@ -7468,8 +7616,14 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7468
7616
|
try {
|
|
7469
7617
|
const randomHex = crypto.randomBytes(32).toString("hex");
|
|
7470
7618
|
const generatedToken = `${prefix}${randomHex}`;
|
|
7471
|
-
const {
|
|
7472
|
-
|
|
7619
|
+
const { result } = await writeCredentialWithRecovery({
|
|
7620
|
+
password: vault.password,
|
|
7621
|
+
machineHash: vault.machineHash,
|
|
7622
|
+
service,
|
|
7623
|
+
value: generatedToken,
|
|
7624
|
+
logFile: LOG_FILE,
|
|
7625
|
+
normalize: false,
|
|
7626
|
+
});
|
|
7473
7627
|
if (result.error) return mcpError(result.error);
|
|
7474
7628
|
return mcpResult(`Token generated and stored under "${service}": ${generatedToken}`);
|
|
7475
7629
|
} catch (err) {
|
|
@@ -8058,10 +8212,67 @@ async function handleMcpTool(vault, name, args) {
|
|
|
8058
8212
|
case "fs_mounts": {
|
|
8059
8213
|
const { mounts, error } = await getFileserverMounts(vault);
|
|
8060
8214
|
if (error) return mcpError(error);
|
|
8061
|
-
if (!mounts || mounts.length === 0) return mcpResult("No fileserver mounts configured. Create one:\n1. Use clauth dashboard or clauth_enable to add a service with key_type='fileserver'\n2. Set the secret value to JSON: {\"path\": \"C:/Dev/regen-root\", \"access\": \"
|
|
8215
|
+
if (!mounts || mounts.length === 0) return mcpResult("No fileserver mounts configured. Create one:\n1. Use clauth dashboard or clauth_enable to add a service with key_type='fileserver'\n2. Set the secret value to JSON: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwdg\"} (add 'g' to allow the git verbs: fs_commit, fs_use_branch, fs_repo_status)");
|
|
8062
8216
|
return mcpResult(JSON.stringify(mounts, null, 2));
|
|
8063
8217
|
}
|
|
8064
8218
|
|
|
8219
|
+
case "fs_repo_status": {
|
|
8220
|
+
const r = await resolveInMount(".", args.mount, vault);
|
|
8221
|
+
if (r.error) return mcpError(r.error);
|
|
8222
|
+
if (!checkAccess(r.mount, "g")) return mcpError("Git access denied on this mount. Add 'g' to the mount's access string (e.g. 'rwdg') to enable the git verbs.");
|
|
8223
|
+
try {
|
|
8224
|
+
const { result, error } = await fsGit.repoStatus(r.resolved);
|
|
8225
|
+
if (error) return mcpError(error);
|
|
8226
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
8227
|
+
} catch (err) {
|
|
8228
|
+
return mcpError(`Status failed: ${err.message}`);
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
|
|
8232
|
+
case "fs_use_branch": {
|
|
8233
|
+
const r = await resolveInMount(".", args.mount, vault);
|
|
8234
|
+
if (r.error) return mcpError(r.error);
|
|
8235
|
+
if (!checkAccess(r.mount, "g")) return mcpError("Git access denied on this mount. Add 'g' to the mount's access string (e.g. 'rwdg') to enable the git verbs.");
|
|
8236
|
+
try {
|
|
8237
|
+
const { result, error } = await fsGit.useBranch(r.resolved, args.branch, args.create === true);
|
|
8238
|
+
if (error) return mcpError(error);
|
|
8239
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
8240
|
+
} catch (err) {
|
|
8241
|
+
return mcpError(`Branch switch failed: ${err.message}`);
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
|
|
8245
|
+
case "fs_commit": {
|
|
8246
|
+
const r = await resolveInMount(".", args.mount, vault);
|
|
8247
|
+
if (r.error) return mcpError(r.error);
|
|
8248
|
+
if (!checkAccess(r.mount, "g")) return mcpError("Git access denied on this mount. Add 'g' to the mount's access string (e.g. 'rwdg') to enable the git verbs.");
|
|
8249
|
+
const push = args.push !== false;
|
|
8250
|
+
// Fetch the push token from the vault up front (commit happens first, so
|
|
8251
|
+
// a missing token still preserves the local commit).
|
|
8252
|
+
let token = null, tokenError = null;
|
|
8253
|
+
if (push) {
|
|
8254
|
+
const sec = await vaultRetrieveValue(vault, "github");
|
|
8255
|
+
if (sec.error || !sec.value) tokenError = `could not read the 'github' token (${sec.error || "empty"})`;
|
|
8256
|
+
else token = sec.value;
|
|
8257
|
+
}
|
|
8258
|
+
try {
|
|
8259
|
+
const { result, error } = await fsGit.commit(r.resolved, {
|
|
8260
|
+
message: args.message,
|
|
8261
|
+
paths: args.paths,
|
|
8262
|
+
push,
|
|
8263
|
+
remote: args.remote,
|
|
8264
|
+
token,
|
|
8265
|
+
tokenError,
|
|
8266
|
+
authorName: args.author_name,
|
|
8267
|
+
authorEmail: args.author_email,
|
|
8268
|
+
});
|
|
8269
|
+
if (error) return mcpError(error);
|
|
8270
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
8271
|
+
} catch (err) {
|
|
8272
|
+
return mcpError(`Commit failed: ${err.message}`);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
|
|
8065
8276
|
case "monkey_dispatch": {
|
|
8066
8277
|
const { prompt, job_id } = args;
|
|
8067
8278
|
if (!prompt) return mcpError("prompt required");
|
|
@@ -8251,6 +8462,7 @@ function createMcpServer(initPassword, whitelist) {
|
|
|
8251
8462
|
whitelist,
|
|
8252
8463
|
failCount: 0,
|
|
8253
8464
|
MAX_FAILS: 10,
|
|
8465
|
+
writeEnabled: process.env.CLAUTH_MCP_WRITE === "1",
|
|
8254
8466
|
};
|
|
8255
8467
|
|
|
8256
8468
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
@@ -8289,7 +8501,7 @@ function createMcpServer(initPassword, whitelist) {
|
|
|
8289
8501
|
if (msg.method === "tools/list") {
|
|
8290
8502
|
return send({
|
|
8291
8503
|
jsonrpc: "2.0", id,
|
|
8292
|
-
result: { tools: MCP_TOOLS }
|
|
8504
|
+
result: { tools: filterMcpToolsForWriteMode(MCP_TOOLS) }
|
|
8293
8505
|
});
|
|
8294
8506
|
}
|
|
8295
8507
|
|