@lifeaitools/clauth 2.10.1 → 2.10.2

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/cli/index.js CHANGED
@@ -150,7 +150,7 @@ import { runInstall } from './commands/install.js';
150
150
  import { runUninstall } from './commands/uninstall.js';
151
151
  import { runScrub } from './commands/scrub.js';
152
152
  import { runServe, MCP_TOOLS } from './commands/serve.js';
153
- import { deregisterPlugin, listPlugins, registerPlugin, syncPluginsFromRepos, SYNC_REPO_NAMES, SYNC_SKIP_STATES } from './supervisor-registry.js';
153
+ import { deregisterPlugin, isMcpServerPlugin, listPlugins, registerPlugin, syncPluginsFromRepos, SYNC_REPO_NAMES, SYNC_SKIP_STATES } from './supervisor-registry.js';
154
154
  import { runOps } from './commands/ops.js';
155
155
  import { runOpsInstall } from './commands/ops-install.js';
156
156
  import { runNpm, runPublish } from './commands/npm.js';
@@ -927,20 +927,6 @@ tunnelCmd
927
927
  // ──────────────────────────────────────────────
928
928
  // clauth mcp list
929
929
  // ──────────────────────────────────────────────
930
- // Known MCP-server plugin ids in the managed fleet. Positive allowlist
931
- // rather than a naming heuristic (credential-name conventions and health
932
- // route "kind" vary across these) — dev-center and any future non-MCP
933
- // pm2-managed surface stay excluded by construction. Deliberately distinct
934
- // from `clauth list` (vault credential services); this never touches those.
935
- const MCP_SERVER_PLUGIN_IDS = new Set([
936
- "fs-mcp",
937
- "web-research",
938
- "regen-media",
939
- "regen-media-local",
940
- "codeflow-mcp",
941
- "rdc-skills",
942
- ]);
943
-
944
930
  const mcpCmd = program.command("mcp").description("Inspect clauth's own MCP tool catalog and managed MCP-server surfaces");
945
931
 
946
932
  mcpCmd
@@ -951,7 +937,7 @@ mcpCmd
951
937
  for (const tool of MCP_TOOLS) {
952
938
  console.log(` ${chalk.white(tool.name)} ${chalk.gray(tool.description || "")}`);
953
939
  }
954
- const mcpPlugins = listPlugins().filter((p) => MCP_SERVER_PLUGIN_IDS.has(p.id));
940
+ const mcpPlugins = listPlugins().filter(isMcpServerPlugin);
955
941
  console.log(chalk.cyan(`\n Managed MCP-server surfaces (${mcpPlugins.length}):\n`));
956
942
  if (!mcpPlugins.length) {
957
943
  console.log(chalk.gray(" none discovered — is the daemon running? clauth serve"));
@@ -1047,10 +1033,16 @@ pluginCmd
1047
1033
  .command('deregister <id>')
1048
1034
  .description('Remove one managed plugin directory by id and re-run discovery. Removing an unregistered id is a safe no-op.')
1049
1035
  .option('--dry-run', 'Resolve and print the directory that would be deleted, without deleting it')
1036
+ .option('--expected-root <path>', 'Refuse unless the registered plugin originated from this package root')
1037
+ .option('--force', 'Recovery only: remove managed metadata even when process cleanup fails; receipt records the bypass')
1050
1038
  .action((id, opts) => {
1051
1039
  let receipt;
1052
1040
  try {
1053
- receipt = deregisterPlugin(id, 'cli', { dryRun: Boolean(opts.dryRun) });
1041
+ receipt = deregisterPlugin(id, 'cli', {
1042
+ dryRun: Boolean(opts.dryRun),
1043
+ expectedRoot: opts.expectedRoot,
1044
+ force: Boolean(opts.force),
1045
+ });
1054
1046
  } catch (error) {
1055
1047
  console.error(` ✗ deregister_failed: ${error instanceof Error ? error.message : String(error)}`);
1056
1048
  process.exitCode = 1;
@@ -20,6 +20,65 @@ const DEFAULT_HEALTH_TIMEOUT_MS = 2500;
20
20
  const HEALTH_RECONCILE_COOLDOWN_MS = 15000;
21
21
  const DOCUMENTATION_FIELDS = ["architecture", "operator_guide", "install", "runbook", "tool_reference", "release", "agent_context"];
22
22
 
23
+ function hasMcpToken(value) {
24
+ return /(^|[^a-z0-9])mcp([^a-z0-9]|$)/i.test(String(value || ""));
25
+ }
26
+
27
+ function routeDeclaresMcp(route) {
28
+ if (!route || typeof route !== "object") return false;
29
+ if (hasMcpToken(route.id) || hasMcpToken(route.kind)) return true;
30
+ try {
31
+ return new URL(String(route.url || "")).pathname
32
+ .split("/")
33
+ .some((segment) => segment.toLowerCase() === "mcp");
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function normalizeCapabilities(value) {
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { kinds: [] };
41
+ const kinds = Array.isArray(value.kinds)
42
+ ? [...new Set(value.kinds.map((kind) => String(kind || "").trim().toLowerCase())
43
+ .filter((kind) => /^[a-z0-9_.-]+$/.test(kind)))]
44
+ : [];
45
+ return { kinds };
46
+ }
47
+
48
+ function normalizeMcpContract(value) {
49
+ if (value === true) return { declared: true, transport: null, url: null, stdio: [], tools: [] };
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
51
+ return {
52
+ declared: true,
53
+ transport: value.transport ? String(value.transport) : null,
54
+ url: value.url ? String(value.url) : null,
55
+ stdio: normalizeCommand(value.stdio, "mcp.stdio"),
56
+ tools: Array.isArray(value.tools)
57
+ ? value.tools.map((tool) => String(tool || "").trim()).filter((tool) => /^[a-zA-Z0-9_.-]+$/.test(tool))
58
+ : [],
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Classify an MCP server from its own manifest contract. New plugins declare
64
+ * `mcp` or `capabilities.kinds`; route/name/document signals retain backwards
65
+ * compatibility with manifests created before those fields existed.
66
+ */
67
+ export function isMcpServerPlugin(plugin) {
68
+ if (!plugin || typeof plugin !== "object") return false;
69
+ if (plugin.mcp === true || (plugin.mcp && typeof plugin.mcp === "object")) return true;
70
+ if ((plugin.capabilities?.kinds || []).some((kind) => ["mcp", "mcp-server"].includes(String(kind).toLowerCase()))) return true;
71
+
72
+ const routes = [
73
+ ...(Array.isArray(plugin.routes) ? plugin.routes : []),
74
+ ...(Array.isArray(plugin.surfaces) ? plugin.surfaces.flatMap((surface) => surface?.routes || []) : []),
75
+ ];
76
+ if (routes.some(routeDeclaresMcp)) return true;
77
+ if (hasMcpToken(plugin.id)) return true;
78
+ if ((plugin.surfaces || []).some((surface) => hasMcpToken(surface?.id) || hasMcpToken(surface?.name))) return true;
79
+ return hasMcpToken(plugin.documentation?.agent_context);
80
+ }
81
+
23
82
  export function getSupervisorPort() {
24
83
  return Number(process.env.CLAUTH_SUPERVISOR_PORT || DEFAULT_SUPERVISOR_PORT);
25
84
  }
@@ -454,6 +513,7 @@ export function validatePluginManifest(manifest, sourcePath = "") {
454
513
  core: manifest.core === true,
455
514
  enable_default: manifest.enable_default === true,
456
515
  sourcePath,
516
+ package_root: manifest._clauth?.package_root ? path.resolve(String(manifest._clauth.package_root)) : null,
457
517
  destination: normalizeDestination(manifest.destination),
458
518
  lifecycle_owner: normalizeLifecycleOwner(manifest.lifecycle_owner),
459
519
  credentials: Array.isArray(manifest.credentials) ? manifest.credentials.map((c) => ({
@@ -462,6 +522,8 @@ export function validatePluginManifest(manifest, sourcePath = "") {
462
522
  description: String(c.description || ""),
463
523
  required: c.required !== false,
464
524
  })).filter((c) => /^[a-zA-Z0-9_.-]+$/.test(c.name)) : [],
525
+ capabilities: normalizeCapabilities(manifest.capabilities),
526
+ mcp: normalizeMcpContract(manifest.mcp),
465
527
  surfaces: [],
466
528
  routes: Array.isArray(manifest.routes) ? manifest.routes : [],
467
529
  test: manifest.test && typeof manifest.test === "object" ? {
@@ -472,6 +534,7 @@ export function validatePluginManifest(manifest, sourcePath = "") {
472
534
  } : null,
473
535
  };
474
536
  plugin.surfaces = (Array.isArray(manifest.surfaces) ? manifest.surfaces : []).map((surface) => normalizeSurface(surface, plugin));
537
+ plugin.mcp_server = isMcpServerPlugin(plugin);
475
538
  return plugin;
476
539
  }
477
540
 
@@ -635,7 +698,10 @@ export function registerPlugin(manifestPath, actor = "localhost") {
635
698
  .replace(/%PACKAGE_ROOT%/gi, packageRoot);
636
699
  let manifest;
637
700
  try {
638
- manifest = validatePluginManifest(JSON.parse(raw), manifestPath);
701
+ const parsed = JSON.parse(raw);
702
+ parsed._clauth = { package_root: packageRoot };
703
+ raw = `${JSON.stringify(parsed, null, 2)}\n`;
704
+ manifest = validatePluginManifest(parsed, manifestPath);
639
705
  } catch (error) {
640
706
  return operation("plugin.register", { manifest_path: manifestPath }, null, {
641
707
  ok: false, state: "manifest_invalid", error: error instanceof Error ? error.message : String(error),
@@ -820,7 +886,7 @@ export function syncPluginsFromRepos(repoRoots = {}, actor = "localhost") {
820
886
  // reaching this function is attacker-influenced input (a CLI arg or an HTTP
821
887
  // field, with no manifest validation upstream to lean on) and this deletes
822
888
  // recursively, so both guards below are load-bearing.
823
- export function deregisterPlugin(id, actor = "localhost", { dryRun = false } = {}) {
889
+ export function deregisterPlugin(id, actor = "localhost", { dryRun = false, expectedRoot = null, force = false } = {}) {
824
890
  const pluginId = String(id ?? "").trim();
825
891
  // Guard 1 — charset + all-dots, the same rule a manifest id must satisfy.
826
892
  //
@@ -838,7 +904,7 @@ export function deregisterPlugin(id, actor = "localhost", { dryRun = false } = {
838
904
  }
839
905
  const roots = rootEntries();
840
906
  const managedRoots = roots.filter((entry) => entry.source === "managed");
841
- const prior = (loadSupervisorState().plugins || []).find((plugin) => plugin.id === pluginId) || null;
907
+ let prior = (loadSupervisorState().plugins || []).find((plugin) => plugin.id === pluginId) || null;
842
908
 
843
909
  // Resolve the plugin's ACTUAL directory rather than assuming a flat
844
910
  // <first-managed-root>/<id> layout. Three real layouts exist that assumption
@@ -899,6 +965,26 @@ export function deregisterPlugin(id, actor = "localhost", { dryRun = false } = {
899
965
  ok: false, state: "not_managed", error: "resolved plugin directory is not inside a managed plugin root",
900
966
  }, actor);
901
967
  }
968
+ if (!prior) {
969
+ prior = discoverPlugins().plugins.find((plugin) => plugin.id === pluginId) || null;
970
+ }
971
+ if (expectedRoot) {
972
+ const expected = path.resolve(String(expectedRoot));
973
+ if (!prior?.package_root) {
974
+ return operation("plugin.deregister", { plugin_id: pluginId }, prior, {
975
+ ok: false, state: "ownership_unverified", error: "registered plugin has no package-root ownership record; re-register it before deregistering",
976
+ }, actor);
977
+ }
978
+ const actual = path.resolve(prior.package_root);
979
+ const matches = process.platform === "win32"
980
+ ? actual.toLowerCase() === expected.toLowerCase()
981
+ : actual === expected;
982
+ if (!matches) {
983
+ return operation("plugin.deregister", { plugin_id: pluginId }, prior, {
984
+ ok: false, state: "ownership_mismatch", error: `registered package root ${actual} does not match expected root ${expected}`,
985
+ }, actor);
986
+ }
987
+ }
902
988
  if (dryRun) {
903
989
  return operation("plugin.deregister.dry_run", { plugin_id: pluginId }, prior, {
904
990
  ok: true,
@@ -906,8 +992,43 @@ export function deregisterPlugin(id, actor = "localhost", { dryRun = false } = {
906
992
  target_dir: targetDir,
907
993
  plugin_state: prior?.state || "unknown",
908
994
  surfaces: (prior?.surfaces || []).map((surface) => surface.id),
995
+ package_root: prior?.package_root || null,
909
996
  }, actor);
910
997
  }
998
+
999
+ const cleanup = [];
1000
+ const seenStopCommands = new Set();
1001
+ for (const surface of prior?.surfaces || []) {
1002
+ if (surface.lifecycle_owner !== "clauth" || surface.destination !== "local/clauth/pm2") continue;
1003
+ const signature = `${surface.cwd || ""}\0${JSON.stringify(surface.stop || [])}`;
1004
+ if (seenStopCommands.has(signature)) continue;
1005
+ seenStopCommands.add(signature);
1006
+ if (!Array.isArray(surface.stop) || surface.stop.length === 0) {
1007
+ const unavailable = { surface_id: surface.id, ok: false, state: "command_missing", forced: Boolean(force) };
1008
+ cleanup.push(unavailable);
1009
+ if (!force) {
1010
+ return operation("plugin.deregister", { plugin_id: pluginId }, prior, {
1011
+ ok: false, state: "surface_cleanup_unavailable", error: `managed surface ${surface.id} has no stop command`, cleanup,
1012
+ }, actor);
1013
+ }
1014
+ continue;
1015
+ }
1016
+ const stopReceipt = runSurfaceAction(`${pluginId}:${surface.id}`, "stop", actor);
1017
+ const summary = {
1018
+ surface_id: surface.id,
1019
+ ok: stopReceipt.resulting_state?.ok === true,
1020
+ state: stopReceipt.resulting_state?.state || stopReceipt.error || "unknown",
1021
+ };
1022
+ cleanup.push(summary);
1023
+ if (!summary.ok) {
1024
+ summary.forced = Boolean(force);
1025
+ if (!force) {
1026
+ return operation("plugin.deregister", { plugin_id: pluginId }, prior, {
1027
+ ok: false, state: "surface_cleanup_failed", error: `failed to stop managed surface ${surface.id}`, cleanup,
1028
+ }, actor);
1029
+ }
1030
+ }
1031
+ }
911
1032
  try {
912
1033
  fs.rmSync(targetDir, { recursive: true, force: true });
913
1034
  } catch (error) {
@@ -919,9 +1040,12 @@ export function deregisterPlugin(id, actor = "localhost", { dryRun = false } = {
919
1040
  const after = discovery.plugins.find((plugin) => plugin.id === pluginId);
920
1041
  return operation("plugin.deregister", { plugin_id: pluginId }, prior, {
921
1042
  ok: true,
922
- state: "deregistered",
1043
+ state: force && cleanup.some((entry) => !entry.ok) ? "deregistered_forced" : "deregistered",
923
1044
  plugin_state: after?.state || "not_found",
924
1045
  surfaces: (prior?.surfaces || []).map((surface) => surface.id),
1046
+ cleanup,
1047
+ forced: Boolean(force),
1048
+ evidence: force && cleanup.some((entry) => !entry.ok) ? ["force_cleanup_bypass=true"] : [],
925
1049
  }, actor);
926
1050
  }
927
1051
 
@@ -9,6 +9,7 @@ import {
9
9
  deregisterPlugin,
10
10
  discoverPlugins,
11
11
  getClauthPm2Home,
12
+ isMcpServerPlugin,
12
13
  listPlugins,
13
14
  listSurfaces,
14
15
  probeAllSurfaceHealth,
@@ -88,6 +89,7 @@ function baseManifest(id, overrides = {}) {
88
89
  lifecycle_owner: "clauth",
89
90
  port: 39111,
90
91
  health: "/health",
92
+ stop: [process.execPath, "--version"],
91
93
  restart: ["node", "--version"],
92
94
  }],
93
95
  test: { command: ["node", "--version"], port: "auto", health: "/health", selfTest: [["node", "--version"]] },
@@ -111,6 +113,50 @@ test("validatePluginManifest accepts LIFEAI plugin contract with isolated test c
111
113
  assert.equal(plugin.documentation.operator_guide, "docs/systems/example/OPERATE.md");
112
114
  });
113
115
 
116
+ test("MCP-server classification is derived from manifest capabilities and legacy contract signals", () => {
117
+ const rtp = validatePluginManifest(baseManifest("rtp", {
118
+ capabilities: { kinds: ["cli", "http-service", "mcp-server"] },
119
+ mcp: {
120
+ transport: "http+stdio",
121
+ url: "http://127.0.0.1:3116/mcp",
122
+ stdio: ["node", "bin/rtp.mjs", "mcp"],
123
+ tools: ["rtp_query", "rtp_parse"],
124
+ },
125
+ }));
126
+ assert.equal(isMcpServerPlugin(rtp), true);
127
+ assert.deepEqual(rtp.capabilities.kinds, ["cli", "http-service", "mcp-server"]);
128
+ assert.deepEqual(rtp.mcp.tools, ["rtp_query", "rtp_parse"]);
129
+
130
+ const legacyMcpManifests = [
131
+ baseManifest("codeflow-mcp"),
132
+ baseManifest("fs-mcp"),
133
+ baseManifest("rdc-skills", {
134
+ routes: [{ id: "provider", kind: "external", url: "https://rdc-skills.example/mcp" }],
135
+ }),
136
+ baseManifest("regen-media", {
137
+ routes: [{ id: "provider", kind: "external", url: "https://media.example/mcp" }],
138
+ }),
139
+ baseManifest("web-research", {
140
+ documentation: {
141
+ architecture: "ARCHITECTURE.md",
142
+ agent_context: ".claude/context/web-research-mcp.md",
143
+ },
144
+ }),
145
+ baseManifest("regen-media-local", {
146
+ documentation: {
147
+ architecture: "ARCHITECTURE.md",
148
+ agent_context: ".claude/context/mcp-endpoint-design.md",
149
+ },
150
+ }),
151
+ ];
152
+ for (const manifest of legacyMcpManifests) {
153
+ assert.equal(isMcpServerPlugin(validatePluginManifest(manifest)), true, manifest.id);
154
+ }
155
+
156
+ assert.equal(isMcpServerPlugin(validatePluginManifest(baseManifest("dev-center"))), false);
157
+ assert.equal(isMcpServerPlugin(validatePluginManifest(baseManifest("factory-test-plugin"))), false);
158
+ });
159
+
114
160
  test("validatePluginManifest accepts empty test command arrays from the v1 template", () => {
115
161
  const plugin = validatePluginManifest(baseManifest("empty-test-command", {
116
162
  test: { command: [], port: "auto", health: "/health", selfTest: [] },
@@ -625,6 +671,7 @@ test("registerPlugin validates, writes into the managed root, and discovers the
625
671
  assert.equal(written, true);
626
672
  const found = listPlugins().find((plugin) => plugin.id === "registered-demo");
627
673
  assert.equal(found.enabled, true);
674
+ assert.equal(found.package_root, path.resolve(sourceDir));
628
675
 
629
676
  const second = registerPlugin(manifestPath, "test");
630
677
  assert.equal(second.resulting_state.state, "unchanged", "re-registering identical content must be a no-op, not a rewrite");
@@ -763,6 +810,8 @@ test("deregisterPlugin removes only the named plugin and leaves siblings intact"
763
810
  const receipt = deregisterPlugin("web-research", "test");
764
811
  assert.equal(receipt.resulting_state.ok, true);
765
812
  assert.equal(receipt.resulting_state.state, "deregistered");
813
+ assert.equal(receipt.resulting_state.cleanup.length, 1);
814
+ assert.equal(receipt.resulting_state.cleanup[0].ok, true);
766
815
  assert.equal(fs.existsSync(path.join(managed, "web-research")), false, "the named plugin directory is gone");
767
816
 
768
817
  for (const sibling of ["codeflow-mcp", "dev-center", "regen-media", "rdc-skills"]) {
@@ -887,6 +936,63 @@ test("deregisterPlugin removes a scoped @scope/pkg plugin instead of falsely rep
887
936
  assert.equal(after.state, "missing_default");
888
937
  }));
889
938
 
939
+ test("deregisterPlugin proves package-root ownership before stopping or deleting", () => withTempSupervisor((root) => {
940
+ const managed = path.join(root, "managed");
941
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
942
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
943
+ const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-owned-source-"));
944
+ const manifestPath = path.join(sourceDir, "clauth-plugin.json");
945
+ fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("owned-demo", { core: true, enable_default: true })), "utf8");
946
+ assert.equal(registerPlugin(manifestPath, "test").resulting_state.ok, true);
947
+
948
+ const wrong = deregisterPlugin("owned-demo", "test", { expectedRoot: path.join(root, "other-install") });
949
+ assert.equal(wrong.resulting_state.ok, false);
950
+ assert.equal(wrong.resulting_state.state, "ownership_mismatch");
951
+ assert.equal(fs.existsSync(path.join(managed, "owned-demo")), true, "ownership mismatch deleted the plugin");
952
+
953
+ const correct = deregisterPlugin("owned-demo", "test", { expectedRoot: sourceDir });
954
+ assert.equal(correct.resulting_state.ok, true);
955
+ assert.equal(correct.resulting_state.state, "deregistered");
956
+ assert.deepEqual(correct.resulting_state.cleanup.map((entry) => entry.ok), [true]);
957
+ assert.equal(fs.existsSync(path.join(managed, "owned-demo")), false);
958
+ fs.rmSync(sourceDir, { recursive: true, force: true });
959
+ }));
960
+
961
+ test("deregisterPlugin requires an explicit audited force to recover stale metadata after its stop executable disappears", () => withTempSupervisor((root) => {
962
+ const managed = path.join(root, "managed");
963
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
964
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
965
+ const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-stale-source-"));
966
+ const manifestPath = path.join(sourceDir, "clauth-plugin.json");
967
+ fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("stale-demo", {
968
+ core: true,
969
+ enable_default: true,
970
+ surfaces: [{
971
+ id: "primary",
972
+ destination: "local/clauth/pm2",
973
+ lifecycle_owner: "clauth",
974
+ port: 39111,
975
+ health: "/health",
976
+ stop: [process.execPath, path.join(sourceDir, "removed-stop-script.cjs")],
977
+ }],
978
+ })), "utf8");
979
+ assert.equal(registerPlugin(manifestPath, "test").resulting_state.ok, true);
980
+
981
+ const ordinary = deregisterPlugin("stale-demo", "test", { expectedRoot: sourceDir });
982
+ assert.equal(ordinary.resulting_state.ok, false);
983
+ assert.equal(ordinary.resulting_state.state, "surface_cleanup_failed");
984
+ assert.equal(fs.existsSync(path.join(managed, "stale-demo")), true, "default failure must preserve recovery metadata");
985
+
986
+ const forced = deregisterPlugin("stale-demo", "test", { force: true });
987
+ assert.equal(forced.resulting_state.ok, true);
988
+ assert.equal(forced.resulting_state.state, "deregistered_forced");
989
+ assert.equal(forced.resulting_state.cleanup[0].ok, false);
990
+ assert.equal(forced.resulting_state.cleanup[0].forced, true);
991
+ assert.deepEqual(forced.evidence, ["force_cleanup_bypass=true"]);
992
+ assert.equal(fs.existsSync(path.join(managed, "stale-demo")), false);
993
+ fs.rmSync(sourceDir, { recursive: true, force: true });
994
+ }));
995
+
890
996
  test("deregisterPlugin finds a plugin in a non-first managed root and refuses a user-root plugin", () => withTempSupervisor((root) => {
891
997
  // CLAUTH_MANAGED_PLUGIN_ROOTS is a path-delimited LIST; honoring only the
892
998
  // first entry silently reports a real plugin as absent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "2.10.1",
3
+ "version": "2.10.2",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {