@lifeaitools/clauth 2.15.3 → 2.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clauth-skill/references/operator-guide.md +12 -2
- package/cli/commands/serve.js +55 -226
- package/cli/http/components/callagent-terminal-component.js +5 -5
- package/cli/http/components/mcp-transport-component.js +17 -10
- package/cli/http/components/oauth-component.js +40 -74
- package/cli/http/components/supervisor-component.js +13 -13
- package/cli/ops/pm2-adapter.js +4 -147
- package/cli/services/file-io.js +3 -21
- package/cli/services/file-io.test.js +1 -33
- package/cli/supervisor-registry.js +59 -454
- package/cli/supervisor-registry.test.js +19 -449
- package/cli/watchdog-registry.js +0 -9
- package/package.json +1 -2
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/supabase/functions/auth-vault/index.ts +49 -38
- package/cli/http/components/test-static-oauth-component.js +0 -233
- package/supabase/migrations/004_remove_audit_and_rate_limiting.sql +0 -29
|
@@ -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
|
|
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
|
|
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,
|
|
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", "
|
|
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", "
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
}));
|
package/cli/watchdog-registry.js
CHANGED
|
@@ -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
|
+
"version": "2.15.4",
|
|
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
|