@kal-elsam/kairo-runtime 0.15.0 → 0.17.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 +76 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +106 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +12 -1
- package/src/global/cockpit/app.js +475 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -0
- package/src/global/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1063 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +444 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/gentle-probe.js +30 -2
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/index.js +2 -1
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- 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
|
+
}
|