@kal-elsam/kairo-runtime 0.4.2 → 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 +22 -1
- 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,175 @@
|
|
|
1
|
+
import { SDD_FILE_OUTCOMES } from "./sdd-evidence.js";
|
|
2
|
+
import { derivePersona, normalizePersonaAgentIds } from "./sdd-persona.js";
|
|
3
|
+
|
|
4
|
+
/** Empty v4 SDD block: persona off, no managed files, no receipt. */
|
|
5
|
+
export function defaultSddState() {
|
|
6
|
+
return {
|
|
7
|
+
persona: "off",
|
|
8
|
+
personaAgentIds: [],
|
|
9
|
+
agentIds: [],
|
|
10
|
+
files: [],
|
|
11
|
+
lastReceiptId: null,
|
|
12
|
+
updatedAt: null
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Coerce any prior/absent shape into a valid v4 SDD block. */
|
|
17
|
+
export function normalizeSddState(raw) {
|
|
18
|
+
if (!raw || typeof raw !== "object") return defaultSddState();
|
|
19
|
+
const personaAgentIds = Object.hasOwn(raw, "personaAgentIds")
|
|
20
|
+
? (Array.isArray(raw.personaAgentIds) ? normalizePersonaAgentIds(raw.personaAgentIds) : [])
|
|
21
|
+
: (raw.persona === "teaching" && Array.isArray(raw.agentIds)
|
|
22
|
+
? normalizePersonaAgentIds(raw.agentIds) : []);
|
|
23
|
+
return {
|
|
24
|
+
persona: derivePersona(personaAgentIds),
|
|
25
|
+
personaAgentIds,
|
|
26
|
+
agentIds: Array.isArray(raw.agentIds) ? [...raw.agentIds] : [],
|
|
27
|
+
files: Array.isArray(raw.files) ? raw.files.map(normalizeSddFile) : [],
|
|
28
|
+
lastReceiptId: typeof raw.lastReceiptId === "string" ? raw.lastReceiptId : null,
|
|
29
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSddFile(entry) {
|
|
34
|
+
return {
|
|
35
|
+
destinationPath: entry.destinationPath,
|
|
36
|
+
relativePath: entry.relativePath ?? "SKILL.md",
|
|
37
|
+
skillId: entry.skillId,
|
|
38
|
+
agentIds: Array.isArray(entry.agentIds) ? [...entry.agentIds] : [],
|
|
39
|
+
hash: entry.hash ?? null,
|
|
40
|
+
skillHash: entry.skillHash ?? null,
|
|
41
|
+
action: entry.action ?? null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function verifiedNoopHash(file) {
|
|
46
|
+
const disk = file.afterHash ?? file.diskHash ?? file.beforeHash ?? null;
|
|
47
|
+
if (disk == null || file.canonicalHash == null || disk !== file.canonicalHash) return null;
|
|
48
|
+
return disk;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Track only applied(+afterHash) or noop with verified hash equality. No legacy fallback. */
|
|
52
|
+
export function shouldTrackSddReceiptFile(file) {
|
|
53
|
+
if (!file || typeof file !== "object") return false;
|
|
54
|
+
if (file.outcome === SDD_FILE_OUTCOMES.APPLIED) return file.afterHash != null;
|
|
55
|
+
if (file.outcome === SDD_FILE_OUTCOMES.NOOP) return verifiedNoopHash(file) != null;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function trackingHash(file) {
|
|
60
|
+
if (file.outcome === SDD_FILE_OUTCOMES.APPLIED) return file.afterHash ?? null;
|
|
61
|
+
if (file.outcome === SDD_FILE_OUTCOMES.NOOP) return verifiedNoopHash(file);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectAgentIds(files) {
|
|
66
|
+
const ids = new Set();
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
for (const id of file.agentIds ?? []) ids.add(id);
|
|
69
|
+
}
|
|
70
|
+
return [...ids].sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Legacy receipts without personaTransition derive consumers from teaching + files. */
|
|
74
|
+
function resolvePersonaAgentIds(receipt, current, files) {
|
|
75
|
+
if (receipt?.personaTransition) return normalizePersonaAgentIds(receipt.personaTransition.after);
|
|
76
|
+
if (!files.length || (receipt.persona ?? current.persona) !== "teaching") return [];
|
|
77
|
+
return current.personaAgentIds.length ? current.personaAgentIds : collectAgentIds(files);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when rollback mutated disk via successful delete/restore (even if global ok=false). */
|
|
81
|
+
export function hasSuccessfulSddRollbackMutations(actions) {
|
|
82
|
+
return (actions ?? []).some((entry) =>
|
|
83
|
+
entry?.ok && !entry.noop && (entry.action === "delete" || entry.action === "restore" || entry.action === "persona")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Merge receipt into v4 SDD block for applied/verified files only. */
|
|
88
|
+
export function recordSddMaterialization(state, { receipt, now = () => new Date().toISOString() } = {}) {
|
|
89
|
+
const current = normalizeSddState(state?.sdd);
|
|
90
|
+
const managed = new Map(current.files.map((file) => [file.destinationPath, file]));
|
|
91
|
+
|
|
92
|
+
for (const file of receipt.files ?? []) {
|
|
93
|
+
if (!shouldTrackSddReceiptFile(file)) continue;
|
|
94
|
+
const hash = trackingHash(file);
|
|
95
|
+
if (hash == null) continue;
|
|
96
|
+
managed.set(file.destinationPath, {
|
|
97
|
+
destinationPath: file.destinationPath,
|
|
98
|
+
relativePath: file.relativePath ?? "SKILL.md",
|
|
99
|
+
skillId: file.skillId,
|
|
100
|
+
agentIds: [...(file.agentIds ?? [])],
|
|
101
|
+
hash,
|
|
102
|
+
skillHash: file.skillHash ?? null,
|
|
103
|
+
action: file.action
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const files = [...managed.values()].sort((left, right) =>
|
|
108
|
+
left.destinationPath.localeCompare(right.destinationPath)
|
|
109
|
+
);
|
|
110
|
+
const personaAgentIds = resolvePersonaAgentIds(receipt, current, files);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...(state ?? {}),
|
|
114
|
+
sdd: {
|
|
115
|
+
persona: derivePersona(personaAgentIds),
|
|
116
|
+
personaAgentIds,
|
|
117
|
+
agentIds: collectAgentIds(files),
|
|
118
|
+
files,
|
|
119
|
+
lastReceiptId: receipt.id ?? current.lastReceiptId,
|
|
120
|
+
updatedAt: now()
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Reconcile each successful delete/restore/persona; refresh agentIds/persona from remaining files. */
|
|
126
|
+
export function reconcileSddStateAfterRollback(state, {
|
|
127
|
+
receipt, actions, now = () => new Date().toISOString()
|
|
128
|
+
} = {}) {
|
|
129
|
+
const current = normalizeSddState(state?.sdd);
|
|
130
|
+
const managed = new Map(current.files.map((file) => [file.destinationPath, { ...file }]));
|
|
131
|
+
const receiptFiles = new Map((receipt?.files ?? []).map((file) => [file.destinationPath, file]));
|
|
132
|
+
const backups = new Map((receipt?.backups ?? []).map((entry) => [entry.path, entry]));
|
|
133
|
+
|
|
134
|
+
for (const action of actions ?? []) {
|
|
135
|
+
if (!action?.ok || action.dryRun || action.action === "persona" || action.action === "skip") continue;
|
|
136
|
+
if (action.action === "delete") {
|
|
137
|
+
managed.delete(action.path);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (action.action !== "restore") continue;
|
|
141
|
+
const file = receiptFiles.get(action.path);
|
|
142
|
+
const beforeHash = backups.get(action.path)?.beforeHash ?? file?.beforeHash ?? null;
|
|
143
|
+
if (beforeHash == null || !file) continue;
|
|
144
|
+
managed.set(action.path, {
|
|
145
|
+
destinationPath: action.path,
|
|
146
|
+
relativePath: file.relativePath ?? "SKILL.md",
|
|
147
|
+
skillId: file.skillId,
|
|
148
|
+
agentIds: [...(file.agentIds ?? [])],
|
|
149
|
+
hash: beforeHash,
|
|
150
|
+
skillHash: file.skillHash ?? null,
|
|
151
|
+
action: file.action
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const files = [...managed.values()].sort((left, right) =>
|
|
156
|
+
left.destinationPath.localeCompare(right.destinationPath)
|
|
157
|
+
);
|
|
158
|
+
const personaAction = (actions ?? []).find((entry) => entry.action === "persona" && entry.ok);
|
|
159
|
+
let personaAgentIds = current.personaAgentIds;
|
|
160
|
+
if (files.length === 0 && !personaAction) personaAgentIds = [];
|
|
161
|
+
else if (personaAction) personaAgentIds = normalizePersonaAgentIds(personaAction.before ?? []);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...(state ?? {}),
|
|
165
|
+
sdd: {
|
|
166
|
+
...current,
|
|
167
|
+
files,
|
|
168
|
+
agentIds: collectAgentIds(files),
|
|
169
|
+
personaAgentIds,
|
|
170
|
+
persona: derivePersona(personaAgentIds),
|
|
171
|
+
lastReceiptId: receipt?.id ?? current.lastReceiptId,
|
|
172
|
+
updatedAt: now()
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { hashBuffer } from "../../hash.js";
|
|
5
|
+
import { resolveAdapter } from "../registry.js";
|
|
6
|
+
import { applySddConfigure } from "./sdd-apply.js";
|
|
7
|
+
import { classifySddVerifyHealth, SDD_HEALTH } from "./sdd-evidence.js";
|
|
8
|
+
import { harnessHomePaths } from "../paths.js";
|
|
9
|
+
import {
|
|
10
|
+
SDD_SKILL_IDS,
|
|
11
|
+
groupSddSkillDestinations,
|
|
12
|
+
resolveSddAgentSelection
|
|
13
|
+
} from "./sdd-destinations.js";
|
|
14
|
+
import {
|
|
15
|
+
classifyPersonaHealth,
|
|
16
|
+
derivePersona,
|
|
17
|
+
normalizePersonaAgentIds,
|
|
18
|
+
SDD_PERSONA_GATE
|
|
19
|
+
} from "./sdd-persona.js";
|
|
20
|
+
import { compareSkillPaths, loadCanonicalSddSkill } from "./sdd-skill-files.js";
|
|
21
|
+
|
|
22
|
+
/** Read-only verify: configured | missing | drifted | conflict (canonical vs disk). */
|
|
23
|
+
export async function verifySddConfigure({
|
|
24
|
+
requestedAgentIds = null,
|
|
25
|
+
detectedAgentIds = [],
|
|
26
|
+
homeDir,
|
|
27
|
+
packageRoot,
|
|
28
|
+
trackedFiles = {},
|
|
29
|
+
personaAgentIds = [],
|
|
30
|
+
exists = existsSync,
|
|
31
|
+
readFileImpl = readFile
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (!homeDir) throw new Error("verifySddConfigure requires homeDir.");
|
|
34
|
+
if (!packageRoot) throw new Error("verifySddConfigure requires packageRoot.");
|
|
35
|
+
|
|
36
|
+
const agentIds = resolveSddAgentSelection({
|
|
37
|
+
requestedIds: requestedAgentIds,
|
|
38
|
+
detectedIds: detectedAgentIds
|
|
39
|
+
});
|
|
40
|
+
const groups = groupSddSkillDestinations(agentIds, homeDir);
|
|
41
|
+
const findings = [];
|
|
42
|
+
|
|
43
|
+
for (const skillId of SDD_SKILL_IDS) {
|
|
44
|
+
const { files, skillHash } = await loadCanonicalSddSkill(skillId, packageRoot);
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const canonicalHash = hashBuffer(file.bytes);
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
const destinationPath = join(group.root, skillId, ...file.relativePath.split("/"));
|
|
49
|
+
const fileExists = exists(destinationPath);
|
|
50
|
+
const diskHash = fileExists ? hashBuffer(await readFileImpl(destinationPath)) : null;
|
|
51
|
+
const trackedHash = trackedFiles[destinationPath] ?? null;
|
|
52
|
+
const health = classifySddVerifyHealth({
|
|
53
|
+
exists: fileExists, canonicalHash, diskHash, trackedHash
|
|
54
|
+
});
|
|
55
|
+
findings.push({
|
|
56
|
+
skillId, relativePath: file.relativePath, destinationPath,
|
|
57
|
+
agentIds: [...group.agentIds], kind: group.kind,
|
|
58
|
+
status: health.status, drift: health.drift, reason: health.reason,
|
|
59
|
+
canonicalHash, skillHash, diskHash, trackedHash
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
findings.sort((a, b) => compareSkillPaths(a.skillId, b.skillId)
|
|
66
|
+
|| compareSkillPaths(a.relativePath, b.relativePath)
|
|
67
|
+
|| compareSkillPaths(a.destinationPath, b.destinationPath));
|
|
68
|
+
|
|
69
|
+
const summary = { configured: 0, missing: 0, drifted: 0, conflict: 0 };
|
|
70
|
+
for (const entry of findings) summary[entry.status] += 1;
|
|
71
|
+
|
|
72
|
+
const consumers = normalizePersonaAgentIds(personaAgentIds);
|
|
73
|
+
const incompleteAgentIds = consumers.filter((id) => {
|
|
74
|
+
const mine = findings.filter((e) => e.agentIds.includes(id));
|
|
75
|
+
return !mine.length || mine.some((e) => e.status !== SDD_HEALTH.CONFIGURED);
|
|
76
|
+
});
|
|
77
|
+
let gatePresent = true;
|
|
78
|
+
for (const id of consumers) {
|
|
79
|
+
const p = join(homeDir, resolveAdapter(id).assets.configFile);
|
|
80
|
+
if (!exists(p) || !String(await readFileImpl(p, "utf8")).includes(SDD_PERSONA_GATE)) {
|
|
81
|
+
gatePresent = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const personaHealth = classifyPersonaHealth({
|
|
86
|
+
personaAgentIds: consumers, gatePresent, incompleteAgentIds,
|
|
87
|
+
assetPresent: exists(join(harnessHomePaths(homeDir).root, "components/sdd-core/personas/teaching.md"))
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
provider: "sdd-core", componentId: "sdd-core", agentIds, findings, summary,
|
|
91
|
+
status: summarizeSddHealth(summary),
|
|
92
|
+
ok: summary.missing === 0 && summary.drifted === 0 && summary.conflict === 0,
|
|
93
|
+
persona: {
|
|
94
|
+
...personaHealth, persona: derivePersona(consumers), personaAgentIds: consumers, incompleteAgentIds
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function summarizeSddHealth(summary) {
|
|
100
|
+
if (summary.conflict > 0) return SDD_HEALTH.CONFLICT;
|
|
101
|
+
if (summary.missing > 0) return SDD_HEALTH.MISSING;
|
|
102
|
+
if (summary.drifted > 0) return SDD_HEALTH.DRIFTED;
|
|
103
|
+
return SDD_HEALTH.CONFIGURED;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Sync = verify then apply; conflicts block writes. */
|
|
107
|
+
export async function syncSddConfigure(options = {}) {
|
|
108
|
+
const verification = await verifySddConfigure(options);
|
|
109
|
+
if (verification.summary.conflict > 0) {
|
|
110
|
+
return {
|
|
111
|
+
...verification,
|
|
112
|
+
synced: false,
|
|
113
|
+
blocked: true,
|
|
114
|
+
applied: false,
|
|
115
|
+
cancelled: false,
|
|
116
|
+
receipt: null,
|
|
117
|
+
reason: "Conflicts present; refusing sync overwrite."
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const result = await applySddConfigure(options);
|
|
121
|
+
return { ...result, verification, synced: Boolean(result.applied), blocked: false };
|
|
122
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COST_CLASSES,
|
|
3
|
+
OPENCODE_API_KEY_ENV,
|
|
4
|
+
PRIVACY_CLASSES,
|
|
5
|
+
TRANSPORT_KINDS,
|
|
6
|
+
createModelDescriptor
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
isDirectTransport,
|
|
10
|
+
normalizeModelId,
|
|
11
|
+
resolveOpencodeTransport
|
|
12
|
+
} from "../transport-registry.js";
|
|
13
|
+
import { fetchJson } from "../http.js";
|
|
14
|
+
import { CAPABILITY_STATES } from "../../capability-states.js";
|
|
15
|
+
import {
|
|
16
|
+
BILLING_MODELS,
|
|
17
|
+
ENTITLEMENT_STATES,
|
|
18
|
+
buildDirectEvidenceBase,
|
|
19
|
+
classifyModelsProbeStatus,
|
|
20
|
+
collectOpencodeCliEvidence
|
|
21
|
+
} from "./opencode-evidence.js";
|
|
22
|
+
|
|
23
|
+
export function resolveOpencodeApiKey({ env = process.env, apiKey = null } = {}) {
|
|
24
|
+
const value = apiKey ?? env[OPENCODE_API_KEY_ENV] ?? null;
|
|
25
|
+
if (value == null) return null;
|
|
26
|
+
const trimmed = String(value).trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function redactSecret(message, secret) {
|
|
31
|
+
if (!message || !secret) return message ?? null;
|
|
32
|
+
return String(message).split(String(secret)).join("[REDACTED]");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function zenCostClassForModel(modelId) {
|
|
36
|
+
const id = normalizeModelId(modelId) ?? "";
|
|
37
|
+
return id === "big-pickle" || id.endsWith("-free") ? COST_CLASSES.FREE : COST_CLASSES.PAID;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function goCostClassForModel() {
|
|
41
|
+
return COST_CLASSES.PAID;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Catalog/detection for OpenCode Go/Zen. Invoke composition ships in a later PR. */
|
|
45
|
+
export function createOpencodeCatalogBackend({
|
|
46
|
+
id,
|
|
47
|
+
label,
|
|
48
|
+
product,
|
|
49
|
+
baseUrl,
|
|
50
|
+
defaultModelId,
|
|
51
|
+
costClassForModel,
|
|
52
|
+
env = process.env,
|
|
53
|
+
fetchImpl = globalThis.fetch,
|
|
54
|
+
apiKey = null,
|
|
55
|
+
collectCliEvidence = collectOpencodeCliEvidence
|
|
56
|
+
}) {
|
|
57
|
+
const resolvedKey = resolveOpencodeApiKey({ env, apiKey });
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
label,
|
|
62
|
+
local: false,
|
|
63
|
+
product,
|
|
64
|
+
baseUrl,
|
|
65
|
+
|
|
66
|
+
async detect() {
|
|
67
|
+
const cliEvidence = collectCliEvidence({ env });
|
|
68
|
+
if (!resolvedKey) return missingKeyDetection(id, label, product, cliEvidence);
|
|
69
|
+
|
|
70
|
+
const probe = await fetchJson(`${baseUrl}/models`, {
|
|
71
|
+
headers: bearerHeaders(resolvedKey),
|
|
72
|
+
timeoutMs: 5000,
|
|
73
|
+
fetchImpl
|
|
74
|
+
});
|
|
75
|
+
const evidence = buildDirectEvidenceBase({
|
|
76
|
+
product,
|
|
77
|
+
hasApiKey: true,
|
|
78
|
+
cliEvidence,
|
|
79
|
+
modelsHttpStatus: probe.status || null,
|
|
80
|
+
modelsOk: probe.ok
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!probe.ok) {
|
|
84
|
+
const entitlement = classifyModelsProbeStatus(probe.status);
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
label,
|
|
88
|
+
state: CAPABILITY_STATES.ERROR,
|
|
89
|
+
detected: true,
|
|
90
|
+
available: false,
|
|
91
|
+
hasApiKey: true,
|
|
92
|
+
cloud: true,
|
|
93
|
+
...evidence,
|
|
94
|
+
entitlement,
|
|
95
|
+
error: redactSecret(probe.error ?? `HTTP ${probe.status}`, resolvedKey),
|
|
96
|
+
recommendation: describeAuthFailure(probe.status, label, product)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
label,
|
|
103
|
+
state: CAPABILITY_STATES.AUTHENTICATED,
|
|
104
|
+
detected: true,
|
|
105
|
+
available: true,
|
|
106
|
+
hasApiKey: true,
|
|
107
|
+
cloud: true,
|
|
108
|
+
...evidence,
|
|
109
|
+
error: null,
|
|
110
|
+
recommendation: `${label} HTTP authenticated via /models. Entitlement remains ${ENTITLEMENT_STATES.UNVERIFIED}; cloud invoke needs --cloud-consent --yes.`
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async listModels() {
|
|
115
|
+
if (!resolvedKey) return [];
|
|
116
|
+
const result = await fetchJson(`${baseUrl}/models`, {
|
|
117
|
+
headers: bearerHeaders(resolvedKey),
|
|
118
|
+
timeoutMs: 10000,
|
|
119
|
+
fetchImpl
|
|
120
|
+
});
|
|
121
|
+
if (!result.ok) return [];
|
|
122
|
+
|
|
123
|
+
const mapped = [];
|
|
124
|
+
for (const remoteId of extractModelIds(result.data)) {
|
|
125
|
+
const modelId = normalizeModelId(remoteId);
|
|
126
|
+
const transport = resolveOpencodeTransport(product, modelId);
|
|
127
|
+
if (!isDirectTransport(transport)) continue;
|
|
128
|
+
mapped.push(createModelDescriptor({
|
|
129
|
+
provider: id,
|
|
130
|
+
modelId,
|
|
131
|
+
local: false,
|
|
132
|
+
costClass: costClassForModel(modelId),
|
|
133
|
+
privacyClass: PRIVACY_CLASSES.CLOUD,
|
|
134
|
+
transport
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
return mapped;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async capabilities() {
|
|
141
|
+
const detection = await this.detect();
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
local: false,
|
|
145
|
+
cloud: true,
|
|
146
|
+
requiresApiKey: true,
|
|
147
|
+
requiresConsent: true,
|
|
148
|
+
streaming: false,
|
|
149
|
+
tools: false,
|
|
150
|
+
transports: [TRANSPORT_KINDS.CHAT_COMPLETIONS, TRANSPORT_KINDS.RESPONSES],
|
|
151
|
+
billingModel: detection.billingModel,
|
|
152
|
+
entitlement: detection.entitlement,
|
|
153
|
+
configured: detection.configured,
|
|
154
|
+
authenticated: detection.authenticated,
|
|
155
|
+
state: detection.state,
|
|
156
|
+
hasApiKey: Boolean(resolvedKey),
|
|
157
|
+
defaultModelId
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function missingKeyDetection(id, label, product, cliEvidence = null) {
|
|
164
|
+
const evidence = buildDirectEvidenceBase({
|
|
165
|
+
product,
|
|
166
|
+
hasApiKey: false,
|
|
167
|
+
cliEvidence,
|
|
168
|
+
modelsHttpStatus: null,
|
|
169
|
+
modelsOk: false
|
|
170
|
+
});
|
|
171
|
+
const viaCli = evidence.configured;
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
label,
|
|
175
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
176
|
+
detected: viaCli,
|
|
177
|
+
available: false,
|
|
178
|
+
hasApiKey: false,
|
|
179
|
+
cloud: true,
|
|
180
|
+
...evidence,
|
|
181
|
+
error: null,
|
|
182
|
+
recommendation: viaCli
|
|
183
|
+
? `${label} appears in OpenCode CLI auth list (${ENTITLEMENT_STATES.UNVERIFIED} entitlement). Intelligence direct HTTP still needs ${OPENCODE_API_KEY_ENV}, or use --backend opencode for CLI runtime.`
|
|
184
|
+
: `Set ${OPENCODE_API_KEY_ENV} for direct ${label} HTTP, or install/authenticate OpenCode CLI for runtime. Kairo never reads auth.json or claims balance without evidence.`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function bearerHeaders(apiKey) {
|
|
189
|
+
return { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractModelIds(payload) {
|
|
193
|
+
const rows = Array.isArray(payload?.data)
|
|
194
|
+
? payload.data
|
|
195
|
+
: Array.isArray(payload?.models)
|
|
196
|
+
? payload.models
|
|
197
|
+
: Array.isArray(payload) ? payload : [];
|
|
198
|
+
return rows
|
|
199
|
+
.map((entry) => (typeof entry === "string" ? entry : entry?.id ?? entry?.name))
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function describeAuthFailure(status, label, product = "zen") {
|
|
204
|
+
if (status === 401 || status === 403) {
|
|
205
|
+
return `${label} rejected the API key (HTTP ${status}). Key is configured but not authenticated for HTTP.`;
|
|
206
|
+
}
|
|
207
|
+
if (status === 429) {
|
|
208
|
+
return `${label} returned HTTP 429 (${ENTITLEMENT_STATES.LIMIT_REACHED}). Go limits do not automatically spend Zen credits.`;
|
|
209
|
+
}
|
|
210
|
+
const billing = product === "go" ? BILLING_MODELS.GO_PLAN : BILLING_MODELS.ZEN_CREDITS;
|
|
211
|
+
return `${label} model discovery failed (${billing} catalog; entitlement ${ENTITLEMENT_STATES.UNKNOWN}).`;
|
|
212
|
+
}
|