@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +76 -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 +106 -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 +12 -1
  13. package/src/global/cockpit/app.js +475 -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 +683 -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 +1263 -0
  21. package/src/global/control-plane/attention.js +141 -0
  22. package/src/global/control-plane/build-report.js +146 -0
  23. package/src/global/control-plane/cli.js +36 -0
  24. package/src/global/control-plane/constants.js +38 -0
  25. package/src/global/control-plane/gentle-adapters.js +183 -0
  26. package/src/global/control-plane/provider.js +69 -0
  27. package/src/global/control-plane/review-status.js +115 -0
  28. package/src/global/control-plane/sdd-status.js +49 -0
  29. package/src/global/control-plane/team.js +63 -0
  30. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  31. package/src/global/conversation/cli.js +53 -0
  32. package/src/global/conversation/codex-sandbox.js +230 -0
  33. package/src/global/conversation/cursor-sandbox.js +215 -0
  34. package/src/global/conversation/project-analysis.js +204 -0
  35. package/src/global/conversation/project-profile.js +178 -0
  36. package/src/global/conversation/project-router.js +149 -0
  37. package/src/global/conversation/project-strategy-store.js +64 -0
  38. package/src/global/conversation/project-strategy.js +514 -0
  39. package/src/global/conversation/sanitized-snapshot.js +169 -0
  40. package/src/global/conversation/secret-scanner.js +71 -0
  41. package/src/global/conversation/service.js +1063 -0
  42. package/src/global/conversation/session-store.js +75 -0
  43. package/src/global/conversation/transcript-store.js +79 -0
  44. package/src/global/conversation/ui.js +195 -0
  45. package/src/global/intelligence/capability-scoring.js +480 -0
  46. package/src/global/intelligence/execution-router.js +444 -0
  47. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  48. package/src/global/intelligence/kairobench-runner.js +85 -0
  49. package/src/global/intelligence/kairobench-source.js +34 -0
  50. package/src/global/intelligence/kairobench-tasks.js +47 -0
  51. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  52. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  53. package/src/global/intelligence/model-capability-registry.js +125 -0
  54. package/src/global/intelligence/model-intelligence.js +1646 -0
  55. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  56. package/src/global/intelligence/quick-ask.js +149 -0
  57. package/src/global/intelligence/role-profiles.js +251 -0
  58. package/src/global/intelligence/skill-catalog.js +67 -0
  59. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  60. package/src/global/mcp/kairo-mcp.js +51 -18
  61. package/src/global/mcp/work-snapshot-rule.js +4 -2
  62. package/src/global/mcp/workspace-binding.js +88 -0
  63. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  64. package/src/global/mcp-install.js +8 -1
  65. package/src/global/observability/artificial-analysis-models.js +118 -0
  66. package/src/global/observability/claude-models.js +31 -0
  67. package/src/global/observability/claude-usage.js +112 -0
  68. package/src/global/observability/codex-models.js +96 -0
  69. package/src/global/observability/codex-usage.js +160 -0
  70. package/src/global/observability/cursor-auth.js +88 -0
  71. package/src/global/observability/cursor-models.js +101 -0
  72. package/src/global/observability/gentle-probe.js +30 -2
  73. package/src/global/observability/huggingface-leaderboard.js +97 -0
  74. package/src/global/observability/index.js +2 -1
  75. package/src/global/observability/opencode-models.js +101 -0
  76. package/src/global/observability/opencode-usage.js +162 -0
  77. package/src/global/paths.js +49 -2
  78. package/src/global/profile.js +23 -1
  79. package/src/global/runtime/execution-adapters/claude.js +63 -30
  80. package/src/global/runtime/execution-adapters/codex.js +9 -2
  81. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  82. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  83. package/src/global/runtime/execution-worktree-manager.js +924 -0
  84. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  85. package/src/global/runtime/execution-worktree-store.js +83 -0
  86. package/src/global/runtime/execution-worktree-types.js +45 -0
  87. package/src/global/runtime/run-events.js +38 -0
  88. package/src/global/runtime/run-manager.js +22 -6
  89. package/src/global/runtime/run-supervisor.js +41 -12
  90. package/src/global/runtime/usage-manager.js +96 -0
  91. package/src/global/runtime/usage-store.js +69 -0
  92. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,230 @@
1
+ // Real OS-level filesystem confinement for the Codex CLI, used ONLY for
2
+ // Bootstrap Analysis (conversation/service.js's runBootstrapAnalysis) —
3
+ // general ASK (intelligence/quick-ask.js's askProvider) keeps using
4
+ // Codex's own `--sandbox read-only`, which is NOT read-confining (see
5
+ // sanitized-snapshot.js's header: it blocks writes only, a real absolute
6
+ // path outside cwd is still readable). This module closes that specific
7
+ // gap with an OS-enforced boundary instead of relying on redaction alone.
8
+ //
9
+ // Mechanism: wrap `codex exec` in an external `sandbox-exec` (macOS SBPL)
10
+ // profile, and pass Codex `--dangerously-bypass-approvals-and-sandbox` so
11
+ // Codex's OWN internal sandboxing is off — Codex's `--sandbox read-only`
12
+ // internally re-invokes sandbox-exec per tool call, and nesting an outer
13
+ // sandbox-exec around that breaks every tool call outright (verified
14
+ // empirically: every Codex tool invocation failed with a sandbox_apply
15
+ // error). With Codex's own sandbox disabled, the external profile becomes
16
+ // the sole enforcement layer.
17
+ //
18
+ // Empirically proven, not assumed (see engram memory
19
+ // "Codex sandbox-exec confinement proven for Bootstrap Analyst
20
+ // isolation"): under this exact wrapper, a real `codex exec` run reads a
21
+ // file inside the confined root correctly, and is denied
22
+ // ("Operation not permitted") reading a file outside it via an absolute
23
+ // path.
24
+ //
25
+ // HONEST LIMIT: SBPL applies uniformly to a sandboxed process and every
26
+ // child it execs — there is no SBPL primitive that grants Codex's own
27
+ // process read/write access to CODEX_HOME while denying that same access
28
+ // to tools Codex spawns. Both are required: Codex fails hard ("failed to
29
+ // initialize in-process app-server client: Operation not permitted",
30
+ // verified empirically) without WRITE access to CODEX_HOME too, not just
31
+ // read. So CODEX_HOME is fully readable and writable by the whole
32
+ // confined tree, not just Codex's top-level process. This does not
33
+ // weaken the actual isolation goal (nothing in that tree can escape the
34
+ // snapshot boundary either way) — it only means a per-process auth/tool
35
+ // split, as asked for in review, cannot be built on sandbox-exec alone.
36
+ //
37
+ // macOS only. Any other platform returns { available: false }; callers
38
+ // must fail closed (isolation_unavailable) and never silently fall back
39
+ // to Codex's own non-confining --sandbox read-only for Bootstrap Analysis.
40
+
41
+ import { spawn as defaultSpawn } from "node:child_process";
42
+ import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
43
+ import { constants as fsConstants } from "node:fs";
44
+ import { tmpdir, homedir } from "node:os";
45
+ import { join } from "node:path";
46
+
47
+ const SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
48
+
49
+ // Bootstrap Analysis is a real project investigation, not a quick
50
+ // question — mirrors service.js's own BOOTSTRAP_ANALYST_TIMEOUT_MS.
51
+ const DEFAULT_TIMEOUT_MS = 180_000;
52
+
53
+ const SAFE_ENV_KEYS = Object.freeze([
54
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
55
+ "TMPDIR", "TERM", "CODEX_HOME", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
56
+ "http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
57
+ ]);
58
+
59
+ function buildSandboxedCodexEnv(sourceEnv = process.env) {
60
+ const env = Object.create(null);
61
+ for (const key of SAFE_ENV_KEYS) {
62
+ if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
63
+ }
64
+ return env;
65
+ }
66
+
67
+ function unknown(error) {
68
+ return { status: "error", answer: null, error: String(error) };
69
+ }
70
+
71
+ export async function isCodexSandboxSupported(deps = {}) {
72
+ if ((deps.platform ?? process.platform) !== "darwin") return false;
73
+ try {
74
+ await (deps.access ?? access)(SANDBOX_EXEC_PATH, fsConstants.X_OK);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Real, checkable isolation status for Codex — never a hardcoded claim.
83
+ * `boundaryVerified` reflects that the sandbox-exec mechanism itself has
84
+ * been empirically proven (canary-denial test) on this platform, not that
85
+ * this specific call was independently re-verified.
86
+ */
87
+ export async function getCodexIsolationStatus(deps = {}) {
88
+ const available = await isCodexSandboxSupported(deps);
89
+ return {
90
+ available,
91
+ platform: deps.platform ?? process.platform,
92
+ boundaryVerified: available,
93
+ reason: available
94
+ ? null
95
+ : "OS-level read confinement for Codex (sandbox-exec) is only implemented for macOS; Codex is not eligible for isolated Bootstrap Analysis on this platform."
96
+ };
97
+ }
98
+
99
+ async function resolvedForms(path, deps) {
100
+ const forms = new Set([path]);
101
+ try {
102
+ forms.add(await (deps.realpath ?? realpath)(path));
103
+ } catch {
104
+ // path may not exist yet — the literal form alone still covers it
105
+ }
106
+ return [...forms];
107
+ }
108
+
109
+ function subpathRules(paths) {
110
+ return paths.map((p) => ` (subpath "${p}")`).join("\n");
111
+ }
112
+
113
+ /**
114
+ * Builds a real SBPL profile confining reads/writes to `snapshotRoot` (the
115
+ * sanitized-snapshot.js temp copy the analyst investigates) plus
116
+ * `codexHome` (Codex's own auth config — without it the CLI can't
117
+ * authenticate at all) and the minimal system paths Codex needs to run.
118
+ * Resolves both the given path and its real path (handles macOS's
119
+ * /tmp -> /private/tmp and /var -> /private/var symlinks automatically,
120
+ * rather than hardcoding either form).
121
+ */
122
+ export async function buildCodexSandboxProfile({ snapshotRoot, codexHome = join(homedir(), ".codex") }, deps = {}) {
123
+ const snapshotForms = await resolvedForms(snapshotRoot, deps);
124
+ const codexHomeForms = await resolvedForms(codexHome, deps);
125
+ const readableExtra = [
126
+ "/usr", "/System", "/bin", "/sbin", "/private/var/db/dyld", "/Library", "/opt", "/private/etc"
127
+ ];
128
+ return `(version 1)
129
+ (deny default)
130
+ (allow process-fork)
131
+ (allow process-exec)
132
+ (allow file-read-metadata (subpath "/"))
133
+ (allow file-read-data (literal "/"))
134
+ (allow file-read*
135
+ ${subpathRules([...snapshotForms, ...codexHomeForms, ...readableExtra])}
136
+ (literal "/dev/null")
137
+ (literal "/dev/urandom")
138
+ (literal "/dev/tty"))
139
+ (allow file-write*
140
+ ${subpathRules([...snapshotForms, ...codexHomeForms, "/private/var/folders", "/private/tmp"])})
141
+ (allow file-read-metadata (subpath "/private/var/folders"))
142
+ (allow sysctl-read)
143
+ (allow mach-lookup)
144
+ (allow signal (target self))
145
+ (allow network*)
146
+ (allow system-socket)
147
+ `;
148
+ }
149
+
150
+ /**
151
+ * Runs a real, OS-sandboxed Codex Bootstrap Analysis question. The ONLY
152
+ * intended caller is conversation/service.js's runBootstrapAnalysis.
153
+ * Fails closed with `status: "error", error: "isolation_unavailable"`
154
+ * (never a silent fallback to Codex's own non-confining --sandbox
155
+ * read-only) when this platform has no verified boundary.
156
+ * @param {object} args
157
+ * @param {string} args.question
158
+ * @param {string|null} [args.model]
159
+ * @param {string} args.snapshotRoot - sanitized-snapshot.js's temp copy
160
+ * @param {string} [args.codexHome]
161
+ */
162
+ export async function runCodexSandboxedBootstrap({
163
+ question, model = null, snapshotRoot, codexHome = join(homedir(), ".codex"),
164
+ spawn = defaultSpawn, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv = process.env, deps = {}
165
+ }) {
166
+ const isolation = await getCodexIsolationStatus(deps);
167
+ if (!isolation.available) {
168
+ return { status: "error", answer: null, error: "isolation_unavailable", isolation };
169
+ }
170
+
171
+ let workDir;
172
+ try {
173
+ workDir = await (deps.mkdtemp ?? mkdtemp)(join(tmpdir(), "kairo-codex-sandbox-"));
174
+ } catch (error) {
175
+ return { ...unknown(error?.message ?? error), isolation };
176
+ }
177
+ const profilePath = join(workDir, "bootstrap.sb");
178
+ const outFile = join(workDir, "answer.txt");
179
+
180
+ try {
181
+ const profile = await buildCodexSandboxProfile({ snapshotRoot, codexHome }, deps);
182
+ await (deps.writeFile ?? writeFile)(profilePath, profile, "utf8");
183
+
184
+ // --skip-git-repo-check: snapshotRoot deliberately excludes .git.
185
+ // --ephemeral: no session files persisted to disk for this run.
186
+ // --ignore-user-config: doesn't load $CODEX_HOME/config.toml (auth
187
+ // itself still resolves via CODEX_HOME, per `codex exec --help`).
188
+ const args = [
189
+ "-f", profilePath, "codex", "exec",
190
+ "--dangerously-bypass-approvals-and-sandbox",
191
+ "--skip-git-repo-check", "--ephemeral", "--ignore-user-config",
192
+ "-o", outFile
193
+ ];
194
+ if (model) args.push("--model", model);
195
+ args.push(question);
196
+
197
+ const env = buildSandboxedCodexEnv(sourceEnv);
198
+ const result = await new Promise((resolve) => {
199
+ let child;
200
+ try {
201
+ child = spawn("sandbox-exec", args, { cwd: snapshotRoot, env, stdio: ["ignore", "pipe", "pipe"] });
202
+ } catch (error) {
203
+ resolve(unknown(error?.message ?? error));
204
+ return;
205
+ }
206
+ let finished = false;
207
+ const timer = setTimeout(() => finish(unknown("sandboxed codex exec timed out")), timeoutMs);
208
+ function finish(res) {
209
+ if (finished) return;
210
+ finished = true;
211
+ clearTimeout(timer);
212
+ try { child.kill?.(); } catch { /* best effort */ }
213
+ resolve(res);
214
+ }
215
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
216
+ child.once?.("close", async () => {
217
+ try {
218
+ const text = (await (deps.readFile ?? readFile)(outFile, "utf8")).trim();
219
+ if (!text) return finish(unknown("sandboxed codex exec produced no final message"));
220
+ finish({ status: "answered", answer: text, error: null });
221
+ } catch (error) {
222
+ finish(unknown(error?.message ?? error));
223
+ }
224
+ });
225
+ });
226
+ return { ...result, isolation };
227
+ } finally {
228
+ await (deps.rm ?? rm)(workDir, { recursive: true, force: true }).catch(() => {});
229
+ }
230
+ }
@@ -0,0 +1,215 @@
1
+ // Real OS-level filesystem confinement for the Cursor CLI (cursor-agent),
2
+ // used ONLY for Bootstrap Analysis, mirroring codex-sandbox.js exactly —
3
+ // general ASK never routes Cursor through this module.
4
+ //
5
+ // Empirically proven necessary AND sufficient, not assumed: Cursor's own
6
+ // `--sandbox enabled` (documented in `cursor-agent --help` as "Explicitly
7
+ // enable or disable sandbox mode") does NOT confine file reads to
8
+ // --workspace — a real absolute-path read outside the workspace
9
+ // succeeded and disclosed real content under `--sandbox enabled` alone.
10
+ // The same external sandbox-exec wrapper approach that closed this gap
11
+ // for Codex (codex-sandbox.js) was then independently canary-tested
12
+ // against the real cursor-agent CLI and DOES hold: wrapping `cursor-agent`
13
+ // in an external macOS sandbox-exec profile, with Cursor's own internal
14
+ // sandbox disabled (`--sandbox disabled`, so only the external wrapper
15
+ // enforces anything — avoids any risk of the kind of nested-sandbox
16
+ // conflict that broke every Codex tool call when both layers tried to
17
+ // sandbox at once), produces a real, held boundary: an in-bounds read
18
+ // succeeds, an out-of-bounds absolute-path read is denied
19
+ // ("Permission denied", not a model claim in prose).
20
+ //
21
+ // Two real gotchas found only by testing the ACTUAL cursor-agent binary
22
+ // (not assumed from Codex's profile):
23
+ // 1. cursor-agent's real binary lives under `~/.local` (a wrapper script
24
+ // at ~/.local/bin/cursor-agent execs the real binary under
25
+ // ~/.local/share/cursor-agent/versions/...) — that whole tree must be
26
+ // readable+executable, or the CLI can't even launch.
27
+ // 2. cursor-agent's stored auth ("Authentication tokens stored
28
+ // securely") lives in the macOS Keychain, not a plain file under its
29
+ // config home — ~/Library/Keychains must be read+write accessible or
30
+ // every real invocation fails with "Authentication required" even
31
+ // though the session is genuinely logged in. The wrapper script also
32
+ // writes to /dev/null, which needs explicit file-write access (unlike
33
+ // Codex's profile, which never needed it).
34
+ //
35
+ // A workspace cursor-agent has never seen before triggers an interactive
36
+ // "Workspace Trust Required" prompt that blocks non-interactive use —
37
+ // `--trust` is required for automation, safe here because snapshotRoot is
38
+ // always Kairo's own freshly-generated temp directory, never an
39
+ // arbitrary user-chosen one (same category of bypass as Codex's
40
+ // --skip-git-repo-check for a snapshot that deliberately excludes .git).
41
+
42
+ import { spawn as defaultSpawn } from "node:child_process";
43
+ import { access, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
44
+ import { constants as fsConstants } from "node:fs";
45
+ import { tmpdir, homedir } from "node:os";
46
+ import { join } from "node:path";
47
+
48
+ const SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
49
+ const DEFAULT_TIMEOUT_MS = 180_000;
50
+
51
+ const SAFE_ENV_KEYS = Object.freeze([
52
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
53
+ "TMPDIR", "TERM", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
54
+ "http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
55
+ ]);
56
+
57
+ function buildSandboxedCursorEnv(sourceEnv = process.env) {
58
+ const env = Object.create(null);
59
+ for (const key of SAFE_ENV_KEYS) {
60
+ if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
61
+ }
62
+ return env;
63
+ }
64
+
65
+ function unknown(error) {
66
+ return { status: "error", answer: null, error: String(error) };
67
+ }
68
+
69
+ export async function isCursorSandboxSupported(deps = {}) {
70
+ if ((deps.platform ?? process.platform) !== "darwin") return false;
71
+ try {
72
+ await (deps.access ?? access)(SANDBOX_EXEC_PATH, fsConstants.X_OK);
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ export async function getCursorIsolationStatus(deps = {}) {
80
+ const available = await isCursorSandboxSupported(deps);
81
+ return {
82
+ available,
83
+ platform: deps.platform ?? process.platform,
84
+ boundaryVerified: available,
85
+ reason: available
86
+ ? null
87
+ : "OS-level read confinement for Cursor (sandbox-exec) is only implemented for macOS; Cursor is not eligible for isolated Bootstrap Analysis on this platform."
88
+ };
89
+ }
90
+
91
+ async function resolvedForms(path, deps) {
92
+ const forms = new Set([path]);
93
+ try {
94
+ forms.add(await (deps.realpath ?? realpath)(path));
95
+ } catch {
96
+ // fine — the literal form alone still covers it
97
+ }
98
+ return [...forms];
99
+ }
100
+
101
+ function subpathRules(paths) {
102
+ return paths.map((p) => ` (subpath "${p}")`).join("\n");
103
+ }
104
+
105
+ export async function buildCursorSandboxProfile({
106
+ snapshotRoot,
107
+ cursorHome = join(homedir(), ".cursor"),
108
+ cursorLocalHome = join(homedir(), ".local"),
109
+ keychainsHome = join(homedir(), "Library", "Keychains")
110
+ }, deps = {}) {
111
+ const snapshotForms = await resolvedForms(snapshotRoot, deps);
112
+ const cursorHomeForms = await resolvedForms(cursorHome, deps);
113
+ const cursorLocalForms = await resolvedForms(cursorLocalHome, deps);
114
+ const keychainsForms = await resolvedForms(keychainsHome, deps);
115
+ const readableExtra = [
116
+ "/usr", "/System", "/bin", "/sbin", "/private/var/db/dyld", "/Library", "/opt", "/private/etc"
117
+ ];
118
+ return `(version 1)
119
+ (deny default)
120
+ (allow process-fork)
121
+ (allow process-exec)
122
+ (allow file-read-metadata (subpath "/"))
123
+ (allow file-read-data (literal "/"))
124
+ (allow file-read*
125
+ ${subpathRules([...snapshotForms, ...cursorHomeForms, ...cursorLocalForms, ...keychainsForms, ...readableExtra])}
126
+ (literal "/dev/null")
127
+ (literal "/dev/urandom")
128
+ (literal "/dev/tty"))
129
+ (allow file-write*
130
+ (literal "/dev/null")
131
+ ${subpathRules([...snapshotForms, ...cursorHomeForms, ...keychainsForms, "/private/var/folders", "/private/tmp"])})
132
+ (allow file-read-metadata (subpath "/private/var/folders"))
133
+ (allow sysctl-read)
134
+ (allow mach-lookup)
135
+ (allow signal (target self))
136
+ (allow network*)
137
+ (allow system-socket)
138
+ `;
139
+ }
140
+
141
+ /**
142
+ * Runs a real, OS-sandboxed Cursor Bootstrap Analysis question. The ONLY
143
+ * intended caller is bootstrap-analyzer-adapters.js's Cursor adapter.
144
+ * Fails closed with `status: "error", error: "isolation_unavailable"`
145
+ * (never a silent fallback to Cursor's own non-confining --sandbox
146
+ * enabled) when this platform has no verified boundary.
147
+ * @param {object} args
148
+ * @param {string} args.question
149
+ * @param {string|null} [args.model] - omit (or pass null) for Cursor Auto
150
+ * @param {string} args.snapshotRoot
151
+ */
152
+ export async function runCursorSandboxedBootstrap({
153
+ question, model = null, snapshotRoot,
154
+ cursorHome = join(homedir(), ".cursor"), cursorLocalHome = join(homedir(), ".local"),
155
+ keychainsHome = join(homedir(), "Library", "Keychains"),
156
+ spawn = defaultSpawn, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv = process.env, deps = {}
157
+ }) {
158
+ const isolation = await getCursorIsolationStatus(deps);
159
+ if (!isolation.available) {
160
+ return { status: "error", answer: null, error: "isolation_unavailable", isolation };
161
+ }
162
+
163
+ let workDir;
164
+ try {
165
+ workDir = await (deps.mkdtemp ?? mkdtemp)(join(tmpdir(), "kairo-cursor-sandbox-"));
166
+ } catch (error) {
167
+ return { ...unknown(error?.message ?? error), isolation };
168
+ }
169
+ const profilePath = join(workDir, "bootstrap.sb");
170
+
171
+ try {
172
+ const profile = await buildCursorSandboxProfile({ snapshotRoot, cursorHome, cursorLocalHome, keychainsHome }, deps);
173
+ await (deps.writeFile ?? writeFile)(profilePath, profile, "utf8");
174
+
175
+ const args = [
176
+ "-f", profilePath, "cursor-agent", "-p", question,
177
+ "--output-format", "json", "--mode", "ask", "--sandbox", "disabled",
178
+ "--workspace", snapshotRoot, "--trust"
179
+ ];
180
+ if (model) args.push("--model", model);
181
+
182
+ const env = buildSandboxedCursorEnv(sourceEnv);
183
+ const result = await new Promise((resolve) => {
184
+ let child;
185
+ try {
186
+ child = spawn("sandbox-exec", args, { cwd: snapshotRoot, env, stdio: ["ignore", "pipe", "pipe"] });
187
+ } catch (error) {
188
+ resolve(unknown(error?.message ?? error));
189
+ return;
190
+ }
191
+ let stdout = "";
192
+ let finished = false;
193
+ const timer = setTimeout(() => finish(unknown("sandboxed cursor-agent -p timed out")), timeoutMs);
194
+ function finish(res) {
195
+ if (finished) return;
196
+ finished = true;
197
+ clearTimeout(timer);
198
+ try { child.kill?.(); } catch { /* best effort */ }
199
+ resolve(res);
200
+ }
201
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
202
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
203
+ child.once?.("close", () => {
204
+ let parsed;
205
+ try { parsed = JSON.parse(stdout); } catch { return finish(unknown("malformed JSON from sandboxed cursor-agent -p")); }
206
+ const answer = parsed?.result ?? parsed?.text ?? parsed?.message ?? null;
207
+ if (typeof answer !== "string") return finish(unknown("no result text in sandboxed cursor-agent -p response"));
208
+ finish({ status: "answered", answer, error: null });
209
+ });
210
+ });
211
+ return { ...result, isolation };
212
+ } finally {
213
+ await (deps.rm ?? rm)(workDir, { recursive: true, force: true }).catch(() => {});
214
+ }
215
+ }
@@ -0,0 +1,204 @@
1
+ // The real Bootstrap Analyst step: a chosen real model actually reads the
2
+ // project (via askProvider — the same real, read-only, no-file-write path
3
+ // ASK mode already uses; never a new execution surface) and returns a
4
+ // structured ProjectAnalysis, validated against a real schema before
5
+ // anything downstream trusts it. An invalid or unparseable response is
6
+ // rejected outright — no ProjectStrategy is ever built from it.
7
+ //
8
+ // The deterministic step (deriveRoleRequirements) then turns that
9
+ // validated analysis into real RoleNeed[] — sanitized against the known
10
+ // capability vocabulary, unioned with the project's own mechanical floor
11
+ // (real test/lint/build commands, already computed by project-profile.js)
12
+ // so a thin or low-confidence analysis can never leave the project with
13
+ // literally zero real role requirements.
14
+
15
+ export const PROJECT_ANALYSIS_SCHEMA = "kairo.project-analysis/v1";
16
+
17
+ // The only real capabilities the scoring engine (capability-scoring.js)
18
+ // actually understands — any other token in the analyst's own output is
19
+ // dropped rather than trusted, so a hallucinated capability name can never
20
+ // corrupt scoring (worst case: a role with zero recognized capabilities
21
+ // simply never activates — fails closed, not open).
22
+ const KNOWN_CAPABILITIES = new Set(["reasoning", "coding", "terminalExecution", "softwareExecution", "instructionFollowing"]);
23
+ const KNOWN_ROLES = new Set(["Explorer", "Architect", "Builder", "Debugger", "Tester", "Reviewer"]);
24
+
25
+ /**
26
+ * The real, limited context package the Bootstrap Analyst receives — never
27
+ * the whole repo dumped in, and never anything the analyst could mistake
28
+ * for permission to write: just what project-profile.js already collected
29
+ * read-only (stack, real build/test/lint commands, real git hotspots,
30
+ * real workflow docs present). The analyst can still read further real
31
+ * files on its own (it runs inside `cwd`), but this is its starting brief.
32
+ * @param {object} profile - computeProjectProfile() result
33
+ * @returns {string}
34
+ */
35
+ export function buildAnalystPrompt(profile) {
36
+ const lines = [
37
+ "You are Kairo's Bootstrap Analyst. Investigate this real project, READ-ONLY — never propose or make any file change.",
38
+ "You may read real files in this working directory to inform your answer, but do not modify anything.",
39
+ "",
40
+ "## Known real evidence",
41
+ `Project: ${profile.projectName}`,
42
+ `Stack: ${profile.stack.join(", ") || "unknown"}`,
43
+ `Architecture pattern: ${profile.architecture?.pattern ?? "unknown"}`,
44
+ `Build command: ${profile.quality.buildCommand ?? "none detected"}`,
45
+ `Test command: ${profile.quality.testCommand ?? "none detected"}`,
46
+ `Lint/typecheck: ${profile.quality.lintCommand ?? profile.quality.typeCheckCommand ?? "none detected"}`,
47
+ `Real git hotspots (most-changed files, last 90 days): ${profile.hotspots.map((h) => h.path).join(", ") || "none"}`,
48
+ `Workflow docs present: ${profile.workflowCapabilities.join(", ") || "none"}`,
49
+ `Known risks: ${profile.risks.map((r) => r.detail).join("; ") || "none"}`,
50
+ "",
51
+ "## Task",
52
+ "Respond with ONLY one JSON object (no prose, no markdown fences) matching exactly this shape:",
53
+ JSON.stringify({
54
+ architectureTraits: ["string"], complexitySignals: ["string"], criticalAreas: ["string"],
55
+ contextNeeds: ["string"], workflowNeeds: ["string"],
56
+ recommendedRoleNeeds: [{
57
+ role: "Explorer|Architect|Builder|Debugger|Tester|Reviewer",
58
+ capabilities: ["reasoning|coding|terminalExecution|softwareExecution|instructionFollowing"],
59
+ reason: "string", evidence: ["real file path you actually read that supports THIS role need"]
60
+ }],
61
+ uncertainties: ["string"], evidenceReferences: ["string"]
62
+ }, null, 2),
63
+ "",
64
+ "Every field must reflect something you actually observed in this project — never invent a trait, risk, or role need you have no real evidence for. If you're not sure about something, put it in `uncertainties` instead of guessing.",
65
+ "Each recommendedRoleNeeds entry's own `evidence` must list the real file path(s) you actually read that support THAT SPECIFIC role need — a role need with no real evidence of its own will be discarded, even if other fields in this response are well-supported."
66
+ ];
67
+ return lines.join("\n");
68
+ }
69
+
70
+ /**
71
+ * Extracts and validates a ProjectAnalysis from the analyst's raw text
72
+ * response. Fails closed: any parse failure or shape mismatch returns
73
+ * `{valid: false}`, never a partially-trusted guess.
74
+ * @param {string} rawText
75
+ * @returns {{valid: true, analysis: object}|{valid: false, error: string}}
76
+ */
77
+ export function parseProjectAnalysis(rawText) {
78
+ const match = String(rawText ?? "").match(/\{[\s\S]*\}/);
79
+ if (!match) return { valid: false, error: "No JSON object found in the analyst's response." };
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(match[0]);
83
+ } catch (error) {
84
+ return { valid: false, error: `Analyst response is not valid JSON: ${error.message}` };
85
+ }
86
+ const arrayFields = ["architectureTraits", "complexitySignals", "criticalAreas", "contextNeeds", "workflowNeeds", "uncertainties", "evidenceReferences"];
87
+ for (const field of arrayFields) {
88
+ if (!Array.isArray(parsed[field])) return { valid: false, error: `Missing or invalid real array field "${field}".` };
89
+ }
90
+ if (!Array.isArray(parsed.recommendedRoleNeeds)) return { valid: false, error: 'Missing or invalid real array field "recommendedRoleNeeds".' };
91
+ for (const need of parsed.recommendedRoleNeeds) {
92
+ if (typeof need?.role !== "string" || !Array.isArray(need.capabilities)) {
93
+ return { valid: false, error: "Each recommendedRoleNeeds entry needs a real role (string) and capabilities (array)." };
94
+ }
95
+ }
96
+ return {
97
+ valid: true,
98
+ analysis: {
99
+ schema: PROJECT_ANALYSIS_SCHEMA,
100
+ architectureTraits: parsed.architectureTraits.map(String),
101
+ complexitySignals: parsed.complexitySignals.map(String),
102
+ criticalAreas: parsed.criticalAreas.map(String),
103
+ contextNeeds: parsed.contextNeeds.map(String),
104
+ workflowNeeds: parsed.workflowNeeds.map(String),
105
+ // `evidence` defaults to an empty array when the analyst omits it —
106
+ // never invented, and a role need with no evidence of its own
107
+ // fails the real per-entry gate in deriveRoleRequirements below,
108
+ // exactly as if it had cited nothing real.
109
+ recommendedRoleNeeds: parsed.recommendedRoleNeeds.map((need) => ({
110
+ role: String(need.role), capabilities: need.capabilities.map(String),
111
+ reason: typeof need.reason === "string" ? need.reason : null,
112
+ evidence: Array.isArray(need.evidence) ? need.evidence.map(String) : []
113
+ })),
114
+ uncertainties: parsed.uncertainties.map(String),
115
+ evidenceReferences: parsed.evidenceReferences.map(String)
116
+ }
117
+ };
118
+ }
119
+
120
+ function normalizePath(path) {
121
+ return String(path ?? "").trim().replace(/^\.\//, "").replace(/\/+$/, "");
122
+ }
123
+
124
+ /**
125
+ * Whether a single cited path corresponds to a real file the analyst
126
+ * actually had access to (the sanitized snapshot's real copied-file
127
+ * list) — a citation to a path that was never even in the snapshot is a
128
+ * real, checkable signal the analyst may be describing exploration it
129
+ * didn't actually do, not evidence it observed. Matching is real-path-
130
+ * based but tolerant of how a model might phrase a reference (a leading
131
+ * "./", or citing just the tail of a longer real path) — an exact
132
+ * string mismatch alone never disqualifies a real match.
133
+ * @param {string} reference
134
+ * @param {string[]} realFilePaths
135
+ * @returns {boolean}
136
+ */
137
+ function referenceMatchesRealFile(reference, realFilePaths) {
138
+ const ref = normalizePath(reference);
139
+ if (!ref) return false;
140
+ return realFilePaths.some((path) => path === ref || path.endsWith(`/${ref}`) || ref.endsWith(`/${path}`));
141
+ }
142
+
143
+ /**
144
+ * Checks which of a list of citations correspond to a real file — used
145
+ * both for the analysis's own top-level evidenceReferences (informational)
146
+ * and, per-entry, for each recommendedRoleNeeds' own `evidence` (see
147
+ * deriveRoleRequirements, which is the one that actually gates on this).
148
+ * @param {string[]} references
149
+ * @param {string[]} realFilePaths - the sanitized snapshot's real copiedFiles
150
+ * @returns {{verified: string[], unverified: string[]}}
151
+ */
152
+ export function validateReferences(references, realFilePaths) {
153
+ const real = realFilePaths.map(normalizePath);
154
+ const verified = [];
155
+ const unverified = [];
156
+ for (const raw of references) {
157
+ (referenceMatchesRealFile(raw, real) ? verified : unverified).push(raw);
158
+ }
159
+ return { verified, unverified };
160
+ }
161
+
162
+ /** Back-compat alias — validates the analysis's own top-level evidenceReferences. @deprecated prefer validateReferences for the per-RoleNeed gate in deriveRoleRequirements. */
163
+ export function validateEvidenceReferences(analysis, realFilePaths) {
164
+ return validateReferences(analysis.evidenceReferences, realFilePaths);
165
+ }
166
+
167
+ /**
168
+ * Deterministically derives real roleRequirements from a validated
169
+ * ProjectAnalysis, unioned with the project's own mechanical floor (real
170
+ * build/test/lint commands — see project-profile.js's detectRoleRequirements)
171
+ * so a thin or low-confidence analysis can never leave a real project with
172
+ * zero role requirements. The analyst's own role/capability tokens are
173
+ * sanitized against the known vocabulary first — an unrecognized one is
174
+ * dropped, never trusted as-is.
175
+ *
176
+ * Evidence is checked PER role need, not once for the whole analysis: a
177
+ * recommendedRoleNeeds entry is only trusted when at least one of ITS OWN
178
+ * `evidence` citations verifies against a real file the analyst actually
179
+ * had access to — a single well-evidenced role need can no longer
180
+ * "vouch for" every other, unrelated role need in the same response.
181
+ * @param {object} analysis - parseProjectAnalysis().analysis
182
+ * @param {Array<{role: string, capabilities: string[], reason: string}>} mechanicalFloor - profile.roleRequirements (the pre-existing command-based detection)
183
+ * @param {string[]} [realFilePaths] - the sanitized snapshot's real copiedFiles; omit only when no real file list is available (falls back to trusting each role need's vocabulary alone, matching this function's pre-sanitized-snapshot behavior)
184
+ * @returns {Array<{role: string, capabilities: string[], reason: string}>}
185
+ */
186
+ export function deriveRoleRequirements(analysis, mechanicalFloor, realFilePaths = null) {
187
+ const byRole = new Map(mechanicalFloor.map((requirement) => [requirement.role, { ...requirement }]));
188
+ for (const need of analysis.recommendedRoleNeeds) {
189
+ if (!KNOWN_ROLES.has(need.role)) continue;
190
+ const capabilities = need.capabilities.filter((c) => KNOWN_CAPABILITIES.has(c));
191
+ if (!capabilities.length) continue;
192
+ // Real-evidence-per-recommendation gate: this specific role need is
193
+ // only trusted when it cites at least one real file of its own.
194
+ if (realFilePaths && !(need.evidence ?? []).some((ref) => referenceMatchesRealFile(ref, realFilePaths))) continue;
195
+ const existing = byRole.get(need.role);
196
+ if (existing) {
197
+ existing.capabilities = [...new Set([...existing.capabilities, ...capabilities])];
198
+ existing.reason = `${existing.reason} Bootstrap Analyst: ${need.reason ?? "real project analysis"}.`;
199
+ } else {
200
+ byRole.set(need.role, { role: need.role, capabilities, reason: `Bootstrap Analyst: ${need.reason ?? "real project analysis"}.` });
201
+ }
202
+ }
203
+ return [...byRole.values()];
204
+ }