@lifeaitools/clauth 1.30.15 → 1.30.16

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.
@@ -82,6 +82,23 @@ function appendSupervisorEvent(event) {
82
82
  appendJsonl(file("events.jsonl"), { ts: now(), ...event });
83
83
  }
84
84
 
85
+ async function probeSurfaceHealth(url, fetchImpl, timeoutMs) {
86
+ try {
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
89
+ try {
90
+ const response = await fetchImpl(url, { signal: controller.signal });
91
+ return response?.ok
92
+ ? { healthy: true, error: null }
93
+ : { healthy: false, error: `HTTP ${response?.status ?? "unknown"}` };
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ } catch (err) {
98
+ return { healthy: false, error: err?.name === "AbortError" ? "health timeout" : String(err?.message || err) };
99
+ }
100
+ }
101
+
85
102
  /**
86
103
  * Probe enabled local clauth-owned surfaces and repair an unavailable one via
87
104
  * the same governed action path exposed to Dev Center. External/plugin-owned
@@ -95,21 +112,9 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
95
112
  const url = healthUrlForSurface(surface);
96
113
  if (!url) continue;
97
114
  const observedAt = now();
98
- let healthy = false;
99
- let error = null;
100
- try {
101
- const controller = new AbortController();
102
- const timer = setTimeout(() => controller.abort(), timeoutMs);
103
- try {
104
- const response = await fetchImpl(url, { signal: controller.signal });
105
- healthy = Boolean(response?.ok);
106
- if (!healthy) error = `HTTP ${response?.status ?? "unknown"}`;
107
- } finally {
108
- clearTimeout(timer);
109
- }
110
- } catch (err) {
111
- error = err?.name === "AbortError" ? "health timeout" : String(err?.message || err);
112
- }
115
+ const health = await probeSurfaceHealth(url, fetchImpl, timeoutMs);
116
+ const healthy = health.healthy;
117
+ const error = health.error;
113
118
 
114
119
  if (healthy) {
115
120
  updateSurfaceState(id, { state: "current", last_health_at: observedAt, last_health_ok: true, last_health_error: null });
@@ -133,9 +138,18 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
133
138
  });
134
139
  appendSupervisorEvent({ kind: "surface_health_failed", surface_id: id, error, health: url });
135
140
  const receipt = runSurfaceAction(id, "reconcile", "supervisor-health-loop");
136
- const repaired = receipt?.resulting_state?.ok === true;
137
- updateSurfaceState(id, { state: repaired ? "current" : "unavailable", last_reconcile_operation_id: receipt?.operationId || null });
138
- inspected.push({ surface_id: id, state: repaired ? "reconciled" : "reconcile_failed", error, operation_id: receipt?.operationId || null, observed_at: observedAt });
141
+ const commandCompleted = receipt?.resulting_state?.ok === true;
142
+ const postHealth = commandCompleted ? await probeSurfaceHealth(url, fetchImpl, timeoutMs) : { healthy: false, error: receipt?.resulting_state?.state || "reconcile_failed" };
143
+ const repaired = commandCompleted && postHealth.healthy;
144
+ updateSurfaceState(id, {
145
+ state: repaired ? "current" : "unavailable",
146
+ last_health_at: now(),
147
+ last_health_ok: repaired,
148
+ last_health_error: repaired ? null : postHealth.error,
149
+ last_reconcile_operation_id: receipt?.operationId || null,
150
+ });
151
+ appendSupervisorEvent({ kind: repaired ? "surface_reconciled" : "surface_reconcile_failed", surface_id: id, operation_id: receipt?.operationId || null, command_completed: commandCompleted, health_ok: postHealth.healthy, error: postHealth.error || null });
152
+ inspected.push({ surface_id: id, state: repaired ? "reconciled" : "reconcile_failed", error: postHealth.error || error, operation_id: receipt?.operationId || null, observed_at: observedAt });
139
153
  }
140
154
  return { inspected };
141
155
  }
@@ -218,14 +218,46 @@ test("health reconciliation marks a failed clauth surface and repairs it through
218
218
  }],
219
219
  }));
220
220
  discoverPlugins();
221
+ let healthCalls = 0;
221
222
  const result = await reconcileSurfaceHealth({
222
- fetchImpl: async () => ({ ok: false, status: 503 }),
223
+ fetchImpl: async () => ({ ok: ++healthCalls > 1, status: 503 }),
223
224
  });
224
225
  assert.equal(result.inspected[0].surface_id, "health-demo:primary");
225
226
  assert.equal(result.inspected[0].state, "reconciled");
226
227
  const surface = listSurfaces().find((item) => item.plugin_id === "health-demo");
227
228
  assert.equal(surface.state, "current");
228
229
  assert.ok(surface.last_reconcile_operation_id);
230
+ assert.equal(healthCalls, 2);
231
+ } finally {
232
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
233
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
234
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
235
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
236
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
237
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
238
+ fs.rmSync(root, { recursive: true, force: true });
239
+ }
240
+ });
241
+
242
+ test("health reconciliation does not claim current when a successful command leaves health down", async () => {
243
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-post-health-"));
244
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
245
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
246
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
247
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
248
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
249
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
250
+ try {
251
+ writePlugin(root, "managed", "post-health-demo", baseManifest("post-health-demo", {
252
+ core: true,
253
+ enable_default: true,
254
+ surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health", restart: [process.execPath, "--version"] }],
255
+ }));
256
+ discoverPlugins();
257
+ const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
258
+ assert.equal(result.inspected[0].state, "reconcile_failed");
259
+ assert.equal(listSurfaces()[0].state, "unavailable");
260
+ assert.equal(listSurfaces()[0].last_health_ok, false);
229
261
  } finally {
230
262
  if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
231
263
  else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.30.15",
3
+ "version": "1.30.16",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {