@lifeaitools/clauth 2.15.3 → 2.15.5

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.
@@ -3,8 +3,6 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
- import { spawn } from "node:child_process";
7
- import { pathToFileURL } from "node:url";
8
6
 
9
7
  import {
10
8
  addTunnelRoute,
@@ -16,20 +14,18 @@ import {
16
14
  listSurfaces,
17
15
  probeAllSurfaceHealth,
18
16
  surfaceOpenUrl,
19
- isGenuinelyIsolatedInstance,
20
17
  reconcileSurfaceHealth,
21
18
  registerPlugin,
22
- resolveIsolatedSupervisorDir,
23
19
  runPluginAction,
24
20
  runSurfaceAction,
25
21
  removeTunnelRoute,
22
+ setPluginEnabled,
26
23
  shellQuote,
27
24
  supervisorHealth,
28
25
  syncPluginsFromRepos,
29
26
  SYNC_REPO_NAMES,
30
27
  SYNC_SKIP_STATES,
31
28
  validatePluginManifest,
32
- withStateLock,
33
29
  } from "./supervisor-registry.js";
34
30
  import { isLoopbackAddress, supervisorLogDto, supervisorRequiresWriteToken } from "./commands/serve.js";
35
31
 
@@ -296,6 +292,8 @@ test("plugin test marks a private candidate and never creates a public route", (
296
292
  writePlugin(root, "managed", "regen-media-local", baseManifest("regen-media-local"));
297
293
  discoverPlugins();
298
294
 
295
+ const enabled = setPluginEnabled("regen-media-local", true);
296
+ assert.equal(enabled.resulting_state.enabled, true);
299
297
  const receipt = runPluginAction("regen-media-local", "test");
300
298
  assert.equal(receipt.resulting_state.state, "candidate_testing");
301
299
  assert.equal(receipt.resulting_state.public_route, false);
@@ -382,118 +380,6 @@ test("surface promotion requires the atomic health-checked path and rolls back o
382
380
  assert.equal(rolledBack.resulting_state.rollback_ok, true);
383
381
  }));
384
382
 
385
- // rdc:review finding (2026-09-02): a healthy promotion always reported
386
- // evidence "plugin_enabled=true" even when no plugin in state.json matched
387
- // surface.plugin_id -- the enabled write silently no-op'd (Array.map found no
388
- // match) while the receipt claimed success. Prove the receipt now tells the
389
- // truth in that case, and still reports the honest positive when a match
390
- // exists.
391
- test("surface promotion reports honestly whether the enabled write actually matched a plugin in state", () => withTempSupervisor(async (root) => {
392
- const managed = path.join(root, "managed");
393
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
394
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
395
- writePlugin(root, "managed", "demo", baseManifest("demo", {
396
- surfaces: [{
397
- id: "demo-surface",
398
- name: "Demo surface",
399
- health: "http://127.0.0.1:3333/health",
400
- restart: [process.execPath, "--version"],
401
- promote: [process.execPath, "--version"],
402
- rollback: [process.execPath, "--version"],
403
- }],
404
- }));
405
- discoverPlugins();
406
- const { runSurfacePromotion } = await import("./supervisor-registry.js");
407
-
408
- const matched = await runSurfacePromotion("demo:demo-surface", "test", { fetchImpl: async () => ({ ok: true }) });
409
- assert.equal(matched.resulting_state.state, "promotion_healthy");
410
- assert.equal(matched.resulting_state.evidence.includes("plugin_enabled=true"), true);
411
-
412
- // Remove the plugin from state.json entirely, out from under the surface
413
- // that still resolves fine via the manifest/registry -- reproduces the gap:
414
- // promote can run against a surface whose plugin no longer has a state.json
415
- // row (e.g. removed by a concurrent deregister).
416
- const statePath = path.join(process.env.CLAUTH_SUPERVISOR_DIR, "state.json");
417
- const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
418
- state.plugins = (state.plugins || []).filter((p) => p.id !== "demo");
419
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
420
-
421
- const unmatched = await runSurfacePromotion("demo:demo-surface", "test", { fetchImpl: async () => ({ ok: true }) });
422
- assert.equal(unmatched.resulting_state.state, "promotion_healthy");
423
- assert.equal(unmatched.resulting_state.evidence.includes("plugin_enabled=false plugin_not_found_in_state"), true);
424
- assert.equal(unmatched.resulting_state.evidence.includes("plugin_enabled=true"), false);
425
- }));
426
-
427
- // rdc:review finding (2026-09-02), second pass -- CONFIRMED regression in the
428
- // first version of the withStateLock fix: runSurfacePromotion snapshotted the
429
- // whole `surfaces` array once at function entry and wrote it back WHOLESALE
430
- // on its pre-action swap and post-action revert. A concurrent write to a
431
- // DIFFERENT surface (e.g. reconcileSurfaceHealth's health update) landing
432
- // during promotion's own await gap (the health probe) was silently
433
- // discarded the moment promotion's revert-write landed -- reopening, in a
434
- // different shape, the exact class of race withStateLock was written to
435
- // close. Fixed by routing every surface mutation in runSurfacePromotion
436
- // through updateSurfaceState() (a single-entity merge-patch) instead of a
437
- // whole-array snapshot/restore. This test reproduces the exact interleaving:
438
- // a promotion paused mid-health-probe, a concurrent reconcile completing a
439
- // write to an UNRELATED surface during that pause, then the promotion
440
- // finishing -- and asserts the concurrent write survives.
441
- test("surface promotion does not clobber a concurrent write to a DIFFERENT surface made during its health-probe pause", () => withTempSupervisor(async (root) => {
442
- const managed = path.join(root, "managed");
443
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
444
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
445
- writePlugin(root, "managed", "demo", baseManifest("demo", {
446
- core: true,
447
- enable_default: true,
448
- surfaces: [
449
- {
450
- id: "demo-surface",
451
- name: "Demo surface",
452
- health: "http://127.0.0.1:3333/health",
453
- restart: [process.execPath, "--version"],
454
- promote: [process.execPath, "--version"],
455
- rollback: [process.execPath, "--version"],
456
- },
457
- {
458
- id: "other-surface",
459
- name: "Other surface",
460
- destination: "local/clauth/pm2",
461
- lifecycle_owner: "clauth",
462
- port: 39115,
463
- health: "/health",
464
- restart: [process.execPath, "--version"],
465
- },
466
- ],
467
- }));
468
- discoverPlugins();
469
- const { runSurfacePromotion } = await import("./supervisor-registry.js");
470
-
471
- let releasePromotionHealthGate;
472
- const promotionHealthGate = new Promise((resolve) => { releasePromotionHealthGate = resolve; });
473
- const promotionPromise = runSurfacePromotion("demo:demo-surface", "test", {
474
- fetchImpl: async () => { await promotionHealthGate; return { ok: true }; },
475
- });
476
-
477
- // Give the promotion's own microtask chain room to reach its health-probe
478
- // await (its pre-action swap + runSurfaceAction already completed
479
- // synchronously by this point; it is now genuinely blocked on the gate).
480
- await new Promise((resolve) => setImmediate(resolve));
481
- await new Promise((resolve) => setImmediate(resolve));
482
-
483
- // Concurrent write to a DIFFERENT surface, completing entirely while the
484
- // promotion above is still paused.
485
- await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: true }) });
486
- const otherAfterReconcile = listSurfaces().find((s) => s.id === "other-surface");
487
- assert.equal(otherAfterReconcile.last_health_ok, true, "reconcile's own write should have landed before the promotion resumes");
488
-
489
- releasePromotionHealthGate();
490
- const result = await promotionPromise;
491
- assert.equal(result.resulting_state.state, "promotion_healthy");
492
-
493
- const otherAfterPromotion = listSurfaces().find((s) => s.id === "other-surface");
494
- assert.equal(otherAfterPromotion.last_health_ok, true, "promotion's own surface writes must not revert a concurrent write to a different surface");
495
- }));
496
-
497
383
  test("health reconciliation marks a failed clauth surface and repairs it through reconcile", async () => {
498
384
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-health-"));
499
385
  const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
@@ -797,37 +683,6 @@ test("registerPlugin validates, writes into the managed root, and discovers the
797
683
  fs.rmSync(sourceDir, { recursive: true, force: true });
798
684
  }));
799
685
 
800
- test("registerPlugin restarts a clauth-owned surface when the manifest actually changed, and skips the restart when unchanged", () => withTempSupervisor((root) => {
801
- // Dave: "plugin install pings clauth to reread -- it should restart the pm2
802
- // -- fix the bug". A changed manifest (a version bump, a fresh npm install)
803
- // used to only re-run discovery -- the live PM2 process kept serving the
804
- // OLD code until something unrelated happened to restart it.
805
- const managed = path.join(root, "managed");
806
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
807
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
808
- const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-register-restart-"));
809
- const manifestPath = path.join(sourceDir, "clauth-plugin.json");
810
- fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("restart-on-change-demo")), "utf8");
811
-
812
- const first = registerPlugin(manifestPath, "test");
813
- assert.equal(first.resulting_state.state, "registered");
814
- assert.equal(first.resulting_state.restarted.length, 1, "a fresh registration must attempt to restart its clauth-owned surface");
815
- assert.equal(first.resulting_state.restarted[0].surface_id, "restart-on-change-demo-surface");
816
- assert.equal(first.resulting_state.restarted[0].ok, true, "the fixture's restart command (node --version) must succeed");
817
-
818
- const unchanged = registerPlugin(manifestPath, "test");
819
- assert.equal(unchanged.resulting_state.state, "unchanged");
820
- assert.equal(unchanged.resulting_state.restarted.length, 0, "re-registering identical content must NOT restart a live service for nothing");
821
-
822
- fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("restart-on-change-demo", { version: "1.0.1" })), "utf8");
823
- const changed = registerPlugin(manifestPath, "test");
824
- assert.equal(changed.resulting_state.state, "registered");
825
- assert.equal(changed.resulting_state.restarted.length, 1, "a genuine content change must restart the surface again");
826
- assert.equal(changed.resulting_state.restarted[0].ok, true);
827
-
828
- fs.rmSync(sourceDir, { recursive: true, force: true });
829
- }));
830
-
831
686
  test("registerPlugin rejects a plugin id that would escape the managed-plugins root", () => withTempSupervisor((root) => {
832
687
  // Code-review finding (confidence 95, live PoC): manifest.id of ".." passed
833
688
  // the old id regex (dot is in the allowed character class with no
@@ -859,75 +714,6 @@ test("validatePluginManifest rejects an all-dots plugin or surface id", () => {
859
714
  }), "clauth-plugin.json"), /may not be all dots/);
860
715
  });
861
716
 
862
- // rdc:review finding (2026-09-02): the charset regex validatePluginManifest()
863
- // uses is not sufficient on Windows -- CON/NUL/AUX/PRN/COM1-9/LPT1-9 pass the
864
- // charset check but are OS-reserved device names, so mkdirSync/writeFileSync
865
- // against a path ending in one throws or targets the device instead of a real
866
- // directory. Covers both call sites inside validatePluginManifest (manifest.id
867
- // via normalizePlugin, surface.id via normalizeSurface) plus deregisterPlugin's
868
- // own duplicated id check, all three of which are meant to be kept in lockstep.
869
- // rdc:review finding (2026-09-02): the two RESERVED_DEVICE_NAMES copies
870
- // (here, and standalone/install-clauth-plugin.mjs) are asserted "kept in
871
- // sync deliberately" by comment alone -- no test compared the two literal
872
- // regex sources, so a future edit to one gives no mechanical signal the
873
- // other also needs it. Reads both files as TEXT (not import -- the
874
- // standalone script does real fs/network work at module-load time from its
875
- // own cwd, which a test must not trigger) and compares the literal regex
876
- // source string.
877
- test("RESERVED_DEVICE_NAMES stays byte-identical between supervisor-registry.js and the standalone installer", () => {
878
- const extract = (filePath) => {
879
- const src = fs.readFileSync(filePath, "utf8");
880
- const match = src.match(/const RESERVED_DEVICE_NAMES = (\/.*\/i);/);
881
- assert.ok(match, `RESERVED_DEVICE_NAMES declaration not found in ${filePath}`);
882
- return match[1];
883
- };
884
- const inRegistry = extract(path.join(process.cwd(), "cli", "supervisor-registry.js"));
885
- const inInstaller = extract(path.join(process.cwd(), "standalone", "install-clauth-plugin.mjs"));
886
- assert.equal(inRegistry, inInstaller, "the two RESERVED_DEVICE_NAMES copies have drifted apart");
887
- });
888
-
889
- test("validatePluginManifest rejects Windows-reserved device names, case-insensitively, at registration", () => {
890
- for (const reserved of ["CON", "nul", "Aux", "prn", "COM1", "lpt9"]) {
891
- assert.throws(
892
- () => validatePluginManifest(baseManifest(reserved, {}), "clauth-plugin.json"),
893
- /reserved device name/,
894
- `manifest.id=${reserved} should be rejected`,
895
- );
896
- assert.throws(
897
- () => validatePluginManifest(baseManifest("valid-id", {
898
- surfaces: [{ id: reserved, lifecycle_owner: "clauth" }],
899
- }), "clauth-plugin.json"),
900
- /reserved device name/,
901
- `surface.id=${reserved} should be rejected`,
902
- );
903
- }
904
- // Names that merely CONTAIN a reserved token are fine -- only an exact
905
- // (case-insensitive) match to the whole id is a real device name.
906
- assert.doesNotThrow(() => validatePluginManifest(baseManifest("nully-plugin", {}), "clauth-plugin.json"));
907
- assert.doesNotThrow(() => validatePluginManifest(baseManifest("console-app", {}), "clauth-plugin.json"));
908
- });
909
-
910
- // rdc:review finding (2026-09-02), confirmed regression + fix: the first
911
- // version of this check also rejected reserved-name ids in deregisterPlugin's
912
- // own Guard 1 -- meaning a plugin that somehow got registered with such an id
913
- // (pre-upgrade, or by any other path) could never be removed again, under
914
- // force:true included, with no remediation. Live-probed the original crash
915
- // premise directly on this host (Node fs.mkdirSync/writeFileSync for
916
- // directories literally named con/nul/aux/prn/com1/lpt1 all succeeded, no
917
- // throw, no device redirection) -- the reserved-name check is retained as
918
- // low-cost defensive hygiene against NEW registrations only; removal must
919
- // never be blocked by it.
920
- test("deregisterPlugin does not reject a Windows-reserved-device-name id -- removal is never blocked by this check", () => withTempSupervisor((root) => {
921
- const managed = path.join(root, "managed");
922
- process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
923
- process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
924
- const receipt = deregisterPlugin("NUL", "test");
925
- assert.notEqual(receipt.resulting_state.state, "invalid_plugin_id");
926
- // No such plugin is registered in this fixture -- safe no-op, same
927
- // contract as "deregisterPlugin on an unregistered id is a safe no-op".
928
- assert.equal(receipt.resulting_state.ok, true);
929
- }));
930
-
931
717
  test("registerPlugin rejects an invalid manifest without writing anything", () => withTempSupervisor((root) => {
932
718
  const managed = path.join(root, "managed");
933
719
  process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
@@ -944,16 +730,8 @@ test("registerPlugin rejects an invalid manifest without writing anything", () =
944
730
  fs.rmSync(sourceDir, { recursive: true, force: true });
945
731
  }));
946
732
 
947
- // Builds a throwaway product repo laid out like the real one, so a sync sweep
733
+ // Builds throwaway product repos laid out like the real ones, so a sync sweep
948
734
  // exercises the real relative manifest paths without reading a live checkout.
949
- //
950
- // rdc-skills is deliberately NOT part of PRODUCT_REPO_MANIFESTS (and so not
951
- // part of `roots` below) -- it is npm-published and self-registers via its
952
- // own postinstall against the INSTALLED package, never via this checkout
953
- // sweep (see the comment on PRODUCT_REPO_MANIFESTS). `rdcSkills` is still
954
- // returned as a plain unrelated directory: several tests below use it purely
955
- // as a stand-in for "some directory that is not a known repo name", to prove
956
- // an unrecognized override is reported rather than silently dropped.
957
735
  function withTempProductRepos(fn) {
958
736
  const base = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sync-repos-"));
959
737
  const regenRoot = path.join(base, "regen-root");
@@ -966,12 +744,11 @@ function withTempProductRepos(fn) {
966
744
  };
967
745
  writeManifest(regenRoot, "packages/codeflow/clauth-plugin.json", baseManifest("codeflow-mcp"));
968
746
  writeManifest(regenRoot, "apps/dev-center/clauth-plugin.json", baseManifest("dev-center"));
969
- writeManifest(regenRoot, "apps/codeflow-explorer/clauth-plugin.json", baseManifest("codeflow-explorer"));
970
747
  writeManifest(regenRoot, "mcp-servers/regen-media/clauth-plugin.json", baseManifest("regen-media"));
971
748
  writeManifest(regenRoot, "mcp-servers/web-research/clauth-plugin.json", baseManifest("web-research"));
972
749
  writeManifest(rdcSkills, "clauth-plugin.json", baseManifest("rdc-skills"));
973
750
  try {
974
- return fn({ base, regenRoot, rdcSkills, writeManifest, roots: { "regen-root": regenRoot } });
751
+ return fn({ base, regenRoot, rdcSkills, writeManifest, roots: { "regen-root": regenRoot, "rdc-skills": rdcSkills } });
975
752
  } finally {
976
753
  fs.rmSync(base, { recursive: true, force: true });
977
754
  }
@@ -994,19 +771,22 @@ test("plugin sync inherits registerPlugin idempotence — a second sweep reports
994
771
  assert.equal(second.every((entry) => entry.ok && entry.state === "unchanged"), true, "re-sweeping identical content must be a no-op");
995
772
  })));
996
773
 
997
- test("plugin sync warns and continues over a missing repo root instead of throwing", () => withTempSupervisor(() => withTempProductRepos(({ base }) => {
998
- // A box that never checked out regen-root must still return a receipt per
999
- // manifest instead of throwing.
774
+ test("plugin sync warns and continues over a missing repo root instead of throwing", () => withTempSupervisor(() => withTempProductRepos(({ base, rdcSkills }) => {
775
+ // A box that never checked out regen-root must still sync what it does have.
1000
776
  const absent = path.join(base, "no-such-checkout");
1001
777
  assert.equal(fs.existsSync(absent), false);
1002
778
  let receipts;
1003
779
  assert.doesNotThrow(() => {
1004
- receipts = syncPluginsFromRepos({ "regen-root": absent }, "test");
780
+ receipts = syncPluginsFromRepos({ "regen-root": absent, "rdc-skills": rdcSkills }, "test");
1005
781
  });
1006
782
  assert.equal(receipts.length, 5, "a skipped repo still yields a receipt per attempted manifest");
1007
783
  const missing = receipts.filter((entry) => entry.state === "repo_root_missing");
1008
- assert.equal(missing.length, 5, "all five regen-root manifests report the missing root");
784
+ assert.equal(missing.length, 4, "all four regen-root manifests report the missing root");
1009
785
  assert.equal(missing.every((entry) => entry.ok === false), true);
786
+ const skills = receipts.find((entry) => entry.repo === "rdc-skills");
787
+ assert.equal(skills.ok, true);
788
+ assert.equal(skills.state, "registered");
789
+ assert.ok(listPlugins().find((plugin) => plugin.id === "rdc-skills"), "the reachable repo still registered");
1010
790
  })));
1011
791
 
1012
792
  test("plugin sync registers the remaining manifests when one is malformed", () => withTempSupervisor(() => withTempProductRepos(({ regenRoot, roots, writeManifest }) => {
@@ -1021,7 +801,7 @@ test("plugin sync registers the remaining manifests when one is malformed", () =
1021
801
  assert.equal(good.length, 4);
1022
802
  assert.equal(good.every((entry) => entry.ok && entry.state === "registered"), true, "one bad manifest must not abort the sweep");
1023
803
  const ids = new Set(listPlugins().map((plugin) => plugin.id));
1024
- for (const id of ["codeflow-mcp", "codeflow-explorer", "regen-media", "web-research"]) {
804
+ for (const id of ["codeflow-mcp", "regen-media", "web-research", "rdc-skills"]) {
1025
805
  assert.ok(ids.has(id), `${id} must still be registered`);
1026
806
  }
1027
807
  })));
@@ -1038,7 +818,7 @@ test("deregisterPlugin removes only the named plugin and leaves siblings intact"
1038
818
  assert.equal(receipt.resulting_state.cleanup[0].ok, true);
1039
819
  assert.equal(fs.existsSync(path.join(managed, "web-research")), false, "the named plugin directory is gone");
1040
820
 
1041
- for (const sibling of ["codeflow-mcp", "dev-center", "codeflow-explorer", "regen-media"]) {
821
+ for (const sibling of ["codeflow-mcp", "dev-center", "regen-media", "rdc-skills"]) {
1042
822
  assert.equal(fs.existsSync(path.join(managed, sibling)), true, `${sibling} must survive`);
1043
823
  }
1044
824
  // discovery re-ran, so the removed managed plugin is reported missing, not current
@@ -1263,30 +1043,25 @@ test("plugin sync reports an unknown repo-root override instead of silently swee
1263
1043
  assert.equal(rejected[0].ok, false);
1264
1044
  assert.match(rejected[0].error, /unknown repo name/);
1265
1045
  assert.equal(SYNC_SKIP_STATES.includes("unknown_repo_name"), false, "a typo'd repo name must fail the sweep, not be skipped");
1266
- assert.deepEqual([...SYNC_REPO_NAMES].sort(), ["regen-root"]);
1046
+ assert.deepEqual([...SYNC_REPO_NAMES].sort(), ["rdc-skills", "regen-root"]);
1267
1047
  })));
1268
1048
 
1269
1049
  test("plugin sync never throws on a malformed repo root — the contract absence must not break", () => withTempSupervisor(() => withTempProductRepos(({ rdcSkills }) => {
1270
1050
  // path.resolve() throws on a non-string; doing that before the existence
1271
1051
  // guard aborted the whole sweep and lost every later repo's receipt.
1272
- // rdc-skills is passed alongside the bad root purely as an unrecognized
1273
- // override key -- it must be reported (unknown_repo_name), not thrown, and
1274
- // its presence must not stop regen-root's own manifests from still being
1275
- // reported too.
1276
1052
  for (const badRoot of [123, " ", {}, [], true]) {
1277
1053
  let receipts;
1278
1054
  assert.doesNotThrow(() => {
1279
1055
  receipts = syncPluginsFromRepos({ "regen-root": badRoot, "rdc-skills": rdcSkills }, "test");
1280
1056
  }, `root ${JSON.stringify(badRoot)} must not throw`);
1281
- assert.equal(receipts.length, 6, "every attempted manifest plus the rejected override still yields a receipt");
1057
+ assert.equal(receipts.length, 5, "every attempted manifest still yields a receipt");
1282
1058
  assert.equal(
1283
1059
  receipts.filter((entry) => entry.repo === "regen-root" && entry.state === "repo_root_unknown").length,
1284
- 5,
1060
+ 4,
1285
1061
  `root ${JSON.stringify(badRoot)} must be reported, not thrown`,
1286
1062
  );
1287
1063
  const skills = receipts.find((entry) => entry.repo === "rdc-skills");
1288
- assert.equal(skills.ok, false, "rdc-skills is not a known sync repo name -- it must be rejected, not swept");
1289
- assert.equal(skills.state, "unknown_repo_name");
1064
+ assert.equal(skills.ok, true, "a later repo must still be swept after a bad earlier root");
1290
1065
  }
1291
1066
  })));
1292
1067
 
@@ -1462,208 +1237,3 @@ test("surfaceOpenUrl never points a browser at an MCP transport endpoint", () =>
1462
1237
  );
1463
1238
  assert.equal(surfaceOpenUrl({}), null);
1464
1239
  });
1465
-
1466
- // rdc:review finding (2026-09-02), part 3 of 3: `--isolated` never set
1467
- // CLAUTH_SUPERVISOR_DIR, so an isolated instance shared state.json with the
1468
- // live daemon by default. resolveIsolatedSupervisorDir is the pure decision
1469
- // extracted out of cli/commands/serve.js's actionForeground so it's testable
1470
- // without spawning a server.
1471
- test("resolveIsolatedSupervisorDir picks a port-scoped dir, and never overrides an explicit CLAUTH_SUPERVISOR_DIR", () => {
1472
- const a = resolveIsolatedSupervisorDir(52440, undefined);
1473
- const b = resolveIsolatedSupervisorDir(53137, undefined);
1474
- assert.notEqual(a, b, "two different ports must not resolve to the same dir");
1475
- assert.match(a, /clauth-isolated/);
1476
- assert.match(a, /52440/);
1477
- assert.equal(resolveIsolatedSupervisorDir(52440, "C:\\custom\\explicit-dir"), "C:\\custom\\explicit-dir");
1478
- });
1479
-
1480
- // CRITICAL rdc:review finding (2026-09-02), confirmed regression: the first
1481
- // version of the isolation fix called resolveIsolatedSupervisorDir() whenever
1482
- // `opts.isolated` was true, with no further check. actionSupervisor() in
1483
- // cli/commands/serve.js unconditionally sets `opts.isolated = true` for an
1484
- // unrelated reason (skip vault password auth on the internal supervisor child
1485
- // process every normal `clauth serve start` spawns). The result: the REAL
1486
- // production supervisor's state.json got silently redirected to an empty
1487
- // temp dir on every normal boot, disabling the whole health-reconcile/
1488
- // auto-repair loop with no error. isGenuinelyIsolatedInstance() is the actual
1489
- // gate now used in serve.js.
1490
- //
1491
- // CORRECTED (4th review round): the first fix keyed this off `port ===
1492
- // supervisorPort`, which collided with test/serve-http-routes.test.mjs's own
1493
- // sanctioned pattern of setting CLAUTH_SUPERVISOR_PORT to match its --port --
1494
- // structurally identical from the outside, so a stricter port-based refusal
1495
- // elsewhere broke 39 tests. This version keys off the unambiguous
1496
- // __CLAUTH_SUPERVISOR_DAEMON marker ensureSupervisorStarted() already sets
1497
- // (and nothing previously read) instead of port matching.
1498
- test("isGenuinelyIsolatedInstance excludes the real internal daemon (via its marker) even when isolated=true, and includes every other isolated invocation", () => {
1499
- // The exact regressed case: actionSupervisor() sets isolated=true for its
1500
- // automatic boot, which DOES carry the internal-daemon marker -- must NOT
1501
- // be treated as a throwaway isolated instance.
1502
- assert.equal(isGenuinelyIsolatedInstance(true, true), false);
1503
- // Any other isolated invocation (serve test, a manual --isolated --port
1504
- // run, or a test fixture deliberately constructing a supervisor-port
1505
- // scenario) never carries the marker and is a genuine throwaway instance.
1506
- assert.equal(isGenuinelyIsolatedInstance(true, false), true);
1507
- // Not isolated at all -- never redirect, regardless of the marker.
1508
- assert.equal(isGenuinelyIsolatedInstance(false, false), false);
1509
- assert.equal(isGenuinelyIsolatedInstance(false, true), false);
1510
- });
1511
-
1512
- // rdc:review finding (2026-09-02), part 1 of 3: state.json's read-modify-write
1513
- // had no lock. withStateLock is the in-process promise-chained mutex added to
1514
- // close it. This test proves the primitive itself provides real mutual
1515
- // exclusion using the textbook counter-race shape (read, await, increment,
1516
- // write) -- the exact shape an async critical section with a network/health
1517
- // probe in the middle has. Without the lock this loses updates (every
1518
- // concurrent reader sees the same pre-race value); with it, none are lost.
1519
- // withStateLock now does real cross-process file I/O (a lockfile alongside
1520
- // state.json) as well as in-process serialization -- MUST run under
1521
- // withTempSupervisor. Without it, every withStateLock call below reads
1522
- // CLAUTH_SUPERVISOR_DIR from whatever the ambient environment happens to be
1523
- // (unset -> the LIVE %APPDATA%/clauth/supervisor/ directory), and this test
1524
- // would create/delete a real state.lock file there.
1525
- test("withStateLock serializes overlapping async critical sections -- no lost updates", () => withTempSupervisor(async () => {
1526
- let counter = 0;
1527
- const N = 25;
1528
- async function unsafeIncrement() {
1529
- const seen = counter;
1530
- await new Promise((resolve) => setImmediate(resolve)); // force a real interleaving window
1531
- counter = seen + 1;
1532
- }
1533
- async function lockedIncrement() {
1534
- return withStateLock(async () => {
1535
- const seen = counter;
1536
- await new Promise((resolve) => setImmediate(resolve));
1537
- counter = seen + 1;
1538
- });
1539
- }
1540
-
1541
- counter = 0;
1542
- await Promise.all(Array.from({ length: N }, () => unsafeIncrement()));
1543
- const unsafeResult = counter;
1544
- assert.ok(unsafeResult < N, `expected the unlocked version to lose updates (got ${unsafeResult}/${N} -- if this ever equals ${N}, the interleaving window isn't forcing a real race and this test needs a stronger delay)`);
1545
-
1546
- counter = 0;
1547
- await Promise.all(Array.from({ length: N }, () => lockedIncrement()));
1548
- assert.equal(counter, N, "withStateLock must serialize every critical section -- zero lost updates");
1549
- }));
1550
-
1551
- // rdc:review finding (2026-09-02), fourth pass -- confirmed, material to the
1552
- // prior interview: an in-process mutex provides ZERO protection between two
1553
- // separate OS processes, and `clauth serve start`'s STANDARD topology
1554
- // (ensureSupervisorStarted's real detached spawn() for the :52439 supervisor
1555
- // child, sharing state.json with the main :52437 daemon by design) is
1556
- // exactly that -- not an edge case. The in-process test above cannot catch
1557
- // this; it never leaves one process. This test spawns two REAL, separate
1558
- // Node processes racing to increment a shared counter file, each increment
1559
- // guarded by the real, exported withStateLock -- proving the cross-process
1560
- // lockfile (acquireCrossProcessStateLock/releaseCrossProcessStateLock)
1561
- // actually serializes across process boundaries, not just within one.
1562
- test("withStateLock provides real cross-process mutual exclusion, not just in-process", () => withTempSupervisor(async (root) => {
1563
- const counterPath = path.join(root, "counter.txt");
1564
- fs.writeFileSync(counterPath, "0");
1565
- const registryUrl = pathToFileURL(path.join(process.cwd(), "cli", "supervisor-registry.js")).href;
1566
- const workerPath = path.join(root, "cross-process-lock-worker.mjs");
1567
- fs.writeFileSync(workerPath, `
1568
- import { withStateLock } from ${JSON.stringify(registryUrl)};
1569
- import fs from "node:fs";
1570
- const counterPath = process.env.COUNTER_PATH;
1571
- const increments = Number(process.env.INCREMENTS);
1572
- async function run() {
1573
- for (let i = 0; i < increments; i++) {
1574
- await withStateLock(async () => {
1575
- const current = Number(fs.readFileSync(counterPath, "utf8"));
1576
- // Force a real interleaving window -- without cross-process mutual
1577
- // exclusion, the other process's concurrent read+write lands here.
1578
- await new Promise((resolve) => setTimeout(resolve, 10));
1579
- fs.writeFileSync(counterPath, String(current + 1));
1580
- });
1581
- }
1582
- }
1583
- run().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });
1584
- `);
1585
-
1586
- const INCREMENTS_PER_WORKER = 12;
1587
- const env = { ...process.env, CLAUTH_SUPERVISOR_DIR: root, COUNTER_PATH: counterPath, INCREMENTS: String(INCREMENTS_PER_WORKER) };
1588
- const spawnWorker = () => new Promise((resolve, reject) => {
1589
- const child = spawn(process.execPath, [workerPath], { env, stdio: ["ignore", "inherit", "inherit"] });
1590
- child.on("error", reject);
1591
- child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`cross-process lock worker exited ${code}`))));
1592
- });
1593
-
1594
- await Promise.all([spawnWorker(), spawnWorker()]);
1595
- const final = Number(fs.readFileSync(counterPath, "utf8"));
1596
- assert.equal(final, INCREMENTS_PER_WORKER * 2, "withStateLock must serialize across two real OS processes sharing CLAUTH_SUPERVISOR_DIR -- zero lost updates");
1597
- }));
1598
-
1599
- // rdc:review finding (2026-09-02), 4th independent round, empirically
1600
- // reproduced against an instrumented mirror of the shipped algorithm: the
1601
- // PREVIOUS reclaim mechanism (unlinkSync a stale lock, then loop back to
1602
- // retry openSync('wx')) was not atomic -- multiple waiters racing the SAME
1603
- // stale lock could each independently decide to reclaim and each
1604
- // independently succeed, becoming simultaneous holders (2-3 in 5 of 12 runs
1605
- // in the reviewer's repro). This test exercises exactly that scenario
1606
- // against the REAL shipped code (not a mirror): pre-seeds a genuinely stale
1607
- // lock (dead pid, 60s old -- past STATE_LOCK_STALE_MS), then spawns several
1608
- // real processes SIMULTANEOUSLY, all of which must race the same stale
1609
- // reclaim on their very first acquisition attempt. The current algorithm
1610
- // closes this via an atomic renameSync claim -- only one racing process can
1611
- // ever successfully rename a given stale directory instance away.
1612
- // rdc:review finding (2026-09-02), 5th round: three consecutive hand-rolled
1613
- // versions of this lock each had a distinct real concurrency bug (see
1614
- // withStateLock's own header comment in supervisor-registry.js for the full
1615
- // history) -- replaced with proper-lockfile, a mature library, rather than a
1616
- // 4th hand-rolled attempt. Its on-disk shape and staleness mechanism are
1617
- // different from any prior version: it locks `<file>.lock` (here,
1618
- // state.json.lock, not state.lock) and judges staleness via the lock
1619
- // directory's own mtime (continuously refreshed while a holder is active),
1620
- // not a point-in-time pid/timestamp record inside it. This test is updated
1621
- // to match that real contract rather than testing an obsolete on-disk shape.
1622
- test("withStateLock's stale-lock reclaim is atomic under multiple processes racing the SAME abandoned lock", () => withTempSupervisor(async (root) => {
1623
- const counterPath = path.join(root, "counter.txt");
1624
- fs.writeFileSync(counterPath, "0");
1625
-
1626
- // Pre-seed a stale lock at proper-lockfile's real path (state.json.lock,
1627
- // sibling to state.json -- see stateLockTargetPath()) with an mtime well
1628
- // past the stale threshold (30s) -- the one signal proper-lockfile's own
1629
- // isLockStale() checks, so this is unambiguously reclaimable by design.
1630
- const lockPath = path.join(root, "state.json.lock");
1631
- fs.mkdirSync(lockPath, { recursive: true });
1632
- const staleTime = new Date(Date.now() - 60000);
1633
- fs.utimesSync(lockPath, staleTime, staleTime);
1634
-
1635
- const registryUrl = pathToFileURL(path.join(process.cwd(), "cli", "supervisor-registry.js")).href;
1636
- const workerPath = path.join(root, "stale-reclaim-worker.mjs");
1637
- fs.writeFileSync(workerPath, `
1638
- import { withStateLock } from ${JSON.stringify(registryUrl)};
1639
- import fs from "node:fs";
1640
- const counterPath = process.env.COUNTER_PATH;
1641
- const increments = Number(process.env.INCREMENTS);
1642
- async function run() {
1643
- for (let i = 0; i < increments; i++) {
1644
- await withStateLock(async () => {
1645
- const current = Number(fs.readFileSync(counterPath, "utf8"));
1646
- await new Promise((resolve) => setTimeout(resolve, 10));
1647
- fs.writeFileSync(counterPath, String(current + 1));
1648
- });
1649
- }
1650
- }
1651
- run().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });
1652
- `);
1653
-
1654
- const WORKER_COUNT = 6;
1655
- const INCREMENTS_PER_WORKER = 5;
1656
- const env = { ...process.env, CLAUTH_SUPERVISOR_DIR: root, COUNTER_PATH: counterPath, INCREMENTS: String(INCREMENTS_PER_WORKER) };
1657
- const spawnWorker = () => new Promise((resolve, reject) => {
1658
- const child = spawn(process.execPath, [workerPath], { env, stdio: ["ignore", "inherit", "inherit"] });
1659
- child.on("error", reject);
1660
- child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`stale-reclaim worker exited ${code}`))));
1661
- });
1662
-
1663
- // All WORKER_COUNT processes launched together -- their first acquisition
1664
- // attempt genuinely races the same pre-seeded stale lock simultaneously,
1665
- // which is the exact condition the prior algorithm failed under.
1666
- await Promise.all(Array.from({ length: WORKER_COUNT }, () => spawnWorker()));
1667
- const final = Number(fs.readFileSync(counterPath, "utf8"));
1668
- assert.equal(final, WORKER_COUNT * INCREMENTS_PER_WORKER, "stale-lock reclaim must be atomic -- zero lost updates even when multiple processes race the same abandoned lock");
1669
- }));
@@ -66,15 +66,6 @@ function validateCommand(command, field) {
66
66
  export function validateWatchdogService(service) {
67
67
  if (!service || typeof service !== "object") throw new Error("service must be an object");
68
68
  if (!service.id || typeof service.id !== "string") throw new Error("service.id is required");
69
- // rdc:review finding (2026-09-02): shaped identically to the id checks in
70
- // cli/supervisor-registry.js (validatePluginManifest, normalizeSurface),
71
- // which also reject Windows-reserved device names (con/nul/aux/prn/
72
- // com1-9/lpt1-9) at registration. NOT extended here deliberately: unlike
73
- // those, service.id is not used to derive a filesystem path anywhere in
74
- // this file today, so there is nothing for a reserved name to collide
75
- // with. Revisit if a future change starts deriving a per-service path from
76
- // service.id -- that would reintroduce the same defect class this comment
77
- // exists to flag before it does.
78
69
  if (!/^[a-zA-Z0-9_.-]+$/.test(service.id)) throw new Error("service.id may contain only letters, numbers, dot, underscore, and dash");
79
70
  if (!service.label || typeof service.label !== "string") throw new Error("service.label is required");
80
71
  if (!VALID_KINDS.has(service.kind)) throw new Error(`service.kind must be one of ${[...VALID_KINDS].join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "2.15.3",
3
+ "version": "2.15.5",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,6 @@
28
28
  "node-fetch": "^3.3.2",
29
29
  "ora": "^8.1.0",
30
30
  "pm2": "^7.0.3",
31
- "proper-lockfile": "^4.1.2",
32
31
  "typescript": "^5.9.3"
33
32
  },
34
33
  "engines": {
Binary file
Binary file
Binary file