@lifeaitools/clauth 2.0.2 → 2.0.3

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.
@@ -1,926 +1,964 @@
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
- deregisterPlugin,
10
- discoverPlugins,
11
- getClauthPm2Home,
12
- listPlugins,
13
- listSurfaces,
14
- reconcileSurfaceHealth,
15
- registerPlugin,
16
- runPluginAction,
17
- runSurfaceAction,
18
- removeTunnelRoute,
19
- setPluginEnabled,
20
- shellQuote,
21
- supervisorHealth,
22
- syncPluginsFromRepos,
23
- SYNC_REPO_NAMES,
24
- SYNC_SKIP_STATES,
25
- validatePluginManifest,
26
- } from "./supervisor-registry.js";
27
- import { isLoopbackAddress, supervisorLogDto, supervisorRequiresWriteToken } from "./commands/serve.js";
28
-
29
- function withTempSupervisor(fn) {
30
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-"));
31
- const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
32
- const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
33
- const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
34
- const oldPm2 = process.env.CLAUTH_PM2_HOME;
35
- process.env.CLAUTH_SUPERVISOR_DIR = root;
36
- // CLAUTH_MANAGED_PLUGIN_ROOTS / CLAUTH_USER_PLUGIN_ROOTS are set as PERSISTENT
37
- // MACHINE-LEVEL env vars on developer boxes, pointing at the LIVE daemon's
38
- // plugin directories. rootEntries() prefers them over CLAUTH_SUPERVISOR_DIR,
39
- // so overriding the supervisor dir ALONE leaves registerPlugin/deregisterPlugin
40
- // writing to (and deleting from) the live fleet. Pin all three to the temp
41
- // root here so a test cannot reach the live daemon by forgetting to.
42
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed-plugins");
43
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user-plugins");
44
- delete process.env.CLAUTH_PM2_HOME;
45
- try {
46
- return fn(root);
47
- } finally {
48
- if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
49
- else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
50
- if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
51
- else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
52
- if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
53
- else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
54
- if (oldPm2 === undefined) delete process.env.CLAUTH_PM2_HOME;
55
- else process.env.CLAUTH_PM2_HOME = oldPm2;
56
- fs.rmSync(root, { recursive: true, force: true });
57
- }
58
- }
59
-
60
- function writePlugin(root, source, id, manifest) {
61
- const dir = path.join(root, source, id);
62
- fs.mkdirSync(dir, { recursive: true });
63
- fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
64
- return dir;
65
- }
66
-
67
- // A scoped npm package installs at <root>/@scope/pkg-name/clauth-plugin.json —
68
- // two levels deep, mirroring node_modules/@lifeaitools/<pkg>/.
69
- function writeScopedPlugin(root, source, scope, id, manifest) {
70
- const dir = path.join(root, source, scope, id);
71
- fs.mkdirSync(dir, { recursive: true });
72
- fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
73
- return dir;
74
- }
75
-
76
- function baseManifest(id, overrides = {}) {
77
- return {
78
- schema: "lifeai.plugin.v1",
79
- id,
80
- version: "1.0.0",
81
- publisher: "LIFEAI",
82
- credentials: [{ name: `${id}-secret`, key_type: "secret", description: "fixture key" }],
83
- surfaces: [{
84
- id: `${id}-surface`,
85
- destination: "local/clauth/pm2",
86
- lifecycle_owner: "clauth",
87
- port: 39111,
88
- health: "/health",
89
- restart: ["node", "--version"],
90
- }],
91
- test: { command: ["node", "--version"], port: "auto", health: "/health", selfTest: [["node", "--version"]] },
92
- documentation: {
93
- architecture: "docs/systems/example/ARCHITECTURE.md",
94
- operator_guide: "docs/systems/example/OPERATE.md",
95
- tool_reference: ".claude/context/mcp-endpoint-design.md",
96
- },
97
- ...overrides,
98
- };
99
- }
100
-
101
- test("validatePluginManifest accepts LIFEAI plugin contract with isolated test context", () => {
102
- const plugin = validatePluginManifest(baseManifest("regen-media-local"), "clauth-plugin.json");
103
- assert.equal(plugin.schema, "lifeai.plugin.v1");
104
- assert.equal(plugin.test.port, "auto");
105
- assert.equal(plugin.surfaces[0].destination, "local/clauth/pm2");
106
- assert.equal(plugin.surfaces[0].lifecycle_owner, "clauth");
107
- assert.equal(plugin.surfaces[0].health, "http://127.0.0.1:39111/health");
108
- assert.equal(plugin.documentation.architecture, "docs/systems/example/ARCHITECTURE.md");
109
- assert.equal(plugin.documentation.operator_guide, "docs/systems/example/OPERATE.md");
110
- });
111
-
112
- test("validatePluginManifest accepts empty test command arrays from the v1 template", () => {
113
- const plugin = validatePluginManifest(baseManifest("empty-test-command", {
114
- test: { command: [], port: "auto", health: "/health", selfTest: [] },
115
- }), "clauth-plugin.json");
116
- assert.deepEqual(plugin.test.command, []);
117
- assert.equal(plugin.test.port, "auto");
118
- });
119
-
120
- test("shellQuote quotes a whitespace-bearing value only when shell is active, and is idempotent", () => {
121
- assert.equal(shellQuote("pm2", true), "pm2");
122
- assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", true), '"C:/Program Files/nodejs/node.exe"');
123
- assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", false), "C:/Program Files/nodejs/node.exe");
124
- assert.equal(shellQuote("one two", false), "one two");
125
- assert.equal(shellQuote('"already quoted value"', true), '"already quoted value"');
126
- });
127
-
128
- test("validatePluginManifest rejects shell metacharacters in command ARGS, not just command[0]", () => {
129
- // The [;&|<>] guard on command[0] alone is defeated by moving the payload
130
- // one index right — args must be validated the same way (verified live
131
- // during code review: spawnSync(['cmd','/c','echo','A & echo INJECTED'],
132
- // {shell:true}) executed a second command from inside one arg string).
133
- assert.throws(() => validatePluginManifest(baseManifest("inject", {
134
- surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "start svc & calc.exe"] }],
135
- })), /shell syntax/);
136
- assert.throws(() => validatePluginManifest(baseManifest("inject2", {
137
- surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "name; rm -rf /"] }],
138
- })), /shell syntax/);
139
- // A legitimate arg with no shell metacharacters still passes.
140
- const ok = validatePluginManifest(baseManifest("legit", {
141
- surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "my-app-name"] }],
142
- }), "clauth-plugin.json");
143
- assert.deepEqual(ok.surfaces[0].restart, ["pm2", "restart", "my-app-name"]);
144
- });
145
-
146
- test("validatePluginManifest rejects invalid manifests and non-local health URLs", () => {
147
- assert.throws(() => validatePluginManifest({ ...baseManifest("bad"), schema: "bad" }), /schema/);
148
- assert.throws(() => validatePluginManifest(baseManifest("bad", {
149
- surfaces: [{ id: "bad", health: "https://example.com/health" }],
150
- })), /localhost-only/);
151
- assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
152
- documentation: { architecture: "../outside.md" },
153
- })), /repository-relative/);
154
- assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
155
- documentation: { operator_guide: "docs/guide.md" },
156
- })), /documentation.architecture is required/);
157
- assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
158
- documentation: { architecture: "https://example.com/architecture.md" },
159
- })), /repository-relative/);
160
- });
161
-
162
- test("discovery merges managed and user roots without silent managed-id shadowing", () => withTempSupervisor((root) => {
163
- const managed = path.join(root, "managed");
164
- const user = path.join(root, "user");
165
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
166
- process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
167
- writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
168
- writePlugin(root, "user", "regen-media-local", baseManifest("regen-media-local", { version: "2.0.0" }));
169
-
170
- const result = discoverPlugins();
171
- const managedPlugin = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "managed");
172
- const invalid = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "user");
173
- assert.equal(managedPlugin.state, "awaiting_enable");
174
- assert.equal(invalid.state, "manifest_invalid");
175
- assert.match(invalid.error, /shadow/);
176
- }));
177
-
178
- test("discovery finds both a scoped npm-package layout and an unscoped layout from one root", () => withTempSupervisor((root) => {
179
- const managed = path.join(root, "managed");
180
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
181
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
182
- // Unscoped: managed/plain-plugin/clauth-plugin.json (one level — existing behavior)
183
- writePlugin(root, "managed", "plain-plugin", baseManifest("plain-plugin"));
184
- // Scoped: managed/@lifeaitools/fs-mcp/clauth-plugin.json (two levels — the WP-1 fix)
185
- writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp"));
186
-
187
- const result = discoverPlugins();
188
- const plain = result.plugins.find((plugin) => plugin.id === "plain-plugin");
189
- const scoped = result.plugins.find((plugin) => plugin.id === "fs-mcp");
190
- assert.ok(plain, "unscoped layout must still be discovered");
191
- assert.equal(plain.state, "awaiting_enable");
192
- assert.ok(scoped, "scoped @scope/pkg layout must now be discovered");
193
- assert.equal(scoped.state, "awaiting_enable");
194
- assert.equal(scoped.manifest_hash?.length > 0, true);
195
- }));
196
-
197
- test("discovery skips a malformed manifest without aborting the rest of the scan", () => withTempSupervisor((root) => {
198
- const managed = path.join(root, "managed");
199
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
200
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
201
- writePlugin(root, "managed", "good-plugin", baseManifest("good-plugin"));
202
- const brokenDir = path.join(managed, "broken-plugin");
203
- fs.mkdirSync(brokenDir, { recursive: true });
204
- fs.writeFileSync(path.join(brokenDir, "clauth-plugin.json"), "{ this is not valid json", "utf8");
205
- // Also prove a malformed SCOPED manifest doesn't abort the scoped-dir descent either.
206
- const brokenScopedDir = path.join(managed, "@lifeaitools", "broken-scoped");
207
- fs.mkdirSync(brokenScopedDir, { recursive: true });
208
- fs.writeFileSync(path.join(brokenScopedDir, "clauth-plugin.json"), "{ also not valid json", "utf8");
209
- writeScopedPlugin(root, "managed", "@lifeaitools", "good-scoped", baseManifest("good-scoped"));
210
-
211
- const result = discoverPlugins();
212
- const good = result.plugins.find((plugin) => plugin.id === "good-plugin");
213
- const broken = result.plugins.find((plugin) => plugin.id === "broken-plugin");
214
- const goodScoped = result.plugins.find((plugin) => plugin.id === "good-scoped");
215
- const brokenScoped = result.plugins.find((plugin) => plugin.id === "broken-scoped");
216
- assert.equal(good.state, "awaiting_enable");
217
- assert.equal(broken.state, "manifest_invalid");
218
- assert.equal(goodScoped.state, "awaiting_enable");
219
- assert.equal(brokenScoped.state, "manifest_invalid");
220
- }));
221
-
222
- test("trusted managed core plugins auto-enable while user plugins remain opt-in", () => withTempSupervisor((root) => {
223
- const managed = path.join(root, "managed");
224
- const user = path.join(root, "user");
225
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
226
- process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
227
- writePlugin(root, "managed", "core-mcp", baseManifest("core-mcp", { core: true, enable_default: true }));
228
- writePlugin(root, "user", "user-mcp", baseManifest("user-mcp", { core: true, enable_default: true }));
229
-
230
- const result = discoverPlugins();
231
- const core = result.plugins.find((plugin) => plugin.id === "core-mcp");
232
- const userPlugin = result.plugins.find((plugin) => plugin.id === "user-mcp");
233
- assert.equal(core.enabled, true);
234
- assert.equal(core.state, "current");
235
- assert.equal(result.events.some((event) => event.kind === "core_plugin_auto_enabled" && event.plugin_id === "core-mcp"), true);
236
- assert.equal(userPlugin.enabled, false);
237
- assert.equal(userPlugin.state, "awaiting_enable");
238
- }));
239
-
240
- test("plugin test marks a private candidate and never creates a public route", () => withTempSupervisor((root) => {
241
- const managed = path.join(root, "managed");
242
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
243
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
244
- writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
245
- discoverPlugins();
246
-
247
- const enabled = setPluginEnabled("regen-media-local", true);
248
- assert.equal(enabled.resulting_state.enabled, true);
249
- const receipt = runPluginAction("regen-media-local", "test");
250
- assert.equal(receipt.resulting_state.state, "candidate_testing");
251
- assert.equal(receipt.resulting_state.public_route, false);
252
- assert.equal(listPlugins().find((plugin) => plugin.id === "regen-media-local").candidate.public_route, false);
253
- }));
254
-
255
- test("surface actions use dedicated clauth PM2 home and keep CodeFlow observe-only", () => withTempSupervisor((root) => {
256
- const managed = path.join(root, "managed");
257
- process.env.REGEN_ROOT = "C:/Dev/regen-root";
258
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
259
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
260
- writePlugin(root, "managed", "demo", baseManifest("demo", {
261
- surfaces: [{
262
- id: "demo-surface",
263
- name: "Demo surface",
264
- health: "http://127.0.0.1:3333/health",
265
- cwd: "$REGEN_ROOT/mcp-servers/regen-media",
266
- restart: [process.execPath, "--version"],
267
- }],
268
- }));
269
- writePlugin(root, "managed", "codeflow", baseManifest("codeflow", {
270
- surfaces: [{ id: "codeflow-mcp", lifecycle_owner: "clauth", destination: "local/clauth/pm2", restart: ["node", "--version"] }],
271
- }));
272
- discoverPlugins();
273
- const receipt = runSurfaceAction("demo:demo-surface", "restart");
274
- assert.equal(receipt.resulting_state.ok, true);
275
- assert.match(receipt.resulting_state.evidence[0], /CLAUTH_PM2_HOME/);
276
- assert.equal(receipt.prior_state.cwd.replace(/\\/g, "/"), "C:/Dev/regen-root/mcp-servers/regen-media");
277
- assert.equal(getClauthPm2Home(), path.join(root, "pm2-home"));
278
-
279
- const codeflow = runSurfaceAction("codeflow:codeflow-mcp", "restart");
280
- assert.equal(codeflow.resulting_state.ok, false);
281
- assert.equal(codeflow.resulting_state.reason, "codeflow_self_owned");
282
- assert.equal(listSurfaces().length, 2);
283
- assert.equal(supervisorHealth().surfaces, 2);
284
- }));
285
-
286
- test("reconcile falls back to the declared start command when restart reports a missing process", () => withTempSupervisor((root) => {
287
- const managed = path.join(root, "managed");
288
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
289
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
290
- writePlugin(root, "managed", "fallback-demo", baseManifest("fallback-demo", {
291
- core: true,
292
- enable_default: true,
293
- surfaces: [{
294
- id: "primary",
295
- destination: "local/clauth/pm2",
296
- lifecycle_owner: "clauth",
297
- port: 39114,
298
- health: "/health",
299
- start: [process.execPath, "--version"],
300
- restart: [process.execPath, "-e", "process.exit(1)"],
301
- }],
302
- }));
303
- discoverPlugins();
304
- const receipt = runSurfaceAction("fallback-demo:primary", "reconcile");
305
- assert.equal(receipt.resulting_state.ok, true);
306
- assert.equal(receipt.resulting_state.evidence.includes("reconcile_start_fallback=true"), true);
307
- }));
308
-
309
- test("surface promote and rollback never fall through to restart commands", () => withTempSupervisor((root) => {
310
- const managed = path.join(root, "managed");
311
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
312
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
313
- writePlugin(root, "managed", "demo", baseManifest("demo", {
314
- surfaces: [{
315
- id: "demo-surface",
316
- name: "Demo surface",
317
- health: "http://127.0.0.1:3333/health",
318
- restart: [process.execPath, "--version"],
319
- }],
320
- }));
321
- discoverPlugins();
322
-
323
- for (const action of ["promote", "rollback"]) {
324
- const receipt = runSurfaceAction("demo:demo-surface", action);
325
- assert.equal(receipt.resulting_state.ok, false);
326
- assert.equal(receipt.resulting_state.state, "unsupported_surface_action");
327
- assert.equal(receipt.resulting_state.evidence[0], "surface action did not execute a process command");
328
- }
329
- }));
330
-
331
- test("health reconciliation marks a failed clauth surface and repairs it through reconcile", async () => {
332
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-health-"));
333
- const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
334
- const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
335
- const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
336
- process.env.CLAUTH_SUPERVISOR_DIR = root;
337
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
338
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
339
- try {
340
- writePlugin(root, "managed", "health-demo", baseManifest("health-demo", {
341
- core: true,
342
- enable_default: true,
343
- surfaces: [{
344
- id: "primary",
345
- destination: "local/clauth/pm2",
346
- lifecycle_owner: "clauth",
347
- port: 39111,
348
- health: "/health",
349
- restart: [process.execPath, "--version"],
350
- }],
351
- }));
352
- discoverPlugins();
353
- let healthCalls = 0;
354
- const result = await reconcileSurfaceHealth({
355
- fetchImpl: async () => ({ ok: ++healthCalls > 1, status: 503 }),
356
- });
357
- assert.equal(result.inspected[0].surface_id, "health-demo:primary");
358
- assert.equal(result.inspected[0].state, "reconciled");
359
- const surface = listSurfaces().find((item) => item.plugin_id === "health-demo");
360
- assert.equal(surface.state, "current");
361
- assert.ok(surface.last_reconcile_operation_id);
362
- assert.equal(healthCalls, 2);
363
- } finally {
364
- if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
365
- else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
366
- if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
367
- else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
368
- if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
369
- else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
370
- fs.rmSync(root, { recursive: true, force: true });
371
- }
372
- });
373
-
374
- test("health reconciliation does not claim current when a successful command leaves health down", async () => {
375
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-post-health-"));
376
- const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
377
- const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
378
- const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
379
- process.env.CLAUTH_SUPERVISOR_DIR = root;
380
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
381
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
382
- try {
383
- writePlugin(root, "managed", "post-health-demo", baseManifest("post-health-demo", {
384
- core: true,
385
- enable_default: true,
386
- surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health", restart: [process.execPath, "--version"] }],
387
- }));
388
- discoverPlugins();
389
- const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
390
- assert.equal(result.inspected[0].state, "reconcile_failed");
391
- assert.equal(listSurfaces()[0].state, "unavailable");
392
- assert.equal(listSurfaces()[0].last_health_ok, false);
393
- } finally {
394
- if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
395
- else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
396
- if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
397
- else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
398
- if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
399
- else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
400
- fs.rmSync(root, { recursive: true, force: true });
401
- }
402
- });
403
-
404
- test("health reconciliation never restarts external or plugin-owned surfaces", async () => {
405
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-observe-"));
406
- const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
407
- const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
408
- const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
409
- process.env.CLAUTH_SUPERVISOR_DIR = root;
410
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
411
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
412
- try {
413
- // Both fixtures are deliberately PROBE-ABLE — local destination + port, so
414
- // healthUrlForSurface() yields a real URL for each. That is what gives this
415
- // test teeth.
416
- //
417
- // An earlier revision made the external fixture `vultr/` with no port. It
418
- // read as equivalent and was not: portless + relative health resolves to a
419
- // null URL, so reconcileSurfaceHealth's `if (!url) continue` skipped it
420
- // BEFORE the owner/destination guard was consulted. Delete the guard this
421
- // test is named for and it still passed green — it proved the surface was
422
- // unprobe-able, not that it was probe-able and correctly left alone.
423
- //
424
- // Now the ONLY thing that can exclude external-demo is lifecycle_owner, and
425
- // clauth-owned-demo is the positive control proving the probe machinery
426
- // actually runs against this fixture shape. If `inspected` ever comes back
427
- // empty, the control failed and the negative result means nothing.
428
- writePlugin(root, "managed", "external-demo", baseManifest("external-demo", {
429
- core: true,
430
- enable_default: true,
431
- destination: "local/clauth/pm2",
432
- lifecycle_owner: "external",
433
- surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "external", port: 39112, health: "/health" }],
434
- }));
435
- writePlugin(root, "managed", "clauth-owned-demo", baseManifest("clauth-owned-demo", {
436
- core: true,
437
- enable_default: true,
438
- destination: "local/clauth/pm2",
439
- lifecycle_owner: "clauth",
440
- surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health" }],
441
- }));
442
- discoverPlugins();
443
- const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
444
- const inspectedIds = result.inspected.map((s) => (typeof s === "string" ? s : s.surface_id));
445
- // Positive control: the probe machinery ran at all.
446
- assert.ok(inspectedIds.some((id) => String(id).includes("clauth-owned-demo")),
447
- `control failed — clauth-owned surface was not probed, so "external was skipped" proves nothing (inspected: ${JSON.stringify(result.inspected)})`);
448
- // The actual assertion: an externally-owned surface is never touched.
449
- assert.ok(!inspectedIds.some((id) => String(id).includes("external-demo")),
450
- `external-demo was probed — the lifecycle_owner guard is not excluding it (inspected: ${JSON.stringify(result.inspected)})`);
451
- const external = listSurfaces().find((s) => s.plugin_id === "external-demo");
452
- assert.equal(external.state, "current");
453
- } finally {
454
- if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
455
- else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
456
- if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
457
- else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
458
- if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
459
- else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
460
- fs.rmSync(root, { recursive: true, force: true });
461
- }
462
- });
463
-
464
- test("supervisor write-token policy is temporarily relaxed only for the localhost supervisor port", () => {
465
- assert.equal(supervisorRequiresWriteToken(52439, {}), false);
466
- assert.equal(supervisorRequiresWriteToken(52439, { CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN: "1" }), true);
467
- assert.equal(supervisorRequiresWriteToken(52437, {}), true);
468
- assert.equal(isLoopbackAddress("127.0.0.1"), true);
469
- assert.equal(isLoopbackAddress("::1"), true);
470
- assert.equal(isLoopbackAddress("::ffff:127.0.0.1"), true);
471
- assert.equal(isLoopbackAddress("192.168.1.25"), false);
472
- });
473
-
474
- test("supervisor log DTO strips raw operation payloads", () => {
475
- const dto = supervisorLogDto({
476
- ts: "2026-07-30T00:00:00.000Z",
477
- kind: "operation",
478
- operationId: "op-secret",
479
- actor: "localhost",
480
- action: "restart",
481
- target: { surface_id: "demo", command: ["node", "--token=secret"] },
482
- prior_state: { start: ["node", "--token=secret"], cwd: "C:/secrets" },
483
- resulting_state: {
484
- ok: false,
485
- state: "operation_failed",
486
- stderr: "SECRET=abc123",
487
- evidence: ["CLAUTH_PM2_HOME=C:/safe"],
488
- },
489
- stderr: "SECRET=abc123",
490
- completed_at: "2026-07-30T00:00:01.000Z",
491
- });
492
-
493
- const json = JSON.stringify(dto);
494
- assert.equal(dto.operationId, "op-secret");
495
- assert.equal(dto.target.surface_id, "demo");
496
- assert.equal(dto.resulting_state.state, "operation_failed");
497
- assert.equal(json.includes("prior_state"), false);
498
- assert.equal(json.includes("stderr"), false);
499
- assert.equal(json.includes("SECRET"), false);
500
- assert.equal(json.includes("--token"), false);
501
- });
502
-
503
- test("registerPlugin validates, writes into the managed root, and discovers the plugin — idempotent on re-register", () => withTempSupervisor((root) => {
504
- const managed = path.join(root, "managed");
505
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
506
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
507
- const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-source-"));
508
- const manifestPath = path.join(sourceDir, "clauth-plugin.json");
509
- fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("registered-demo", { core: true, enable_default: true })), "utf8");
510
-
511
- const first = registerPlugin(manifestPath, "test");
512
- assert.equal(first.resulting_state.ok, true);
513
- assert.equal(first.resulting_state.state, "registered");
514
- assert.equal(first.resulting_state.plugin_state, "current");
515
- const written = fs.existsSync(path.join(managed, "registered-demo", "clauth-plugin.json"));
516
- assert.equal(written, true);
517
- const found = listPlugins().find((plugin) => plugin.id === "registered-demo");
518
- assert.equal(found.enabled, true);
519
-
520
- const second = registerPlugin(manifestPath, "test");
521
- assert.equal(second.resulting_state.state, "unchanged", "re-registering identical content must be a no-op, not a rewrite");
522
-
523
- fs.rmSync(sourceDir, { recursive: true, force: true });
524
- }));
525
-
526
- test("registerPlugin rejects a plugin id that would escape the managed-plugins root", () => withTempSupervisor((root) => {
527
- // Code-review finding (confidence 95, live PoC): manifest.id of ".." passed
528
- // the old id regex (dot is in the allowed character class with no
529
- // exclusion of all-dots forms) and path.join(managedRoot, "..") wrote
530
- // clauth-plugin.json one level ABOVE the managed root. Two independent
531
- // fixes now close this: the id regex rejects all-dots ids, and
532
- // registerPlugin asserts containment at the write site so a future regex
533
- // relaxation can't reopen it.
534
- const managed = path.join(root, "managed");
535
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
536
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
537
- const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-traversal-"));
538
- const manifestPath = path.join(sourceDir, "clauth-plugin.json");
539
- fs.writeFileSync(manifestPath, JSON.stringify({ schema: "lifeai.plugin.v1", id: "..", version: "1.0.0", publisher: "t", documentation: { architecture: "a.md" }, surfaces: [] }), "utf8");
540
-
541
- const receipt = registerPlugin(manifestPath, "test");
542
- assert.equal(receipt.resulting_state.ok, false);
543
- assert.equal(receipt.resulting_state.state, "manifest_invalid");
544
- const escapedPath = path.join(managed, "..", "clauth-plugin.json");
545
- assert.equal(fs.existsSync(escapedPath), false, "must never write outside the managed-plugins root");
546
-
547
- fs.rmSync(sourceDir, { recursive: true, force: true });
548
- }));
549
-
550
- test("validatePluginManifest rejects an all-dots plugin or surface id", () => {
551
- assert.throws(() => validatePluginManifest(baseManifest("..", {}), "clauth-plugin.json"), /may not be all dots/);
552
- assert.throws(() => validatePluginManifest(baseManifest("valid-id", {
553
- surfaces: [{ id: ".", lifecycle_owner: "clauth" }],
554
- }), "clauth-plugin.json"), /may not be all dots/);
555
- });
556
-
557
- test("registerPlugin rejects an invalid manifest without writing anything", () => withTempSupervisor((root) => {
558
- const managed = path.join(root, "managed");
559
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
560
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
561
- const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-bad-"));
562
- const manifestPath = path.join(sourceDir, "clauth-plugin.json");
563
- fs.writeFileSync(manifestPath, JSON.stringify({ schema: "wrong", id: "bad" }), "utf8");
564
-
565
- const receipt = registerPlugin(manifestPath, "test");
566
- assert.equal(receipt.resulting_state.ok, false);
567
- assert.equal(receipt.resulting_state.state, "manifest_invalid");
568
- assert.equal(fs.existsSync(path.join(managed, "bad")), false);
569
-
570
- fs.rmSync(sourceDir, { recursive: true, force: true });
571
- }));
572
-
573
- // Builds throwaway product repos laid out like the real ones, so a sync sweep
574
- // exercises the real relative manifest paths without reading a live checkout.
575
- function withTempProductRepos(fn) {
576
- const base = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sync-repos-"));
577
- const regenRoot = path.join(base, "regen-root");
578
- const rdcSkills = path.join(base, "rdc-skills");
579
- const writeManifest = (repoRoot, relPath, manifest) => {
580
- const full = path.join(repoRoot, relPath);
581
- fs.mkdirSync(path.dirname(full), { recursive: true });
582
- fs.writeFileSync(full, typeof manifest === "string" ? manifest : `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
583
- return full;
584
- };
585
- writeManifest(regenRoot, "packages/codeflow/clauth-plugin.json", baseManifest("codeflow-mcp"));
586
- writeManifest(regenRoot, "apps/dev-center/clauth-plugin.json", baseManifest("dev-center"));
587
- writeManifest(regenRoot, "mcp-servers/regen-media/clauth-plugin.json", baseManifest("regen-media"));
588
- writeManifest(regenRoot, "mcp-servers/web-research/clauth-plugin.json", baseManifest("web-research"));
589
- writeManifest(rdcSkills, "clauth-plugin.json", baseManifest("rdc-skills"));
590
- try {
591
- return fn({ base, regenRoot, rdcSkills, writeManifest, roots: { "regen-root": regenRoot, "rdc-skills": rdcSkills } });
592
- } finally {
593
- fs.rmSync(base, { recursive: true, force: true });
594
- }
595
- }
596
-
597
- test("plugin sync inherits registerPlugin idempotence — a second sweep reports every manifest unchanged", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
598
- const first = syncPluginsFromRepos(roots, "test");
599
- assert.equal(first.length, 5, "one receipt per attempted manifest");
600
- assert.deepEqual(
601
- first.filter((entry) => !entry.ok).map((entry) => `${entry.repo}:${entry.state}`),
602
- [],
603
- "every manifest in a complete checkout must register",
604
- );
605
- assert.equal(first.every((entry) => entry.state === "registered"), true);
606
-
607
- // registerPlugin sha256-compares before writing; sync must not defeat that by
608
- // re-writing or re-hashing on its own.
609
- const second = syncPluginsFromRepos(roots, "test");
610
- assert.equal(second.length, 5);
611
- assert.equal(second.every((entry) => entry.ok && entry.state === "unchanged"), true, "re-sweeping identical content must be a no-op");
612
- })));
613
-
614
- test("plugin sync warns and continues over a missing repo root instead of throwing", () => withTempSupervisor(() => withTempProductRepos(({ base, rdcSkills }) => {
615
- // A box that never checked out regen-root must still sync what it does have.
616
- const absent = path.join(base, "no-such-checkout");
617
- assert.equal(fs.existsSync(absent), false);
618
- let receipts;
619
- assert.doesNotThrow(() => {
620
- receipts = syncPluginsFromRepos({ "regen-root": absent, "rdc-skills": rdcSkills }, "test");
621
- });
622
- assert.equal(receipts.length, 5, "a skipped repo still yields a receipt per attempted manifest");
623
- const missing = receipts.filter((entry) => entry.state === "repo_root_missing");
624
- assert.equal(missing.length, 4, "all four regen-root manifests report the missing root");
625
- assert.equal(missing.every((entry) => entry.ok === false), true);
626
- const skills = receipts.find((entry) => entry.repo === "rdc-skills");
627
- assert.equal(skills.ok, true);
628
- assert.equal(skills.state, "registered");
629
- assert.ok(listPlugins().find((plugin) => plugin.id === "rdc-skills"), "the reachable repo still registered");
630
- })));
631
-
632
- test("plugin sync registers the remaining manifests when one is malformed", () => withTempSupervisor(() => withTempProductRepos(({ regenRoot, roots, writeManifest }) => {
633
- writeManifest(regenRoot, "apps/dev-center/clauth-plugin.json", "{ this is not valid json");
634
-
635
- const receipts = syncPluginsFromRepos(roots, "test");
636
- assert.equal(receipts.length, 5);
637
- const bad = receipts.find((entry) => entry.path.includes("dev-center"));
638
- assert.equal(bad.ok, false);
639
- assert.equal(bad.state, "manifest_invalid");
640
- const good = receipts.filter((entry) => entry !== bad);
641
- assert.equal(good.length, 4);
642
- assert.equal(good.every((entry) => entry.ok && entry.state === "registered"), true, "one bad manifest must not abort the sweep");
643
- const ids = new Set(listPlugins().map((plugin) => plugin.id));
644
- for (const id of ["codeflow-mcp", "regen-media", "web-research", "rdc-skills"]) {
645
- assert.ok(ids.has(id), `${id} must still be registered`);
646
- }
647
- })));
648
-
649
- test("deregisterPlugin removes only the named plugin and leaves siblings intact", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
650
- syncPluginsFromRepos(roots, "test");
651
- const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
652
- assert.equal(fs.existsSync(path.join(managed, "web-research")), true);
653
-
654
- const receipt = deregisterPlugin("web-research", "test");
655
- assert.equal(receipt.resulting_state.ok, true);
656
- assert.equal(receipt.resulting_state.state, "deregistered");
657
- assert.equal(fs.existsSync(path.join(managed, "web-research")), false, "the named plugin directory is gone");
658
-
659
- for (const sibling of ["codeflow-mcp", "dev-center", "regen-media", "rdc-skills"]) {
660
- assert.equal(fs.existsSync(path.join(managed, sibling)), true, `${sibling} must survive`);
661
- }
662
- // discovery re-ran, so the removed managed plugin is reported missing, not current
663
- const after = listPlugins().find((plugin) => plugin.id === "web-research");
664
- assert.equal(after.state, "missing_default");
665
- assert.equal(after.enabled, false);
666
- })));
667
-
668
- test("deregisterPlugin on an unregistered id is a safe no-op, not an error", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
669
- syncPluginsFromRepos(roots, "test");
670
- const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
671
- const before = fs.readdirSync(managed).sort();
672
-
673
- const receipt = deregisterPlugin("never-registered-plugin", "test");
674
- assert.equal(receipt.resulting_state.ok, true, "a no-op is success, not failure");
675
- assert.equal(receipt.resulting_state.state, "not_registered");
676
- assert.deepEqual(fs.readdirSync(managed).sort(), before, "nothing else may be touched");
677
- })));
678
-
679
- test("deregisterPlugin rejects an id that would escape the managed-plugins root", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
680
- // Mirrors the registerPlugin traversal test. The id here is raw CLI/HTTP
681
- // input with no manifest validation upstream, and the operation DELETES
682
- // recursively — so an id resolving to the root itself must reject too, which
683
- // is where this is strictly stricter than registerPlugin's assertion.
684
- syncPluginsFromRepos(roots, "test");
685
- const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
686
- const sentinel = path.join(managed, "..", "sentinel-outside-root.txt");
687
- fs.writeFileSync(sentinel, "must survive", "utf8");
688
- const before = fs.readdirSync(managed).sort();
689
-
690
- const evilIds = [
691
- "..", "../..", "../sentinel-outside-root.txt", "..\\..", "/etc", "", ".",
692
- // These three are stopped by the CHARSET guard alone — the containment
693
- // assert waves all of them through, because path.resolve() normalizes the
694
- // `..` away and Windows re-anchors a drive-relative path, so each lands
695
- // back INSIDE the root (measured during code review). They are pinned here
696
- // so the two guards are independently tested and nobody "simplifies" the
697
- // charset rule believing containment is a safety net for it.
698
- "sub/../web-research",
699
- "C:web-research",
700
- "web-research::$DATA",
701
- ];
702
- for (const evil of evilIds) {
703
- const receipt = deregisterPlugin(evil, "test");
704
- assert.equal(receipt.resulting_state.ok, false, `id ${JSON.stringify(evil)} must be rejected`);
705
- assert.equal(receipt.resulting_state.state, "invalid_plugin_id");
706
- }
707
- assert.equal(fs.existsSync(path.join(managed, "web-research")), true, "a charset-rejected alias must not have deleted the real plugin");
708
-
709
- assert.equal(fs.existsSync(sentinel), true, "must never delete outside the managed-plugins root");
710
- assert.equal(fs.existsSync(managed), true, "must never delete the managed-plugins root itself");
711
- assert.deepEqual(fs.readdirSync(managed).sort(), before, "no registered plugin may be removed by a rejected id");
712
- })));
713
-
714
- test("validatePluginManifest rejects a port on any non-local destination, and keeps local ports", () => {
715
- // A remote surface is reached by URL; its port is the deployment registry's
716
- // fact. Copying it into the manifest creates a second home for one fact that
717
- // then drifts (regen-media 3121 vs 3120, dev-center 3012 vs 3003). Worse, a
718
- // ported remote surface makes localhostHealth() synthesize
719
- // http://127.0.0.1:<port>/health for a service on another box, pointing the
720
- // health reconciler at the wrong machine.
721
- //
722
- // The rule is keyed on the validated DESTINATION enum, never on the surface's
723
- // free-text id/role — keying on the label was tried in review and failed both
724
- // ways: renaming the surface bypassed it, and a local surface named "remote"
725
- // was falsely rejected. Both directions are asserted below.
726
- for (const destination of ["vultr/clauth/pm2", "coolify/clauth/docker"]) {
727
- for (const port of [3110, "auto", 0]) {
728
- assert.throws(() => validatePluginManifest(baseManifest("remote-port", {
729
- surfaces: [{ id: "remote", destination, lifecycle_owner: "external", port }],
730
- })), /must not declare a port/, `${destination} + port ${JSON.stringify(port)} must be rejected`);
731
- }
732
- }
733
- // Renaming the surface must NOT bypass the rule — this is the bypass that
734
- // keying on the `remote` label allowed.
735
- for (const id of ["vultr", "prod", "primary", "local"]) {
736
- assert.throws(() => validatePluginManifest(baseManifest("renamed-remote", {
737
- surfaces: [{ id, destination: "vultr/clauth/pm2", lifecycle_owner: "external", port: 3110 }],
738
- })), /must not declare a port/, `a non-local surface named "${id}" must still be rejected`);
739
- }
740
-
741
- // A remote surface WITHOUT a port is accepted — the shape all four shipped
742
- // remote surfaces already use.
743
- const remote = validatePluginManifest(baseManifest("remote-ok", {
744
- surfaces: [{ id: "remote", destination: "vultr/clauth/pm2", lifecycle_owner: "external" }],
745
- }), "clauth-plugin.json");
746
- assert.equal(remote.surfaces[0].port, null);
747
-
748
- // A LOCAL surface keeps its port on purpose — that port describes how the
749
- // service runs on a developer box. This is not drift; do not "fix" it. A
750
- // local surface merely NAMED "remote" is local, and must not be rejected.
751
- for (const [id, destination] of [["local", "local/clauth/pm2"], ["primary", "local/clauth/pm2"], ["remote", "local/clauth/daemon"]]) {
752
- const localSurface = validatePluginManifest(baseManifest(`${id}-ok`, {
753
- surfaces: [{ id, destination, lifecycle_owner: "clauth", port: 3109, health: "/health" }],
754
- }), "clauth-plugin.json");
755
- assert.equal(localSurface.surfaces[0].port, 3109, `${destination} surface must keep its port`);
756
- assert.equal(localSurface.surfaces[0].health, "http://127.0.0.1:3109/health");
757
- }
758
- });
759
-
760
- test("deregisterPlugin removes a scoped @scope/pkg plugin instead of falsely reporting it absent", () => withTempSupervisor((root) => {
761
- // A removal verb that reports ✓ while the plugin stays installed AND enabled
762
- // is the one receipt this must never get wrong. The flat <root>/<id> probe
763
- // misses the scoped layout entirely, so the location is resolved from the
764
- // discovery_root/sourcePath discovery already records.
765
- const managed = path.join(root, "managed");
766
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
767
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
768
- writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp", { core: true, enable_default: true }));
769
- discoverPlugins();
770
- assert.equal(listPlugins().find((plugin) => plugin.id === "fs-mcp").enabled, true, "precondition: scoped plugin is enabled");
771
-
772
- const receipt = deregisterPlugin("fs-mcp", "test");
773
- assert.equal(receipt.resulting_state.state, "deregistered", "must not report not_registered for a scoped plugin");
774
- assert.equal(receipt.resulting_state.ok, true);
775
- assert.equal(fs.existsSync(path.join(managed, "@lifeaitools", "fs-mcp")), false, "the scoped package directory is actually gone");
776
- const after = listPlugins().find((plugin) => plugin.id === "fs-mcp");
777
- assert.equal(after.enabled, false, "a deregistered plugin must not remain enabled");
778
- assert.equal(after.state, "missing_default");
779
- }));
780
-
781
- test("deregisterPlugin finds a plugin in a non-first managed root and refuses a user-root plugin", () => withTempSupervisor((root) => {
782
- // CLAUTH_MANAGED_PLUGIN_ROOTS is a path-delimited LIST; honoring only the
783
- // first entry silently reports a real plugin as absent.
784
- const managedA = path.join(root, "managed-a");
785
- const managedB = path.join(root, "managed-b");
786
- const user = path.join(root, "user");
787
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = [managedA, managedB].join(path.delimiter);
788
- process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
789
- writePlugin(root, "managed-b", "second-root-plugin", baseManifest("second-root-plugin"));
790
- writePlugin(root, "user", "user-only-plugin", baseManifest("user-only-plugin"));
791
- discoverPlugins();
792
-
793
- const second = deregisterPlugin("second-root-plugin", "test");
794
- assert.equal(second.resulting_state.state, "deregistered", "a plugin in the 2nd managed root must be found");
795
- assert.equal(fs.existsSync(path.join(managedB, "second-root-plugin")), false);
796
-
797
- // A user-root plugin is out of this verb's remit — refuse it explicitly
798
- // rather than reporting a green "not_registered" that implies it is gone.
799
- const userReceipt = deregisterPlugin("user-only-plugin", "test");
800
- assert.equal(userReceipt.resulting_state.ok, false);
801
- assert.equal(userReceipt.resulting_state.state, "not_managed");
802
- assert.equal(fs.existsSync(path.join(user, "user-only-plugin")), true, "the user plugin must be left intact");
803
- }));
804
-
805
- test("deregisterPlugin --dry-run resolves the target without deleting it", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
806
- syncPluginsFromRepos(roots, "test");
807
- const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
808
-
809
- const receipt = deregisterPlugin("web-research", "test", { dryRun: true });
810
- assert.equal(receipt.resulting_state.ok, true);
811
- assert.equal(receipt.resulting_state.state, "would_deregister");
812
- assert.equal(receipt.resulting_state.target_dir, path.resolve(managed, "web-research"));
813
- assert.equal(fs.existsSync(path.join(managed, "web-research")), true, "a dry run must not delete anything");
814
- assert.equal(listPlugins().find((plugin) => plugin.id === "web-research").state !== "missing_default", true);
815
- })));
816
-
817
- test("plugin sync reports an unknown repo-root override instead of silently sweeping the default checkout", () => withTempSupervisor(() => withTempProductRepos(({ roots, base }) => {
818
- // A typo'd override name dropped on the floor means the sweep reads the
819
- // DEFAULT checkout while printing ✓ on every line — success from the wrong repo.
820
- const receipts = syncPluginsFromRepos({ ...roots, regen_root: path.join(base, "typo-checkout") }, "test");
821
- const rejected = receipts.filter((entry) => entry.state === "unknown_repo_name");
822
- assert.equal(rejected.length, 1);
823
- assert.equal(rejected[0].repo, "regen_root");
824
- assert.equal(rejected[0].ok, false);
825
- assert.match(rejected[0].error, /unknown repo name/);
826
- assert.equal(SYNC_SKIP_STATES.includes("unknown_repo_name"), false, "a typo'd repo name must fail the sweep, not be skipped");
827
- assert.deepEqual([...SYNC_REPO_NAMES].sort(), ["rdc-skills", "regen-root"]);
828
- })));
829
-
830
- test("plugin sync never throws on a malformed repo root — the contract absence must not break", () => withTempSupervisor(() => withTempProductRepos(({ rdcSkills }) => {
831
- // path.resolve() throws on a non-string; doing that before the existence
832
- // guard aborted the whole sweep and lost every later repo's receipt.
833
- for (const badRoot of [123, " ", {}, [], true]) {
834
- let receipts;
835
- assert.doesNotThrow(() => {
836
- receipts = syncPluginsFromRepos({ "regen-root": badRoot, "rdc-skills": rdcSkills }, "test");
837
- }, `root ${JSON.stringify(badRoot)} must not throw`);
838
- assert.equal(receipts.length, 5, "every attempted manifest still yields a receipt");
839
- assert.equal(
840
- receipts.filter((entry) => entry.repo === "regen-root" && entry.state === "repo_root_unknown").length,
841
- 4,
842
- `root ${JSON.stringify(badRoot)} must be reported, not thrown`,
843
- );
844
- const skills = receipts.find((entry) => entry.repo === "rdc-skills");
845
- assert.equal(skills.ok, true, "a later repo must still be swept after a bad earlier root");
846
- }
847
- })));
848
-
849
- test("regression: scoped @scope/pkg and unscoped discovery both still work alongside deregister", () => withTempSupervisor((root) => {
850
- // Epic 63d4d778 WP-1 taught findManifestFiles to descend one extra level for
851
- // an @scope directory (node_modules/@lifeaitools/fs-mcp). Neither the sync
852
- // sweep nor deregisterPlugin may regress that.
853
- const managed = path.join(root, "managed");
854
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
855
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
856
- writePlugin(root, "managed", "plain-plugin", baseManifest("plain-plugin"));
857
- writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp"));
858
-
859
- const discovered = discoverPlugins();
860
- assert.ok(discovered.plugins.find((plugin) => plugin.id === "plain-plugin"), "unscoped layout still discovered");
861
- assert.ok(discovered.plugins.find((plugin) => plugin.id === "fs-mcp"), "scoped layout still discovered");
862
-
863
- // Deregistering the unscoped plugin must not disturb the scoped tree.
864
- const receipt = deregisterPlugin("plain-plugin", "test");
865
- assert.equal(receipt.resulting_state.ok, true);
866
- assert.equal(fs.existsSync(path.join(managed, "plain-plugin")), false);
867
- assert.equal(fs.existsSync(path.join(managed, "@lifeaitools", "fs-mcp", "clauth-plugin.json")), true, "scoped package must be untouched");
868
-
869
- const after = discoverPlugins();
870
- assert.ok(after.plugins.find((plugin) => plugin.id === "fs-mcp" && plugin.state !== "missing_default"), "scoped plugin still discoverable after a sibling deregister");
871
- }));
872
-
873
- test("tunnel route add and remove produce reversible operation receipts", () => withTempSupervisor((root) => {
874
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
875
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
876
- discoverPlugins();
877
-
878
- const added = addTunnelRoute("cf-main", {
879
- id: "route-1",
880
- hostname: "media.example.test",
881
- service_url: "http://127.0.0.1:3120",
882
- });
883
- assert.equal(added.resulting_state.ok, true);
884
- assert.equal(added.resulting_state.state, "route_recorded");
885
-
886
- const removed = removeTunnelRoute("cf-main", "route-1");
887
- assert.equal(removed.resulting_state.ok, true);
888
- assert.equal(removed.resulting_state.state, "route_removed");
889
- }));
890
-
891
- test("command arguments expand path tokens, not just cwd", () => {
892
- // Regression: expandPathToken() was applied ONLY to `cwd`, so a manifest that
893
- // named a root inside a COMMAND ARGUMENT shipped the literal string to the
894
- // shell. dev-center's restart is
895
- // ["pwsh","-NoProfile","-File","$LIFEAI_ENV/services/restart-dev-center.ps1"]
896
- // and pwsh answered "not recognized as the name of a script file" with exit
897
- // 64 -- a restart that fails while the service stays up, which reads as a
898
- // flaky action rather than an unresolved path.
899
- const oldEnv = process.env.LIFEAI_ENV;
900
- const oldRoot = process.env.REGEN_ROOT;
901
- process.env.LIFEAI_ENV = "C:/tmp/env-root";
902
- process.env.REGEN_ROOT = "C:/tmp/regen-root";
903
- try {
904
- const plugin = validatePluginManifest(baseManifest("token-expansion", {
905
- surfaces: [{
906
- id: "primary",
907
- destination: "local/clauth/pm2",
908
- lifecycle_owner: "clauth",
909
- port: 3003,
910
- health: "/health",
911
- restart: ["pwsh", "-NoProfile", "-File", "$LIFEAI_ENV/services/restart-dev-center.ps1"],
912
- start: ["node", "${REGEN_ROOT}/scripts/start.mjs"],
913
- }],
914
- }), "clauth-plugin.json");
915
- const s = plugin.surfaces[0];
916
- assert.ok(!s.restart.some((a) => a.includes("$LIFEAI_ENV")),
917
- `LIFEAI_ENV left unexpanded: ${JSON.stringify(s.restart)}`);
918
- assert.ok(s.restart.some((a) => a.includes("C:/tmp/env-root")),
919
- `LIFEAI_ENV did not expand to its value: ${JSON.stringify(s.restart)}`);
920
- assert.ok(s.start.some((a) => a.includes("C:/tmp/regen-root")),
921
- `REGEN_ROOT did not expand in a command arg: ${JSON.stringify(s.start)}`);
922
- } finally {
923
- if (oldEnv === undefined) delete process.env.LIFEAI_ENV; else process.env.LIFEAI_ENV = oldEnv;
924
- if (oldRoot === undefined) delete process.env.REGEN_ROOT; else process.env.REGEN_ROOT = oldRoot;
925
- }
926
- });
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
+ deregisterPlugin,
10
+ discoverPlugins,
11
+ getClauthPm2Home,
12
+ listPlugins,
13
+ listSurfaces,
14
+ reconcileSurfaceHealth,
15
+ registerPlugin,
16
+ runPluginAction,
17
+ runSurfaceAction,
18
+ removeTunnelRoute,
19
+ setPluginEnabled,
20
+ shellQuote,
21
+ supervisorHealth,
22
+ syncPluginsFromRepos,
23
+ SYNC_REPO_NAMES,
24
+ SYNC_SKIP_STATES,
25
+ validatePluginManifest,
26
+ } from "./supervisor-registry.js";
27
+ import { isLoopbackAddress, supervisorLogDto, supervisorRequiresWriteToken } from "./commands/serve.js";
28
+
29
+ function withTempSupervisor(fn) {
30
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-"));
31
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
32
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
33
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
34
+ const oldPm2 = process.env.CLAUTH_PM2_HOME;
35
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
36
+ // CLAUTH_MANAGED_PLUGIN_ROOTS / CLAUTH_USER_PLUGIN_ROOTS are set as PERSISTENT
37
+ // MACHINE-LEVEL env vars on developer boxes, pointing at the LIVE daemon's
38
+ // plugin directories. rootEntries() prefers them over CLAUTH_SUPERVISOR_DIR,
39
+ // so overriding the supervisor dir ALONE leaves registerPlugin/deregisterPlugin
40
+ // writing to (and deleting from) the live fleet. Pin all three to the temp
41
+ // root here so a test cannot reach the live daemon by forgetting to.
42
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed-plugins");
43
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user-plugins");
44
+ delete process.env.CLAUTH_PM2_HOME;
45
+ try {
46
+ return fn(root);
47
+ } finally {
48
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
49
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
50
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
51
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
52
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
53
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
54
+ if (oldPm2 === undefined) delete process.env.CLAUTH_PM2_HOME;
55
+ else process.env.CLAUTH_PM2_HOME = oldPm2;
56
+ fs.rmSync(root, { recursive: true, force: true });
57
+ }
58
+ }
59
+
60
+ function writePlugin(root, source, id, manifest) {
61
+ const dir = path.join(root, source, id);
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
64
+ return dir;
65
+ }
66
+
67
+ // A scoped npm package installs at <root>/@scope/pkg-name/clauth-plugin.json —
68
+ // two levels deep, mirroring node_modules/@lifeaitools/<pkg>/.
69
+ function writeScopedPlugin(root, source, scope, id, manifest) {
70
+ const dir = path.join(root, source, scope, id);
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ fs.writeFileSync(path.join(dir, "clauth-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
73
+ return dir;
74
+ }
75
+
76
+ function baseManifest(id, overrides = {}) {
77
+ return {
78
+ schema: "lifeai.plugin.v1",
79
+ id,
80
+ version: "1.0.0",
81
+ publisher: "LIFEAI",
82
+ credentials: [{ name: `${id}-secret`, key_type: "secret", description: "fixture key" }],
83
+ surfaces: [{
84
+ id: `${id}-surface`,
85
+ destination: "local/clauth/pm2",
86
+ lifecycle_owner: "clauth",
87
+ port: 39111,
88
+ health: "/health",
89
+ restart: ["node", "--version"],
90
+ }],
91
+ test: { command: ["node", "--version"], port: "auto", health: "/health", selfTest: [["node", "--version"]] },
92
+ documentation: {
93
+ architecture: "docs/systems/example/ARCHITECTURE.md",
94
+ operator_guide: "docs/systems/example/OPERATE.md",
95
+ tool_reference: ".claude/context/mcp-endpoint-design.md",
96
+ },
97
+ ...overrides,
98
+ };
99
+ }
100
+
101
+ test("validatePluginManifest accepts LIFEAI plugin contract with isolated test context", () => {
102
+ const plugin = validatePluginManifest(baseManifest("regen-media-local"), "clauth-plugin.json");
103
+ assert.equal(plugin.schema, "lifeai.plugin.v1");
104
+ assert.equal(plugin.test.port, "auto");
105
+ assert.equal(plugin.surfaces[0].destination, "local/clauth/pm2");
106
+ assert.equal(plugin.surfaces[0].lifecycle_owner, "clauth");
107
+ assert.equal(plugin.surfaces[0].health, "http://127.0.0.1:39111/health");
108
+ assert.equal(plugin.documentation.architecture, "docs/systems/example/ARCHITECTURE.md");
109
+ assert.equal(plugin.documentation.operator_guide, "docs/systems/example/OPERATE.md");
110
+ });
111
+
112
+ test("validatePluginManifest accepts empty test command arrays from the v1 template", () => {
113
+ const plugin = validatePluginManifest(baseManifest("empty-test-command", {
114
+ test: { command: [], port: "auto", health: "/health", selfTest: [] },
115
+ }), "clauth-plugin.json");
116
+ assert.deepEqual(plugin.test.command, []);
117
+ assert.equal(plugin.test.port, "auto");
118
+ });
119
+
120
+ test("shellQuote quotes a whitespace-bearing value only when shell is active, and is idempotent", () => {
121
+ assert.equal(shellQuote("pm2", true), "pm2");
122
+ assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", true), '"C:/Program Files/nodejs/node.exe"');
123
+ assert.equal(shellQuote("C:/Program Files/nodejs/node.exe", false), "C:/Program Files/nodejs/node.exe");
124
+ assert.equal(shellQuote("one two", false), "one two");
125
+ assert.equal(shellQuote('"already quoted value"', true), '"already quoted value"');
126
+ });
127
+
128
+ test("validatePluginManifest rejects shell metacharacters in command ARGS, not just command[0]", () => {
129
+ // The [;&|<>] guard on command[0] alone is defeated by moving the payload
130
+ // one index right — args must be validated the same way (verified live
131
+ // during code review: spawnSync(['cmd','/c','echo','A & echo INJECTED'],
132
+ // {shell:true}) executed a second command from inside one arg string).
133
+ assert.throws(() => validatePluginManifest(baseManifest("inject", {
134
+ surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "start svc & calc.exe"] }],
135
+ })), /shell syntax/);
136
+ assert.throws(() => validatePluginManifest(baseManifest("inject2", {
137
+ surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "name; rm -rf /"] }],
138
+ })), /shell syntax/);
139
+ // A legitimate arg with no shell metacharacters still passes.
140
+ const ok = validatePluginManifest(baseManifest("legit", {
141
+ surfaces: [{ id: "primary", lifecycle_owner: "clauth", restart: ["pm2", "restart", "my-app-name"] }],
142
+ }), "clauth-plugin.json");
143
+ assert.deepEqual(ok.surfaces[0].restart, ["pm2", "restart", "my-app-name"]);
144
+ });
145
+
146
+ test("validatePluginManifest rejects invalid manifests and non-local health URLs", () => {
147
+ assert.throws(() => validatePluginManifest({ ...baseManifest("bad"), schema: "bad" }), /schema/);
148
+ assert.throws(() => validatePluginManifest(baseManifest("bad", {
149
+ surfaces: [{ id: "bad", health: "https://example.com/health" }],
150
+ })), /localhost-only/);
151
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
152
+ documentation: { architecture: "../outside.md" },
153
+ })), /repository-relative/);
154
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
155
+ documentation: { operator_guide: "docs/guide.md" },
156
+ })), /documentation.architecture is required/);
157
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
158
+ documentation: { architecture: "https://example.com/architecture.md" },
159
+ })), /repository-relative/);
160
+ });
161
+
162
+ test("discovery merges managed and user roots without silent managed-id shadowing", () => withTempSupervisor((root) => {
163
+ const managed = path.join(root, "managed");
164
+ const user = path.join(root, "user");
165
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
166
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
167
+ writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
168
+ writePlugin(root, "user", "regen-media-local", baseManifest("regen-media-local", { version: "2.0.0" }));
169
+
170
+ const result = discoverPlugins();
171
+ const managedPlugin = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "managed");
172
+ const invalid = result.plugins.find((plugin) => plugin.id === "regen-media-local" && plugin.source === "user");
173
+ assert.equal(managedPlugin.state, "awaiting_enable");
174
+ assert.equal(invalid.state, "manifest_invalid");
175
+ assert.match(invalid.error, /shadow/);
176
+ }));
177
+
178
+ test("discovery finds both a scoped npm-package layout and an unscoped layout from one root", () => withTempSupervisor((root) => {
179
+ const managed = path.join(root, "managed");
180
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
181
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
182
+ // Unscoped: managed/plain-plugin/clauth-plugin.json (one level — existing behavior)
183
+ writePlugin(root, "managed", "plain-plugin", baseManifest("plain-plugin"));
184
+ // Scoped: managed/@lifeaitools/fs-mcp/clauth-plugin.json (two levels — the WP-1 fix)
185
+ writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp"));
186
+
187
+ const result = discoverPlugins();
188
+ const plain = result.plugins.find((plugin) => plugin.id === "plain-plugin");
189
+ const scoped = result.plugins.find((plugin) => plugin.id === "fs-mcp");
190
+ assert.ok(plain, "unscoped layout must still be discovered");
191
+ assert.equal(plain.state, "awaiting_enable");
192
+ assert.ok(scoped, "scoped @scope/pkg layout must now be discovered");
193
+ assert.equal(scoped.state, "awaiting_enable");
194
+ assert.equal(scoped.manifest_hash?.length > 0, true);
195
+ }));
196
+
197
+ test("discovery skips a malformed manifest without aborting the rest of the scan", () => withTempSupervisor((root) => {
198
+ const managed = path.join(root, "managed");
199
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
200
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
201
+ writePlugin(root, "managed", "good-plugin", baseManifest("good-plugin"));
202
+ const brokenDir = path.join(managed, "broken-plugin");
203
+ fs.mkdirSync(brokenDir, { recursive: true });
204
+ fs.writeFileSync(path.join(brokenDir, "clauth-plugin.json"), "{ this is not valid json", "utf8");
205
+ // Also prove a malformed SCOPED manifest doesn't abort the scoped-dir descent either.
206
+ const brokenScopedDir = path.join(managed, "@lifeaitools", "broken-scoped");
207
+ fs.mkdirSync(brokenScopedDir, { recursive: true });
208
+ fs.writeFileSync(path.join(brokenScopedDir, "clauth-plugin.json"), "{ also not valid json", "utf8");
209
+ writeScopedPlugin(root, "managed", "@lifeaitools", "good-scoped", baseManifest("good-scoped"));
210
+
211
+ const result = discoverPlugins();
212
+ const good = result.plugins.find((plugin) => plugin.id === "good-plugin");
213
+ const broken = result.plugins.find((plugin) => plugin.id === "broken-plugin");
214
+ const goodScoped = result.plugins.find((plugin) => plugin.id === "good-scoped");
215
+ const brokenScoped = result.plugins.find((plugin) => plugin.id === "broken-scoped");
216
+ assert.equal(good.state, "awaiting_enable");
217
+ assert.equal(broken.state, "manifest_invalid");
218
+ assert.equal(goodScoped.state, "awaiting_enable");
219
+ assert.equal(brokenScoped.state, "manifest_invalid");
220
+ }));
221
+
222
+ test("trusted managed core plugins auto-enable while user plugins remain opt-in", () => withTempSupervisor((root) => {
223
+ const managed = path.join(root, "managed");
224
+ const user = path.join(root, "user");
225
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
226
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
227
+ writePlugin(root, "managed", "core-mcp", baseManifest("core-mcp", { core: true, enable_default: true }));
228
+ writePlugin(root, "user", "user-mcp", baseManifest("user-mcp", { core: true, enable_default: true }));
229
+
230
+ const result = discoverPlugins();
231
+ const core = result.plugins.find((plugin) => plugin.id === "core-mcp");
232
+ const userPlugin = result.plugins.find((plugin) => plugin.id === "user-mcp");
233
+ assert.equal(core.enabled, true);
234
+ assert.equal(core.state, "current");
235
+ assert.equal(result.events.some((event) => event.kind === "core_plugin_auto_enabled" && event.plugin_id === "core-mcp"), true);
236
+ assert.equal(userPlugin.enabled, false);
237
+ assert.equal(userPlugin.state, "awaiting_enable");
238
+ }));
239
+
240
+ test("plugin test marks a private candidate and never creates a public route", () => withTempSupervisor((root) => {
241
+ const managed = path.join(root, "managed");
242
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
243
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
244
+ writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
245
+ discoverPlugins();
246
+
247
+ const enabled = setPluginEnabled("regen-media-local", true);
248
+ assert.equal(enabled.resulting_state.enabled, true);
249
+ const receipt = runPluginAction("regen-media-local", "test");
250
+ assert.equal(receipt.resulting_state.state, "candidate_testing");
251
+ assert.equal(receipt.resulting_state.public_route, false);
252
+ assert.equal(listPlugins().find((plugin) => plugin.id === "regen-media-local").candidate.public_route, false);
253
+ }));
254
+
255
+ test("surface actions use dedicated clauth PM2 home and keep CodeFlow observe-only", () => withTempSupervisor((root) => {
256
+ const managed = path.join(root, "managed");
257
+ process.env.REGEN_ROOT = "C:/Dev/regen-root";
258
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
259
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
260
+ writePlugin(root, "managed", "demo", baseManifest("demo", {
261
+ surfaces: [{
262
+ id: "demo-surface",
263
+ name: "Demo surface",
264
+ health: "http://127.0.0.1:3333/health",
265
+ cwd: "$REGEN_ROOT/mcp-servers/regen-media",
266
+ restart: [process.execPath, "--version"],
267
+ }],
268
+ }));
269
+ writePlugin(root, "managed", "codeflow", baseManifest("codeflow", {
270
+ surfaces: [{ id: "codeflow-mcp", lifecycle_owner: "clauth", destination: "local/clauth/pm2", restart: ["node", "--version"] }],
271
+ }));
272
+ discoverPlugins();
273
+ const receipt = runSurfaceAction("demo:demo-surface", "restart");
274
+ assert.equal(receipt.resulting_state.ok, true);
275
+ assert.match(receipt.resulting_state.evidence[0], /CLAUTH_PM2_HOME/);
276
+ assert.equal(receipt.prior_state.cwd.replace(/\\/g, "/"), "C:/Dev/regen-root/mcp-servers/regen-media");
277
+ assert.equal(getClauthPm2Home(), path.join(root, "pm2-home"));
278
+
279
+ const codeflow = runSurfaceAction("codeflow:codeflow-mcp", "restart");
280
+ assert.equal(codeflow.resulting_state.ok, false);
281
+ assert.equal(codeflow.resulting_state.reason, "codeflow_self_owned");
282
+ assert.equal(listSurfaces().length, 2);
283
+ assert.equal(supervisorHealth().surfaces, 2);
284
+ }));
285
+
286
+ test("reconcile falls back to the declared start command when restart reports a missing process", () => withTempSupervisor((root) => {
287
+ const managed = path.join(root, "managed");
288
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
289
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
290
+ writePlugin(root, "managed", "fallback-demo", baseManifest("fallback-demo", {
291
+ core: true,
292
+ enable_default: true,
293
+ surfaces: [{
294
+ id: "primary",
295
+ destination: "local/clauth/pm2",
296
+ lifecycle_owner: "clauth",
297
+ port: 39114,
298
+ health: "/health",
299
+ start: [process.execPath, "--version"],
300
+ restart: [process.execPath, "-e", "process.exit(1)"],
301
+ }],
302
+ }));
303
+ discoverPlugins();
304
+ const receipt = runSurfaceAction("fallback-demo:primary", "reconcile");
305
+ assert.equal(receipt.resulting_state.ok, true);
306
+ assert.equal(receipt.resulting_state.evidence.includes("reconcile_start_fallback=true"), true);
307
+ }));
308
+
309
+ test("surface promote and rollback never fall through to restart commands", () => withTempSupervisor((root) => {
310
+ const managed = path.join(root, "managed");
311
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
312
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
313
+ writePlugin(root, "managed", "demo", baseManifest("demo", {
314
+ surfaces: [{
315
+ id: "demo-surface",
316
+ name: "Demo surface",
317
+ health: "http://127.0.0.1:3333/health",
318
+ restart: [process.execPath, "--version"],
319
+ }],
320
+ }));
321
+ discoverPlugins();
322
+
323
+ for (const action of ["promote", "rollback"]) {
324
+ const receipt = runSurfaceAction("demo:demo-surface", action);
325
+ assert.equal(receipt.resulting_state.ok, false);
326
+ assert.equal(receipt.resulting_state.state, "unsupported_surface_action");
327
+ assert.equal(receipt.resulting_state.evidence[0], "surface action did not execute a process command");
328
+ }
329
+ }));
330
+
331
+ test("health reconciliation marks a failed clauth surface and repairs it through reconcile", async () => {
332
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-health-"));
333
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
334
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
335
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
336
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
337
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
338
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
339
+ try {
340
+ writePlugin(root, "managed", "health-demo", baseManifest("health-demo", {
341
+ core: true,
342
+ enable_default: true,
343
+ surfaces: [{
344
+ id: "primary",
345
+ destination: "local/clauth/pm2",
346
+ lifecycle_owner: "clauth",
347
+ port: 39111,
348
+ health: "/health",
349
+ restart: [process.execPath, "--version"],
350
+ }],
351
+ }));
352
+ discoverPlugins();
353
+ let healthCalls = 0;
354
+ const result = await reconcileSurfaceHealth({
355
+ fetchImpl: async () => ({ ok: ++healthCalls > 1, status: 503 }),
356
+ });
357
+ assert.equal(result.inspected[0].surface_id, "health-demo:primary");
358
+ assert.equal(result.inspected[0].state, "reconciled");
359
+ const surface = listSurfaces().find((item) => item.plugin_id === "health-demo");
360
+ assert.equal(surface.state, "current");
361
+ assert.ok(surface.last_reconcile_operation_id);
362
+ assert.equal(healthCalls, 2);
363
+ } finally {
364
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
365
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
366
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
367
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
368
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
369
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
370
+ fs.rmSync(root, { recursive: true, force: true });
371
+ }
372
+ });
373
+
374
+ test("health reconciliation does not claim current when a successful command leaves health down", async () => {
375
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-post-health-"));
376
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
377
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
378
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
379
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
380
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
381
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
382
+ try {
383
+ writePlugin(root, "managed", "post-health-demo", baseManifest("post-health-demo", {
384
+ core: true,
385
+ enable_default: true,
386
+ surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health", restart: [process.execPath, "--version"] }],
387
+ }));
388
+ discoverPlugins();
389
+ const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
390
+ assert.equal(result.inspected[0].state, "reconcile_failed");
391
+ assert.equal(listSurfaces()[0].state, "unavailable");
392
+ assert.equal(listSurfaces()[0].last_health_ok, false);
393
+ } finally {
394
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
395
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
396
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
397
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
398
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
399
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
400
+ fs.rmSync(root, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ test("health reconciliation never restarts external or plugin-owned surfaces", async () => {
405
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-observe-"));
406
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
407
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
408
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
409
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
410
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
411
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
412
+ try {
413
+ // Both fixtures are deliberately PROBE-ABLE — local destination + port, so
414
+ // healthUrlForSurface() yields a real URL for each. That is what gives this
415
+ // test teeth.
416
+ //
417
+ // An earlier revision made the external fixture `vultr/` with no port. It
418
+ // read as equivalent and was not: portless + relative health resolves to a
419
+ // null URL, so reconcileSurfaceHealth's `if (!url) continue` skipped it
420
+ // BEFORE the owner/destination guard was consulted. Delete the guard this
421
+ // test is named for and it still passed green — it proved the surface was
422
+ // unprobe-able, not that it was probe-able and correctly left alone.
423
+ //
424
+ // Now the ONLY thing that can exclude external-demo is lifecycle_owner, and
425
+ // clauth-owned-demo is the positive control proving the probe machinery
426
+ // actually runs against this fixture shape. If `inspected` ever comes back
427
+ // empty, the control failed and the negative result means nothing.
428
+ writePlugin(root, "managed", "external-demo", baseManifest("external-demo", {
429
+ core: true,
430
+ enable_default: true,
431
+ destination: "local/clauth/pm2",
432
+ lifecycle_owner: "external",
433
+ surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "external", port: 39112, health: "/health" }],
434
+ }));
435
+ writePlugin(root, "managed", "clauth-owned-demo", baseManifest("clauth-owned-demo", {
436
+ core: true,
437
+ enable_default: true,
438
+ destination: "local/clauth/pm2",
439
+ lifecycle_owner: "clauth",
440
+ surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health" }],
441
+ }));
442
+ discoverPlugins();
443
+ const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
444
+ const inspectedIds = result.inspected.map((s) => (typeof s === "string" ? s : s.surface_id));
445
+ // Positive control: the probe machinery ran at all.
446
+ assert.ok(inspectedIds.some((id) => String(id).includes("clauth-owned-demo")),
447
+ `control failed — clauth-owned surface was not probed, so "external was skipped" proves nothing (inspected: ${JSON.stringify(result.inspected)})`);
448
+ // The actual assertion: an externally-owned surface is never touched.
449
+ assert.ok(!inspectedIds.some((id) => String(id).includes("external-demo")),
450
+ `external-demo was probed — the lifecycle_owner guard is not excluding it (inspected: ${JSON.stringify(result.inspected)})`);
451
+ const external = listSurfaces().find((s) => s.plugin_id === "external-demo");
452
+ assert.equal(external.state, "current");
453
+ } finally {
454
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
455
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
456
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
457
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
458
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
459
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
460
+ fs.rmSync(root, { recursive: true, force: true });
461
+ }
462
+ });
463
+
464
+ test("supervisor write-token policy is temporarily relaxed only for the localhost supervisor port", () => {
465
+ assert.equal(supervisorRequiresWriteToken(52439, {}), false);
466
+ assert.equal(supervisorRequiresWriteToken(52439, { CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN: "1" }), true);
467
+ assert.equal(supervisorRequiresWriteToken(52437, {}), true);
468
+ assert.equal(isLoopbackAddress("127.0.0.1"), true);
469
+ assert.equal(isLoopbackAddress("::1"), true);
470
+ assert.equal(isLoopbackAddress("::ffff:127.0.0.1"), true);
471
+ assert.equal(isLoopbackAddress("192.168.1.25"), false);
472
+ });
473
+
474
+ test("supervisor log DTO strips raw operation payloads", () => {
475
+ const dto = supervisorLogDto({
476
+ ts: "2026-07-30T00:00:00.000Z",
477
+ kind: "operation",
478
+ operationId: "op-secret",
479
+ actor: "localhost",
480
+ action: "restart",
481
+ target: { surface_id: "demo", command: ["node", "--token=secret"] },
482
+ prior_state: { start: ["node", "--token=secret"], cwd: "C:/secrets" },
483
+ resulting_state: {
484
+ ok: false,
485
+ state: "operation_failed",
486
+ stderr: "SECRET=abc123",
487
+ evidence: ["CLAUTH_PM2_HOME=C:/safe"],
488
+ },
489
+ stderr: "SECRET=abc123",
490
+ completed_at: "2026-07-30T00:00:01.000Z",
491
+ });
492
+
493
+ const json = JSON.stringify(dto);
494
+ assert.equal(dto.operationId, "op-secret");
495
+ assert.equal(dto.target.surface_id, "demo");
496
+ assert.equal(dto.resulting_state.state, "operation_failed");
497
+ assert.equal(json.includes("prior_state"), false);
498
+ assert.equal(json.includes("stderr"), false);
499
+ assert.equal(json.includes("SECRET"), false);
500
+ assert.equal(json.includes("--token"), false);
501
+ });
502
+
503
+ test("registerPlugin validates, writes into the managed root, and discovers the plugin — idempotent on re-register", () => withTempSupervisor((root) => {
504
+ const managed = path.join(root, "managed");
505
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
506
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
507
+ const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-source-"));
508
+ const manifestPath = path.join(sourceDir, "clauth-plugin.json");
509
+ fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("registered-demo", { core: true, enable_default: true })), "utf8");
510
+
511
+ const first = registerPlugin(manifestPath, "test");
512
+ assert.equal(first.resulting_state.ok, true);
513
+ assert.equal(first.resulting_state.state, "registered");
514
+ assert.equal(first.resulting_state.plugin_state, "current");
515
+ const written = fs.existsSync(path.join(managed, "registered-demo", "clauth-plugin.json"));
516
+ assert.equal(written, true);
517
+ const found = listPlugins().find((plugin) => plugin.id === "registered-demo");
518
+ assert.equal(found.enabled, true);
519
+
520
+ const second = registerPlugin(manifestPath, "test");
521
+ assert.equal(second.resulting_state.state, "unchanged", "re-registering identical content must be a no-op, not a rewrite");
522
+
523
+ fs.rmSync(sourceDir, { recursive: true, force: true });
524
+ }));
525
+
526
+ test("registerPlugin rejects a plugin id that would escape the managed-plugins root", () => withTempSupervisor((root) => {
527
+ // Code-review finding (confidence 95, live PoC): manifest.id of ".." passed
528
+ // the old id regex (dot is in the allowed character class with no
529
+ // exclusion of all-dots forms) and path.join(managedRoot, "..") wrote
530
+ // clauth-plugin.json one level ABOVE the managed root. Two independent
531
+ // fixes now close this: the id regex rejects all-dots ids, and
532
+ // registerPlugin asserts containment at the write site so a future regex
533
+ // relaxation can't reopen it.
534
+ const managed = path.join(root, "managed");
535
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
536
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
537
+ const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-traversal-"));
538
+ const manifestPath = path.join(sourceDir, "clauth-plugin.json");
539
+ fs.writeFileSync(manifestPath, JSON.stringify({ schema: "lifeai.plugin.v1", id: "..", version: "1.0.0", publisher: "t", documentation: { architecture: "a.md" }, surfaces: [] }), "utf8");
540
+
541
+ const receipt = registerPlugin(manifestPath, "test");
542
+ assert.equal(receipt.resulting_state.ok, false);
543
+ assert.equal(receipt.resulting_state.state, "manifest_invalid");
544
+ const escapedPath = path.join(managed, "..", "clauth-plugin.json");
545
+ assert.equal(fs.existsSync(escapedPath), false, "must never write outside the managed-plugins root");
546
+
547
+ fs.rmSync(sourceDir, { recursive: true, force: true });
548
+ }));
549
+
550
+ test("validatePluginManifest rejects an all-dots plugin or surface id", () => {
551
+ assert.throws(() => validatePluginManifest(baseManifest("..", {}), "clauth-plugin.json"), /may not be all dots/);
552
+ assert.throws(() => validatePluginManifest(baseManifest("valid-id", {
553
+ surfaces: [{ id: ".", lifecycle_owner: "clauth" }],
554
+ }), "clauth-plugin.json"), /may not be all dots/);
555
+ });
556
+
557
+ test("registerPlugin rejects an invalid manifest without writing anything", () => withTempSupervisor((root) => {
558
+ const managed = path.join(root, "managed");
559
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
560
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
561
+ const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-bad-"));
562
+ const manifestPath = path.join(sourceDir, "clauth-plugin.json");
563
+ fs.writeFileSync(manifestPath, JSON.stringify({ schema: "wrong", id: "bad" }), "utf8");
564
+
565
+ const receipt = registerPlugin(manifestPath, "test");
566
+ assert.equal(receipt.resulting_state.ok, false);
567
+ assert.equal(receipt.resulting_state.state, "manifest_invalid");
568
+ assert.equal(fs.existsSync(path.join(managed, "bad")), false);
569
+
570
+ fs.rmSync(sourceDir, { recursive: true, force: true });
571
+ }));
572
+
573
+ // Builds throwaway product repos laid out like the real ones, so a sync sweep
574
+ // exercises the real relative manifest paths without reading a live checkout.
575
+ function withTempProductRepos(fn) {
576
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sync-repos-"));
577
+ const regenRoot = path.join(base, "regen-root");
578
+ const rdcSkills = path.join(base, "rdc-skills");
579
+ const writeManifest = (repoRoot, relPath, manifest) => {
580
+ const full = path.join(repoRoot, relPath);
581
+ fs.mkdirSync(path.dirname(full), { recursive: true });
582
+ fs.writeFileSync(full, typeof manifest === "string" ? manifest : `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
583
+ return full;
584
+ };
585
+ writeManifest(regenRoot, "packages/codeflow/clauth-plugin.json", baseManifest("codeflow-mcp"));
586
+ writeManifest(regenRoot, "apps/dev-center/clauth-plugin.json", baseManifest("dev-center"));
587
+ writeManifest(regenRoot, "mcp-servers/regen-media/clauth-plugin.json", baseManifest("regen-media"));
588
+ writeManifest(regenRoot, "mcp-servers/web-research/clauth-plugin.json", baseManifest("web-research"));
589
+ writeManifest(rdcSkills, "clauth-plugin.json", baseManifest("rdc-skills"));
590
+ try {
591
+ return fn({ base, regenRoot, rdcSkills, writeManifest, roots: { "regen-root": regenRoot, "rdc-skills": rdcSkills } });
592
+ } finally {
593
+ fs.rmSync(base, { recursive: true, force: true });
594
+ }
595
+ }
596
+
597
+ test("plugin sync inherits registerPlugin idempotence — a second sweep reports every manifest unchanged", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
598
+ const first = syncPluginsFromRepos(roots, "test");
599
+ assert.equal(first.length, 5, "one receipt per attempted manifest");
600
+ assert.deepEqual(
601
+ first.filter((entry) => !entry.ok).map((entry) => `${entry.repo}:${entry.state}`),
602
+ [],
603
+ "every manifest in a complete checkout must register",
604
+ );
605
+ assert.equal(first.every((entry) => entry.state === "registered"), true);
606
+
607
+ // registerPlugin sha256-compares before writing; sync must not defeat that by
608
+ // re-writing or re-hashing on its own.
609
+ const second = syncPluginsFromRepos(roots, "test");
610
+ assert.equal(second.length, 5);
611
+ assert.equal(second.every((entry) => entry.ok && entry.state === "unchanged"), true, "re-sweeping identical content must be a no-op");
612
+ })));
613
+
614
+ test("plugin sync warns and continues over a missing repo root instead of throwing", () => withTempSupervisor(() => withTempProductRepos(({ base, rdcSkills }) => {
615
+ // A box that never checked out regen-root must still sync what it does have.
616
+ const absent = path.join(base, "no-such-checkout");
617
+ assert.equal(fs.existsSync(absent), false);
618
+ let receipts;
619
+ assert.doesNotThrow(() => {
620
+ receipts = syncPluginsFromRepos({ "regen-root": absent, "rdc-skills": rdcSkills }, "test");
621
+ });
622
+ assert.equal(receipts.length, 5, "a skipped repo still yields a receipt per attempted manifest");
623
+ const missing = receipts.filter((entry) => entry.state === "repo_root_missing");
624
+ assert.equal(missing.length, 4, "all four regen-root manifests report the missing root");
625
+ assert.equal(missing.every((entry) => entry.ok === false), true);
626
+ const skills = receipts.find((entry) => entry.repo === "rdc-skills");
627
+ assert.equal(skills.ok, true);
628
+ assert.equal(skills.state, "registered");
629
+ assert.ok(listPlugins().find((plugin) => plugin.id === "rdc-skills"), "the reachable repo still registered");
630
+ })));
631
+
632
+ test("plugin sync registers the remaining manifests when one is malformed", () => withTempSupervisor(() => withTempProductRepos(({ regenRoot, roots, writeManifest }) => {
633
+ writeManifest(regenRoot, "apps/dev-center/clauth-plugin.json", "{ this is not valid json");
634
+
635
+ const receipts = syncPluginsFromRepos(roots, "test");
636
+ assert.equal(receipts.length, 5);
637
+ const bad = receipts.find((entry) => entry.path.includes("dev-center"));
638
+ assert.equal(bad.ok, false);
639
+ assert.equal(bad.state, "manifest_invalid");
640
+ const good = receipts.filter((entry) => entry !== bad);
641
+ assert.equal(good.length, 4);
642
+ assert.equal(good.every((entry) => entry.ok && entry.state === "registered"), true, "one bad manifest must not abort the sweep");
643
+ const ids = new Set(listPlugins().map((plugin) => plugin.id));
644
+ for (const id of ["codeflow-mcp", "regen-media", "web-research", "rdc-skills"]) {
645
+ assert.ok(ids.has(id), `${id} must still be registered`);
646
+ }
647
+ })));
648
+
649
+ test("deregisterPlugin removes only the named plugin and leaves siblings intact", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
650
+ syncPluginsFromRepos(roots, "test");
651
+ const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
652
+ assert.equal(fs.existsSync(path.join(managed, "web-research")), true);
653
+
654
+ const receipt = deregisterPlugin("web-research", "test");
655
+ assert.equal(receipt.resulting_state.ok, true);
656
+ assert.equal(receipt.resulting_state.state, "deregistered");
657
+ assert.equal(fs.existsSync(path.join(managed, "web-research")), false, "the named plugin directory is gone");
658
+
659
+ for (const sibling of ["codeflow-mcp", "dev-center", "regen-media", "rdc-skills"]) {
660
+ assert.equal(fs.existsSync(path.join(managed, sibling)), true, `${sibling} must survive`);
661
+ }
662
+ // discovery re-ran, so the removed managed plugin is reported missing, not current
663
+ const after = listPlugins().find((plugin) => plugin.id === "web-research");
664
+ assert.equal(after.state, "missing_default");
665
+ assert.equal(after.enabled, false);
666
+ })));
667
+
668
+ test("deregisterPlugin on an unregistered id is a safe no-op, not an error", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
669
+ syncPluginsFromRepos(roots, "test");
670
+ const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
671
+ const before = fs.readdirSync(managed).sort();
672
+
673
+ const receipt = deregisterPlugin("never-registered-plugin", "test");
674
+ assert.equal(receipt.resulting_state.ok, true, "a no-op is success, not failure");
675
+ assert.equal(receipt.resulting_state.state, "not_registered");
676
+ assert.deepEqual(fs.readdirSync(managed).sort(), before, "nothing else may be touched");
677
+ })));
678
+
679
+ test("deregisterPlugin rejects an id that would escape the managed-plugins root", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
680
+ // Mirrors the registerPlugin traversal test. The id here is raw CLI/HTTP
681
+ // input with no manifest validation upstream, and the operation DELETES
682
+ // recursively — so an id resolving to the root itself must reject too, which
683
+ // is where this is strictly stricter than registerPlugin's assertion.
684
+ syncPluginsFromRepos(roots, "test");
685
+ const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
686
+ const sentinel = path.join(managed, "..", "sentinel-outside-root.txt");
687
+ fs.writeFileSync(sentinel, "must survive", "utf8");
688
+ const before = fs.readdirSync(managed).sort();
689
+
690
+ const evilIds = [
691
+ "..", "../..", "../sentinel-outside-root.txt", "..\\..", "/etc", "", ".",
692
+ // These three are stopped by the CHARSET guard alone — the containment
693
+ // assert waves all of them through, because path.resolve() normalizes the
694
+ // `..` away and Windows re-anchors a drive-relative path, so each lands
695
+ // back INSIDE the root (measured during code review). They are pinned here
696
+ // so the two guards are independently tested and nobody "simplifies" the
697
+ // charset rule believing containment is a safety net for it.
698
+ "sub/../web-research",
699
+ "C:web-research",
700
+ "web-research::$DATA",
701
+ ];
702
+ for (const evil of evilIds) {
703
+ const receipt = deregisterPlugin(evil, "test");
704
+ assert.equal(receipt.resulting_state.ok, false, `id ${JSON.stringify(evil)} must be rejected`);
705
+ assert.equal(receipt.resulting_state.state, "invalid_plugin_id");
706
+ }
707
+ assert.equal(fs.existsSync(path.join(managed, "web-research")), true, "a charset-rejected alias must not have deleted the real plugin");
708
+
709
+ assert.equal(fs.existsSync(sentinel), true, "must never delete outside the managed-plugins root");
710
+ assert.equal(fs.existsSync(managed), true, "must never delete the managed-plugins root itself");
711
+ assert.deepEqual(fs.readdirSync(managed).sort(), before, "no registered plugin may be removed by a rejected id");
712
+ })));
713
+
714
+ test("validatePluginManifest rejects a port on any non-local destination, and keeps local ports", () => {
715
+ // A remote surface is reached by URL; its port is the deployment registry's
716
+ // fact. Copying it into the manifest creates a second home for one fact that
717
+ // then drifts (regen-media 3121 vs 3120, dev-center 3012 vs 3003). Worse, a
718
+ // ported remote surface makes localhostHealth() synthesize
719
+ // http://127.0.0.1:<port>/health for a service on another box, pointing the
720
+ // health reconciler at the wrong machine.
721
+ //
722
+ // The rule is keyed on the validated DESTINATION enum, never on the surface's
723
+ // free-text id/role — keying on the label was tried in review and failed both
724
+ // ways: renaming the surface bypassed it, and a local surface named "remote"
725
+ // was falsely rejected. Both directions are asserted below.
726
+ for (const destination of ["vultr/clauth/pm2", "coolify/clauth/docker"]) {
727
+ for (const port of [3110, "auto", 0]) {
728
+ assert.throws(() => validatePluginManifest(baseManifest("remote-port", {
729
+ surfaces: [{ id: "remote", destination, lifecycle_owner: "external", port }],
730
+ })), /must not declare a port/, `${destination} + port ${JSON.stringify(port)} must be rejected`);
731
+ }
732
+ }
733
+ // Renaming the surface must NOT bypass the rule — this is the bypass that
734
+ // keying on the `remote` label allowed.
735
+ for (const id of ["vultr", "prod", "primary", "local"]) {
736
+ assert.throws(() => validatePluginManifest(baseManifest("renamed-remote", {
737
+ surfaces: [{ id, destination: "vultr/clauth/pm2", lifecycle_owner: "external", port: 3110 }],
738
+ })), /must not declare a port/, `a non-local surface named "${id}" must still be rejected`);
739
+ }
740
+
741
+ // A remote surface WITHOUT a port is accepted — the shape all four shipped
742
+ // remote surfaces already use.
743
+ const remote = validatePluginManifest(baseManifest("remote-ok", {
744
+ surfaces: [{ id: "remote", destination: "vultr/clauth/pm2", lifecycle_owner: "external" }],
745
+ }), "clauth-plugin.json");
746
+ assert.equal(remote.surfaces[0].port, null);
747
+
748
+ // A LOCAL surface keeps its port on purpose — that port describes how the
749
+ // service runs on a developer box. This is not drift; do not "fix" it. A
750
+ // local surface merely NAMED "remote" is local, and must not be rejected.
751
+ for (const [id, destination] of [["local", "local/clauth/pm2"], ["primary", "local/clauth/pm2"], ["remote", "local/clauth/daemon"]]) {
752
+ const localSurface = validatePluginManifest(baseManifest(`${id}-ok`, {
753
+ surfaces: [{ id, destination, lifecycle_owner: "clauth", port: 3109, health: "/health" }],
754
+ }), "clauth-plugin.json");
755
+ assert.equal(localSurface.surfaces[0].port, 3109, `${destination} surface must keep its port`);
756
+ assert.equal(localSurface.surfaces[0].health, "http://127.0.0.1:3109/health");
757
+ }
758
+ });
759
+
760
+ test("deregisterPlugin removes a scoped @scope/pkg plugin instead of falsely reporting it absent", () => withTempSupervisor((root) => {
761
+ // A removal verb that reports ✓ while the plugin stays installed AND enabled
762
+ // is the one receipt this must never get wrong. The flat <root>/<id> probe
763
+ // misses the scoped layout entirely, so the location is resolved from the
764
+ // discovery_root/sourcePath discovery already records.
765
+ const managed = path.join(root, "managed");
766
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
767
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
768
+ writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp", { core: true, enable_default: true }));
769
+ discoverPlugins();
770
+ assert.equal(listPlugins().find((plugin) => plugin.id === "fs-mcp").enabled, true, "precondition: scoped plugin is enabled");
771
+
772
+ const receipt = deregisterPlugin("fs-mcp", "test");
773
+ assert.equal(receipt.resulting_state.state, "deregistered", "must not report not_registered for a scoped plugin");
774
+ assert.equal(receipt.resulting_state.ok, true);
775
+ assert.equal(fs.existsSync(path.join(managed, "@lifeaitools", "fs-mcp")), false, "the scoped package directory is actually gone");
776
+ const after = listPlugins().find((plugin) => plugin.id === "fs-mcp");
777
+ assert.equal(after.enabled, false, "a deregistered plugin must not remain enabled");
778
+ assert.equal(after.state, "missing_default");
779
+ }));
780
+
781
+ test("deregisterPlugin finds a plugin in a non-first managed root and refuses a user-root plugin", () => withTempSupervisor((root) => {
782
+ // CLAUTH_MANAGED_PLUGIN_ROOTS is a path-delimited LIST; honoring only the
783
+ // first entry silently reports a real plugin as absent.
784
+ const managedA = path.join(root, "managed-a");
785
+ const managedB = path.join(root, "managed-b");
786
+ const user = path.join(root, "user");
787
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = [managedA, managedB].join(path.delimiter);
788
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
789
+ writePlugin(root, "managed-b", "second-root-plugin", baseManifest("second-root-plugin"));
790
+ writePlugin(root, "user", "user-only-plugin", baseManifest("user-only-plugin"));
791
+ discoverPlugins();
792
+
793
+ const second = deregisterPlugin("second-root-plugin", "test");
794
+ assert.equal(second.resulting_state.state, "deregistered", "a plugin in the 2nd managed root must be found");
795
+ assert.equal(fs.existsSync(path.join(managedB, "second-root-plugin")), false);
796
+
797
+ // A user-root plugin is out of this verb's remit — refuse it explicitly
798
+ // rather than reporting a green "not_registered" that implies it is gone.
799
+ const userReceipt = deregisterPlugin("user-only-plugin", "test");
800
+ assert.equal(userReceipt.resulting_state.ok, false);
801
+ assert.equal(userReceipt.resulting_state.state, "not_managed");
802
+ assert.equal(fs.existsSync(path.join(user, "user-only-plugin")), true, "the user plugin must be left intact");
803
+ }));
804
+
805
+ test("deregisterPlugin --dry-run resolves the target without deleting it", () => withTempSupervisor(() => withTempProductRepos(({ roots }) => {
806
+ syncPluginsFromRepos(roots, "test");
807
+ const managed = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
808
+
809
+ const receipt = deregisterPlugin("web-research", "test", { dryRun: true });
810
+ assert.equal(receipt.resulting_state.ok, true);
811
+ assert.equal(receipt.resulting_state.state, "would_deregister");
812
+ assert.equal(receipt.resulting_state.target_dir, path.resolve(managed, "web-research"));
813
+ assert.equal(fs.existsSync(path.join(managed, "web-research")), true, "a dry run must not delete anything");
814
+ assert.equal(listPlugins().find((plugin) => plugin.id === "web-research").state !== "missing_default", true);
815
+ })));
816
+
817
+ test("plugin sync reports an unknown repo-root override instead of silently sweeping the default checkout", () => withTempSupervisor(() => withTempProductRepos(({ roots, base }) => {
818
+ // A typo'd override name dropped on the floor means the sweep reads the
819
+ // DEFAULT checkout while printing ✓ on every line — success from the wrong repo.
820
+ const receipts = syncPluginsFromRepos({ ...roots, regen_root: path.join(base, "typo-checkout") }, "test");
821
+ const rejected = receipts.filter((entry) => entry.state === "unknown_repo_name");
822
+ assert.equal(rejected.length, 1);
823
+ assert.equal(rejected[0].repo, "regen_root");
824
+ assert.equal(rejected[0].ok, false);
825
+ assert.match(rejected[0].error, /unknown repo name/);
826
+ assert.equal(SYNC_SKIP_STATES.includes("unknown_repo_name"), false, "a typo'd repo name must fail the sweep, not be skipped");
827
+ assert.deepEqual([...SYNC_REPO_NAMES].sort(), ["rdc-skills", "regen-root"]);
828
+ })));
829
+
830
+ test("plugin sync never throws on a malformed repo root — the contract absence must not break", () => withTempSupervisor(() => withTempProductRepos(({ rdcSkills }) => {
831
+ // path.resolve() throws on a non-string; doing that before the existence
832
+ // guard aborted the whole sweep and lost every later repo's receipt.
833
+ for (const badRoot of [123, " ", {}, [], true]) {
834
+ let receipts;
835
+ assert.doesNotThrow(() => {
836
+ receipts = syncPluginsFromRepos({ "regen-root": badRoot, "rdc-skills": rdcSkills }, "test");
837
+ }, `root ${JSON.stringify(badRoot)} must not throw`);
838
+ assert.equal(receipts.length, 5, "every attempted manifest still yields a receipt");
839
+ assert.equal(
840
+ receipts.filter((entry) => entry.repo === "regen-root" && entry.state === "repo_root_unknown").length,
841
+ 4,
842
+ `root ${JSON.stringify(badRoot)} must be reported, not thrown`,
843
+ );
844
+ const skills = receipts.find((entry) => entry.repo === "rdc-skills");
845
+ assert.equal(skills.ok, true, "a later repo must still be swept after a bad earlier root");
846
+ }
847
+ })));
848
+
849
+ test("regression: scoped @scope/pkg and unscoped discovery both still work alongside deregister", () => withTempSupervisor((root) => {
850
+ // Epic 63d4d778 WP-1 taught findManifestFiles to descend one extra level for
851
+ // an @scope directory (node_modules/@lifeaitools/fs-mcp). Neither the sync
852
+ // sweep nor deregisterPlugin may regress that.
853
+ const managed = path.join(root, "managed");
854
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
855
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
856
+ writePlugin(root, "managed", "plain-plugin", baseManifest("plain-plugin"));
857
+ writeScopedPlugin(root, "managed", "@lifeaitools", "fs-mcp", baseManifest("fs-mcp"));
858
+
859
+ const discovered = discoverPlugins();
860
+ assert.ok(discovered.plugins.find((plugin) => plugin.id === "plain-plugin"), "unscoped layout still discovered");
861
+ assert.ok(discovered.plugins.find((plugin) => plugin.id === "fs-mcp"), "scoped layout still discovered");
862
+
863
+ // Deregistering the unscoped plugin must not disturb the scoped tree.
864
+ const receipt = deregisterPlugin("plain-plugin", "test");
865
+ assert.equal(receipt.resulting_state.ok, true);
866
+ assert.equal(fs.existsSync(path.join(managed, "plain-plugin")), false);
867
+ assert.equal(fs.existsSync(path.join(managed, "@lifeaitools", "fs-mcp", "clauth-plugin.json")), true, "scoped package must be untouched");
868
+
869
+ const after = discoverPlugins();
870
+ assert.ok(after.plugins.find((plugin) => plugin.id === "fs-mcp" && plugin.state !== "missing_default"), "scoped plugin still discoverable after a sibling deregister");
871
+ }));
872
+
873
+ test("tunnel route add and remove produce reversible operation receipts", () => withTempSupervisor((root) => {
874
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
875
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
876
+ discoverPlugins();
877
+
878
+ const added = addTunnelRoute("cf-main", {
879
+ id: "route-1",
880
+ hostname: "media.example.test",
881
+ service_url: "http://127.0.0.1:3120",
882
+ });
883
+ assert.equal(added.resulting_state.ok, true);
884
+ assert.equal(added.resulting_state.state, "route_recorded");
885
+
886
+ const removed = removeTunnelRoute("cf-main", "route-1");
887
+ assert.equal(removed.resulting_state.ok, true);
888
+ assert.equal(removed.resulting_state.state, "route_removed");
889
+ }));
890
+
891
+ test("command arguments expand path tokens, not just cwd", () => {
892
+ // Regression: expandPathToken() was applied ONLY to `cwd`, so a manifest that
893
+ // named a root inside a COMMAND ARGUMENT shipped the literal string to the
894
+ // shell. dev-center's restart is
895
+ // ["pwsh","-NoProfile","-File","$LIFEAI_ENV/services/restart-dev-center.ps1"]
896
+ // and pwsh answered "not recognized as the name of a script file" with exit
897
+ // 64 -- a restart that fails while the service stays up, which reads as a
898
+ // flaky action rather than an unresolved path.
899
+ const oldEnv = process.env.LIFEAI_ENV;
900
+ const oldRoot = process.env.REGEN_ROOT;
901
+ process.env.LIFEAI_ENV = "C:/tmp/env-root";
902
+ process.env.REGEN_ROOT = "C:/tmp/regen-root";
903
+ try {
904
+ const plugin = validatePluginManifest(baseManifest("token-expansion", {
905
+ surfaces: [{
906
+ id: "primary",
907
+ destination: "local/clauth/pm2",
908
+ lifecycle_owner: "clauth",
909
+ port: 3003,
910
+ health: "/health",
911
+ restart: ["pwsh", "-NoProfile", "-File", "$LIFEAI_ENV/services/restart-dev-center.ps1"],
912
+ start: ["node", "${REGEN_ROOT}/scripts/start.mjs"],
913
+ }],
914
+ }), "clauth-plugin.json");
915
+ const s = plugin.surfaces[0];
916
+ assert.ok(!s.restart.some((a) => a.includes("$LIFEAI_ENV")),
917
+ `LIFEAI_ENV left unexpanded: ${JSON.stringify(s.restart)}`);
918
+ assert.ok(s.restart.some((a) => a.includes("C:/tmp/env-root")),
919
+ `LIFEAI_ENV did not expand to its value: ${JSON.stringify(s.restart)}`);
920
+ assert.ok(s.start.some((a) => a.includes("C:/tmp/regen-root")),
921
+ `REGEN_ROOT did not expand in a command arg: ${JSON.stringify(s.start)}`);
922
+ } finally {
923
+ if (oldEnv === undefined) delete process.env.LIFEAI_ENV; else process.env.LIFEAI_ENV = oldEnv;
924
+ if (oldRoot === undefined) delete process.env.REGEN_ROOT; else process.env.REGEN_ROOT = oldRoot;
925
+ }
926
+ });
927
+
928
+ test("registerPlugin resolves ${PACKAGE_ROOT} to the manifest's real origin", () => withTempSupervisor((root) => {
929
+ // An npm package installs at node_modules/@scope/name/ — a path no shipped
930
+ // manifest can hardcode. registerPlugin COPIES the manifest into the managed
931
+ // root, so by the time discoverPlugins() re-reads it the true origin is gone;
932
+ // the token therefore has to be resolved at registration and baked in.
933
+ //
934
+ // Without it, rdc-skills shipped cwd "C:/Dev/rdc-skills" with core +
935
+ // enable_default, so `npm i -g` on any other machine auto-enabled a CORE
936
+ // plugin pointing at a directory that does not exist there.
937
+ const origin = path.join(root, "pretend-node-modules", "@lifeaitools", "some-mcp");
938
+ fs.mkdirSync(origin, { recursive: true });
939
+ const manifestPath = path.join(origin, "clauth-plugin.json");
940
+ fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("package-root-demo", {
941
+ surfaces: [{
942
+ id: "local",
943
+ destination: "local/clauth/pm2",
944
+ lifecycle_owner: "clauth",
945
+ port: 39114,
946
+ health: "/health",
947
+ cwd: "${PACKAGE_ROOT}",
948
+ start: ["node", "${PACKAGE_ROOT}/bin/server.mjs"],
949
+ }],
950
+ }), null, 2), "utf8");
951
+
952
+ const receipt = registerPlugin(manifestPath, "test");
953
+ assert.equal(receipt.resulting_state.ok, true, JSON.stringify(receipt.resulting_state));
954
+
955
+ const surface = listSurfaces().find((s) => s.plugin_id === "package-root-demo");
956
+ assert.ok(surface, "surface was not registered");
957
+ const expected = origin.replaceAll("\\", "/");
958
+ assert.equal(surface.cwd.replaceAll("\\", "/"), expected,
959
+ `cwd did not resolve to the manifest origin: ${surface.cwd}`);
960
+ assert.ok(surface.start.some((a) => a.replaceAll("\\", "/").includes(expected)),
961
+ `command arg did not resolve to the manifest origin: ${JSON.stringify(surface.start)}`);
962
+ assert.ok(!JSON.stringify(surface).includes("PACKAGE_ROOT"),
963
+ "an unexpanded PACKAGE_ROOT token survived into registered state");
964
+ }));