@kal-elsam/kairo-runtime 0.8.0 → 0.10.0
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/README.md +41 -15
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +6 -6
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/cli.js +24 -1
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/global-doctor.js +2 -0
- package/src/global/ink/cockpit/primitives.js +127 -50
- package/src/global/ink/cockpit-alerts.js +36 -0
- package/src/global/ink/cockpit-changes.js +61 -37
- package/src/global/ink/cockpit-control-center.js +79 -53
- package/src/global/ink/cockpit-controller.js +98 -15
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-focus.js +4 -2
- package/src/global/ink/cockpit-models.js +100 -51
- package/src/global/ink/cockpit-palette.js +109 -0
- package/src/global/ink/cockpit-path-label.js +19 -0
- package/src/global/ink/cockpit-recovery.js +84 -18
- package/src/global/ink/cockpit-reviews.js +14 -10
- package/src/global/ink/cockpit-runs.js +13 -4
- package/src/global/ink/cockpit-settings.js +194 -0
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +119 -116
- package/src/global/ink/orchestrator-app.js +169 -46
- package/src/global/ink/orchestrator-state.js +24 -14
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +27 -0
- package/src/global/ink/use-orchestrator-data.js +58 -0
- package/src/global/ink/ux/live-activity.js +194 -0
- package/src/global/ink/ux/live-alerts.js +159 -0
- package/src/global/ink/ux/live-governance.js +195 -0
- package/src/global/ink/ux/live-orchestration.js +188 -0
- package/src/global/ink/ux/live-overview.js +125 -0
- package/src/global/ink/ux/live-settings.js +189 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +51 -0
- package/src/global/ink/ux/semantic.js +84 -0
- package/src/global/ink/ux/task-flow-app.js +85 -0
- package/src/global/ink/ux/task-flow.js +173 -0
- package/src/global/orchestrator.js +37 -20
- package/src/global/paths.js +3 -0
- package/src/global/runtime/alerts/alert-store.js +216 -0
- package/src/global/runtime/alerts/alert-types.js +59 -0
- package/src/global/runtime/alerts/alert-validate.js +117 -0
- package/src/global/runtime/monitor/monitor-cli.js +62 -0
- package/src/global/runtime/monitor/monitor-platform.js +95 -0
- package/src/global/runtime/monitor/monitor.js +249 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ALERT_STATES = Object.freeze({
|
|
4
|
+
OPEN: "open", RESOLVED: "resolved", DISMISSED: "dismissed"
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const ALERT_SEVERITIES = Object.freeze({
|
|
8
|
+
HIGH: "high", MEDIUM: "medium", LOW: "low"
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function createAlertId() {
|
|
12
|
+
return `alt-${randomBytes(12).toString("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertSafeAlertId(alertId) {
|
|
16
|
+
if (typeof alertId !== "string" || !/^alt-[a-f0-9]{16,32}$/.test(alertId)) {
|
|
17
|
+
throw new Error(`Invalid alert id "${alertId}".`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Stable dedupe key — kind + source + title only (never payloads). */
|
|
22
|
+
export function createAlertFingerprint({ kind, source = null, title }) {
|
|
23
|
+
return createHash("sha256")
|
|
24
|
+
.update([kind, source ?? "", title].map(String).join("\0"))
|
|
25
|
+
.digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAlert({
|
|
29
|
+
alertId = createAlertId(),
|
|
30
|
+
kind,
|
|
31
|
+
severity = ALERT_SEVERITIES.MEDIUM,
|
|
32
|
+
title,
|
|
33
|
+
summary = "",
|
|
34
|
+
source = null,
|
|
35
|
+
state = ALERT_STATES.OPEN,
|
|
36
|
+
createdAt = null,
|
|
37
|
+
updatedAt = null,
|
|
38
|
+
resolvedAt = null
|
|
39
|
+
} = {}) {
|
|
40
|
+
assertSafeAlertId(alertId);
|
|
41
|
+
const safeKind = String(kind ?? "").trim();
|
|
42
|
+
const safeTitle = String(title ?? "").trim();
|
|
43
|
+
if (!safeKind || !safeTitle) throw new Error("Alert kind and title are required.");
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
return {
|
|
46
|
+
version: 1,
|
|
47
|
+
alertId,
|
|
48
|
+
kind: safeKind,
|
|
49
|
+
severity,
|
|
50
|
+
title: safeTitle,
|
|
51
|
+
summary: String(summary ?? "").trim(),
|
|
52
|
+
source: source == null ? null : String(source),
|
|
53
|
+
fingerprint: createAlertFingerprint({ kind: safeKind, source, title: safeTitle }),
|
|
54
|
+
state,
|
|
55
|
+
createdAt: createdAt ?? now,
|
|
56
|
+
updatedAt: updatedAt ?? now,
|
|
57
|
+
resolvedAt: resolvedAt ?? null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALERT_SEVERITIES,
|
|
3
|
+
ALERT_STATES,
|
|
4
|
+
assertSafeAlertId,
|
|
5
|
+
createAlertFingerprint
|
|
6
|
+
} from "./alert-types.js";
|
|
7
|
+
|
|
8
|
+
export const ALERT_VALIDATION_ERROR_CODES = Object.freeze({
|
|
9
|
+
INVALID_ALERT: "invalid_alert",
|
|
10
|
+
FORBIDDEN_FIELD: "forbidden_field",
|
|
11
|
+
FINGERPRINT_MISMATCH: "fingerprint_mismatch"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const SEVERITY_SET = new Set(Object.values(ALERT_SEVERITIES));
|
|
15
|
+
const STATE_SET = new Set(Object.values(ALERT_STATES));
|
|
16
|
+
const FORBIDDEN_KEYS = new Set([
|
|
17
|
+
"prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr",
|
|
18
|
+
"output", "message", "messages", "content", "secret", "secrets", "token", "apiKey"
|
|
19
|
+
]);
|
|
20
|
+
const ALLOWED = new Set([
|
|
21
|
+
"version", "alertId", "kind", "severity", "title", "summary",
|
|
22
|
+
"source", "fingerprint", "state", "createdAt", "updatedAt", "resolvedAt"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export class AlertValidationError extends Error {
|
|
26
|
+
constructor(message, { code, details = null } = {}) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "AlertValidationError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.details = details;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertNoForbiddenKeys(value, label) {
|
|
35
|
+
if (!value || typeof value !== "object") return;
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
for (const item of value) assertNoForbiddenKeys(item, label);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const [key, child] of Object.entries(value)) {
|
|
41
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
42
|
+
throw new AlertValidationError(`Forbidden field "${key}" in ${label}.`, {
|
|
43
|
+
code: ALERT_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
|
|
44
|
+
details: { key, label }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
assertNoForbiddenKeys(child, `${label}.${key}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireString(alert, key, { allowEmpty = false } = {}) {
|
|
52
|
+
const value = alert[key];
|
|
53
|
+
if (typeof value !== "string" || (!allowEmpty && !value.trim())) {
|
|
54
|
+
throw new AlertValidationError(`Invalid alert.${key}: expected string.`, {
|
|
55
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT,
|
|
56
|
+
details: { key }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fail-closed scalar schema + recursive secret scan + derived fingerprint. */
|
|
63
|
+
export function assertAlertSecretFree(alert) {
|
|
64
|
+
if (!alert || typeof alert !== "object" || Array.isArray(alert)) {
|
|
65
|
+
throw new AlertValidationError("Invalid alert: expected object.", {
|
|
66
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
assertNoForbiddenKeys(alert, "alert");
|
|
70
|
+
for (const key of Object.keys(alert)) {
|
|
71
|
+
if (!ALLOWED.has(key)) {
|
|
72
|
+
throw new AlertValidationError(`Unknown field "${key}" in alert.`, {
|
|
73
|
+
code: ALERT_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
|
|
74
|
+
details: { key }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (typeof alert.version !== "number" || !Number.isFinite(alert.version)) {
|
|
79
|
+
throw new AlertValidationError("Invalid alert.version.", {
|
|
80
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
assertSafeAlertId(requireString(alert, "alertId"));
|
|
84
|
+
requireString(alert, "kind");
|
|
85
|
+
requireString(alert, "title");
|
|
86
|
+
requireString(alert, "summary", { allowEmpty: true });
|
|
87
|
+
requireString(alert, "fingerprint");
|
|
88
|
+
requireString(alert, "createdAt");
|
|
89
|
+
requireString(alert, "updatedAt");
|
|
90
|
+
if (!(alert.source === null || typeof alert.source === "string")) {
|
|
91
|
+
throw new AlertValidationError("Invalid alert.source.", {
|
|
92
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!(alert.resolvedAt === null || typeof alert.resolvedAt === "string")) {
|
|
96
|
+
throw new AlertValidationError("Invalid alert.resolvedAt.", {
|
|
97
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!SEVERITY_SET.has(alert.severity) || !STATE_SET.has(alert.state)) {
|
|
101
|
+
throw new AlertValidationError("Unknown alert severity or state.", {
|
|
102
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const expected = createAlertFingerprint({
|
|
106
|
+
kind: alert.kind,
|
|
107
|
+
source: alert.source,
|
|
108
|
+
title: alert.title
|
|
109
|
+
});
|
|
110
|
+
if (alert.fingerprint !== expected) {
|
|
111
|
+
throw new AlertValidationError("Alert fingerprint does not match kind+source+title.", {
|
|
112
|
+
code: ALERT_VALIDATION_ERROR_CODES.FINGERPRINT_MISMATCH,
|
|
113
|
+
details: { expected }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return alert;
|
|
117
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveHomeDir } from "../../paths.js";
|
|
3
|
+
import { printJson } from "../../json-output.js";
|
|
4
|
+
import { commandHeader } from "../../brand/index.js";
|
|
5
|
+
import {
|
|
6
|
+
disableMonitor, enableMonitor, getMonitorStatus, runMonitorTick
|
|
7
|
+
} from "./monitor.js";
|
|
8
|
+
|
|
9
|
+
export async function runGlobalMonitor(options, _manifest, deps = {}) {
|
|
10
|
+
const homeDir = deps.homeDir ?? resolveHomeDir();
|
|
11
|
+
const action = options.monitorAction ?? "status";
|
|
12
|
+
const packageRoot = deps.packageRoot;
|
|
13
|
+
const entry = deps.cliEntry ?? (packageRoot ? join(packageRoot, "bin", "kairo.js") : null);
|
|
14
|
+
try {
|
|
15
|
+
if (action === "enable") {
|
|
16
|
+
await (deps.enableMonitorImpl ?? enableMonitor)(homeDir, {
|
|
17
|
+
cliEntry: entry, nodePath: deps.nodePath ?? process.execPath, platform: deps.platform
|
|
18
|
+
});
|
|
19
|
+
await (deps.runMonitorTickImpl ?? runMonitorTick)(homeDir, {
|
|
20
|
+
packageRoot, workspaceRoot: options.cwd, notifyImpl: deps.notifyImpl
|
|
21
|
+
});
|
|
22
|
+
} else if (action === "disable") {
|
|
23
|
+
await (deps.disableMonitorImpl ?? disableMonitor)(homeDir, { platform: deps.platform });
|
|
24
|
+
} else if (action === "tick") {
|
|
25
|
+
const result = await (deps.runMonitorTickImpl ?? runMonitorTick)(homeDir, {
|
|
26
|
+
packageRoot, workspaceRoot: options.cwd, notifyImpl: deps.notifyImpl
|
|
27
|
+
});
|
|
28
|
+
return done(options, { ok: true, action, raised: result.raised.length, lastTick: result.state.lastTick });
|
|
29
|
+
}
|
|
30
|
+
const status = await (deps.getMonitorStatusImpl ?? getMonitorStatus)(homeDir, {
|
|
31
|
+
platform: deps.platform
|
|
32
|
+
});
|
|
33
|
+
return done(options, { ok: true, action: action === "status" ? "status" : action, ...status });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const message = String(error?.message ?? error);
|
|
36
|
+
if (options.json) printJson({ ok: false, error: message });
|
|
37
|
+
else console.error(message);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return { ok: false, error: message };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function done(options, payload) {
|
|
44
|
+
if (options.json) { printJson(payload); return payload; }
|
|
45
|
+
console.log(commandHeader(`monitor ${payload.action}`));
|
|
46
|
+
if (payload.action === "tick") {
|
|
47
|
+
console.log(`Raised ${payload.raised} alert(s) this tick.`);
|
|
48
|
+
return payload;
|
|
49
|
+
}
|
|
50
|
+
console.log(`Enabled: ${payload.corrupt ? "unavailable" : payload.enabled ? "yes" : "no"} · ${payload.platform}`);
|
|
51
|
+
if (payload.corrupt) console.log("State: corrupt — run monitor disable to repair");
|
|
52
|
+
else {
|
|
53
|
+
const a = payload.autostart;
|
|
54
|
+
console.log(`Autostart: ${a?.loaded ? "loaded" : a?.configured ? "configured (not loaded)" : "off"}`
|
|
55
|
+
+ `${a?.supported === false ? " (unsupported)" : ""}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(`Open alerts: ${payload.openAlerts ?? "unavailable"}`);
|
|
58
|
+
if (payload.lastTickAt) {
|
|
59
|
+
console.log(`Last tick: ${payload.lastTickAt}${payload.lastTick?.complete === false ? " (incomplete)" : ""}`);
|
|
60
|
+
}
|
|
61
|
+
return payload;
|
|
62
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFile = promisify(execFileCb);
|
|
9
|
+
export const MONITOR_LABEL = "local.kairo.monitor";
|
|
10
|
+
|
|
11
|
+
export function resolveMonitorPlatform(platform = process.platform) {
|
|
12
|
+
if (platform === "darwin") {
|
|
13
|
+
const agentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
14
|
+
return {
|
|
15
|
+
id: "darwin", supportsAutostart: true, supportsNotify: true,
|
|
16
|
+
agentsDir, plistPath: join(agentsDir, `${MONITOR_LABEL}.plist`)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { id: platform, supportsAutostart: false, supportsNotify: platform === "linux" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function gui() { return `gui/${process.getuid?.() ?? 501}`; }
|
|
23
|
+
|
|
24
|
+
function plist({ nodePath, cliEntry, homeDir, intervalSec }) {
|
|
25
|
+
const e = (v) => String(v).replaceAll("&", "&").replaceAll("<", "<")
|
|
26
|
+
.replaceAll(">", ">").replaceAll("\"", """);
|
|
27
|
+
const log = join(homeDir, ".harness", "monitor");
|
|
28
|
+
const n = Math.max(60, Number(intervalSec) || 300);
|
|
29
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
30
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
31
|
+
<plist version="1.0"><dict>
|
|
32
|
+
<key>Label</key><string>${MONITOR_LABEL}</string>
|
|
33
|
+
<key>ProgramArguments</key><array><string>${e(nodePath)}</string><string>${e(cliEntry)}</string><string>monitor</string><string>tick</string></array>
|
|
34
|
+
<key>StartInterval</key><integer>${n}</integer><key>RunAtLoad</key><true/>
|
|
35
|
+
<key>EnvironmentVariables</key><dict><key>HARNESS_HOME</key><string>${e(homeDir)}</string></dict>
|
|
36
|
+
<key>StandardOutPath</key><string>${e(join(log, "out.log"))}</string>
|
|
37
|
+
<key>StandardErrorPath</key><string>${e(join(log, "err.log"))}</string>
|
|
38
|
+
</dict></plist>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function notifyNewAlert({
|
|
42
|
+
title, body, platform = resolveMonitorPlatform(), execFileImpl = execFile
|
|
43
|
+
} = {}) {
|
|
44
|
+
const t = String(title ?? "Kairo").slice(0, 80);
|
|
45
|
+
const b = String(body ?? "").slice(0, 180).replace(/[\r\n]+/g, " ");
|
|
46
|
+
try {
|
|
47
|
+
if (platform.id === "darwin") {
|
|
48
|
+
await execFileImpl("osascript", [
|
|
49
|
+
"-e", `display notification ${JSON.stringify(b)} with title ${JSON.stringify(t)}`
|
|
50
|
+
], { shell: false, timeout: 5000 });
|
|
51
|
+
return { sent: true };
|
|
52
|
+
}
|
|
53
|
+
if (platform.id === "linux") {
|
|
54
|
+
await execFileImpl("notify-send", [t, b], { shell: false, timeout: 5000 });
|
|
55
|
+
return { sent: true };
|
|
56
|
+
}
|
|
57
|
+
} catch { /* degrade */ }
|
|
58
|
+
return { sent: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function installAutostart({
|
|
62
|
+
homeDir, platform = resolveMonitorPlatform(), nodePath, cliEntry,
|
|
63
|
+
intervalSec = 300, execFileImpl = execFile
|
|
64
|
+
} = {}) {
|
|
65
|
+
if (!platform.supportsAutostart) {
|
|
66
|
+
return {
|
|
67
|
+
supported: false, configured: false, loaded: false, installed: false,
|
|
68
|
+
detail: `unsupported on ${platform.id}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
await mkdir(platform.agentsDir, { recursive: true });
|
|
72
|
+
await mkdir(join(homeDir, ".harness", "monitor"), { recursive: true });
|
|
73
|
+
await writeFile(platform.plistPath, plist({ nodePath, cliEntry, homeDir, intervalSec }));
|
|
74
|
+
try {
|
|
75
|
+
await execFileImpl("launchctl", ["bootout", `${gui()}/${MONITOR_LABEL}`], { shell: false }).catch(() => {});
|
|
76
|
+
await execFileImpl("launchctl", ["bootstrap", gui(), platform.plistPath], { shell: false });
|
|
77
|
+
return { supported: true, configured: true, loaded: true, installed: true, detail: "LaunchAgent loaded" };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
supported: true, configured: true, loaded: false, installed: false,
|
|
81
|
+
detail: `plist configured; not loaded (${error?.message ?? error})`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function removeAutostart({
|
|
87
|
+
platform = resolveMonitorPlatform(), execFileImpl = execFile
|
|
88
|
+
} = {}) {
|
|
89
|
+
if (!platform.supportsAutostart) {
|
|
90
|
+
return { supported: false, configured: false, loaded: false, installed: false };
|
|
91
|
+
}
|
|
92
|
+
await execFileImpl("launchctl", ["bootout", `${gui()}/${MONITOR_LABEL}`], { shell: false }).catch(() => {});
|
|
93
|
+
if (platform.plistPath && existsSync(platform.plistPath)) await unlink(platform.plistPath).catch(() => {});
|
|
94
|
+
return { supported: true, configured: false, loaded: false, installed: false };
|
|
95
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { harnessHomePaths } from "../../paths.js";
|
|
4
|
+
import { detectGlobalDrift, hasRepairableDrift } from "../../drift.js";
|
|
5
|
+
import { writeAtomicJson } from "../write-atomic-json.js";
|
|
6
|
+
import { listAlerts, saveAlert } from "../alerts/alert-store.js";
|
|
7
|
+
import { ALERT_SEVERITIES, ALERT_STATES } from "../alerts/alert-types.js";
|
|
8
|
+
import { listRunRecords } from "../run-store.js";
|
|
9
|
+
import { isRunAlive } from "../run-liveness.js";
|
|
10
|
+
import { RUN_STATES, isActiveRunState } from "../run-types.js";
|
|
11
|
+
import {
|
|
12
|
+
installAutostart, notifyNewAlert, removeAutostart, resolveMonitorPlatform
|
|
13
|
+
} from "./monitor-platform.js";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
installAutostart, notifyNewAlert, removeAutostart, resolveMonitorPlatform
|
|
17
|
+
} from "./monitor-platform.js";
|
|
18
|
+
|
|
19
|
+
const SOURCE = "monitor";
|
|
20
|
+
const INTERVAL = 300;
|
|
21
|
+
|
|
22
|
+
export class MonitorStateError extends Error {
|
|
23
|
+
constructor(message, { code = "corrupt_monitor_state" } = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "MonitorStateError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defaultMonitorState() {
|
|
31
|
+
return {
|
|
32
|
+
version: 1, enabled: false, intervalSec: INTERVAL,
|
|
33
|
+
lastTickAt: null, lastTick: null,
|
|
34
|
+
autostart: {
|
|
35
|
+
platform: null, installed: false, supported: false, configured: false, loaded: false
|
|
36
|
+
},
|
|
37
|
+
updatedAt: null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertMonitorState(raw) {
|
|
42
|
+
if (!raw || typeof raw !== "object" || raw.version !== 1 || typeof raw.enabled !== "boolean") {
|
|
43
|
+
throw new MonitorStateError("Monitor state schema invalid.");
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isFinite(raw.intervalSec) || raw.intervalSec < 1) {
|
|
46
|
+
throw new MonitorStateError("Monitor state.intervalSec invalid.");
|
|
47
|
+
}
|
|
48
|
+
const a = raw.autostart;
|
|
49
|
+
if (!a || typeof a !== "object"
|
|
50
|
+
|| typeof a.supported !== "boolean"
|
|
51
|
+
|| typeof a.configured !== "boolean"
|
|
52
|
+
|| typeof a.loaded !== "boolean"
|
|
53
|
+
|| typeof a.installed !== "boolean") {
|
|
54
|
+
throw new MonitorStateError("Monitor state.autostart invalid.");
|
|
55
|
+
}
|
|
56
|
+
if (a.loaded && !a.configured) {
|
|
57
|
+
throw new MonitorStateError("Monitor state.autostart loaded requires configured.");
|
|
58
|
+
}
|
|
59
|
+
if (a.installed !== a.loaded) {
|
|
60
|
+
throw new MonitorStateError("Monitor state.autostart installed must equal loaded.");
|
|
61
|
+
}
|
|
62
|
+
if (!a.supported && (a.configured || a.loaded || a.installed)) {
|
|
63
|
+
throw new MonitorStateError("Monitor state.autostart unsupported with lifecycle flags.");
|
|
64
|
+
}
|
|
65
|
+
if (!raw.enabled && (a.configured || a.loaded || a.installed)) {
|
|
66
|
+
throw new MonitorStateError("Monitor disabled with active autostart lifecycle.");
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...defaultMonitorState(),
|
|
70
|
+
...raw,
|
|
71
|
+
version: 1,
|
|
72
|
+
enabled: raw.enabled,
|
|
73
|
+
intervalSec: raw.intervalSec,
|
|
74
|
+
autostart: { ...defaultMonitorState().autostart, ...a }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function readMonitorState(homeDir, { repair = false } = {}) {
|
|
79
|
+
const path = harnessHomePaths(homeDir).monitorStatePath;
|
|
80
|
+
if (!existsSync(path)) return defaultMonitorState();
|
|
81
|
+
try {
|
|
82
|
+
return assertMonitorState(JSON.parse(await readFile(path, "utf8")));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (repair) return defaultMonitorState();
|
|
85
|
+
if (error instanceof MonitorStateError) throw error;
|
|
86
|
+
throw new MonitorStateError("Monitor state unreadable.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function writeMonitorState(homeDir, patch, { repair = false } = {}) {
|
|
91
|
+
const { monitorDir, monitorStatePath } = harnessHomePaths(homeDir);
|
|
92
|
+
await mkdir(monitorDir, { recursive: true });
|
|
93
|
+
const next = {
|
|
94
|
+
...await readMonitorState(homeDir, { repair }), ...patch,
|
|
95
|
+
updatedAt: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
await writeAtomicJson(monitorStatePath, next);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function raise(homeDir, input, notifyImpl) {
|
|
102
|
+
const result = await saveAlert({ ...input, source: SOURCE }, { homeDir });
|
|
103
|
+
if (!result.deduped) {
|
|
104
|
+
await notifyImpl({ title: "Kairo", body: `${result.alert.severity} · ${result.alert.title}` });
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function autostartRecord(platform, a) {
|
|
110
|
+
return {
|
|
111
|
+
platform: platform.id, supported: a.supported,
|
|
112
|
+
configured: Boolean(a.configured), loaded: Boolean(a.loaded),
|
|
113
|
+
installed: Boolean(a.loaded), detail: a.detail ?? null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function runMonitorTick(homeDir, deps = {}) {
|
|
118
|
+
const {
|
|
119
|
+
notifyImpl = notifyNewAlert, detectDriftImpl = detectGlobalDrift,
|
|
120
|
+
listRunsImpl = listRunRecords, isRunAliveImpl = isRunAlive,
|
|
121
|
+
packageRoot = null, workspaceRoot = null
|
|
122
|
+
} = deps;
|
|
123
|
+
const raised = [];
|
|
124
|
+
await mkdir(harnessHomePaths(homeDir).monitorDir, { recursive: true });
|
|
125
|
+
try {
|
|
126
|
+
const paths = harnessHomePaths(homeDir);
|
|
127
|
+
const state = existsSync(paths.statePath)
|
|
128
|
+
? JSON.parse(await readFile(paths.statePath, "utf8")) : null;
|
|
129
|
+
if (hasRepairableDrift(await detectDriftImpl({
|
|
130
|
+
homeDir, paths, state, packageRoot, workspaceRoot, context: { homeDir }
|
|
131
|
+
}))) {
|
|
132
|
+
raised.push(await raise(homeDir, {
|
|
133
|
+
kind: "monitor.drift", title: "Managed configuration drift",
|
|
134
|
+
summary: "Managed configs drifted. Run kairo sync.", severity: ALERT_SEVERITIES.HIGH
|
|
135
|
+
}, notifyImpl));
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
raised.push(await raise(homeDir, {
|
|
139
|
+
kind: "monitor.drift", title: "Governance scan unavailable",
|
|
140
|
+
summary: "Monitor could not complete the drift scan.", severity: ALERT_SEVERITIES.MEDIUM
|
|
141
|
+
}, notifyImpl));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let runsOk = true; let dead = 0; let failed = 0;
|
|
145
|
+
try {
|
|
146
|
+
for (const run of await listRunsImpl(homeDir, { limit: 40 })) {
|
|
147
|
+
if (isActiveRunState(run.state) && !(await isRunAliveImpl(homeDir, run))) dead += 1;
|
|
148
|
+
if (run.state === RUN_STATES.FAILED) failed += 1;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
runsOk = false;
|
|
152
|
+
raised.push(await raise(homeDir, {
|
|
153
|
+
kind: "monitor.runs-unavailable", title: "Run monitoring unavailable",
|
|
154
|
+
summary: "Monitor could not inspect agent run health this tick.",
|
|
155
|
+
severity: ALERT_SEVERITIES.MEDIUM
|
|
156
|
+
}, notifyImpl));
|
|
157
|
+
}
|
|
158
|
+
if (runsOk && dead > 0) {
|
|
159
|
+
raised.push(await raise(homeDir, {
|
|
160
|
+
kind: "run.orphaned", title: "Orphaned agent run",
|
|
161
|
+
summary: `${dead} active run(s) have no live process.`, severity: ALERT_SEVERITIES.HIGH
|
|
162
|
+
}, notifyImpl));
|
|
163
|
+
}
|
|
164
|
+
if (runsOk && failed > 0) {
|
|
165
|
+
raised.push(await raise(homeDir, {
|
|
166
|
+
kind: "run.failed", title: "Agent run failed",
|
|
167
|
+
summary: `${failed} failed run(s) need attention.`, severity: ALERT_SEVERITIES.MEDIUM
|
|
168
|
+
}, notifyImpl));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const lastTick = {
|
|
172
|
+
raised: raised.length,
|
|
173
|
+
created: raised.filter((r) => !r.deduped).length,
|
|
174
|
+
deduped: raised.filter((r) => r.deduped).length,
|
|
175
|
+
complete: runsOk, runs: runsOk ? "ok" : "unavailable"
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
state: await writeMonitorState(homeDir, {
|
|
179
|
+
lastTickAt: new Date().toISOString(), lastTick
|
|
180
|
+
}),
|
|
181
|
+
raised
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function enableMonitor(homeDir, {
|
|
186
|
+
cliEntry, nodePath = process.execPath, platform = resolveMonitorPlatform(), intervalSec = INTERVAL
|
|
187
|
+
} = {}) {
|
|
188
|
+
const autostart = await installAutostart({ homeDir, platform, nodePath, cliEntry, intervalSec });
|
|
189
|
+
try {
|
|
190
|
+
return await writeMonitorState(homeDir, {
|
|
191
|
+
enabled: true, intervalSec, autostart: autostartRecord(platform, autostart)
|
|
192
|
+
}, { repair: true });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (autostart.loaded) await removeAutostart({ platform });
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function disableMonitor(homeDir, { platform = resolveMonitorPlatform() } = {}) {
|
|
200
|
+
return writeMonitorState(homeDir, {
|
|
201
|
+
enabled: false, autostart: autostartRecord(platform, await removeAutostart({ platform }))
|
|
202
|
+
}, { repair: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function getMonitorStatus(homeDir, { platform = resolveMonitorPlatform() } = {}) {
|
|
206
|
+
try {
|
|
207
|
+
const state = await readMonitorState(homeDir);
|
|
208
|
+
let openAlerts = 0;
|
|
209
|
+
try {
|
|
210
|
+
openAlerts = (await listAlerts({ homeDir, state: ALERT_STATES.OPEN })).length;
|
|
211
|
+
} catch { openAlerts = null; }
|
|
212
|
+
return {
|
|
213
|
+
available: true, corrupt: false, enabled: state.enabled, intervalSec: state.intervalSec,
|
|
214
|
+
lastTickAt: state.lastTickAt, lastTick: state.lastTick, autostart: state.autostart,
|
|
215
|
+
platform: platform.id,
|
|
216
|
+
notify: { supported: platform.supportsNotify, backend: platform.id }, openAlerts
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error?.code !== "corrupt_monitor_state") throw error;
|
|
220
|
+
return {
|
|
221
|
+
available: false, corrupt: true, enabled: null, intervalSec: null,
|
|
222
|
+
lastTickAt: null, lastTick: null, autostart: null, platform: platform.id,
|
|
223
|
+
notify: { supported: platform.supportsNotify, backend: platform.id },
|
|
224
|
+
openAlerts: null, error: error.message
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function monitorDoctorCheck(homeDir) {
|
|
230
|
+
const s = await getMonitorStatus(homeDir);
|
|
231
|
+
if (s.corrupt || s.available === false) {
|
|
232
|
+
return {
|
|
233
|
+
name: "monitor", status: "stale", category: "monitor",
|
|
234
|
+
detail: "corrupt state — run kairo monitor disable to repair"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (s.enabled && s.autostart?.supported && !s.autostart?.loaded) {
|
|
238
|
+
return {
|
|
239
|
+
name: "monitor", status: "stale", category: "monitor",
|
|
240
|
+
detail: "enabled but autostart not loaded"
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
name: "monitor", status: "ok", category: "monitor",
|
|
245
|
+
detail: s.enabled
|
|
246
|
+
? `enabled · last ${s.lastTickAt ?? "none"} · autostart ${s.autostart?.loaded ? "loaded" : "off"}`
|
|
247
|
+
: "disabled (opt-in · kairo monitor enable)"
|
|
248
|
+
};
|
|
249
|
+
}
|