@kal-elsam/kairo-runtime 0.4.3 → 0.5.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 +124 -17
- package/global-template/components/catalog.json +63 -6
- package/global-template/components/sdd-core/personas/teaching.md +42 -0
- package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
- package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +29 -8
- package/scripts/smoke-test.sh +9 -0
- package/src/cli.js +54 -5
- package/src/global/action-planner.js +17 -4
- package/src/global/component-authoring.js +1 -2
- package/src/global/component-ecosystem-checks.js +74 -15
- package/src/global/component-health.js +56 -0
- package/src/global/component-integration-cli.js +302 -0
- package/src/global/component-manifest.js +214 -0
- package/src/global/component-registry.js +19 -2
- package/src/global/component-resolve.js +51 -0
- package/src/global/components/sdd-core.js +6 -0
- package/src/global/control-plane-proposals.js +229 -0
- package/src/global/control-plane-snapshot.js +212 -0
- package/src/global/dashboard-guidance.js +27 -3
- package/src/global/diff.js +2 -5
- package/src/global/global-cli.js +16 -1
- package/src/global/global-doctor.js +75 -2
- package/src/global/global-installer.js +43 -6
- package/src/global/governance-actions.js +212 -0
- package/src/global/governance-repair.js +13 -0
- package/src/global/ink/cockpit-changes.js +124 -0
- package/src/global/ink/cockpit-control-center.js +106 -0
- package/src/global/ink/cockpit-controller.js +17 -0
- package/src/global/ink/cockpit-enter.js +27 -0
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +98 -46
- package/src/global/ink/cockpit-proposals.js +82 -0
- package/src/global/ink/cockpit-recovery.js +120 -0
- package/src/global/ink/cockpit-runs.js +47 -0
- package/src/global/ink/cockpit-scan.js +60 -0
- package/src/global/ink/cockpit-views.js +144 -74
- package/src/global/ink/orchestrator-app.js +156 -42
- package/src/global/ink/orchestrator-state.js +6 -0
- package/src/global/ink/use-orchestrator-data.js +239 -17
- package/src/global/integrations/engram-apply.js +105 -0
- package/src/global/integrations/engram-evidence.js +218 -0
- package/src/global/integrations/engram-exec.js +107 -0
- package/src/global/integrations/engram-plan.js +60 -0
- package/src/global/integrations/engram-provider.js +22 -0
- package/src/global/integrations/engram-receipts.js +116 -0
- package/src/global/integrations/engram-rollback.js +131 -0
- package/src/global/integrations/index.js +12 -0
- package/src/global/integrations/provider-contract.js +83 -0
- package/src/global/integrations/provider-registry.js +47 -0
- package/src/global/integrations/sdd-apply.js +190 -0
- package/src/global/integrations/sdd-destinations.js +95 -0
- package/src/global/integrations/sdd-evidence.js +89 -0
- package/src/global/integrations/sdd-fs-guard.js +183 -0
- package/src/global/integrations/sdd-lifecycle.js +84 -0
- package/src/global/integrations/sdd-persona.js +71 -0
- package/src/global/integrations/sdd-plan.js +87 -0
- package/src/global/integrations/sdd-provider.js +21 -0
- package/src/global/integrations/sdd-receipts.js +44 -0
- package/src/global/integrations/sdd-rollback.js +269 -0
- package/src/global/integrations/sdd-skill-files.js +61 -0
- package/src/global/integrations/sdd-state.js +175 -0
- package/src/global/integrations/sdd-verify.js +122 -0
- package/src/global/intelligence/backends/opencode-catalog.js +212 -0
- package/src/global/intelligence/backends/opencode-cli.js +247 -0
- package/src/global/intelligence/backends/opencode-evidence.js +129 -0
- package/src/global/intelligence/backends/opencode-http.js +152 -0
- package/src/global/intelligence/backends/opencode-invoke.js +83 -0
- package/src/global/intelligence/backends/opencode-providers.js +67 -0
- package/src/global/intelligence/backends/opencode-runtime.js +242 -0
- package/src/global/intelligence/context-compiler.js +95 -50
- package/src/global/intelligence/index.js +38 -1
- package/src/global/intelligence/orchestrate.js +4 -2
- package/src/global/intelligence/registry.js +62 -6
- package/src/global/intelligence/router.js +188 -70
- package/src/global/intelligence/transport-registry.js +147 -0
- package/src/global/intelligence/types.js +21 -2
- package/src/global/intelligence-cli.js +48 -17
- package/src/global/intelligence-session.js +37 -0
- package/src/global/json-output.js +1 -0
- package/src/global/load-component-catalog.js +10 -28
- package/src/global/load-workspace-component-catalog.js +22 -63
- package/src/global/state-migration.js +21 -1
- package/src/global/state.js +13 -8
- package/src/global/status.js +4 -17
- package/src/global/sync.js +2 -5
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const ENGRAM_SETUP_TIMEOUT_MS = 180_000;
|
|
4
|
+
const LOG_LIMIT = 480;
|
|
5
|
+
|
|
6
|
+
/** Absolute `engram setup <slug>` without shell. SIGTERM→SIGKILL on timeout. */
|
|
7
|
+
export function runEngramSetup({
|
|
8
|
+
binaryPath,
|
|
9
|
+
slug,
|
|
10
|
+
env = process.env,
|
|
11
|
+
spawnImpl = spawn,
|
|
12
|
+
timeoutMs = ENGRAM_SETUP_TIMEOUT_MS,
|
|
13
|
+
terminationGraceMs = 1000,
|
|
14
|
+
killGraceMs = 1000
|
|
15
|
+
} = {}) {
|
|
16
|
+
assertSafeSetupInvocation(binaryPath, slug);
|
|
17
|
+
const args = ["setup", slug];
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawnImpl(binaryPath, args, { env, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let settled = false;
|
|
24
|
+
let timedOut = false;
|
|
25
|
+
let timers = [];
|
|
26
|
+
const clearTimers = () => { for (const t of timers) clearTimeout(t); timers = []; };
|
|
27
|
+
const schedule = (fn, ms) => { timers.push(setTimeout(fn, ms)); };
|
|
28
|
+
const settle = (result) => {
|
|
29
|
+
if (settled) return;
|
|
30
|
+
settled = true;
|
|
31
|
+
clearTimers();
|
|
32
|
+
detachIo();
|
|
33
|
+
resolve(result);
|
|
34
|
+
};
|
|
35
|
+
const finish = ({ status = null, signal = null, terminationFailed = false } = {}) => {
|
|
36
|
+
settle({
|
|
37
|
+
ok: status === 0 && !timedOut && !terminationFailed,
|
|
38
|
+
command: [binaryPath, ...args],
|
|
39
|
+
slug, status, signal, timedOut, terminationFailed,
|
|
40
|
+
stdout: redactEngramLog(stdout),
|
|
41
|
+
stderr: redactEngramLog(stderr)
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const onStdout = (c) => { stdout = appendLimited(stdout, c, LOG_LIMIT * 4); };
|
|
45
|
+
const onStderr = (c) => { stderr = appendLimited(stderr, c, LOG_LIMIT); };
|
|
46
|
+
const onError = (error) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true; clearTimers(); detachIo(); reject(error);
|
|
49
|
+
};
|
|
50
|
+
const onClose = (status, signal) => finish({ status: status ?? null, signal: signal ?? null });
|
|
51
|
+
const detachIo = () => {
|
|
52
|
+
child.stdout?.off?.("data", onStdout);
|
|
53
|
+
child.stderr?.off?.("data", onStderr);
|
|
54
|
+
child.off?.("close", onClose);
|
|
55
|
+
};
|
|
56
|
+
schedule(() => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
timedOut = true;
|
|
59
|
+
safeKill(child, "SIGTERM");
|
|
60
|
+
schedule(() => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
safeKill(child, "SIGKILL");
|
|
63
|
+
schedule(() => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
try { child.unref?.(); } catch { /* ignore */ }
|
|
66
|
+
finish({ terminationFailed: true });
|
|
67
|
+
}, killGraceMs);
|
|
68
|
+
}, terminationGraceMs);
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
child.stdout?.on("data", onStdout);
|
|
71
|
+
child.stderr?.on("data", onStderr);
|
|
72
|
+
child.on("error", onError);
|
|
73
|
+
child.on("close", onClose);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertSafeSetupInvocation(binaryPath, slug) {
|
|
78
|
+
if (typeof binaryPath !== "string" || !binaryPath.startsWith("/") || binaryPath.includes("\0")) {
|
|
79
|
+
throw new Error("Engram setup requires an absolute binary path.");
|
|
80
|
+
}
|
|
81
|
+
if (typeof slug !== "string" || !/^[a-z][a-z0-9-]*$/.test(slug)) {
|
|
82
|
+
throw new Error(`Invalid Engram setup slug "${slug}".`);
|
|
83
|
+
}
|
|
84
|
+
if (/engram\.db/i.test(binaryPath) || /engram\.db/i.test(slug)) {
|
|
85
|
+
throw new Error("Engram setup refuses paths that reference the memory database.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function redactEngramLog(text, { limit = LOG_LIMIT } = {}) {
|
|
90
|
+
let out = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
91
|
+
out = out.replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]");
|
|
92
|
+
out = out.replace(/\b(sk-[A-Za-z0-9_-]+)\b/g, "[REDACTED]");
|
|
93
|
+
out = out.replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*)\s*[:=]\s*\S+/gi, "$1=[REDACTED]");
|
|
94
|
+
if (out.length > limit) out = `${out.slice(0, Math.max(0, limit - 1))}…`;
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function appendLimited(current, chunk, limit) {
|
|
99
|
+
if (current.length >= limit) return current;
|
|
100
|
+
const next = current + String(chunk);
|
|
101
|
+
return next.length > limit ? next.slice(0, limit) : next;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function safeKill(child, signal) {
|
|
105
|
+
try { return typeof child.kill === "function" ? child.kill(signal) !== false : false; }
|
|
106
|
+
catch { return false; }
|
|
107
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveEngramAgentSelection,
|
|
3
|
+
engramSetupSlugForAgent,
|
|
4
|
+
ENGRAM_INTEGRATION_STATUS,
|
|
5
|
+
inspectEngramIntegration
|
|
6
|
+
} from "./engram-evidence.js";
|
|
7
|
+
|
|
8
|
+
/** Dry-run / apply plan. Dry-run never writes or executes setup. */
|
|
9
|
+
export function planEngramConfigure({
|
|
10
|
+
requestedAgentIds = null,
|
|
11
|
+
detectedAgentIds = [],
|
|
12
|
+
env = process.env,
|
|
13
|
+
homeDir,
|
|
14
|
+
inspect = inspectEngramIntegration,
|
|
15
|
+
dryRun = true
|
|
16
|
+
} = {}) {
|
|
17
|
+
const agentIds = resolveEngramAgentSelection({ requestedIds: requestedAgentIds, detectedIds: detectedAgentIds });
|
|
18
|
+
const inspection = inspect({ env, homeDir, agentIds });
|
|
19
|
+
const { binary } = inspection;
|
|
20
|
+
const canSetup = binary.supported === true && Boolean(binary.path);
|
|
21
|
+
|
|
22
|
+
const actions = agentIds.map((agentId) => {
|
|
23
|
+
const agent = inspection.agents.find((entry) => entry.id === agentId) ?? {
|
|
24
|
+
id: agentId,
|
|
25
|
+
slug: engramSetupSlugForAgent(agentId),
|
|
26
|
+
status: ENGRAM_INTEGRATION_STATUS.UNCONFIGURED
|
|
27
|
+
};
|
|
28
|
+
const slug = agent.slug ?? engramSetupSlugForAgent(agentId);
|
|
29
|
+
let action = "setup";
|
|
30
|
+
let reason = "Official `engram setup` required.";
|
|
31
|
+
if (!canSetup) {
|
|
32
|
+
action = "blocked";
|
|
33
|
+
reason = binary.guidance ?? `Engram is ${binary.status}.`;
|
|
34
|
+
} else if (agent.status === ENGRAM_INTEGRATION_STATUS.CONFIGURED) {
|
|
35
|
+
action = "noop";
|
|
36
|
+
reason = "Already configured (idempotent re-setup still available on apply).";
|
|
37
|
+
} else if (agent.status === ENGRAM_INTEGRATION_STATUS.CONFLICT) {
|
|
38
|
+
reason = "Conflict detected; official setup may repair agent config.";
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
agentId, slug, action, reason, status: agent.status,
|
|
42
|
+
command: canSetup ? [binary.path, "setup", slug] : null,
|
|
43
|
+
executes: false, writes: false
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
provider: "engram",
|
|
49
|
+
componentId: "engram-memory",
|
|
50
|
+
dryRun: Boolean(dryRun),
|
|
51
|
+
executes: false,
|
|
52
|
+
writes: false,
|
|
53
|
+
binary,
|
|
54
|
+
agents: inspection.agents,
|
|
55
|
+
actions,
|
|
56
|
+
blocked: !canSetup,
|
|
57
|
+
guidance: binary.guidance,
|
|
58
|
+
nextStatusIfApplied: canSetup ? ENGRAM_INTEGRATION_STATUS.RESTART_REQUIRED : binary.status
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { inspectEngramIntegration } from "./engram-evidence.js";
|
|
2
|
+
import { planEngramConfigure } from "./engram-plan.js";
|
|
3
|
+
import {
|
|
4
|
+
applyEngramConfigureWithReceipt,
|
|
5
|
+
rollbackEngramReceipt
|
|
6
|
+
} from "./engram-rollback.js";
|
|
7
|
+
|
|
8
|
+
export function createEngramProvider({
|
|
9
|
+
inspect = inspectEngramIntegration,
|
|
10
|
+
plan = planEngramConfigure,
|
|
11
|
+
apply = applyEngramConfigureWithReceipt,
|
|
12
|
+
rollback = rollbackEngramReceipt
|
|
13
|
+
} = {}) {
|
|
14
|
+
return {
|
|
15
|
+
id: "engram",
|
|
16
|
+
async inspect(context = {}) { return inspect(context); },
|
|
17
|
+
async plan(context = {}) { return plan(context); },
|
|
18
|
+
async apply(context = {}) { return apply(context); },
|
|
19
|
+
async verify(context = {}) { return inspect(context); },
|
|
20
|
+
async rollback(context = {}) { return rollback(context); }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { copyFile, mkdir, readFile, writeFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { harnessHomePaths } from "../paths.js";
|
|
6
|
+
import { inspectEngramAgentConfig } from "./engram-evidence.js";
|
|
7
|
+
|
|
8
|
+
const MEMORY_DB = /engram\.db$/i;
|
|
9
|
+
|
|
10
|
+
export function engramIntegrationsDir(homeDir) {
|
|
11
|
+
return join(harnessHomePaths(homeDir).root, "integrations", "engram");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function engramReceiptPath(homeDir, receiptId) {
|
|
15
|
+
assertSafeReceiptId(receiptId);
|
|
16
|
+
return join(engramIntegrationsDir(homeDir), `${receiptId}.json`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function engramReceiptBackupDir(homeDir, receiptId) {
|
|
20
|
+
assertSafeReceiptId(receiptId);
|
|
21
|
+
return join(engramIntegrationsDir(homeDir), "backups", receiptId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertSafeReceiptId(receiptId) {
|
|
25
|
+
if (typeof receiptId !== "string" || !/^engram-[A-Za-z0-9-]+$/.test(receiptId)) {
|
|
26
|
+
throw new Error(`Invalid Engram receipt id "${receiptId}".`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hashFileContents(contents) {
|
|
31
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function hashFileIfPresent(path) {
|
|
35
|
+
if (!existsSync(path)) return null;
|
|
36
|
+
if (MEMORY_DB.test(path)) throw new Error("Refusing to hash Engram memory database.");
|
|
37
|
+
return hashFileContents(await readFile(path));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function captureEngramObservedFiles(agentIds, { homeDir } = {}) {
|
|
41
|
+
const files = [];
|
|
42
|
+
for (const agentId of agentIds) {
|
|
43
|
+
for (const item of inspectEngramAgentConfig(agentId, { homeDir }).evidence) {
|
|
44
|
+
if (MEMORY_DB.test(item.path)) continue;
|
|
45
|
+
const hash = await hashFileIfPresent(item.path);
|
|
46
|
+
files.push({
|
|
47
|
+
agentId,
|
|
48
|
+
path: item.path,
|
|
49
|
+
kind: item.kind,
|
|
50
|
+
ownership: item.kind === "plugin" ? "provider" : "kairo",
|
|
51
|
+
present: hash != null,
|
|
52
|
+
hash
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function diffEngramObservedFiles(before = [], after = []) {
|
|
60
|
+
const beforeByPath = new Map(before.map((e) => [e.path, e]));
|
|
61
|
+
const afterByPath = new Map(after.map((e) => [e.path, e]));
|
|
62
|
+
const changes = [];
|
|
63
|
+
for (const path of new Set([...beforeByPath.keys(), ...afterByPath.keys()])) {
|
|
64
|
+
const prev = beforeByPath.get(path);
|
|
65
|
+
const next = afterByPath.get(path);
|
|
66
|
+
if (!prev?.present && next?.present) {
|
|
67
|
+
changes.push({
|
|
68
|
+
path, agentId: next.agentId, kind: next.kind, ownership: next.ownership,
|
|
69
|
+
change: "created", beforeHash: null, afterHash: next.hash
|
|
70
|
+
});
|
|
71
|
+
} else if (prev?.present && next?.present && prev.hash !== next.hash) {
|
|
72
|
+
changes.push({
|
|
73
|
+
path, agentId: next.agentId, kind: next.kind, ownership: next.ownership,
|
|
74
|
+
change: "modified", beforeHash: prev.hash, afterHash: next.hash
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return changes;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function backupObservedFiles(before, { homeDir, receiptId } = {}) {
|
|
82
|
+
const backups = [];
|
|
83
|
+
const root = engramReceiptBackupDir(homeDir, receiptId);
|
|
84
|
+
for (const entry of before) {
|
|
85
|
+
if (!entry.present || entry.ownership !== "kairo") continue;
|
|
86
|
+
const backupPath = join(root, Buffer.from(entry.path).toString("base64url"));
|
|
87
|
+
await mkdir(root, { recursive: true });
|
|
88
|
+
await copyFile(entry.path, backupPath);
|
|
89
|
+
backups.push({ path: entry.path, backupPath, beforeHash: entry.hash });
|
|
90
|
+
}
|
|
91
|
+
return backups;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function saveEngramReceipt(receipt, { homeDir } = {}) {
|
|
95
|
+
assertSafeReceiptId(receipt.id);
|
|
96
|
+
if (receipt.touchedMemoryDb) {
|
|
97
|
+
throw new Error("Refusing to persist a receipt that touched the Engram memory database.");
|
|
98
|
+
}
|
|
99
|
+
await mkdir(engramIntegrationsDir(homeDir), { recursive: true });
|
|
100
|
+
const path = engramReceiptPath(homeDir, receipt.id);
|
|
101
|
+
const sanitized = { ...receipt, persisted: true };
|
|
102
|
+
await writeFile(path, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
|
|
103
|
+
return { path, receipt: sanitized };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadEngramReceipt(receiptId, { homeDir } = {}) {
|
|
107
|
+
const path = engramReceiptPath(homeDir, receiptId);
|
|
108
|
+
if (!existsSync(path)) throw new Error(`Engram receipt not found: ${receiptId}`);
|
|
109
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function listEngramReceipts({ homeDir } = {}) {
|
|
113
|
+
const dir = engramIntegrationsDir(homeDir);
|
|
114
|
+
if (!existsSync(dir)) return [];
|
|
115
|
+
return (await readdir(dir)).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -5)).sort();
|
|
116
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
assertExplicitApplyConsent,
|
|
6
|
+
promptApplyConfirmation,
|
|
7
|
+
shouldPromptApplyConfirmation
|
|
8
|
+
} from "../apply-confirmation.js";
|
|
9
|
+
import { resolveHomeDir } from "../paths.js";
|
|
10
|
+
import { applyEngramConfigure } from "./engram-apply.js";
|
|
11
|
+
import {
|
|
12
|
+
backupObservedFiles,
|
|
13
|
+
captureEngramObservedFiles,
|
|
14
|
+
diffEngramObservedFiles,
|
|
15
|
+
hashFileIfPresent,
|
|
16
|
+
loadEngramReceipt,
|
|
17
|
+
saveEngramReceipt
|
|
18
|
+
} from "./engram-receipts.js";
|
|
19
|
+
|
|
20
|
+
/** Snapshot+backup before setup, then persist a secret-free receipt. */
|
|
21
|
+
export async function applyEngramConfigureWithReceipt(options = {}) {
|
|
22
|
+
const homeDir = options.homeDir ?? resolveHomeDir();
|
|
23
|
+
const plannedAgents = options.requestedAgentIds ?? options.detectedAgentIds ?? [];
|
|
24
|
+
const receiptId = options.receiptId ?? `engram-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
25
|
+
const before = options.dryRun ? [] : await captureEngramObservedFiles(plannedAgents, { homeDir });
|
|
26
|
+
const backups = options.dryRun ? [] : await backupObservedFiles(before, { homeDir, receiptId });
|
|
27
|
+
|
|
28
|
+
const result = await applyEngramConfigure({ ...options, homeDir, receiptId });
|
|
29
|
+
if (options.dryRun || result.cancelled || !result.receipt) return result;
|
|
30
|
+
|
|
31
|
+
const after = await captureEngramObservedFiles(result.receipt.agentsRequested, { homeDir });
|
|
32
|
+
const changes = diffEngramObservedFiles(before, after);
|
|
33
|
+
const receipt = {
|
|
34
|
+
...result.receipt,
|
|
35
|
+
observedBefore: before,
|
|
36
|
+
observedAfter: after,
|
|
37
|
+
changes,
|
|
38
|
+
backups,
|
|
39
|
+
providerOwnedResidue: changes.filter((c) => c.ownership === "provider")
|
|
40
|
+
};
|
|
41
|
+
const saved = await saveEngramReceipt(receipt, { homeDir });
|
|
42
|
+
return { ...result, receipt: saved.receipt, receiptPath: saved.path };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function rollbackEngramReceipt({
|
|
46
|
+
receiptId,
|
|
47
|
+
homeDir,
|
|
48
|
+
dryRun = false,
|
|
49
|
+
yes = false,
|
|
50
|
+
json = false,
|
|
51
|
+
interactive = null,
|
|
52
|
+
confirm = promptApplyConfirmation
|
|
53
|
+
} = {}) {
|
|
54
|
+
assertExplicitApplyConsent({
|
|
55
|
+
applying: !dryRun, dryRun, json, yes, interactive, command: "components rollback"
|
|
56
|
+
});
|
|
57
|
+
const receipt = await loadEngramReceipt(receiptId, { homeDir });
|
|
58
|
+
if (receipt.touchedMemoryDb) {
|
|
59
|
+
throw new Error("Receipt indicates memory DB access; refusing rollback.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (shouldPromptApplyConfirmation({ applying: true, dryRun, json, confirm: yes, interactive })) {
|
|
63
|
+
const accepted = await confirm({
|
|
64
|
+
command: `components rollback engram-memory --receipt ${receiptId}`,
|
|
65
|
+
question: `Rollback Engram receipt ${receiptId}? [Y/n]: `
|
|
66
|
+
});
|
|
67
|
+
if (!accepted) return { dryRun, cancelled: true, receiptId, actions: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const actions = [];
|
|
71
|
+
for (const change of receipt.changes ?? []) {
|
|
72
|
+
if (change.ownership === "provider") {
|
|
73
|
+
actions.push({
|
|
74
|
+
path: change.path,
|
|
75
|
+
action: "residue",
|
|
76
|
+
ok: true,
|
|
77
|
+
reason: "Provider-owned plugin/marketplace artifact; not uninstalled automatically."
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (change.change === "modified") {
|
|
82
|
+
actions.push(await restoreModified(change, receipt, { dryRun }));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (change.change === "created") {
|
|
86
|
+
actions.push(await deleteCreated(change, { dryRun }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
dryRun,
|
|
92
|
+
cancelled: false,
|
|
93
|
+
receiptId,
|
|
94
|
+
ok: actions.every((a) => a.ok !== false),
|
|
95
|
+
actions,
|
|
96
|
+
touchedMemoryDb: false
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function restoreModified(change, receipt, { dryRun }) {
|
|
101
|
+
const currentHash = await hashFileIfPresent(change.path);
|
|
102
|
+
if (currentHash !== change.afterHash) {
|
|
103
|
+
return { path: change.path, action: "skip", ok: true, reason: "File changed after setup; refusing restore." };
|
|
104
|
+
}
|
|
105
|
+
const backup = (receipt.backups ?? []).find((entry) => entry.path === change.path);
|
|
106
|
+
if (!backup?.backupPath || !existsSync(backup.backupPath)) {
|
|
107
|
+
return { path: change.path, action: "skip", ok: false, reason: "Backup missing." };
|
|
108
|
+
}
|
|
109
|
+
if (!dryRun) {
|
|
110
|
+
await mkdir(dirname(change.path), { recursive: true });
|
|
111
|
+
await copyFile(backup.backupPath, change.path);
|
|
112
|
+
}
|
|
113
|
+
return { path: change.path, action: "restore", ok: true, backupPath: backup.backupPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function deleteCreated(change, { dryRun }) {
|
|
117
|
+
const currentHash = await hashFileIfPresent(change.path);
|
|
118
|
+
if (currentHash == null) {
|
|
119
|
+
return { path: change.path, action: "skip", ok: true, reason: "Already absent." };
|
|
120
|
+
}
|
|
121
|
+
if (currentHash !== change.afterHash) {
|
|
122
|
+
return {
|
|
123
|
+
path: change.path,
|
|
124
|
+
action: "skip",
|
|
125
|
+
ok: true,
|
|
126
|
+
reason: "Created file was edited after setup; refusing delete."
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!dryRun) await rm(change.path, { force: true });
|
|
130
|
+
return { path: change.path, action: "delete", ok: true };
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createEngramProvider } from "./engram-provider.js";
|
|
2
|
+
import { createSddCoreProvider } from "./sdd-provider.js";
|
|
3
|
+
import { registerIntegrationProvider, getIntegrationProvider } from "./provider-registry.js";
|
|
4
|
+
|
|
5
|
+
export function ensureIntegrationProvidersRegistered() {
|
|
6
|
+
if (!getIntegrationProvider("engram")) {
|
|
7
|
+
registerIntegrationProvider(createEngramProvider());
|
|
8
|
+
}
|
|
9
|
+
if (!getIntegrationProvider("sdd-core")) {
|
|
10
|
+
registerIntegrationProvider(createSddCoreProvider());
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Integration provider contract — metadata only; no executable commands in manifests. */
|
|
2
|
+
|
|
3
|
+
export const INTEGRATION_PROVIDER_IDS = Object.freeze(["engram", "sdd-core"]);
|
|
4
|
+
export const PROVIDER_METHODS = Object.freeze(["inspect", "plan", "apply", "verify", "rollback"]);
|
|
5
|
+
|
|
6
|
+
const EXECUTABLE_FIELDS = Object.freeze([
|
|
7
|
+
"commands",
|
|
8
|
+
"command",
|
|
9
|
+
"execute",
|
|
10
|
+
"bin",
|
|
11
|
+
"setup",
|
|
12
|
+
"apply",
|
|
13
|
+
"shell",
|
|
14
|
+
"argv",
|
|
15
|
+
"script",
|
|
16
|
+
"exec"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function assertProviderContract(provider) {
|
|
20
|
+
if (provider == null || typeof provider !== "object" || Array.isArray(provider)) {
|
|
21
|
+
throw new Error("Integration provider must be an object.");
|
|
22
|
+
}
|
|
23
|
+
if (typeof provider.id !== "string" || !INTEGRATION_PROVIDER_IDS.includes(provider.id)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Integration provider id must be one of: ${INTEGRATION_PROVIDER_IDS.join(", ")}.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
for (const method of PROVIDER_METHODS) {
|
|
29
|
+
if (typeof provider[method] !== "function") {
|
|
30
|
+
throw new Error(`Integration provider "${provider.id}" is missing ${method}().`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return provider;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeIntegrationMetadata(integration, { componentId, source = "bundled" } = {}) {
|
|
37
|
+
if (integration === undefined) return null;
|
|
38
|
+
if (!isObject(integration)) {
|
|
39
|
+
throw new Error(`Component "${componentId}" integration must be an object.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
assertNoExecutableFields(integration, { componentId, source });
|
|
43
|
+
|
|
44
|
+
const { provider } = integration;
|
|
45
|
+
if (typeof provider !== "string" || !INTEGRATION_PROVIDER_IDS.includes(provider)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Component "${componentId}" integration.provider must be one of: ${INTEGRATION_PROVIDER_IDS.join(", ")}.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allowed = new Set(["provider"]);
|
|
52
|
+
for (const key of Object.keys(integration)) {
|
|
53
|
+
if (!allowed.has(key)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Component "${componentId}" integration declares unsupported field "${key}".`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { provider };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertNoExecutableFields(integration, { componentId, source }) {
|
|
64
|
+
for (const key of Object.keys(integration)) {
|
|
65
|
+
const isExecutable = EXECUTABLE_FIELDS.includes(key)
|
|
66
|
+
|| /Command$/i.test(key)
|
|
67
|
+
|| /commands?/i.test(key);
|
|
68
|
+
if (!isExecutable) continue;
|
|
69
|
+
|
|
70
|
+
if (source === "workspace") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Workspace component "${componentId}" cannot declare executable integration.${key}.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Component "${componentId}" integration cannot declare executable field "${key}".`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isObject(value) {
|
|
82
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
83
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assertProviderContract } from "./provider-contract.js";
|
|
2
|
+
|
|
3
|
+
const providers = new Map();
|
|
4
|
+
|
|
5
|
+
export function registerIntegrationProvider(provider) {
|
|
6
|
+
assertProviderContract(provider);
|
|
7
|
+
if (providers.has(provider.id)) {
|
|
8
|
+
throw new Error(`Integration provider "${provider.id}" is already registered.`);
|
|
9
|
+
}
|
|
10
|
+
const frozen = Object.freeze({
|
|
11
|
+
id: provider.id,
|
|
12
|
+
inspect: provider.inspect,
|
|
13
|
+
plan: provider.plan,
|
|
14
|
+
apply: provider.apply,
|
|
15
|
+
verify: provider.verify,
|
|
16
|
+
rollback: provider.rollback
|
|
17
|
+
});
|
|
18
|
+
providers.set(provider.id, frozen);
|
|
19
|
+
return frozen;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getIntegrationProvider(id) {
|
|
23
|
+
return providers.get(id) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requireIntegrationProvider(id) {
|
|
27
|
+
const provider = getIntegrationProvider(id);
|
|
28
|
+
if (!provider) {
|
|
29
|
+
throw new Error(`Unknown integration provider "${id}".`);
|
|
30
|
+
}
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function listIntegrationProviders() {
|
|
35
|
+
return [...providers.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveComponentIntegrationProvider(component) {
|
|
39
|
+
const providerId = component?.integration?.provider;
|
|
40
|
+
if (!providerId) return null;
|
|
41
|
+
return requireIntegrationProvider(providerId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Test helper — clears registry between tests. */
|
|
45
|
+
export function resetIntegrationProvidersForTests() {
|
|
46
|
+
providers.clear();
|
|
47
|
+
}
|