@lifeaitools/clauth 1.30.11 → 1.30.13

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.
@@ -989,6 +989,8 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
989
989
  .supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
990
990
  .supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
991
991
  .supervisor-log{font-family:'Courier New',monospace;font-size:.7rem;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow:auto}
992
+ .supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:#86efac;background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
993
+ .supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
992
994
  </style>
993
995
  </head>
994
996
  <body>
@@ -1216,6 +1218,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1216
1218
  </div>
1217
1219
  </div>
1218
1220
  <div id="supervisor-status" class="supervisor-sub">Loading supervisor state…</div>
1221
+ <div id="supervisor-feedback" class="supervisor-feedback" role="status"></div>
1219
1222
  <div class="supervisor-grid" style="margin-top:10px">
1220
1223
  <div class="supervisor-card">
1221
1224
  <h4>Plugins</h4>
@@ -1506,6 +1509,14 @@ async function supervisorJson(path, options) {
1506
1509
  return data;
1507
1510
  }
1508
1511
 
1512
+ function supervisorFeedback(text, bad) {
1513
+ const el = document.getElementById("supervisor-feedback");
1514
+ if (!el) return;
1515
+ el.textContent = text || "";
1516
+ el.className = "supervisor-feedback" + (bad ? " bad" : "");
1517
+ el.style.display = text ? "block" : "none";
1518
+ }
1519
+
1509
1520
  async function loadSupervisorCockpit() {
1510
1521
  const status = document.getElementById("supervisor-status");
1511
1522
  if (status) status.textContent = "Loading supervisor state…";
@@ -1530,9 +1541,10 @@ async function loadSupervisorCockpit() {
1530
1541
  const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1531
1542
  return (e.ts || e.created_at || "") + " " + label;
1532
1543
  }).join("\\n") || "No events yet.";
1533
- if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status || "unknown"), "ok") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
1544
+ if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
1534
1545
  } catch (err) {
1535
1546
  if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
1547
+ supervisorFeedback("Supervisor unavailable — actions are disabled until the local control plane returns.", true);
1536
1548
  }
1537
1549
  }
1538
1550
 
@@ -1546,31 +1558,36 @@ function renderSupervisorPlugin(plugin) {
1546
1558
 
1547
1559
  function renderSupervisorSurface(surface) {
1548
1560
  const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
1549
- return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + '</div>' +
1561
+ const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
1562
+ return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) + '</div>' +
1550
1563
  '<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
1551
1564
  '<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
1552
- '<div class="supervisor-row-actions">' + ["reconcile","restart","test","rollback"].map(a => '<button class="supervisor-action" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
1565
+ '<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
1553
1566
  }
1554
1567
 
1555
1568
  async function rescanSupervisorPlugins() {
1556
1569
  try {
1557
1570
  await supervisorJson("/v1/plugins/rescan", { method: "POST", headers: writeHeaders() });
1571
+ supervisorFeedback("Plugin rescan completed.", false);
1558
1572
  await loadSupervisorCockpit();
1559
- } catch (err) { alert("Supervisor rescan failed: " + (err.message || err)); }
1573
+ } catch (err) { supervisorFeedback("Plugin rescan failed: " + (err.message || err), true); }
1560
1574
  }
1561
1575
 
1562
1576
  async function runSupervisorPlugin(id, action) {
1563
1577
  try {
1564
- await supervisorJson("/v1/plugins/" + encodeURIComponent(id) + "/" + encodeURIComponent(action), { method: "POST", headers: writeHeaders() });
1578
+ const receipt = await supervisorJson("/v1/plugins/" + encodeURIComponent(id) + "/" + encodeURIComponent(action), { method: "POST", headers: writeHeaders() });
1579
+ supervisorFeedback("Plugin " + action + " receipt " + (receipt.operationId || "recorded") + " · " + ((receipt.resulting_state && receipt.resulting_state.state) || "completed"), false);
1565
1580
  await loadSupervisorCockpit();
1566
- } catch (err) { alert("Plugin " + action + " failed: " + (err.message || err)); }
1581
+ } catch (err) { supervisorFeedback("Plugin " + action + " failed: " + (err.message || err), true); }
1567
1582
  }
1568
1583
 
1569
1584
  async function runSupervisorSurface(id, action) {
1570
1585
  try {
1571
- await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
1586
+ const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
1587
+ const result = receipt.resulting_state || {};
1588
+ supervisorFeedback("Surface " + action + " receipt " + (receipt.operationId || "recorded") + " · " + (result.state || "completed"), result.ok === false);
1572
1589
  await loadSupervisorCockpit();
1573
- } catch (err) { alert("Surface " + action + " failed: " + (err.message || err)); }
1590
+ } catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
1574
1591
  }
1575
1592
 
1576
1593
  // ── Unlock ──────────────────────────────────
@@ -140,6 +140,10 @@ export function validatePluginManifest(manifest, sourcePath = "") {
140
140
  id,
141
141
  version,
142
142
  publisher: String(manifest.publisher || "unknown"),
143
+ // Only trusted managed manifests may opt into automatic startup. User
144
+ // plugins remain awaiting_enable until an operator explicitly enables them.
145
+ core: manifest.core === true,
146
+ enable_default: manifest.enable_default === true,
143
147
  sourcePath,
144
148
  destination: normalizeDestination(manifest.destination),
145
149
  lifecycle_owner: normalizeLifecycleOwner(manifest.lifecycle_owner),
@@ -226,15 +230,17 @@ export function discoverPlugins() {
226
230
  throw new Error("user plugin cannot silently shadow a managed plugin id");
227
231
  }
228
232
  const prior = byId.get(manifest.id);
229
- const state = prior?.enabled ? (prior.manifest_hash === hash ? "current" : "version_drift") : "awaiting_enable";
230
- const plugin = { ...manifest, source, discovery_root: root, manifest_hash: hash, state, enabled: Boolean(prior?.enabled), discovered_at: now() };
233
+ const autoEnabled = source === "managed" && manifest.core === true && manifest.enable_default === true;
234
+ const enabled = Boolean(prior?.enabled || autoEnabled);
235
+ const state = enabled ? (prior?.manifest_hash === hash || autoEnabled ? "current" : "version_drift") : "awaiting_enable";
236
+ const plugin = { ...manifest, source, discovery_root: root, manifest_hash: hash, state, enabled, discovered_at: now() };
231
237
  plugins.push(plugin);
232
238
  for (const surface of plugin.surfaces) surfaces.push({ ...surface, enabled: plugin.enabled, state });
233
239
  for (const credential of plugin.credentials) {
234
240
  events.push({ kind: "credential_required", plugin_id: plugin.id, credential, value_set: false });
235
241
  }
236
242
  seen.add(plugin.id);
237
- events.push({ kind: "plugin_discovered", plugin_id: plugin.id, state, source, manifest_hash: hash });
243
+ events.push({ kind: autoEnabled && !prior?.enabled ? "core_plugin_auto_enabled" : "plugin_discovered", plugin_id: plugin.id, state, source, manifest_hash: hash });
238
244
  } catch (error) {
239
245
  const id = path.basename(path.dirname(manifestPath));
240
246
  plugins.push({
@@ -109,6 +109,24 @@ test("discovery merges managed and user roots without silent managed-id shadowin
109
109
  assert.match(invalid.error, /shadow/);
110
110
  }));
111
111
 
112
+ test("trusted managed core plugins auto-enable while user plugins remain opt-in", () => withTempSupervisor((root) => {
113
+ const managed = path.join(root, "managed");
114
+ const user = path.join(root, "user");
115
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
116
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = user;
117
+ writePlugin(root, "managed", "core-mcp", baseManifest("core-mcp", { core: true, enable_default: true }));
118
+ writePlugin(root, "user", "user-mcp", baseManifest("user-mcp", { core: true, enable_default: true }));
119
+
120
+ const result = discoverPlugins();
121
+ const core = result.plugins.find((plugin) => plugin.id === "core-mcp");
122
+ const userPlugin = result.plugins.find((plugin) => plugin.id === "user-mcp");
123
+ assert.equal(core.enabled, true);
124
+ assert.equal(core.state, "current");
125
+ assert.equal(result.events.some((event) => event.kind === "core_plugin_auto_enabled" && event.plugin_id === "core-mcp"), true);
126
+ assert.equal(userPlugin.enabled, false);
127
+ assert.equal(userPlugin.state, "awaiting_enable");
128
+ }));
129
+
112
130
  test("plugin test marks a private candidate and never creates a public route", () => withTempSupervisor((root) => {
113
131
  const managed = path.join(root, "managed");
114
132
  process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
@@ -0,0 +1,16 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const serveSource = fs.readFileSync(path.join(here, 'commands', 'serve.js'), 'utf8');
9
+
10
+ test('clauth supervisor UI exposes every lifecycle action and receipt feedback', () => {
11
+ for (const action of ['start', 'stop', 'restart', 'reconcile', 'test', 'promote', 'rollback']) {
12
+ assert.match(serveSource, new RegExp('"' + action + '"'));
13
+ }
14
+ assert.match(serveSource, /supervisorFeedback\(/);
15
+ assert.match(serveSource, /supervisor-feedback/);
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.30.11",
3
+ "version": "1.30.13",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {