@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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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;
|