@lifeaitools/clauth 1.30.16 → 1.30.18

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.
@@ -13,6 +13,7 @@ const ACTIONS = new Set(["start", "stop", "restart", "reconcile", "test", "promo
13
13
  const DEFAULT_HEALTH_RECONCILE_INTERVAL_MS = 10000;
14
14
  const DEFAULT_HEALTH_TIMEOUT_MS = 2500;
15
15
  const HEALTH_RECONCILE_COOLDOWN_MS = 15000;
16
+ const DOCUMENTATION_FIELDS = ["architecture", "operator_guide", "install", "runbook", "tool_reference", "release", "agent_context"];
16
17
 
17
18
  export function getSupervisorPort() {
18
19
  return Number(process.env.CLAUTH_SUPERVISOR_PORT || DEFAULT_SUPERVISOR_PORT);
@@ -188,6 +189,25 @@ function normalizeDestination(destination) {
188
189
  return value;
189
190
  }
190
191
 
192
+ function normalizeDocumentation(value) {
193
+ if (value == null) return null;
194
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("documentation must be an object");
195
+ const documentation = {};
196
+ for (const field of DOCUMENTATION_FIELDS) {
197
+ if (value[field] == null) continue;
198
+ if (typeof value[field] !== "string" || !value[field].trim()) {
199
+ throw new Error(`documentation.${field} must be a non-empty path`);
200
+ }
201
+ const ref = value[field].trim().replaceAll("\\", "/");
202
+ if (ref.startsWith("/") || ref.includes("..") || /^[a-z]+:/i.test(ref)) {
203
+ throw new Error(`documentation.${field} must be a repository-relative path`);
204
+ }
205
+ documentation[field] = ref;
206
+ }
207
+ if (!documentation.architecture) throw new Error("documentation.architecture is required");
208
+ return documentation;
209
+ }
210
+
191
211
  function localhostHealth(pathOrUrl, port) {
192
212
  if (!pathOrUrl) return null;
193
213
  if (/^https?:\/\//i.test(pathOrUrl)) {
@@ -237,6 +257,7 @@ export function validatePluginManifest(manifest, sourcePath = "") {
237
257
  id,
238
258
  version,
239
259
  publisher: String(manifest.publisher || "unknown"),
260
+ documentation: normalizeDocumentation(manifest.documentation),
240
261
  // Only trusted managed manifests may opt into automatic startup. User
241
262
  // plugins remain awaiting_enable until an operator explicitly enables them.
242
263
  core: manifest.core === true,
@@ -494,20 +515,33 @@ export function runSurfaceAction(id, action, actor = "localhost") {
494
515
  if (!command || command.length === 0) {
495
516
  return operation(action, { surface_id: id }, surface, { ok: false, state: "command_missing" }, actor);
496
517
  }
497
- const [cmd, ...args] = command;
498
- const result = spawnSync(cmd, args, {
499
- cwd: surface.cwd || undefined,
500
- env: { ...process.env, CLAUTH_PM2_HOME: getClauthPm2Home(), PM2_HOME: getClauthPm2Home() },
501
- windowsHide: true,
502
- encoding: "utf8",
503
- timeout: Number(surface.timeoutMs || 30000),
504
- });
518
+ const execute = (selectedCommand) => {
519
+ const [cmd, ...args] = selectedCommand;
520
+ return spawnSync(cmd, args, {
521
+ cwd: surface.cwd || undefined,
522
+ env: { ...process.env, CLAUTH_PM2_HOME: getClauthPm2Home(), PM2_HOME: getClauthPm2Home() },
523
+ windowsHide: true,
524
+ encoding: "utf8",
525
+ timeout: Number(surface.timeoutMs || 30000),
526
+ });
527
+ };
528
+ let result = execute(command);
529
+ let fallbackUsed = false;
530
+ // PM2 restart returns non-zero when the process was deleted. Reconcile is
531
+ // allowed to fall back to the declared start command; explicit restart keeps
532
+ // its strict failure semantics for operator-requested actions.
533
+ if (action === "reconcile" && result.status !== 0 && Array.isArray(surface.start) && surface.start.length > 0 && command !== surface.start) {
534
+ const restartStatus = result.status;
535
+ result = execute(surface.start);
536
+ fallbackUsed = true;
537
+ result.stderr = `restart exited ${restartStatus}; start fallback attempted\n${result.stderr || ""}`;
538
+ }
505
539
  return operation(action, { surface_id: id }, surface, {
506
540
  ok: result.status === 0,
507
541
  state: result.status === 0 ? "operation_completed" : "operation_failed",
508
542
  status: result.status,
509
543
  stderr: result.stderr?.slice(0, 2000),
510
- evidence: [`CLAUTH_PM2_HOME=${getClauthPm2Home()}`],
544
+ evidence: [`CLAUTH_PM2_HOME=${getClauthPm2Home()}`, ...(fallbackUsed ? ["reconcile_start_fallback=true"] : [])],
511
545
  }, actor);
512
546
  }
513
547
 
@@ -66,6 +66,11 @@ function baseManifest(id, overrides = {}) {
66
66
  restart: ["node", "--version"],
67
67
  }],
68
68
  test: { command: ["node", "--version"], port: "auto", health: "/health", selfTest: [["node", "--version"]] },
69
+ documentation: {
70
+ architecture: "docs/systems/example/ARCHITECTURE.md",
71
+ operator_guide: "docs/systems/example/OPERATE.md",
72
+ tool_reference: ".claude/context/mcp-endpoint-design.md",
73
+ },
69
74
  ...overrides,
70
75
  };
71
76
  }
@@ -77,6 +82,8 @@ test("validatePluginManifest accepts LIFEAI plugin contract with isolated test c
77
82
  assert.equal(plugin.surfaces[0].destination, "local/clauth/pm2");
78
83
  assert.equal(plugin.surfaces[0].lifecycle_owner, "clauth");
79
84
  assert.equal(plugin.surfaces[0].health, "http://127.0.0.1:39111/health");
85
+ assert.equal(plugin.documentation.architecture, "docs/systems/example/ARCHITECTURE.md");
86
+ assert.equal(plugin.documentation.operator_guide, "docs/systems/example/OPERATE.md");
80
87
  });
81
88
 
82
89
  test("validatePluginManifest accepts empty test command arrays from the v1 template", () => {
@@ -92,6 +99,15 @@ test("validatePluginManifest rejects invalid manifests and non-local health URLs
92
99
  assert.throws(() => validatePluginManifest(baseManifest("bad", {
93
100
  surfaces: [{ id: "bad", health: "https://example.com/health" }],
94
101
  })), /localhost-only/);
102
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
103
+ documentation: { architecture: "../outside.md" },
104
+ })), /repository-relative/);
105
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
106
+ documentation: { operator_guide: "docs/guide.md" },
107
+ })), /documentation.architecture is required/);
108
+ assert.throws(() => validatePluginManifest(baseManifest("bad-docs", {
109
+ documentation: { architecture: "https://example.com/architecture.md" },
110
+ })), /repository-relative/);
95
111
  });
96
112
 
97
113
  test("discovery merges managed and user roots without silent managed-id shadowing", () => withTempSupervisor((root) => {
@@ -174,6 +190,29 @@ test("surface actions use dedicated clauth PM2 home and keep CodeFlow observe-on
174
190
  assert.equal(supervisorHealth().surfaces, 2);
175
191
  }));
176
192
 
193
+ test("reconcile falls back to the declared start command when restart reports a missing process", () => withTempSupervisor((root) => {
194
+ const managed = path.join(root, "managed");
195
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
196
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
197
+ writePlugin(root, "managed", "fallback-demo", baseManifest("fallback-demo", {
198
+ core: true,
199
+ enable_default: true,
200
+ surfaces: [{
201
+ id: "primary",
202
+ destination: "local/clauth/pm2",
203
+ lifecycle_owner: "clauth",
204
+ port: 39114,
205
+ health: "/health",
206
+ start: [process.execPath, "--version"],
207
+ restart: [process.execPath, "-e", "process.exit(1)"],
208
+ }],
209
+ }));
210
+ discoverPlugins();
211
+ const receipt = runSurfaceAction("fallback-demo:primary", "reconcile");
212
+ assert.equal(receipt.resulting_state.ok, true);
213
+ assert.equal(receipt.resulting_state.evidence.includes("reconcile_start_fallback=true"), true);
214
+ }));
215
+
177
216
  test("surface promote and rollback never fall through to restart commands", () => withTempSupervisor((root) => {
178
217
  const managed = path.join(root, "managed");
179
218
  process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.30.16",
3
+ "version": "1.30.18",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {