@lifeaitools/clauth 2.1.1 → 2.10.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/references/keys-guide.md +270 -270
- package/cli/api.js +238 -238
- package/cli/commands/install.js +413 -396
- package/cli/commands/login.test.js +49 -1
- package/cli/commands/ops-install.js +11 -10
- package/cli/commands/ops.test.js +270 -0
- package/cli/commands/scrub.test.js +70 -0
- package/cli/commands/serve.js +2618 -11238
- package/cli/commands/uninstall.js +164 -164
- package/cli/dashboard/dashboard.js +592 -0
- package/cli/dashboard/global.css +65 -0
- package/cli/dashboard/panel-element.js +79 -0
- package/cli/dashboard/panels/build-status-panel.css +12 -0
- package/cli/dashboard/panels/build-status-panel.js +86 -0
- package/cli/dashboard/panels/panel-registry.js +58 -0
- package/cli/dashboard/panels/services-panel.css +95 -0
- package/cli/dashboard/panels/services-panel.js +872 -0
- package/cli/dashboard/panels/surfaces-panel.css +59 -0
- package/cli/dashboard/panels/surfaces-panel.js +474 -0
- package/cli/dashboard/panels/toolbar-panel.css +42 -0
- package/cli/dashboard/panels/toolbar-panel.js +374 -0
- package/cli/dashboard/panels/tunnel-panel.css +61 -0
- package/cli/dashboard/panels/tunnel-panel.js +688 -0
- package/cli/dashboard/panels/webdav-panel.css +38 -0
- package/cli/dashboard/panels/webdav-panel.js +166 -0
- package/cli/fingerprint.test.js +95 -0
- package/cli/http/components/callagent-terminal-component.js +310 -0
- package/cli/http/components/dashboard-component.js +123 -0
- package/cli/http/components/inbox-component.js +41 -0
- package/cli/http/components/mcp-transport-component.js +275 -0
- package/cli/http/components/oauth-component.js +207 -0
- package/cli/http/components/service-crud-component.js +288 -0
- package/cli/http/components/supervisor-component.js +104 -0
- package/cli/http/components/system-component.js +184 -0
- package/cli/http/components/tunnel-component.js +508 -0
- package/cli/http/components/vault-auth-component.js +264 -0
- package/cli/http/components/watchdog-component.js +28 -0
- package/cli/http/request-utils.js +21 -0
- package/cli/http/servers/ops-server.js +335 -0
- package/cli/index.js +5 -91
- package/cli/lib/fs-git.test.js +467 -0
- package/cli/mcp/providers/call-agent-provider.js +30 -0
- package/cli/mcp/providers/clauth-core-provider.js +461 -0
- package/cli/mcp/providers/ops-provider.js +42 -0
- package/cli/mcp/providers/terminal-provider.js +81 -0
- package/cli/mcp/tool-registry.js +48 -0
- package/cli/mcp/tool-schemas.js +822 -0
- package/cli/mcp/tool-schemas.test.js +49 -0
- package/cli/ops/pm2-preflight.js +54 -0
- package/cli/recovery.js +6 -0
- package/cli/recovery.test.js +179 -0
- package/cli/services/agent-pool-instance.js +38 -0
- package/cli/services/claude-binary.js +29 -0
- package/cli/services/delegation-lane-instance.js +35 -0
- package/cli/services/delegation-lane-instance.test.js +37 -0
- package/cli/services/file-io.js +38 -0
- package/cli/services/file-io.test.js +10 -0
- package/cli/services/fs-mount.js +66 -0
- package/cli/services/fs-mount.test.js +14 -0
- package/cli/services/fs-upload-sessions.js +20 -0
- package/cli/services/fs-upload-sessions.test.js +14 -0
- package/cli/services/git-exec.js +37 -0
- package/cli/services/git-exec.test.js +41 -0
- package/cli/services/git-import-guard.js +28 -0
- package/cli/services/git-import-guard.test.js +18 -0
- package/cli/services/service-status.js +191 -0
- package/cli/services/service-status.test.js +39 -0
- package/cli/services/terminal-dispatch.js +135 -0
- package/cli/services/terminal-dispatch.test.js +9 -0
- package/cli/services/terminal-registry.js +39 -0
- package/cli/services/terminal-registry.test.js +31 -0
- package/cli/services/terminal-sessions.js +100 -0
- package/cli/services/terminal-sessions.test.js +26 -0
- package/cli/services/terminal-window.js +106 -0
- package/cli/services/vault-read.js +14 -0
- package/cli/services/vault-read.test.js +35 -0
- package/cli/supervisor-registry.js +116 -0
- package/cli/supervisor-registry.test.js +165 -0
- package/cli/supervisor-ui.test.js +230 -30
- package/cli/webdav-config.js +84 -0
- package/cli/webdav-config.test.js +79 -0
- package/cli/webdav-obscure.js +23 -0
- package/cli/webdav-obscure.test.js +41 -0
- package/cli/webdav-rclone-discovery.js +44 -0
- package/cli/webdav-service.js +30 -118
- package/cli/webdav-service.test.js +107 -0
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +2 -2
- package/scripts/bootstrap.cjs +121 -121
- 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/assets/codevelop/launcher-active.cmd.template +0 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
- package/cli/commands/codevelop.js +0 -1190
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// cli/mcp/providers/clauth-core-provider.js
|
|
2
|
+
// clauth's own core vault-operation MCP tools, as their own provider file
|
|
3
|
+
// registering against the shared cli/mcp/tool-registry.js. Third and
|
|
4
|
+
// largest file-level split (after terminal-provider.js and
|
|
5
|
+
// ops-provider.js) — 17 registrations: ping, unlock, lock, status,
|
|
6
|
+
// knowledge, list, search, get, inject, enable, disable, test, scrub,
|
|
7
|
+
// set_project, self_check, generate_token, enroll_machine.
|
|
8
|
+
//
|
|
9
|
+
// Unlike the two earlier providers, this group had no single pre-existing
|
|
10
|
+
// service module to lean on — its dependencies span several existing
|
|
11
|
+
// modules (fingerprint.js, api.js, services/service-status.js,
|
|
12
|
+
// recovery.js, conf-path.js, enrollment-script.js) plus two small,
|
|
13
|
+
// genuinely generic local helpers (writeTempSecret, copyToClipboard) that
|
|
14
|
+
// had no callers outside this group (confirmed via grep before moving) and
|
|
15
|
+
// moved alongside it. call_agent stayed in serve.js -- it depends on
|
|
16
|
+
// runCallAgent, a non-trivial serve.js-local function not yet extracted.
|
|
17
|
+
//
|
|
18
|
+
// LOG_FILE is intentionally NOT imported from serve.js: that constant has
|
|
19
|
+
// 67 references there (clauth's own daemon-log path, used well beyond MCP
|
|
20
|
+
// tools) and stays serve.js's own. clauth_generate_token's use of it here
|
|
21
|
+
// is the one place this provider needs it, so the same trivial one-line
|
|
22
|
+
// expression is repeated rather than threading an import for a single
|
|
23
|
+
// constant used once.
|
|
24
|
+
//
|
|
25
|
+
// serve.js imports this file for its side effect (the registerTool() calls
|
|
26
|
+
// below run at import time) — see the `import "../mcp/providers/
|
|
27
|
+
// clauth-core-provider.js";` line in cli/commands/serve.js.
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import crypto from "node:crypto";
|
|
32
|
+
import { execSync } from "node:child_process";
|
|
33
|
+
import Conf from "conf";
|
|
34
|
+
import { mcpResult, mcpError, registerTool, requireMcpWrite } from "../tool-registry.js";
|
|
35
|
+
import { deriveToken } from "../../fingerprint.js";
|
|
36
|
+
import * as api from "../../api.js";
|
|
37
|
+
import { getConfOptions } from "../../conf-path.js";
|
|
38
|
+
import { writeCredentialWithRecovery } from "../../recovery.js";
|
|
39
|
+
import { writeEnrollmentScript } from "../../enrollment-script.js";
|
|
40
|
+
import {
|
|
41
|
+
ENV_MAP,
|
|
42
|
+
invalidateServiceStatusCache,
|
|
43
|
+
sanitizeServiceMetadata,
|
|
44
|
+
getCachedServiceStatus,
|
|
45
|
+
searchServices,
|
|
46
|
+
} from "../../services/service-status.js";
|
|
47
|
+
|
|
48
|
+
const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
|
|
49
|
+
|
|
50
|
+
function writeTempSecret(service, value) {
|
|
51
|
+
const filePath = path.join(os.tmpdir(), `.clauth-${service}`);
|
|
52
|
+
fs.writeFileSync(filePath, value, { mode: 0o600 });
|
|
53
|
+
setTimeout(() => { try { fs.unlinkSync(filePath); } catch {} }, 30_000);
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function copyToClipboard(value) {
|
|
58
|
+
const platform = os.platform();
|
|
59
|
+
if (platform === "win32") {
|
|
60
|
+
execSync("clip", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
61
|
+
} else if (platform === "darwin") {
|
|
62
|
+
execSync("pbcopy", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
63
|
+
} else {
|
|
64
|
+
execSync("xclip -selection clipboard", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerTool("clauth_ping", async (args, vault) => {
|
|
69
|
+
return mcpResult(
|
|
70
|
+
vault.password
|
|
71
|
+
? `unlocked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
|
|
72
|
+
: `locked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
registerTool("clauth_unlock", async (args, vault) => {
|
|
77
|
+
const pw = args.password;
|
|
78
|
+
if (!pw) return mcpError("password is required");
|
|
79
|
+
try {
|
|
80
|
+
const { token, timestamp } = deriveToken(pw, vault.machineHash);
|
|
81
|
+
const result = await api.test(pw, vault.machineHash, token, timestamp);
|
|
82
|
+
if (result.error) return mcpError(`Unlock failed: ${result.error}`);
|
|
83
|
+
vault.password = pw;
|
|
84
|
+
return mcpResult("Vault unlocked");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return mcpError(`Unlock failed: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
registerTool("clauth_lock", async (args, vault) => {
|
|
91
|
+
vault.password = null;
|
|
92
|
+
return mcpResult("Vault locked — password cleared from memory");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
registerTool("clauth_status", async (args, vault) => {
|
|
96
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
97
|
+
try {
|
|
98
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
99
|
+
const result = await getCachedServiceStatus({
|
|
100
|
+
password: vault.password,
|
|
101
|
+
machineHash: vault.machineHash,
|
|
102
|
+
token,
|
|
103
|
+
timestamp,
|
|
104
|
+
whitelist: vault.whitelist,
|
|
105
|
+
});
|
|
106
|
+
if (result.error) return mcpError(result.error);
|
|
107
|
+
const services = result.services || [];
|
|
108
|
+
const lines = ["SERVICE TYPE PROJECT STATUS KEY LAST RETRIEVED",
|
|
109
|
+
"------- ---- ------- ------ --- --------------"];
|
|
110
|
+
for (const s of services) {
|
|
111
|
+
const hasStoredKey = !!(s.has_key || s.vault_key);
|
|
112
|
+
const status = s.enabled ? "ACTIVE" : (hasStoredKey ? "SUSPENDED" : "NO KEY");
|
|
113
|
+
const hasKey = hasStoredKey ? "yes" : "—";
|
|
114
|
+
const lastGet = s.last_retrieved ? new Date(s.last_retrieved).toLocaleDateString() : "never";
|
|
115
|
+
const proj = (s.project || "—").padEnd(22);
|
|
116
|
+
lines.push(`${s.name.padEnd(24)} ${(s.key_type || "").padEnd(12)} ${proj} ${status.padEnd(12)} ${hasKey.padEnd(6)} ${lastGet}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(`\nmetadata cache: ${result.cached ? "hit" : "miss"} (${result.cache_ttl_ms}ms TTL)`);
|
|
119
|
+
return mcpResult(lines.join("\n"));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return mcpError(err.message);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
registerTool("clauth_knowledge", async (args, vault) => {
|
|
126
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
127
|
+
try {
|
|
128
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
129
|
+
const result = await getCachedServiceStatus({
|
|
130
|
+
password: vault.password,
|
|
131
|
+
machineHash: vault.machineHash,
|
|
132
|
+
token,
|
|
133
|
+
timestamp,
|
|
134
|
+
project: args.project,
|
|
135
|
+
whitelist: vault.whitelist,
|
|
136
|
+
force: args.refresh === true,
|
|
137
|
+
});
|
|
138
|
+
if (result.error) return mcpError(result.error);
|
|
139
|
+
return mcpResult(JSON.stringify({
|
|
140
|
+
services: (result.services || []).map(sanitizeServiceMetadata),
|
|
141
|
+
cached: !!result.cached,
|
|
142
|
+
cache_ttl_ms: result.cache_ttl_ms,
|
|
143
|
+
contract: {
|
|
144
|
+
safe_for_agents: true,
|
|
145
|
+
raw_secret_rule: "Fetch one exact /v/<service> only when a command needs that secret.",
|
|
146
|
+
bulk_rule: "Do not walk every service name or bulk-retrieve all secrets.",
|
|
147
|
+
},
|
|
148
|
+
}, null, 2));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return mcpError(err.message);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
registerTool("clauth_list", async (args, vault) => {
|
|
155
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
156
|
+
try {
|
|
157
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
158
|
+
const result = await getCachedServiceStatus({
|
|
159
|
+
password: vault.password,
|
|
160
|
+
machineHash: vault.machineHash,
|
|
161
|
+
token,
|
|
162
|
+
timestamp,
|
|
163
|
+
whitelist: vault.whitelist,
|
|
164
|
+
});
|
|
165
|
+
if (result.error) return mcpError(result.error);
|
|
166
|
+
const services = result.services || [];
|
|
167
|
+
return mcpResult(services.map(s => s.name).join(", "));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return mcpError(err.message);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
registerTool("clauth_search", async (args, vault) => {
|
|
174
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
175
|
+
try {
|
|
176
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
177
|
+
const result = await searchServices({
|
|
178
|
+
password: vault.password,
|
|
179
|
+
machineHash: vault.machineHash,
|
|
180
|
+
token,
|
|
181
|
+
timestamp,
|
|
182
|
+
query: args.query,
|
|
183
|
+
project: args.project,
|
|
184
|
+
includeAddresses: args.addresses === true,
|
|
185
|
+
whitelist: vault.whitelist
|
|
186
|
+
});
|
|
187
|
+
if (result.error) return mcpError(result.error);
|
|
188
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return mcpError(err.message);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
registerTool("clauth_get", async (args, vault) => {
|
|
195
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
196
|
+
const service = (args.service || "").toLowerCase();
|
|
197
|
+
const target = args.target || "file";
|
|
198
|
+
if (!service) return mcpError("service is required");
|
|
199
|
+
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
|
200
|
+
return mcpError(`Service '${service}' not in whitelist`);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
204
|
+
const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
|
|
205
|
+
if (result.error) return mcpError(result.error);
|
|
206
|
+
const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
|
|
207
|
+
|
|
208
|
+
if (target === "clipboard") {
|
|
209
|
+
try {
|
|
210
|
+
copyToClipboard(value);
|
|
211
|
+
return mcpResult(`${service} copied to clipboard`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return mcpError(`Clipboard failed: ${err.message}. Use target 'file' instead.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (target === "stdout") {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text", text: `WARNING: secret in response — will appear in transcript. Use 'file' or 'clipboard' mode instead.\n\n${value}` }],
|
|
220
|
+
isError: true
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Remote caller (claude.ai via tunnel) — can't read local temp files
|
|
225
|
+
// Return value inline in MCP response (safe: stays in AI context, not a shell transcript)
|
|
226
|
+
if (vault.remote) {
|
|
227
|
+
const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
|
|
228
|
+
return mcpResult(`${envVar}=${value}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Default: file (local callers)
|
|
232
|
+
const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
|
|
233
|
+
const filePath = writeTempSecret(service, value);
|
|
234
|
+
return mcpResult(`${service} → ${filePath} (auto-deletes in 30s)\nEnv var: ${envVar}\nUsage: export ${envVar}=$(cat ${filePath.replace(/\\/g, "/")})`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return mcpError(err.message);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
registerTool("clauth_inject", async (args, vault) => {
|
|
241
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
242
|
+
const services = args.services || [];
|
|
243
|
+
if (!services.length) return mcpError("services array is required");
|
|
244
|
+
if (services.length > 5 && args.allow_many !== true) {
|
|
245
|
+
return mcpError("Refusing to retrieve more than 5 services at once. Use clauth_knowledge for discovery, then request only the exact secrets needed. Pass allow_many=true only for an intentional bulk operation.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lines = [];
|
|
249
|
+
const errors = [];
|
|
250
|
+
for (const svc of services) {
|
|
251
|
+
const service = svc.toLowerCase();
|
|
252
|
+
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
|
253
|
+
errors.push(`${service}: not in whitelist`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
258
|
+
const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
|
|
259
|
+
if (result.error) { errors.push(`${service}: ${result.error}`); continue; }
|
|
260
|
+
const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
|
|
261
|
+
const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
|
|
262
|
+
lines.push(`export ${envVar}="${value.replace(/"/g, '\\"')}"`);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
errors.push(`${service}: ${err.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!lines.length) return mcpError(`No services retrieved:\n${errors.join("\n")}`);
|
|
269
|
+
|
|
270
|
+
// Remote caller — return env vars inline
|
|
271
|
+
if (vault.remote) {
|
|
272
|
+
let msg = lines.join("\n");
|
|
273
|
+
if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
|
|
274
|
+
return mcpResult(msg);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Local caller — write temp env file
|
|
278
|
+
const envFilePath = path.join(os.tmpdir(), ".clauth-env");
|
|
279
|
+
fs.writeFileSync(envFilePath, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
280
|
+
setTimeout(() => { try { fs.unlinkSync(envFilePath); } catch {} }, 30_000);
|
|
281
|
+
|
|
282
|
+
let msg = `${lines.length} service(s) → ${envFilePath.replace(/\\/g, "/")} (auto-deletes in 30s)\nUsage: source ${envFilePath.replace(/\\/g, "/")}`;
|
|
283
|
+
if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
|
|
284
|
+
return mcpResult(msg);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
registerTool("clauth_enable", async (args, vault) => {
|
|
288
|
+
const writeError = requireMcpWrite(vault);
|
|
289
|
+
if (writeError) return writeError;
|
|
290
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
291
|
+
const service = (args.service || "").toLowerCase();
|
|
292
|
+
if (!service) return mcpError("service is required");
|
|
293
|
+
try {
|
|
294
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
295
|
+
const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, true);
|
|
296
|
+
if (result.error) return mcpError(result.error);
|
|
297
|
+
invalidateServiceStatusCache(vault.machineHash);
|
|
298
|
+
return mcpResult(`${service} enabled`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return mcpError(err.message);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
registerTool("clauth_disable", async (args, vault) => {
|
|
305
|
+
const writeError = requireMcpWrite(vault);
|
|
306
|
+
if (writeError) return writeError;
|
|
307
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
308
|
+
const service = (args.service || "").toLowerCase();
|
|
309
|
+
if (!service) return mcpError("service is required");
|
|
310
|
+
try {
|
|
311
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
312
|
+
const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, false);
|
|
313
|
+
if (result.error) return mcpError(result.error);
|
|
314
|
+
invalidateServiceStatusCache(vault.machineHash);
|
|
315
|
+
return mcpResult(`${service} disabled`);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
return mcpError(err.message);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
registerTool("clauth_test", async (args, vault) => {
|
|
322
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
323
|
+
try {
|
|
324
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
325
|
+
const result = await api.test(vault.password, vault.machineHash, token, timestamp);
|
|
326
|
+
if (result.error) return mcpError(`FAIL: ${result.error}`);
|
|
327
|
+
return mcpResult(`PASS — machine: ${vault.machineHash.slice(0, 16)}... | window: ${new Date(result.timestamp).toISOString()}`);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return mcpError(`FAIL: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
registerTool("clauth_scrub", async (args, vault) => {
|
|
334
|
+
const target = args.target || "latest";
|
|
335
|
+
try {
|
|
336
|
+
const { runScrub: doScrub } = await import("../../commands/scrub.js");
|
|
337
|
+
// runScrub writes to stdout via chalk — capture isn't clean in MCP mode.
|
|
338
|
+
// Call it and trust it works; report success.
|
|
339
|
+
await doScrub(target === "all" ? "all" : undefined, {});
|
|
340
|
+
return mcpResult(`Scrub complete (target: ${target})`);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return mcpError(`Scrub failed: ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
registerTool("clauth_set_project", async (args, vault) => {
|
|
347
|
+
const writeError = requireMcpWrite(vault);
|
|
348
|
+
if (writeError) return writeError;
|
|
349
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
350
|
+
const service = (args.service || "").toLowerCase();
|
|
351
|
+
const project = args.project;
|
|
352
|
+
if (!service) return mcpError("service is required");
|
|
353
|
+
if (project === undefined) return mcpError("project is required (empty string to clear)");
|
|
354
|
+
try {
|
|
355
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
356
|
+
const result = await api.updateService(vault.password, vault.machineHash, token, timestamp, service, { project: project || "" });
|
|
357
|
+
if (result.error) return mcpError(result.error);
|
|
358
|
+
invalidateServiceStatusCache(vault.machineHash);
|
|
359
|
+
return mcpResult(project ? `${service} → project: ${project}` : `${service} → project cleared`);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
return mcpError(err.message);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
registerTool("clauth_self_check", async (args, vault) => {
|
|
366
|
+
// Test connectivity via the Cloudflare tunnel and/or localhost daemon
|
|
367
|
+
const tunnelUrl = vault.tunnelUrl;
|
|
368
|
+
const results = [];
|
|
369
|
+
|
|
370
|
+
// Check localhost daemon
|
|
371
|
+
try {
|
|
372
|
+
const r = await fetch("http://127.0.0.1:52437/ping", { signal: AbortSignal.timeout(3000) });
|
|
373
|
+
const data = await r.json();
|
|
374
|
+
results.push(`Local daemon: PASS (${data.locked ? "locked" : "unlocked"}, failures: ${data.failures ?? 0})`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
results.push(`Local daemon: FAIL — http://127.0.0.1:52437 not reachable (${err.message})`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check tunnel
|
|
380
|
+
if (tunnelUrl) {
|
|
381
|
+
try {
|
|
382
|
+
const r = await fetch(`${tunnelUrl}/ping`, { signal: AbortSignal.timeout(5000) });
|
|
383
|
+
const data = await r.json();
|
|
384
|
+
results.push(`Tunnel: PASS — ${tunnelUrl} reachable (${data.locked ? "locked" : "unlocked"})`);
|
|
385
|
+
results.push(`claude.ai SSE endpoint: ${tunnelUrl}/sse`);
|
|
386
|
+
results.push(`claude.ai MCP endpoint: ${tunnelUrl}/mcp`);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
results.push(`Tunnel: FAIL — ${tunnelUrl} not reachable (${err.message})`);
|
|
389
|
+
results.push("claude.ai MCP connector will not work until tunnel is restored.");
|
|
390
|
+
results.push("Fix: clauth serve start --tunnel clauth.prtrust.fund");
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
results.push("Tunnel: NOT RUNNING — claude.ai connector requires the tunnel.");
|
|
394
|
+
results.push("Start with: clauth serve start --tunnel clauth.prtrust.fund");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return mcpResult(results.join("\n"));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
registerTool("clauth_generate_token", async (args, vault) => {
|
|
401
|
+
const writeError = requireMcpWrite(vault);
|
|
402
|
+
if (writeError) return writeError;
|
|
403
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
404
|
+
const service = (args.service || "").trim().toLowerCase();
|
|
405
|
+
const prefix = args.prefix || "";
|
|
406
|
+
if (!service) return mcpError("service name is required");
|
|
407
|
+
try {
|
|
408
|
+
const randomHex = crypto.randomBytes(32).toString("hex");
|
|
409
|
+
const generatedToken = `${prefix}${randomHex}`;
|
|
410
|
+
const { result } = await writeCredentialWithRecovery({
|
|
411
|
+
password: vault.password,
|
|
412
|
+
machineHash: vault.machineHash,
|
|
413
|
+
service,
|
|
414
|
+
value: generatedToken,
|
|
415
|
+
logFile: LOG_FILE,
|
|
416
|
+
normalize: false,
|
|
417
|
+
});
|
|
418
|
+
if (result.error) return mcpError(result.error);
|
|
419
|
+
invalidateServiceStatusCache(vault.machineHash);
|
|
420
|
+
return mcpResult(`Token generated and stored under "${service}"`);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return mcpError(`generate_token failed: ${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
registerTool("clauth_enroll_machine", async (args, vault) => {
|
|
427
|
+
const writeError = requireMcpWrite(vault);
|
|
428
|
+
if (writeError) return writeError;
|
|
429
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
430
|
+
try {
|
|
431
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
432
|
+
const result = await api.createEnrollment(
|
|
433
|
+
vault.password,
|
|
434
|
+
vault.machineHash,
|
|
435
|
+
token,
|
|
436
|
+
timestamp,
|
|
437
|
+
args.label,
|
|
438
|
+
Number(args.ttl_minutes || 60),
|
|
439
|
+
args.install_id
|
|
440
|
+
);
|
|
441
|
+
if (result.error) return mcpError(result.error);
|
|
442
|
+
const localConfig = new Conf(getConfOptions());
|
|
443
|
+
const supabaseUrl = localConfig.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL || "";
|
|
444
|
+
const anonKey = localConfig.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY || "";
|
|
445
|
+
const scriptPath = writeEnrollmentScript({
|
|
446
|
+
supabaseUrl,
|
|
447
|
+
anonKey,
|
|
448
|
+
enrollmentCode: result.enrollment_code,
|
|
449
|
+
label: args.label,
|
|
450
|
+
});
|
|
451
|
+
return mcpResult(JSON.stringify({
|
|
452
|
+
enrollment_code: result.enrollment_code,
|
|
453
|
+
install_id: result.install_id,
|
|
454
|
+
expires_at: result.expires_at,
|
|
455
|
+
script_path: scriptPath,
|
|
456
|
+
new_computer_steps: [`Run ${scriptPath} on the new computer. It installs clauth, enrolls with this one-time code, installs startup, then deletes itself.`]
|
|
457
|
+
}, null, 2));
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return mcpError(`enroll_machine failed: ${err.message}`);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// cli/mcp/providers/ops-provider.js
|
|
2
|
+
// The clauth_ops_* MCP tools (Vultr deployment-control passthrough), as
|
|
3
|
+
// their own provider file registering against the shared
|
|
4
|
+
// cli/mcp/tool-registry.js. Second file-level split (after
|
|
5
|
+
// terminal-provider.js) — chosen next because callOpsFromMcp's only two
|
|
6
|
+
// dependencies (vaultRetrieveValue, requestOps) already live in their own
|
|
7
|
+
// clean modules, and callOpsFromMcp itself is used by nothing outside these
|
|
8
|
+
// 7 registrations (confirmed via grep before moving it), so it's safe to
|
|
9
|
+
// move alongside them rather than keep shared.
|
|
10
|
+
//
|
|
11
|
+
// serve.js imports this file for its side effect (the registerTool() calls
|
|
12
|
+
// below run at import time) — see the `import "../mcp/providers/
|
|
13
|
+
// ops-provider.js";` line in cli/commands/serve.js.
|
|
14
|
+
import { mcpResult, mcpError, registerTool } from "../tool-registry.js";
|
|
15
|
+
import { vaultRetrieveValue } from "../../services/vault-read.js";
|
|
16
|
+
import { requestOps } from "../../commands/ops.js";
|
|
17
|
+
|
|
18
|
+
async function callOpsFromMcp(vault, method, requestPath, body, { write = false } = {}) {
|
|
19
|
+
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.");
|
|
20
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
21
|
+
const endpoint = process.env.CLAUTH_OPS_APPROVED_ORIGIN;
|
|
22
|
+
if (!endpoint) return mcpError("service_not_available");
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(endpoint);
|
|
25
|
+
if (url.protocol !== "https:") return mcpError("service_not_available");
|
|
26
|
+
const service = process.env.CLAUTH_OPS_TOKEN_SERVICE || "vultr-ops-api-token";
|
|
27
|
+
const credential = await vaultRetrieveValue(vault, service);
|
|
28
|
+
if (credential.error || !credential.value) return mcpError("service_not_available");
|
|
29
|
+
const payload = await requestOps({ endpoint: url.toString().replace(/\/$/, ""), token: String(credential.value), method, path: requestPath, body });
|
|
30
|
+
return mcpResult(JSON.stringify(payload, null, 2));
|
|
31
|
+
} catch {
|
|
32
|
+
return mcpError("service_not_available");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerTool("clauth_ops_catalog", async (args, vault) => callOpsFromMcp(vault, "GET", "/v1/ops/catalog"));
|
|
37
|
+
registerTool("clauth_ops_processes", async (args, vault) => callOpsFromMcp(vault, "GET", "/v1/ops/processes"));
|
|
38
|
+
registerTool("clauth_ops_describe", async (args, vault) => callOpsFromMcp(vault, "GET", `/v1/ops/processes/${encodeURIComponent(args.application || "")}`));
|
|
39
|
+
registerTool("clauth_ops_deploy", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/deployments", { application: args.application, ...(args.ref ? { ref: args.ref } : {}) }, { write: true }));
|
|
40
|
+
registerTool("clauth_ops_promote", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/promotions", { application_uuid: args.application_uuid }, { write: true }));
|
|
41
|
+
registerTool("clauth_ops_job", async (args, vault) => callOpsFromMcp(vault, "GET", `/v1/ops/jobs/${encodeURIComponent(args.job_id || "")}`));
|
|
42
|
+
registerTool("clauth_ops_run", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/operations", { operation: args.operation, input: args.input && typeof args.input === "object" ? args.input : {} }, { write: true }));
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// cli/mcp/providers/terminal-provider.js
|
|
2
|
+
// The terminal-session + channel-event MCP tools, as their own provider file
|
|
3
|
+
// registering against the shared cli/mcp/tool-registry.js — the first true
|
|
4
|
+
// file-level split of what was, until now, one large registerTool(...) block
|
|
5
|
+
// living entirely inside cli/commands/serve.js. Chosen as the first slice to
|
|
6
|
+
// move because every dependency it needs (startTerminalSession et al.,
|
|
7
|
+
// channelEvents) already lives in its own clean service module — nothing
|
|
8
|
+
// here reaches back into serve.js-local state, unlike clauth_*'s vault
|
|
9
|
+
// operations or call_agent's runCallAgent (still serve.js-local, deferred).
|
|
10
|
+
//
|
|
11
|
+
// serve.js imports this file for its side effect (the registerTool() calls
|
|
12
|
+
// below run at import time) — see the `import "../mcp/providers/
|
|
13
|
+
// terminal-provider.js";` line in cli/commands/serve.js.
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { mcpResult, mcpError, registerTool } from "../tool-registry.js";
|
|
16
|
+
import { channelEvents, defaultTerminalCwd } from "../../services/terminal-registry.js";
|
|
17
|
+
import { startTerminalSession, listTerminalSessions, stopTerminalSession } from "../../services/terminal-sessions.js";
|
|
18
|
+
import { sendTerminalMessage, recvTerminalResponse } from "../../services/terminal-dispatch.js";
|
|
19
|
+
import { showTerminalSession } from "../../services/terminal-window.js";
|
|
20
|
+
|
|
21
|
+
registerTool("terminal_start", async (args, vault) => {
|
|
22
|
+
const { name, knowledge_tier, context_md, use_warm, cwd: requestedCwd } = args;
|
|
23
|
+
if (!name) return mcpError("name required");
|
|
24
|
+
const cwd = requestedCwd ? path.resolve(String(requestedCwd)) : defaultTerminalCwd();
|
|
25
|
+
const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null, cwd, use_warm === true);
|
|
26
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
27
|
+
return mcpResult(JSON.stringify(result));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool("terminal_send", async (args, vault) => {
|
|
31
|
+
const { session_id, message } = args;
|
|
32
|
+
if (!session_id || !message) return mcpError("session_id and message required");
|
|
33
|
+
const result = sendTerminalMessage(session_id, message);
|
|
34
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
35
|
+
return mcpResult(JSON.stringify(result));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
registerTool("terminal_list", async (args, vault) => {
|
|
39
|
+
return mcpResult(JSON.stringify(listTerminalSessions()));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
registerTool("terminal_show", async (args, vault) => {
|
|
43
|
+
const { session_id } = args;
|
|
44
|
+
if (!session_id) return mcpError("session_id required");
|
|
45
|
+
const result = showTerminalSession(session_id);
|
|
46
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
47
|
+
return mcpResult(JSON.stringify(result));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
registerTool("terminal_recv", async (args, vault) => {
|
|
51
|
+
const { session_id } = args;
|
|
52
|
+
if (!session_id) return mcpError("session_id required");
|
|
53
|
+
const result = recvTerminalResponse(session_id);
|
|
54
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
55
|
+
return mcpResult(JSON.stringify(result));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
registerTool("terminal_stop", async (args, vault) => {
|
|
59
|
+
const { session_id } = args;
|
|
60
|
+
if (!session_id) return mcpError("session_id required");
|
|
61
|
+
const result = stopTerminalSession(session_id);
|
|
62
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
63
|
+
return mcpResult(JSON.stringify(result));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
registerTool("channel_list", async (args, vault) => {
|
|
67
|
+
const { since, limit } = args || {};
|
|
68
|
+
let events = channelEvents.slice();
|
|
69
|
+
if (since) {
|
|
70
|
+
const cutoff = new Date(since).getTime();
|
|
71
|
+
events = events.filter((e) => new Date(e.created_at).getTime() > cutoff);
|
|
72
|
+
}
|
|
73
|
+
events = events.slice(-(limit ? Math.min(Number(limit), 100) : 50));
|
|
74
|
+
return mcpResult(JSON.stringify({ events, count: events.length }));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
registerTool("channel_clear", async (args, vault) => {
|
|
78
|
+
const cleared = channelEvents.length;
|
|
79
|
+
channelEvents.length = 0;
|
|
80
|
+
return mcpResult(JSON.stringify({ cleared }));
|
|
81
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// cli/mcp/tool-registry.js
|
|
2
|
+
// The shared interface every MCP tool provider registers against. Extracted
|
|
3
|
+
// out of cli/commands/serve.js so a provider can be its own file, importing
|
|
4
|
+
// this module, rather than every tool having to live inside serve.js itself
|
|
5
|
+
// (the same Dependency Inversion fix as the frontend's ClauthPanelRegistry
|
|
6
|
+
// (cli/dashboard/panels/panel-registry.js): serve.js and every provider now
|
|
7
|
+
// depend on THIS shared abstraction, not on each other directly).
|
|
8
|
+
//
|
|
9
|
+
// mcpResult/mcpError are generic MCP response shapers, not clauth-specific —
|
|
10
|
+
// any provider needs them, so they live here rather than being duplicated
|
|
11
|
+
// per file. registerTool/hasRegisteredTool/callRegisteredTool are the
|
|
12
|
+
// registry itself (register once, dispatch by lookup — Open/Closed: a new
|
|
13
|
+
// tool means one registerTool() call, never editing a central switch).
|
|
14
|
+
// requireMcpWrite is likewise tool-agnostic (checks vault.writeEnabled,
|
|
15
|
+
// nothing else) so it lives here too rather than being re-implemented by
|
|
16
|
+
// every provider that gates a write tool.
|
|
17
|
+
|
|
18
|
+
const mcpToolHandlers = new Map();
|
|
19
|
+
|
|
20
|
+
export function mcpResult(text) {
|
|
21
|
+
return { content: [{ type: "text", text }] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mcpError(text) {
|
|
25
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerTool(name, handler) {
|
|
29
|
+
if (mcpToolHandlers.has(name)) {
|
|
30
|
+
throw new Error(`MCP tool "${name}" already registered`);
|
|
31
|
+
}
|
|
32
|
+
mcpToolHandlers.set(name, handler);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasRegisteredTool(name) {
|
|
36
|
+
return mcpToolHandlers.has(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function callRegisteredTool(name, args, vault) {
|
|
40
|
+
return mcpToolHandlers.get(name)(args, vault);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Takes vault explicitly (not a closure) so any provider's handler can call
|
|
44
|
+
// it with whichever vault it was itself given.
|
|
45
|
+
export function requireMcpWrite(vault) {
|
|
46
|
+
if (vault.writeEnabled) return null;
|
|
47
|
+
return mcpError("MCP write tools are disabled by default. Launch clauth with CLAUTH_MCP_WRITE=1 for an explicit write-capable session.");
|
|
48
|
+
}
|