@kal-elsam/kairo-runtime 0.16.0 → 0.18.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +2 -1
  3. package/scripts/cockpit-smoke.mjs +1 -1
  4. package/scripts/ux-smoke-test.sh +3 -3
  5. package/src/cli.js +96 -11
  6. package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
  7. package/src/global/architect/architect-cli.js +76 -0
  8. package/src/global/architect/architect-codex.js +146 -0
  9. package/src/global/architect/architect-manager.js +125 -0
  10. package/src/global/architect/architect-store.js +377 -0
  11. package/src/global/architect/architect-types.js +47 -0
  12. package/src/global/cli-help.js +10 -1
  13. package/src/global/cockpit/app.js +493 -0
  14. package/src/global/cockpit/card.js +111 -0
  15. package/src/global/cockpit/cli.js +33 -0
  16. package/src/global/cockpit/gauge.js +31 -0
  17. package/src/global/cockpit/project-overlay.js +693 -0
  18. package/src/global/cockpit/rows.js +148 -0
  19. package/src/global/cockpit/theme.js +118 -0
  20. package/src/global/cockpit/view.js +1298 -0
  21. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  22. package/src/global/conversation/cli.js +53 -0
  23. package/src/global/conversation/codex-sandbox.js +230 -0
  24. package/src/global/conversation/cursor-sandbox.js +215 -0
  25. package/src/global/conversation/project-analysis.js +204 -0
  26. package/src/global/conversation/project-profile.js +178 -0
  27. package/src/global/conversation/project-router.js +149 -0
  28. package/src/global/conversation/project-strategy-store.js +64 -0
  29. package/src/global/conversation/project-strategy.js +514 -0
  30. package/src/global/conversation/sanitized-snapshot.js +169 -0
  31. package/src/global/conversation/secret-scanner.js +71 -0
  32. package/src/global/conversation/service.js +1090 -0
  33. package/src/global/conversation/session-store.js +75 -0
  34. package/src/global/conversation/transcript-store.js +79 -0
  35. package/src/global/conversation/ui.js +195 -0
  36. package/src/global/intelligence/capability-scoring.js +480 -0
  37. package/src/global/intelligence/execution-router.js +466 -0
  38. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  39. package/src/global/intelligence/kairobench-runner.js +85 -0
  40. package/src/global/intelligence/kairobench-source.js +34 -0
  41. package/src/global/intelligence/kairobench-tasks.js +47 -0
  42. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  43. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  44. package/src/global/intelligence/model-capability-registry.js +125 -0
  45. package/src/global/intelligence/model-intelligence.js +1646 -0
  46. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  47. package/src/global/intelligence/quick-ask.js +149 -0
  48. package/src/global/intelligence/role-profiles.js +251 -0
  49. package/src/global/intelligence/skill-catalog.js +67 -0
  50. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  51. package/src/global/mcp/kairo-mcp.js +51 -18
  52. package/src/global/mcp/work-snapshot-rule.js +4 -2
  53. package/src/global/mcp/workspace-binding.js +88 -0
  54. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  55. package/src/global/mcp-install.js +8 -1
  56. package/src/global/observability/artificial-analysis-models.js +118 -0
  57. package/src/global/observability/claude-models.js +31 -0
  58. package/src/global/observability/claude-usage.js +112 -0
  59. package/src/global/observability/codex-models.js +96 -0
  60. package/src/global/observability/codex-usage.js +160 -0
  61. package/src/global/observability/cursor-auth.js +88 -0
  62. package/src/global/observability/cursor-models.js +101 -0
  63. package/src/global/observability/huggingface-leaderboard.js +97 -0
  64. package/src/global/observability/opencode-models.js +101 -0
  65. package/src/global/observability/opencode-usage.js +162 -0
  66. package/src/global/paths.js +49 -2
  67. package/src/global/profile.js +23 -1
  68. package/src/global/runtime/execution-adapters/claude.js +63 -30
  69. package/src/global/runtime/execution-adapters/codex.js +9 -2
  70. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  71. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  72. package/src/global/runtime/execution-worktree-manager.js +924 -0
  73. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  74. package/src/global/runtime/execution-worktree-store.js +83 -0
  75. package/src/global/runtime/execution-worktree-types.js +45 -0
  76. package/src/global/runtime/run-events.js +38 -0
  77. package/src/global/runtime/run-manager.js +22 -6
  78. package/src/global/runtime/run-supervisor.js +41 -12
  79. package/src/global/runtime/usage-manager.js +96 -0
  80. package/src/global/runtime/usage-store.js +69 -0
  81. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,169 @@
1
+ // The real defense the Bootstrap Analyst runs against: never the real
2
+ // project directory. A read-only sandbox (Codex) or an unrestricted
3
+ // permission mode (Claude, today) only ever governs WRITES — neither
4
+ // stops the model provider's own backend from receiving the real
5
+ // CONTENT of whatever the model reads as part of normal inference.
6
+ // "Read-only" is not "secret-safe".
7
+ //
8
+ // This builds a real temporary copy of the project's real text files,
9
+ // with every real matched secret redacted (secret-scanner.js) and every
10
+ // path context-compiler.js's own isPrivatePath already excludes from
11
+ // context skipped outright — the analyst's `cwd` points at THIS copy,
12
+ // so a normal investigation (listing/reading files within its working
13
+ // directory, which is how these agents actually explore) never sees an
14
+ // unredacted secret.
15
+ //
16
+ // HONEST LIMIT, verified empirically against the real Codex CLI, not
17
+ // assumed: `codex exec --sandbox read-only` does NOT confine file READS
18
+ // to cwd — a real test run against a real absolute path outside the
19
+ // snapshot successfully read that file's content. This is not a bug in
20
+ // Codex; "read-only" there means "no writes", full-disk read access is
21
+ // the documented default. So THIS MODULE is not a hard filesystem
22
+ // sandbox by itself, and must never be described as one.
23
+ //
24
+ // For Codex specifically, that gap is now closed by a real, empirically
25
+ // proven OS-level boundary: see codex-sandbox.js, which wraps Bootstrap
26
+ // Analysis's Codex invocation in an external macOS sandbox-exec (SBPL)
27
+ // profile confined to this module's snapshotRoot. conversation/service.js's
28
+ // runBootstrapAnalysis routes Codex through that wrapper, never through
29
+ // plain askProvider, and fails closed (isolation_unavailable) rather than
30
+ // silently degrading on a platform without a verified boundary.
31
+ //
32
+ // For every other provider (Claude today) and for Codex on a non-macOS
33
+ // platform, the remaining mitigation is still redaction-only: the analyst
34
+ // is never told the real project's absolute path (buildAnalystPrompt only
35
+ // ever passes profile.projectName, never a path), and .git is always
36
+ // excluded (no remote URL or other path hints to reconstruct it from) —
37
+ // so escaping requires the model to already know or guess a path it was
38
+ // never given, not merely wanting to. Closing that gap for other
39
+ // providers/platforms needs their own verified OS-level boundary; there
40
+ // is none today.
41
+
42
+ import { mkdtemp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
43
+ import { tmpdir } from "node:os";
44
+ import { join, relative, dirname } from "node:path";
45
+ import { isPrivatePath } from "../intelligence/context-compiler.js";
46
+ import { redactSecrets } from "./secret-scanner.js";
47
+
48
+ // Directories that are never real source the analyst needs to read, and
49
+ // are often huge (vendor code, build output, VCS internals) — skipped
50
+ // outright rather than wastefully copied and scanned.
51
+ const EXCLUDED_DIR_NAMES = new Set([
52
+ ".git", "node_modules", ".next", "dist", "build", ".turbo", ".cache", "coverage",
53
+ ".codegraph", "graphify-out", ".venv", "__pycache__", ".pnpm"
54
+ ]);
55
+
56
+ // A conservative allowlist of real source/text extensions — binary files
57
+ // (images, fonts, compiled artifacts) are never copied; there's nothing
58
+ // for the analyst to usefully read in them, and skipping avoids wasting
59
+ // the file/byte budget below on files that can't meaningfully leak text
60
+ // secrets anyway (a real secret embedded in a binary is a much rarer,
61
+ // separate risk this module doesn't attempt to solve).
62
+ const TEXT_EXTENSIONS = new Set([
63
+ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".json", ".md", ".mdx", ".yml", ".yaml",
64
+ ".toml", ".env.example", ".txt", ".css", ".scss", ".html", ".py", ".go", ".rs", ".rb",
65
+ ".java", ".kt", ".swift", ".sh", ".graphql", ".sql", ".xml", ".vue", ".svelte"
66
+ ]);
67
+
68
+ const DEFAULT_MAX_FILES = 400;
69
+ const DEFAULT_MAX_TOTAL_BYTES = 4 * 1024 * 1024; // 4 MiB — a real, bounded budget, not the whole repo
70
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
71
+
72
+ function isTextFile(filePath) {
73
+ for (const ext of TEXT_EXTENSIONS) {
74
+ if (filePath.endsWith(ext)) return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ async function walk(root, dir, budget, report, deps) {
80
+ let entries;
81
+ try {
82
+ entries = await deps.readdir(dir, { withFileTypes: true });
83
+ } catch {
84
+ return; // unreadable dir — skip, never throw and abort the whole snapshot
85
+ }
86
+ for (const entry of entries) {
87
+ if (budget.filesCopied >= budget.maxFiles || budget.totalBytes >= budget.maxTotalBytes) return;
88
+ const absolute = join(dir, entry.name);
89
+ const rel = relative(root, absolute).replace(/\\/g, "/");
90
+ if (entry.isDirectory()) {
91
+ if (EXCLUDED_DIR_NAMES.has(entry.name)) continue;
92
+ await walk(root, absolute, budget, report, deps);
93
+ continue;
94
+ }
95
+ if (!entry.isFile()) continue;
96
+ if (isPrivatePath(rel)) {
97
+ report.excludedPrivatePaths.push(rel);
98
+ continue;
99
+ }
100
+ if (!isTextFile(entry.name)) continue;
101
+ let stats;
102
+ try {
103
+ stats = await deps.stat(absolute);
104
+ } catch {
105
+ continue;
106
+ }
107
+ if (stats.size > budget.maxFileBytes) {
108
+ report.excludedOversized.push(rel);
109
+ continue;
110
+ }
111
+ let content;
112
+ try {
113
+ content = await deps.readFile(absolute, "utf8");
114
+ } catch {
115
+ continue; // likely binary despite the extension allowlist, or unreadable — skip, never guess
116
+ }
117
+ const { text, redactedCount } = redactSecrets(content);
118
+ const destination = join(budget.snapshotRoot, rel);
119
+ await deps.mkdir(dirname(destination), { recursive: true });
120
+ await deps.writeFile(destination, text, "utf8");
121
+ report.copiedFiles.push(rel);
122
+ budget.filesCopied += 1;
123
+ budget.totalBytes += Buffer.byteLength(text, "utf8");
124
+ report.filesCopied += 1;
125
+ report.secretsRedacted += redactedCount;
126
+ if (redactedCount > 0) report.redactedFiles.push(rel);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Builds a real, bounded, secret-redacted temporary copy of a project's
132
+ * real text files — the Bootstrap Analyst's `cwd` must point here, never
133
+ * at the real project root. Always call the returned `cleanup()` when
134
+ * done (a `finally` in the caller), even on failure — never leave a
135
+ * sanitized copy lying around.
136
+ * @param {string} projectRoot
137
+ * @param {{maxFiles?: number, maxTotalBytes?: number, maxFileBytes?: number}} [options]
138
+ * @param {object} [deps] - injectable real fs/promises functions, for tests only (defaults to the real ones)
139
+ * @returns {Promise<{snapshotRoot: string, filesCopied: number, secretsRedacted: number, redactedFiles: string[], excludedPrivatePaths: string[], excludedOversized: string[], cleanup: () => Promise<void>}>}
140
+ */
141
+ export async function buildSanitizedSnapshot(projectRoot, options = {}, deps = {}) {
142
+ const fsDeps = {
143
+ readdir: deps.readdir ?? readdir, stat: deps.stat ?? stat, readFile: deps.readFile ?? readFile,
144
+ mkdir: deps.mkdir ?? mkdir, writeFile: deps.writeFile ?? writeFile
145
+ };
146
+ const mkdtempImpl = deps.mkdtemp ?? mkdtemp;
147
+ const snapshotRoot = await mkdtempImpl(join(tmpdir(), "kairo-analyst-snapshot-"));
148
+ const budget = {
149
+ snapshotRoot,
150
+ filesCopied: 0, totalBytes: 0,
151
+ maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
152
+ maxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
153
+ maxFileBytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES
154
+ };
155
+ const report = { filesCopied: 0, secretsRedacted: 0, redactedFiles: [], copiedFiles: [], excludedPrivatePaths: [], excludedOversized: [] };
156
+ const cleanup = () => rm(snapshotRoot, { recursive: true, force: true }).catch(() => {});
157
+ // walk() itself only swallows per-entry errors (an unreadable dir/file
158
+ // is skipped, never abandons the whole snapshot) — but an unexpected
159
+ // failure (e.g. disk full mid-copy) can still throw. The mkdtemp'd
160
+ // directory must never be left behind just because building it failed
161
+ // partway through.
162
+ try {
163
+ await walk(projectRoot, projectRoot, budget, report, fsDeps);
164
+ } catch (error) {
165
+ await cleanup();
166
+ throw error;
167
+ }
168
+ return { snapshotRoot, ...report, cleanup };
169
+ }
@@ -0,0 +1,71 @@
1
+ // A real, local, pattern-based secret scanner — never network I/O, never
2
+ // a provider call. This is the local step of "repo real -> escaneo local
3
+ // de secretos -> snapshot temporal sanitizado -> analista sobre snapshot
4
+ // read-only": nothing containing a matched secret should ever reach a
5
+ // remote model provider unredacted.
6
+ //
7
+ // Honest limitation, stated plainly rather than implied: pattern/regex
8
+ // matching (the same approach tools like gitleaks/detect-secrets use)
9
+ // catches well-known secret SHAPES and generic "key/secret/token =
10
+ // <long string>" assignments — it does NOT catch every possible secret
11
+ // (a custom internal token format with no recognizable shape can still
12
+ // slip through). This is a real, meaningful reduction in exposure, never
13
+ // a guarantee of zero leakage — treat it as a mandatory floor, not a
14
+ // substitute for the user's own judgment about what's sensitive.
15
+
16
+ const SECRET_PATTERNS = [
17
+ { name: "aws-access-key-id", pattern: /AKIA[0-9A-Z]{16}/g },
18
+ { name: "google-api-key", pattern: /AIza[0-9A-Za-z_-]{35}/g },
19
+ { name: "github-token", pattern: /gh[pousr]_[A-Za-z0-9]{20,}/g },
20
+ { name: "slack-token", pattern: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
21
+ { name: "private-key-block", pattern: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/g },
22
+ { name: "jwt", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
23
+ // The generic catch-all: a variable/key name that reads like a secret,
24
+ // assigned a long opaque string — this is what actually catches a
25
+ // hardcoded provider key (Azure, OpenAI, a custom API key) with no
26
+ // other recognizable shape, which is exactly what the real crm finding was.
27
+ { name: "generic-credential-assignment", pattern: /(?:api[_-]?key|secret|token|password|passwd|credential|auth[_-]?key)\s*[:=]\s*["']([A-Za-z0-9_\-/+=]{16,})["']/gi }
28
+ ];
29
+
30
+ /**
31
+ * Scans real text content for known secret shapes. Never mutates, never
32
+ * touches disk — pure, so it's cheap to run over every file a sanitized
33
+ * snapshot is about to include.
34
+ * @param {string} text
35
+ * @returns {Array<{name: string, index: number, length: number}>}
36
+ */
37
+ export function scanTextForSecrets(text) {
38
+ const findings = [];
39
+ for (const { name, pattern } of SECRET_PATTERNS) {
40
+ pattern.lastIndex = 0;
41
+ let match;
42
+ while ((match = pattern.exec(text))) {
43
+ findings.push({ name, index: match.index, length: match[0].length });
44
+ if (match[0].length === 0) pattern.lastIndex += 1; // guard against zero-width matches looping forever
45
+ }
46
+ }
47
+ return findings;
48
+ }
49
+
50
+ /**
51
+ * Replaces every matched secret span with a fixed-width redaction marker
52
+ * (never a value derived from the real secret, and never just deleted —
53
+ * deleting could shift surrounding real code in a way that misleads the
54
+ * analyst about the file's real structure).
55
+ * @param {string} text
56
+ * @returns {{text: string, redactedCount: number}}
57
+ */
58
+ export function redactSecrets(text) {
59
+ const findings = scanTextForSecrets(text).sort((a, b) => a.index - b.index);
60
+ if (!findings.length) return { text, redactedCount: 0 };
61
+ let result = "";
62
+ let cursor = 0;
63
+ for (const finding of findings) {
64
+ if (finding.index < cursor) continue; // overlapping match already covered
65
+ result += text.slice(cursor, finding.index);
66
+ result += "[REDACTED-SECRET]";
67
+ cursor = finding.index + finding.length;
68
+ }
69
+ result += text.slice(cursor);
70
+ return { text: result, redactedCount: findings.length };
71
+ }