@kal-elsam/kairo-runtime 0.27.0 → 0.28.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/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/global/cockpit/app.js +36 -3
- package/src/global/cockpit/project-overlay.js +3 -0
- package/src/global/conversation/service.js +125 -2
- package/src/global/observability/codex-models.js +1 -1
- package/src/global/observability/codex-usage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.28.0 — 2026-09-19 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. `/models --verify-access` verifies Claude model entitlement on demand.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `/models --verify-access [--refresh]` — sequential, human-triggered Claude
|
|
15
|
+
entitlement probes with a real cost statement before any spawn. Without
|
|
16
|
+
`--refresh`, only unverified or TTL-expired models are probed; an all-
|
|
17
|
+
unverified sweep does not persist evidence.
|
|
18
|
+
- One-line Spanish preflight notice when Claude models still have unverified
|
|
19
|
+
access (recommendable, never auto-launched).
|
|
20
|
+
|
|
8
21
|
## 0.27.0 — 2026-09-19 (Kairo Runtime)
|
|
9
22
|
|
|
10
23
|
Minor release. Claude model entitlement now gates PROJECT TEAM pools and routes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -258,7 +258,7 @@ export async function runCockpitApp({
|
|
|
258
258
|
// blocks with no indication of which command produced which one.
|
|
259
259
|
pushTranscript("user", task);
|
|
260
260
|
if (command === "/help") {
|
|
261
|
-
pushTranscript("kairo", "Shift+Tab cycles ASK/PLAN/AGENT · /project interactive overlay (or analyze/analyst/approve/refresh/status/cursor exhausted|available subcommands for scripted use) · /plan <task> force a plan · /usage automatic-provider status (Codex/Claude/Go) · /providers all connections incl. Zen/Cursor (manual) · /models CAPABILITY + EFFICIENT picks (--evidence for raw metrics) · /why eligibility detail · /clear · /quit");
|
|
261
|
+
pushTranscript("kairo", "Shift+Tab cycles ASK/PLAN/AGENT · /project interactive overlay (or analyze/analyst/approve/refresh/status/cursor exhausted|available subcommands for scripted use) · /plan <task> force a plan · /usage automatic-provider status (Codex/Claude/Go) · /providers all connections incl. Zen/Cursor (manual) · /models CAPABILITY + EFFICIENT picks (--evidence for raw metrics; --verify-access [--refresh] for Claude entitlement) · /why eligibility detail · /clear · /quit");
|
|
262
262
|
} else if (command === "/usage") {
|
|
263
263
|
for (const line of view.usageLines()) pushTranscript("kairo", line);
|
|
264
264
|
} else if (command === "/providers") {
|
|
@@ -271,8 +271,38 @@ export async function runCockpitApp({
|
|
|
271
271
|
// the plain-language why (capability, efficient alternative,
|
|
272
272
|
// fallback), never raw metrics/percentages/ids/sources. Those
|
|
273
273
|
// stay behind the explicit --evidence flag for technical audit.
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
// --verify-access [--refresh] is the only path that spawns Claude
|
|
275
|
+
// entitlement probes (never on snapshot / first poll).
|
|
276
|
+
const flags = new Set(task.slice(command.length).trim().split(/\s+/).filter(Boolean));
|
|
277
|
+
if (flags.has("--verify-access")) {
|
|
278
|
+
const refresh = flags.has("--refresh");
|
|
279
|
+
editor.disableSubmit = true;
|
|
280
|
+
editor.setText("");
|
|
281
|
+
return runAction("Verifying Claude model access", async () => {
|
|
282
|
+
const summary = await service.verifyClaudeEntitlements({
|
|
283
|
+
cwd,
|
|
284
|
+
refresh,
|
|
285
|
+
beforeProbe: ({ costStatement }) => {
|
|
286
|
+
pushTranscript("kairo", costStatement);
|
|
287
|
+
},
|
|
288
|
+
onProgress: ({ modelId, index, total }) => {
|
|
289
|
+
view.beginAction(`Verifying Claude access (${index + 1}/${total}): ${modelId}`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const allowed = summary.results.filter((r) => r.status === "allowed").length;
|
|
293
|
+
const denied = summary.results.filter((r) => r.status === "denied").length;
|
|
294
|
+
const unverified = summary.results.filter((r) => r.status === "unverified").length;
|
|
295
|
+
if (summary.probed.length === 0) {
|
|
296
|
+
pushTranscript("kairo", "Claude access already verified for the current catalog (use --refresh to re-probe).");
|
|
297
|
+
} else {
|
|
298
|
+
pushTranscript(
|
|
299
|
+
"kairo",
|
|
300
|
+
`Claude access check: ${summary.probed.length} probed · ${allowed} allowed · ${denied} denied · ${unverified} unverified${summary.persisted ? " · cache updated" : " · cache unchanged"}.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}).finally(() => { editor.disableSubmit = false; });
|
|
304
|
+
}
|
|
305
|
+
const explainLines = flags.has("--evidence") ? view.aiTeamDetailLines() : view.modelsExplainLines();
|
|
276
306
|
for (const line of explainLines) pushTranscript("kairo", line);
|
|
277
307
|
} else if (command === "/why") {
|
|
278
308
|
// Drill-down for FIT: which providers were excluded and the exact
|
|
@@ -318,6 +348,9 @@ export async function runCockpitApp({
|
|
|
318
348
|
return runAction("Analyzing project locally (read-only)", async () => {
|
|
319
349
|
const preflight = await service.preflightProject({ cwd });
|
|
320
350
|
view.pendingProjectAnalysis = preflight;
|
|
351
|
+
if (preflight.unverifiedClaudeNotice) {
|
|
352
|
+
pushTranscript("kairo", preflight.unverifiedClaudeNotice);
|
|
353
|
+
}
|
|
321
354
|
if (!preflight.alternatives.length) {
|
|
322
355
|
pushTranscript("kairo", "No real Project Analyst candidate is available right now (ASK only supports Codex/Claude today).");
|
|
323
356
|
return;
|
|
@@ -160,6 +160,9 @@ export class ProjectOverlay {
|
|
|
160
160
|
this.view.beginAction("Reading project evidence locally");
|
|
161
161
|
try {
|
|
162
162
|
this.preflight = await this.service.preflightProject({ cwd: this.cwd });
|
|
163
|
+
if (this.preflight.unverifiedClaudeNotice) {
|
|
164
|
+
this.onNarrate?.(this.preflight.unverifiedClaudeNotice);
|
|
165
|
+
}
|
|
163
166
|
const models = this.preflight.analystCatalog?.models ?? [];
|
|
164
167
|
if (!models.length) {
|
|
165
168
|
this.state = S.NO_ANALYST;
|
|
@@ -41,9 +41,15 @@ import { readArtificialAnalysisModels } from "../observability/artificial-analys
|
|
|
41
41
|
import { readHuggingFaceLeaderboard } from "../observability/huggingface-leaderboard.js";
|
|
42
42
|
import {
|
|
43
43
|
DEFAULT_ENTITLEMENT_TTL_MS,
|
|
44
|
+
mergeEntitlementResults,
|
|
44
45
|
readClaudeEntitlementCache,
|
|
45
|
-
resolveClaudeEntitlements
|
|
46
|
+
resolveClaudeEntitlements,
|
|
47
|
+
writeClaudeEntitlementCache
|
|
46
48
|
} from "../observability/claude-entitlement-store.js";
|
|
49
|
+
import {
|
|
50
|
+
ENTITLEMENT,
|
|
51
|
+
probeClaudeModelEntitlements
|
|
52
|
+
} from "../observability/claude-model-entitlement.js";
|
|
47
53
|
import {
|
|
48
54
|
annotateWithRegistryEvidence, bestEfficientModelPerRoleGlobal, bestModelPerRole, bestModelPerRoleGlobal, buildAiTeam,
|
|
49
55
|
buildEfficientTeam, scoreAvailableModels, summarizeCatalogCoverage
|
|
@@ -64,6 +70,35 @@ export const CONVERSATION_SCHEMA = "kairo.conversation/v1";
|
|
|
64
70
|
// 30s default.
|
|
65
71
|
const BOOTSTRAP_ANALYST_TIMEOUT_MS = 180_000;
|
|
66
72
|
|
|
73
|
+
/** Max Claude entitlement probes per `/models --verify-access` sweep. */
|
|
74
|
+
const CLAUDE_ENTITLEMENT_MAX_PROBES = 12;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cost statement printed before any Claude entitlement spawn. Economy is
|
|
78
|
+
* measured: denied probes cost $0; allowed ones about one cent each.
|
|
79
|
+
* @param {{ pendingCount?: number }} [options]
|
|
80
|
+
*/
|
|
81
|
+
export function buildClaudeEntitlementVerifyCostStatement({ pendingCount = 0 } = {}) {
|
|
82
|
+
const n = Math.max(0, Number(pendingCount) || 0);
|
|
83
|
+
if (n <= 0) {
|
|
84
|
+
return "No Claude models need access verification right now (denied probes cost $0; allowed ones about one cent each).";
|
|
85
|
+
}
|
|
86
|
+
return `About to verify ${n} Claude model${n === 1 ? "" : "s"}: models your plan denies cost $0; allowed ones cost about one cent each.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One-line preflight notice when unverified Claude models exist.
|
|
91
|
+
* @param {number} count
|
|
92
|
+
*/
|
|
93
|
+
export function buildUnverifiedClaudePreflightNotice(count) {
|
|
94
|
+
const n = Math.max(0, Number(count) || 0);
|
|
95
|
+
return `${n} modelos de Claude tienen acceso sin verificar — se pueden recomendar pero nunca lanzar automáticamente. Corré /models --verify-access para verificar (los que tu plan deniega cuestan $0; los permitidos, alrededor de un centavo cada uno).`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPersistableEntitlementStatus(status) {
|
|
99
|
+
return status === ENTITLEMENT.ALLOWED || status === ENTITLEMENT.DENIED;
|
|
100
|
+
}
|
|
101
|
+
|
|
67
102
|
function publicPlan(record, execution = null) {
|
|
68
103
|
const status = record.status ?? record;
|
|
69
104
|
return {
|
|
@@ -352,6 +387,9 @@ export function createConversationService(deps = {}) {
|
|
|
352
387
|
const readOpenCodeGoCached = createCachedProbe(readOpenCodeGoImpl, opencodeGoUsageTtlMs);
|
|
353
388
|
const readOpenCodeStatsCached = createCachedProbe(readOpenCodeStatsImpl, opencodeUsageTtlMs);
|
|
354
389
|
const readClaudeEntitlementCacheImpl = deps.readClaudeEntitlementCache ?? readClaudeEntitlementCache;
|
|
390
|
+
const writeClaudeEntitlementCacheImpl = deps.writeClaudeEntitlementCache ?? writeClaudeEntitlementCache;
|
|
391
|
+
const mergeEntitlementResultsImpl = deps.mergeEntitlementResults ?? mergeEntitlementResults;
|
|
392
|
+
const probeClaudeModelEntitlementsImpl = deps.probeClaudeModelEntitlements ?? probeClaudeModelEntitlements;
|
|
355
393
|
const readClaudeEntitlementCacheCached = createCachedProbe(
|
|
356
394
|
() => readClaudeEntitlementCacheImpl(homeDir), claudeEntitlementCacheTtlMs
|
|
357
395
|
);
|
|
@@ -821,7 +859,92 @@ export function createConversationService(deps = {}) {
|
|
|
821
859
|
const candidates = { scoredAll, eligibility, registry, providerCapacity, claudeEntitlement };
|
|
822
860
|
const alternatives = computeBootstrapAnalystAlternatives(candidates);
|
|
823
861
|
const analystCatalog = computeBootstrapAnalystCatalog({ ...candidates, unscoredModels });
|
|
824
|
-
|
|
862
|
+
const unverifiedCount = Object.values(claudeEntitlement).filter(
|
|
863
|
+
(entry) => entry?.status === ENTITLEMENT.UNVERIFIED
|
|
864
|
+
).length;
|
|
865
|
+
const unverifiedClaudeNotice = unverifiedCount > 0
|
|
866
|
+
? buildUnverifiedClaudePreflightNotice(unverifiedCount)
|
|
867
|
+
: null;
|
|
868
|
+
return { profile, alternatives, candidates, analystCatalog, projectRoot, unverifiedClaudeNotice };
|
|
869
|
+
},
|
|
870
|
+
/**
|
|
871
|
+
* `/models --verify-access [--refresh]`: the only service path that
|
|
872
|
+
* spawns `probeClaudeModelEntitlements`. Without refresh, only
|
|
873
|
+
* unverified / TTL-expired catalog ids are probed; with refresh, every
|
|
874
|
+
* catalog id is probed (still capped at maxProbes=12). Persist only when
|
|
875
|
+
* at least one allowed/denied result exists — an all-unverified sweep
|
|
876
|
+
* never invents evidence on disk.
|
|
877
|
+
*
|
|
878
|
+
* @param {object} args
|
|
879
|
+
* @param {string} args.cwd
|
|
880
|
+
* @param {boolean} [args.refresh]
|
|
881
|
+
* @param {(info: { pendingCount: number, pendingIds: string[], costStatement: string }) => (void|Promise<void>)} [args.beforeProbe]
|
|
882
|
+
* Called after the probe set is known and BEFORE any spawn — app.js
|
|
883
|
+
* prints the cost statement here.
|
|
884
|
+
* @param {(info: { modelId: string, index: number, total: number, result: object }) => void} [args.onProgress]
|
|
885
|
+
*/
|
|
886
|
+
async verifyClaudeEntitlements({ cwd, refresh = false, beforeProbe = null, onProgress = null } = {}) {
|
|
887
|
+
const projectRoot = cwd ? await root(cwd) : null;
|
|
888
|
+
const catalog = readClaudeModelsImpl();
|
|
889
|
+
const catalogIds = (catalog.models ?? []).map((m) => m.id).filter(Boolean);
|
|
890
|
+
let auth = null;
|
|
891
|
+
try {
|
|
892
|
+
auth = await verifyClaudeSubscriptionAuthImpl({});
|
|
893
|
+
} catch {
|
|
894
|
+
auth = null;
|
|
895
|
+
}
|
|
896
|
+
const subscriptionType = auth?.subscriptionType ?? null;
|
|
897
|
+
const cache = await readClaudeEntitlementCacheImpl(homeDir);
|
|
898
|
+
const nowMs = now();
|
|
899
|
+
const ttlMs = deps.claudeEntitlementTtlMs ?? DEFAULT_ENTITLEMENT_TTL_MS;
|
|
900
|
+
const resolved = resolveClaudeEntitlements({
|
|
901
|
+
cache,
|
|
902
|
+
subscriptionType,
|
|
903
|
+
catalogIds,
|
|
904
|
+
now: nowMs,
|
|
905
|
+
ttlMs
|
|
906
|
+
});
|
|
907
|
+
const pendingIds = (refresh
|
|
908
|
+
? catalogIds
|
|
909
|
+
: catalogIds.filter((id) => resolved[id]?.status === ENTITLEMENT.UNVERIFIED)
|
|
910
|
+
).slice(0, CLAUDE_ENTITLEMENT_MAX_PROBES);
|
|
911
|
+
const costStatement = buildClaudeEntitlementVerifyCostStatement({ pendingCount: pendingIds.length });
|
|
912
|
+
if (typeof beforeProbe === "function") {
|
|
913
|
+
await beforeProbe({ pendingCount: pendingIds.length, pendingIds, costStatement });
|
|
914
|
+
}
|
|
915
|
+
if (pendingIds.length === 0) {
|
|
916
|
+
return {
|
|
917
|
+
probed: [],
|
|
918
|
+
results: [],
|
|
919
|
+
costStatement,
|
|
920
|
+
persisted: false,
|
|
921
|
+
pendingCount: 0,
|
|
922
|
+
refresh: Boolean(refresh),
|
|
923
|
+
subscriptionType
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const results = await probeClaudeModelEntitlementsImpl({
|
|
927
|
+
modelIds: pendingIds,
|
|
928
|
+
maxProbes: CLAUDE_ENTITLEMENT_MAX_PROBES,
|
|
929
|
+
cwd: projectRoot ?? process.cwd(),
|
|
930
|
+
onProgress
|
|
931
|
+
});
|
|
932
|
+
const hasPersistable = results.some((result) => isPersistableEntitlementStatus(result?.status));
|
|
933
|
+
let persisted = false;
|
|
934
|
+
if (hasPersistable) {
|
|
935
|
+
const merged = mergeEntitlementResultsImpl(cache, { subscriptionType, results });
|
|
936
|
+
await writeClaudeEntitlementCacheImpl(homeDir, merged);
|
|
937
|
+
persisted = true;
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
probed: pendingIds,
|
|
941
|
+
results,
|
|
942
|
+
costStatement,
|
|
943
|
+
persisted,
|
|
944
|
+
pendingCount: pendingIds.length,
|
|
945
|
+
refresh: Boolean(refresh),
|
|
946
|
+
subscriptionType
|
|
947
|
+
};
|
|
825
948
|
},
|
|
826
949
|
/**
|
|
827
950
|
* `/project analyst quality|efficient --confirm` (ANALYZING -> SUGGESTED):
|
|
@@ -89,7 +89,7 @@ export async function readCodexModels({
|
|
|
89
89
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
90
|
|
|
91
91
|
writeRequest(child, 1, "initialize", {
|
|
92
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.28.0" },
|
|
93
93
|
capabilities: {}
|
|
94
94
|
});
|
|
95
95
|
});
|
|
@@ -151,7 +151,7 @@ export async function readCodexUsage({
|
|
|
151
151
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
152
|
|
|
153
153
|
writeRequest(child, 1, "initialize", {
|
|
154
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.28.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|