@lifeaitools/clauth 1.30.6 → 1.30.7
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 +17 -75
- package/README.md +10 -70
- package/cli/api.js +11 -110
- package/cli/commands/scrub.js +109 -205
- package/cli/commands/serve.js +850 -3059
- package/cli/fingerprint.js +10 -0
- package/cli/index.js +58 -22
- package/cli/studio-debug.js +8 -679
- package/cli/supervisor-registry.js +440 -0
- package/cli/supervisor-registry.test.js +173 -0
- package/package.json +3 -10
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/postinstall.js +0 -25
- package/cli/api.classify.test.js +0 -75
- package/cli/commands/agent-cron.js +0 -396
- package/cli/commands/agent-pool.js +0 -1962
- package/cli/commands/scrub.test.js +0 -115
- package/cli/enrollment-script.js +0 -82
- package/cli/webdav-service.js +0 -339
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const SCHEMA = "lifeai.plugin.v1";
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
9
|
+
const DEFAULT_SUPERVISOR_PORT = 52439;
|
|
10
|
+
const DESTINATIONS = new Set(["local/clauth/pm2", "vultr/clauth/pm2", "coolify/clauth/docker"]);
|
|
11
|
+
const OWNERS = new Set(["clauth", "plugin", "external"]);
|
|
12
|
+
const ACTIONS = new Set(["start", "stop", "restart", "reconcile", "test", "promote", "rollback"]);
|
|
13
|
+
|
|
14
|
+
export function getSupervisorPort() {
|
|
15
|
+
return Number(process.env.CLAUTH_SUPERVISOR_PORT || DEFAULT_SUPERVISOR_PORT);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSupervisorDir() {
|
|
19
|
+
if (process.env.CLAUTH_SUPERVISOR_DIR) return process.env.CLAUTH_SUPERVISOR_DIR;
|
|
20
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
21
|
+
return path.join(appdata, "clauth", "supervisor");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getClauthPm2Home() {
|
|
25
|
+
if (process.env.CLAUTH_PM2_HOME) return process.env.CLAUTH_PM2_HOME;
|
|
26
|
+
return path.join(getSupervisorDir(), "pm2-home");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function file(name) {
|
|
30
|
+
return path.join(getSupervisorDir(), name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(filePath, fallback) {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeJson(filePath, value) {
|
|
43
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
44
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appendJsonl(filePath, value) {
|
|
48
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
49
|
+
fs.appendFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sha256(value) {
|
|
53
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function now() {
|
|
57
|
+
return new Date().toISOString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeCommand(command, field) {
|
|
61
|
+
if (!command) return [];
|
|
62
|
+
if (!Array.isArray(command)) throw new Error(`${field} must be a command array`);
|
|
63
|
+
if (command.length === 0) return [];
|
|
64
|
+
const [cmd, ...args] = command;
|
|
65
|
+
if (typeof cmd !== "string" || !cmd.trim()) throw new Error(`${field}[0] is required`);
|
|
66
|
+
if (/[;&|<>]/.test(cmd)) throw new Error(`${field}[0] must be an executable path/name, not shell syntax`);
|
|
67
|
+
return [cmd, ...args.map(String)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function expandPathToken(value) {
|
|
71
|
+
if (!value) return null;
|
|
72
|
+
const root = process.env.REGEN_ROOT || process.env.LIFEAI_REPO_ROOT || "C:/Dev/regen-root";
|
|
73
|
+
return String(value)
|
|
74
|
+
.replace(/\$\{REGEN_ROOT\}/g, root)
|
|
75
|
+
.replace(/\$REGEN_ROOT/g, root)
|
|
76
|
+
.replace(/%REGEN_ROOT%/gi, root)
|
|
77
|
+
.replace(/\$\{LIFEAI_REPO_ROOT\}/g, root)
|
|
78
|
+
.replace(/\$LIFEAI_REPO_ROOT/g, root)
|
|
79
|
+
.replace(/%LIFEAI_REPO_ROOT%/gi, root);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeLifecycleOwner(owner) {
|
|
83
|
+
const value = owner || "clauth";
|
|
84
|
+
if (!OWNERS.has(value)) throw new Error(`lifecycle_owner must be one of ${[...OWNERS].join(", ")}`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeDestination(destination) {
|
|
89
|
+
const value = destination || "local/clauth/pm2";
|
|
90
|
+
if (!DESTINATIONS.has(value)) throw new Error(`destination must be one of ${[...DESTINATIONS].join(", ")}`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function localhostHealth(pathOrUrl, port) {
|
|
95
|
+
if (!pathOrUrl) return null;
|
|
96
|
+
if (/^https?:\/\//i.test(pathOrUrl)) {
|
|
97
|
+
const url = new URL(pathOrUrl);
|
|
98
|
+
if (!["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname)) {
|
|
99
|
+
throw new Error("health URLs must be localhost-only");
|
|
100
|
+
}
|
|
101
|
+
return url.toString();
|
|
102
|
+
}
|
|
103
|
+
const safePath = String(pathOrUrl).startsWith("/") ? String(pathOrUrl) : `/${pathOrUrl}`;
|
|
104
|
+
return port ? `http://127.0.0.1:${port}${safePath}` : safePath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeSurface(surface, plugin) {
|
|
108
|
+
if (!surface || typeof surface !== "object") throw new Error("surface must be an object");
|
|
109
|
+
const id = String(surface.id || "").trim();
|
|
110
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(id)) throw new Error("surface.id may contain only letters, numbers, dot, underscore, and dash");
|
|
111
|
+
const destination = normalizeDestination(surface.destination || plugin.destination);
|
|
112
|
+
const lifecycle_owner = normalizeLifecycleOwner(surface.lifecycle_owner || plugin.lifecycle_owner);
|
|
113
|
+
const port = surface.port === undefined || surface.port === null || surface.port === "auto" ? surface.port ?? null : Number(surface.port);
|
|
114
|
+
if (port !== null && port !== "auto" && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("surface.port must be auto or a TCP port");
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
plugin_id: plugin.id,
|
|
118
|
+
name: surface.name || id,
|
|
119
|
+
destination,
|
|
120
|
+
lifecycle_owner,
|
|
121
|
+
port,
|
|
122
|
+
health: localhostHealth(surface.health || "/health", port === "auto" ? null : port),
|
|
123
|
+
cwd: expandPathToken(surface.cwd || plugin.cwd),
|
|
124
|
+
start: normalizeCommand(surface.start || plugin.start, "surface.start"),
|
|
125
|
+
stop: normalizeCommand(surface.stop || plugin.stop, "surface.stop"),
|
|
126
|
+
restart: normalizeCommand(surface.restart || plugin.restart, "surface.restart"),
|
|
127
|
+
routes: Array.isArray(surface.routes) ? surface.routes : [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function validatePluginManifest(manifest, sourcePath = "") {
|
|
132
|
+
if (!manifest || typeof manifest !== "object") throw new Error("manifest must be an object");
|
|
133
|
+
if (manifest.schema !== SCHEMA) throw new Error(`schema must be ${SCHEMA}`);
|
|
134
|
+
const id = String(manifest.id || "").trim();
|
|
135
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(id)) throw new Error("id may contain only letters, numbers, dot, underscore, and dash");
|
|
136
|
+
const version = String(manifest.version || "").trim();
|
|
137
|
+
if (!version) throw new Error("version is required");
|
|
138
|
+
const plugin = {
|
|
139
|
+
schema: SCHEMA,
|
|
140
|
+
id,
|
|
141
|
+
version,
|
|
142
|
+
publisher: String(manifest.publisher || "unknown"),
|
|
143
|
+
sourcePath,
|
|
144
|
+
destination: normalizeDestination(manifest.destination),
|
|
145
|
+
lifecycle_owner: normalizeLifecycleOwner(manifest.lifecycle_owner),
|
|
146
|
+
credentials: Array.isArray(manifest.credentials) ? manifest.credentials.map((c) => ({
|
|
147
|
+
name: String(c.name || c.id || "").trim(),
|
|
148
|
+
key_type: String(c.key_type || c.type || "secret"),
|
|
149
|
+
description: String(c.description || ""),
|
|
150
|
+
required: c.required !== false,
|
|
151
|
+
})).filter((c) => /^[a-zA-Z0-9_.-]+$/.test(c.name)) : [],
|
|
152
|
+
surfaces: [],
|
|
153
|
+
routes: Array.isArray(manifest.routes) ? manifest.routes : [],
|
|
154
|
+
test: manifest.test && typeof manifest.test === "object" ? {
|
|
155
|
+
command: normalizeCommand(manifest.test.command, "test.command"),
|
|
156
|
+
port: manifest.test.port || "auto",
|
|
157
|
+
health: manifest.test.health || "/health",
|
|
158
|
+
selfTest: Array.isArray(manifest.test.selfTest) ? manifest.test.selfTest.map((cmd, i) => normalizeCommand(cmd, `test.selfTest[${i}]`)) : [],
|
|
159
|
+
} : null,
|
|
160
|
+
};
|
|
161
|
+
plugin.surfaces = (Array.isArray(manifest.surfaces) ? manifest.surfaces : []).map((surface) => normalizeSurface(surface, plugin));
|
|
162
|
+
return plugin;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function rootEntries() {
|
|
166
|
+
const managed = (process.env.CLAUTH_MANAGED_PLUGIN_ROOTS || "").split(path.delimiter).filter(Boolean);
|
|
167
|
+
const user = (process.env.CLAUTH_USER_PLUGIN_ROOTS || "").split(path.delimiter).filter(Boolean);
|
|
168
|
+
if (managed.length === 0) managed.push(path.join(getSupervisorDir(), "managed-plugins"));
|
|
169
|
+
if (user.length === 0) user.push(path.join(getSupervisorDir(), "user-plugins"));
|
|
170
|
+
return [
|
|
171
|
+
...managed.map((root) => ({ root, source: "managed" })),
|
|
172
|
+
...user.map((root) => ({ root, source: "user" })),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function findManifestFiles(root) {
|
|
177
|
+
const out = [];
|
|
178
|
+
if (!fs.existsSync(root)) return out;
|
|
179
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
180
|
+
const full = path.join(root, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
const candidate = path.join(full, "clauth-plugin.json");
|
|
183
|
+
if (fs.existsSync(candidate)) out.push(candidate);
|
|
184
|
+
} else if (entry.isFile() && entry.name === "clauth-plugin.json") {
|
|
185
|
+
out.push(full);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function loadSupervisorState() {
|
|
192
|
+
return readJson(file("state.json"), { plugins: [], surfaces: [], routes: [], observations: [], operations: [] });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function saveSupervisorState(state) {
|
|
196
|
+
writeJson(file("state.json"), state);
|
|
197
|
+
writeJson(file("last-known-good.json"), {
|
|
198
|
+
signed_at: now(),
|
|
199
|
+
hash: sha256(JSON.stringify(state)),
|
|
200
|
+
state,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function existingById(state) {
|
|
205
|
+
return new Map((state.plugins || []).map((plugin) => [plugin.id, plugin]));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function discoverPlugins() {
|
|
209
|
+
const previous = loadSupervisorState();
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
const byId = existingById(previous);
|
|
212
|
+
const plugins = [];
|
|
213
|
+
const surfaces = [];
|
|
214
|
+
const events = [];
|
|
215
|
+
const managedIds = new Set();
|
|
216
|
+
|
|
217
|
+
for (const { root, source } of rootEntries()) {
|
|
218
|
+
fs.mkdirSync(root, { recursive: true });
|
|
219
|
+
for (const manifestPath of findManifestFiles(root)) {
|
|
220
|
+
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
221
|
+
const hash = sha256(raw);
|
|
222
|
+
try {
|
|
223
|
+
const manifest = validatePluginManifest(JSON.parse(raw), manifestPath);
|
|
224
|
+
if (source === "managed") managedIds.add(manifest.id);
|
|
225
|
+
if (source === "user" && managedIds.has(manifest.id)) {
|
|
226
|
+
throw new Error("user plugin cannot silently shadow a managed plugin id");
|
|
227
|
+
}
|
|
228
|
+
const prior = byId.get(manifest.id);
|
|
229
|
+
const state = prior?.enabled ? (prior.manifest_hash === hash ? "current" : "version_drift") : "awaiting_enable";
|
|
230
|
+
const plugin = { ...manifest, source, discovery_root: root, manifest_hash: hash, state, enabled: Boolean(prior?.enabled), discovered_at: now() };
|
|
231
|
+
plugins.push(plugin);
|
|
232
|
+
for (const surface of plugin.surfaces) surfaces.push({ ...surface, enabled: plugin.enabled, state });
|
|
233
|
+
for (const credential of plugin.credentials) {
|
|
234
|
+
events.push({ kind: "credential_required", plugin_id: plugin.id, credential, value_set: false });
|
|
235
|
+
}
|
|
236
|
+
seen.add(plugin.id);
|
|
237
|
+
events.push({ kind: "plugin_discovered", plugin_id: plugin.id, state, source, manifest_hash: hash });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const id = path.basename(path.dirname(manifestPath));
|
|
240
|
+
plugins.push({
|
|
241
|
+
id,
|
|
242
|
+
source,
|
|
243
|
+
sourcePath: manifestPath,
|
|
244
|
+
manifest_hash: hash,
|
|
245
|
+
state: "manifest_invalid",
|
|
246
|
+
enabled: false,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
discovered_at: now(),
|
|
249
|
+
});
|
|
250
|
+
events.push({ kind: "plugin_quarantined", plugin_id: id, source, error: error instanceof Error ? error.message : String(error) });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const prior of previous.plugins || []) {
|
|
256
|
+
if (!seen.has(prior.id) && prior.state !== "manifest_invalid") {
|
|
257
|
+
plugins.push({ ...prior, enabled: false, state: prior.source === "managed" ? "missing_default" : "missing", missing_since: now() });
|
|
258
|
+
events.push({ kind: "plugin_missing", plugin_id: prior.id, source: prior.source });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const next = { ...previous, plugins, surfaces, routes: previous.routes || [], observations: previous.observations || [], operations: previous.operations || [] };
|
|
263
|
+
saveSupervisorState(next);
|
|
264
|
+
for (const event of events) appendJsonl(file("events.jsonl"), { ts: now(), ...event });
|
|
265
|
+
return { plugins, surfaces, events };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function listPlugins() {
|
|
269
|
+
return loadSupervisorState().plugins || [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function listSurfaces() {
|
|
273
|
+
return loadSupervisorState().surfaces || [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function listRoutes() {
|
|
277
|
+
return loadSupervisorState().routes || [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function listTunnels() {
|
|
281
|
+
return readJson(file("tunnels.json"), { tunnels: [] }).tunnels;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function writeTunnels(tunnels) {
|
|
285
|
+
writeJson(file("tunnels.json"), { tunnels });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function readSupervisorEvents(limit = 100) {
|
|
289
|
+
const p = file("events.jsonl");
|
|
290
|
+
if (!fs.existsSync(p)) return [];
|
|
291
|
+
return fs.readFileSync(p, "utf8").split(/\r?\n/).filter(Boolean).slice(-limit).map((line) => {
|
|
292
|
+
try { return JSON.parse(line); } catch { return { raw: line }; }
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function operation(action, target, prior, result, actor = "localhost") {
|
|
297
|
+
const receipt = {
|
|
298
|
+
operationId: crypto.randomUUID(),
|
|
299
|
+
actor,
|
|
300
|
+
action,
|
|
301
|
+
target,
|
|
302
|
+
prior_state: prior || null,
|
|
303
|
+
resulting_state: result,
|
|
304
|
+
evidence: result?.evidence || [],
|
|
305
|
+
created_at: now(),
|
|
306
|
+
completed_at: now(),
|
|
307
|
+
};
|
|
308
|
+
const state = loadSupervisorState();
|
|
309
|
+
state.operations = [receipt, ...(state.operations || [])].slice(0, 500);
|
|
310
|
+
saveSupervisorState(state);
|
|
311
|
+
appendJsonl(file("events.jsonl"), { ts: now(), kind: "operation", ...receipt });
|
|
312
|
+
return receipt;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function setPluginEnabled(id, enabled, actor = "localhost") {
|
|
316
|
+
const state = loadSupervisorState();
|
|
317
|
+
const prior = (state.plugins || []).find((plugin) => plugin.id === id);
|
|
318
|
+
if (!prior) return { error: "plugin_not_found" };
|
|
319
|
+
if (prior.state === "manifest_invalid") return { error: "manifest_invalid" };
|
|
320
|
+
const nextPlugin = { ...prior, enabled: Boolean(enabled), state: enabled ? "current" : "awaiting_enable" };
|
|
321
|
+
state.plugins = state.plugins.map((plugin) => plugin.id === id ? nextPlugin : plugin);
|
|
322
|
+
state.surfaces = (state.surfaces || []).map((surface) => surface.plugin_id === id ? { ...surface, enabled: Boolean(enabled), state: nextPlugin.state } : surface);
|
|
323
|
+
saveSupervisorState(state);
|
|
324
|
+
return operation(enabled ? "enable" : "disable", { plugin_id: id }, prior, nextPlugin, actor);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function runPluginAction(id, action, actor = "localhost") {
|
|
328
|
+
if (!["test", "promote"].includes(action)) return { error: "invalid_action" };
|
|
329
|
+
const state = loadSupervisorState();
|
|
330
|
+
const prior = (state.plugins || []).find((plugin) => plugin.id === id);
|
|
331
|
+
if (!prior) return { error: "plugin_not_found" };
|
|
332
|
+
if (prior.state === "manifest_invalid") return { error: "manifest_invalid" };
|
|
333
|
+
const resultingState = action === "test" ? "candidate_testing" : "promotion_ready";
|
|
334
|
+
const nextPlugin = {
|
|
335
|
+
...prior,
|
|
336
|
+
state: resultingState,
|
|
337
|
+
candidate: action === "test"
|
|
338
|
+
? {
|
|
339
|
+
port: prior.test?.port === "auto" || !prior.test?.port ? 0 : prior.test.port,
|
|
340
|
+
public_route: false,
|
|
341
|
+
bind: "127.0.0.1",
|
|
342
|
+
tested_at: now(),
|
|
343
|
+
}
|
|
344
|
+
: prior.candidate || null,
|
|
345
|
+
};
|
|
346
|
+
state.plugins = state.plugins.map((plugin) => plugin.id === id ? nextPlugin : plugin);
|
|
347
|
+
saveSupervisorState(state);
|
|
348
|
+
return operation(action, { plugin_id: id }, prior, {
|
|
349
|
+
ok: true,
|
|
350
|
+
state: resultingState,
|
|
351
|
+
private: true,
|
|
352
|
+
public_route: false,
|
|
353
|
+
evidence: ["plugin candidate context does not create a Cloudflare route"],
|
|
354
|
+
}, actor);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function findSurface(id) {
|
|
358
|
+
return (loadSupervisorState().surfaces || []).find((surface) => surface.id === id || `${surface.plugin_id}:${surface.id}` === id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function runSurfaceAction(id, action, actor = "localhost") {
|
|
362
|
+
if (!ACTIONS.has(action)) return { error: "invalid_action" };
|
|
363
|
+
const surface = findSurface(id);
|
|
364
|
+
if (!surface) return { error: "surface_not_found" };
|
|
365
|
+
if (surface.lifecycle_owner === "external") {
|
|
366
|
+
return operation(action, { surface_id: id }, surface, { ok: false, state: "observed_only", reason: "external_owner" }, actor);
|
|
367
|
+
}
|
|
368
|
+
if (surface.plugin_id === "codeflow" || surface.tags?.includes("codeflow")) {
|
|
369
|
+
return operation(action, { surface_id: id }, surface, { ok: false, state: "observed_only", reason: "codeflow_self_owned" }, actor);
|
|
370
|
+
}
|
|
371
|
+
if (action === "test") {
|
|
372
|
+
const port = surface.port === "auto" || !surface.port ? 0 : surface.port;
|
|
373
|
+
return operation(action, { surface_id: id }, surface, { ok: true, state: "candidate_testing", port, private: true, public_route: false, evidence: ["candidate surfaces bind localhost only"] }, actor);
|
|
374
|
+
}
|
|
375
|
+
const command = action === "stop" ? surface.stop : action === "start" ? surface.start : surface.restart;
|
|
376
|
+
if (surface.lifecycle_owner === "plugin") {
|
|
377
|
+
return operation(action, { surface_id: id }, surface, { ok: true, state: "delegated_to_plugin_adapter", evidence: ["plugin lifecycle owner retained"] }, actor);
|
|
378
|
+
}
|
|
379
|
+
if (!command || command.length === 0) {
|
|
380
|
+
return operation(action, { surface_id: id }, surface, { ok: false, state: "command_missing" }, actor);
|
|
381
|
+
}
|
|
382
|
+
const [cmd, ...args] = command;
|
|
383
|
+
const result = spawnSync(cmd, args, {
|
|
384
|
+
cwd: surface.cwd || undefined,
|
|
385
|
+
env: { ...process.env, CLAUTH_PM2_HOME: getClauthPm2Home(), PM2_HOME: getClauthPm2Home() },
|
|
386
|
+
windowsHide: true,
|
|
387
|
+
encoding: "utf8",
|
|
388
|
+
timeout: Number(surface.timeoutMs || 30000),
|
|
389
|
+
});
|
|
390
|
+
return operation(action, { surface_id: id }, surface, {
|
|
391
|
+
ok: result.status === 0,
|
|
392
|
+
state: result.status === 0 ? "operation_completed" : "operation_failed",
|
|
393
|
+
status: result.status,
|
|
394
|
+
stderr: result.stderr?.slice(0, 2000),
|
|
395
|
+
evidence: [`CLAUTH_PM2_HOME=${getClauthPm2Home()}`],
|
|
396
|
+
}, actor);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function addTunnelRoute(tunnelId, route, actor = "localhost") {
|
|
400
|
+
const tunnels = listTunnels();
|
|
401
|
+
const tunnel = tunnels.find((item) => item.id === tunnelId);
|
|
402
|
+
const prior = tunnel || { id: tunnelId, provider: "cloudflare", routes: [] };
|
|
403
|
+
const routeId = String(route?.routeId || route?.id || crypto.randomUUID());
|
|
404
|
+
const nextRoute = {
|
|
405
|
+
id: routeId,
|
|
406
|
+
hostname: String(route?.hostname || "").trim(),
|
|
407
|
+
service_url: String(route?.service_url || route?.serviceUrl || "").trim(),
|
|
408
|
+
desired_state: "enabled",
|
|
409
|
+
public_route: true,
|
|
410
|
+
};
|
|
411
|
+
if (!nextRoute.hostname || !nextRoute.service_url) {
|
|
412
|
+
return operation("tunnel_route_add", { tunnel_id: tunnelId }, prior, { ok: false, state: "invalid_route" }, actor);
|
|
413
|
+
}
|
|
414
|
+
const nextTunnel = { ...prior, routes: [nextRoute, ...(prior.routes || []).filter((item) => item.id !== routeId)] };
|
|
415
|
+
writeTunnels([nextTunnel, ...tunnels.filter((item) => item.id !== tunnelId)]);
|
|
416
|
+
return operation("tunnel_route_add", { tunnel_id: tunnelId, route_id: routeId }, prior, { ok: true, state: "route_recorded", route: nextRoute }, actor);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function removeTunnelRoute(tunnelId, routeId, actor = "localhost") {
|
|
420
|
+
const tunnels = listTunnels();
|
|
421
|
+
const tunnel = tunnels.find((item) => item.id === tunnelId);
|
|
422
|
+
if (!tunnel) return operation("tunnel_route_remove", { tunnel_id: tunnelId, route_id: routeId }, {}, { ok: false, state: "tunnel_not_found" }, actor);
|
|
423
|
+
const prior = tunnel;
|
|
424
|
+
const nextTunnel = { ...tunnel, routes: (tunnel.routes || []).filter((item) => item.id !== routeId) };
|
|
425
|
+
writeTunnels([nextTunnel, ...tunnels.filter((item) => item.id !== tunnelId)]);
|
|
426
|
+
return operation("tunnel_route_remove", { tunnel_id: tunnelId, route_id: routeId }, prior, { ok: true, state: "route_removed", route_id: routeId }, actor);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function supervisorHealth() {
|
|
430
|
+
const state = loadSupervisorState();
|
|
431
|
+
return {
|
|
432
|
+
status: "ok",
|
|
433
|
+
port: getSupervisorPort(),
|
|
434
|
+
schema: "clauth.supervisor.v1",
|
|
435
|
+
pm2_home: getClauthPm2Home(),
|
|
436
|
+
plugins: (state.plugins || []).length,
|
|
437
|
+
surfaces: (state.surfaces || []).length,
|
|
438
|
+
operations: (state.operations || []).length,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
addTunnelRoute,
|
|
9
|
+
discoverPlugins,
|
|
10
|
+
getClauthPm2Home,
|
|
11
|
+
listPlugins,
|
|
12
|
+
listSurfaces,
|
|
13
|
+
runPluginAction,
|
|
14
|
+
runSurfaceAction,
|
|
15
|
+
removeTunnelRoute,
|
|
16
|
+
setPluginEnabled,
|
|
17
|
+
supervisorHealth,
|
|
18
|
+
validatePluginManifest,
|
|
19
|
+
} from "./supervisor-registry.js";
|
|
20
|
+
|
|
21
|
+
function withTempSupervisor(fn) {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-"));
|
|
23
|
+
const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
|
|
24
|
+
const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
|
|
25
|
+
const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
|
|
26
|
+
const oldPm2 = process.env.CLAUTH_PM2_HOME;
|
|
27
|
+
process.env.CLAUTH_SUPERVISOR_DIR = root;
|
|
28
|
+
delete process.env.CLAUTH_PM2_HOME;
|
|
29
|
+
try {
|
|
30
|
+
return fn(root);
|
|
31
|
+
} finally {
|
|
32
|
+
if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
|
|
33
|
+
else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
|
|
34
|
+
if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
|
|
35
|
+
else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
|
|
36
|
+
if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
|
|
37
|
+
else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
|
|
38
|
+
if (oldPm2 === undefined) delete process.env.CLAUTH_PM2_HOME;
|
|
39
|
+
else process.env.CLAUTH_PM2_HOME = oldPm2;
|
|
40
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writePlugin(root, source, id, manifest) {
|
|
45
|
+
const dir = path.join(root, source, id);
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function baseManifest(id, overrides = {}) {
|
|
52
|
+
return {
|
|
53
|
+
schema: "lifeai.plugin.v1",
|
|
54
|
+
id,
|
|
55
|
+
version: "1.0.0",
|
|
56
|
+
publisher: "LIFEAI",
|
|
57
|
+
credentials: [{ name: `${id}-secret`, key_type: "secret", description: "fixture key" }],
|
|
58
|
+
surfaces: [{
|
|
59
|
+
id: `${id}-surface`,
|
|
60
|
+
destination: "local/clauth/pm2",
|
|
61
|
+
lifecycle_owner: "clauth",
|
|
62
|
+
port: 39111,
|
|
63
|
+
health: "/health",
|
|
64
|
+
restart: ["node", "--version"],
|
|
65
|
+
}],
|
|
66
|
+
test: { command: ["node", "--version"], port: "auto", health: "/health", selfTest: [["node", "--version"]] },
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("validatePluginManifest accepts LIFEAI plugin contract with isolated test context", () => {
|
|
72
|
+
const plugin = validatePluginManifest(baseManifest("regen-media-local"), "clauth-plugin.json");
|
|
73
|
+
assert.equal(plugin.schema, "lifeai.plugin.v1");
|
|
74
|
+
assert.equal(plugin.test.port, "auto");
|
|
75
|
+
assert.equal(plugin.surfaces[0].destination, "local/clauth/pm2");
|
|
76
|
+
assert.equal(plugin.surfaces[0].lifecycle_owner, "clauth");
|
|
77
|
+
assert.equal(plugin.surfaces[0].health, "http://127.0.0.1:39111/health");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("validatePluginManifest accepts empty test command arrays from the v1 template", () => {
|
|
81
|
+
const plugin = validatePluginManifest(baseManifest("empty-test-command", {
|
|
82
|
+
test: { command: [], port: "auto", health: "/health", selfTest: [] },
|
|
83
|
+
}), "clauth-plugin.json");
|
|
84
|
+
assert.deepEqual(plugin.test.command, []);
|
|
85
|
+
assert.equal(plugin.test.port, "auto");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("validatePluginManifest rejects invalid manifests and non-local health URLs", () => {
|
|
89
|
+
assert.throws(() => validatePluginManifest({ ...baseManifest("bad"), schema: "bad" }), /schema/);
|
|
90
|
+
assert.throws(() => validatePluginManifest(baseManifest("bad", {
|
|
91
|
+
surfaces: [{ id: "bad", health: "https://example.com/health" }],
|
|
92
|
+
})), /localhost-only/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("discovery merges managed and user roots without silent managed-id shadowing", () => withTempSupervisor((root) => {
|
|
96
|
+
const managed = path.join(root, "managed");
|
|
97
|
+
const user = path.join(root, "user");
|
|
98
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
99
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
|
|
100
|
+
writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
|
|
101
|
+
writePlugin(root, "user", "regen-media-local", baseManifest("regen-media-local", { version: "2.0.0" }));
|
|
102
|
+
|
|
103
|
+
const result = discoverPlugins();
|
|
104
|
+
const managedPlugin = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "managed");
|
|
105
|
+
const invalid = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "user");
|
|
106
|
+
assert.equal(managedPlugin.state, "awaiting_enable");
|
|
107
|
+
assert.equal(invalid.state, "manifest_invalid");
|
|
108
|
+
assert.match(invalid.error, /shadow/);
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
test("plugin test marks a private candidate and never creates a public route", () => withTempSupervisor((root) => {
|
|
112
|
+
const managed = path.join(root, "managed");
|
|
113
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
114
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
115
|
+
writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
|
|
116
|
+
discoverPlugins();
|
|
117
|
+
|
|
118
|
+
const enabled = setPluginEnabled("regen-media-local", true);
|
|
119
|
+
assert.equal(enabled.resulting_state.enabled, true);
|
|
120
|
+
const receipt = runPluginAction("regen-media-local", "test");
|
|
121
|
+
assert.equal(receipt.resulting_state.state, "candidate_testing");
|
|
122
|
+
assert.equal(receipt.resulting_state.public_route, false);
|
|
123
|
+
assert.equal(listPlugins().find((plugin) => plugin.id === "regen-media-local").candidate.public_route, false);
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
test("surface actions use dedicated clauth PM2 home and keep CodeFlow observe-only", () => withTempSupervisor((root) => {
|
|
127
|
+
const managed = path.join(root, "managed");
|
|
128
|
+
process.env.REGEN_ROOT = "C:/Dev/regen-root";
|
|
129
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
130
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
131
|
+
writePlugin(root, "managed", "demo", baseManifest("demo", {
|
|
132
|
+
surfaces: [{
|
|
133
|
+
id: "demo-surface",
|
|
134
|
+
name: "Demo surface",
|
|
135
|
+
health: "http://127.0.0.1:3333/health",
|
|
136
|
+
cwd: "$REGEN_ROOT/mcp-servers/regen-media",
|
|
137
|
+
restart: [process.execPath, "--version"],
|
|
138
|
+
}],
|
|
139
|
+
}));
|
|
140
|
+
writePlugin(root, "managed", "codeflow", baseManifest("codeflow", {
|
|
141
|
+
surfaces: [{ id: "codeflow-mcp", lifecycle_owner: "clauth", destination: "local/clauth/pm2", restart: ["node", "--version"] }],
|
|
142
|
+
}));
|
|
143
|
+
discoverPlugins();
|
|
144
|
+
const receipt = runSurfaceAction("demo:demo-surface", "restart");
|
|
145
|
+
assert.equal(receipt.resulting_state.ok, true);
|
|
146
|
+
assert.match(receipt.resulting_state.evidence[0], /CLAUTH_PM2_HOME/);
|
|
147
|
+
assert.equal(receipt.prior_state.cwd.replace(/\\/g, "/"), "C:/Dev/regen-root/mcp-servers/regen-media");
|
|
148
|
+
assert.equal(getClauthPm2Home(), path.join(root, "pm2-home"));
|
|
149
|
+
|
|
150
|
+
const codeflow = runSurfaceAction("codeflow:codeflow-mcp", "restart");
|
|
151
|
+
assert.equal(codeflow.resulting_state.ok, false);
|
|
152
|
+
assert.equal(codeflow.resulting_state.reason, "codeflow_self_owned");
|
|
153
|
+
assert.equal(listSurfaces().length, 2);
|
|
154
|
+
assert.equal(supervisorHealth().surfaces, 2);
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
test("tunnel route add and remove produce reversible operation receipts", () => withTempSupervisor((root) => {
|
|
158
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
|
|
159
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
160
|
+
discoverPlugins();
|
|
161
|
+
|
|
162
|
+
const added = addTunnelRoute("cf-main", {
|
|
163
|
+
id: "route-1",
|
|
164
|
+
hostname: "media.example.test",
|
|
165
|
+
service_url: "http://127.0.0.1:3120",
|
|
166
|
+
});
|
|
167
|
+
assert.equal(added.resulting_state.ok, true);
|
|
168
|
+
assert.equal(added.resulting_state.state, "route_recorded");
|
|
169
|
+
|
|
170
|
+
const removed = removeTunnelRoute("cf-main", "route-1");
|
|
171
|
+
assert.equal(removed.resulting_state.ok, true);
|
|
172
|
+
assert.equal(removed.resulting_state.state, "route_removed");
|
|
173
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/clauth",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.7",
|
|
4
4
|
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,27 +8,20 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bash scripts/build.sh",
|
|
11
|
-
"test": "node --test cli/commands/scrub.test.js cli/api.classify.test.js test/studio-debug.test.mjs test/dashboard-key-generator.test.mjs && node test-auth-verdict.mjs",
|
|
12
|
-
"test:studio-debug": "node --test test/studio-debug.test.mjs",
|
|
13
|
-
"test:agent-pool": "node test/agent-pool.test.mjs",
|
|
14
|
-
"test:call-agent-10": "node test/call-agent-10-skills.test.mjs",
|
|
15
|
-
"test:call-agent-guard": "node test/call-agent-guard.test.mjs",
|
|
16
|
-
"test:tintin-settings": "node test/tintin-settings.test.mjs",
|
|
17
11
|
"postinstall": "node scripts/postinstall.js",
|
|
18
12
|
"worker:start": "node cli/index.js serve",
|
|
19
13
|
"worker:stop": "curl -s http://127.0.0.1:52437/shutdown 2>nul || taskkill /F /IM cloudflared.exe 2>nul & exit 0",
|
|
20
14
|
"worker:restart": "npm run worker:stop && timeout /t 3 /nobreak >nul && npm run worker:start"
|
|
21
15
|
},
|
|
22
16
|
"dependencies": {
|
|
23
|
-
"@vscode/ripgrep": "^1.15.9",
|
|
24
17
|
"chalk": "^5.3.0",
|
|
25
18
|
"commander": "^12.1.0",
|
|
26
19
|
"conf": "^13.0.0",
|
|
27
|
-
"fast-glob": "^3.3.2",
|
|
28
20
|
"inquirer": "^10.1.0",
|
|
29
21
|
"node-fetch": "^3.3.2",
|
|
30
22
|
"ora": "^8.1.0",
|
|
31
|
-
"
|
|
23
|
+
"@vscode/ripgrep": "^1.15.9",
|
|
24
|
+
"fast-glob": "^3.3.2"
|
|
32
25
|
},
|
|
33
26
|
"engines": {
|
|
34
27
|
"node": ">=18.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|