@danielblomma/cortex-mcp 2.2.5 → 2.4.1

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 (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +336 -212
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
package/bin/cortex.mjs CHANGED
@@ -5,6 +5,11 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawn } from "node:child_process";
7
7
  import { normalizeProjectRoot } from "./wsl.mjs";
8
+ import {
9
+ callDaemon,
10
+ probeVerifiedDaemon,
11
+ stopVerifiedDaemon,
12
+ } from "./daemon-control.mjs";
8
13
  import {
9
14
  bullet,
10
15
  printBullet,
@@ -81,20 +86,21 @@ function printHelp() {
81
86
  console.log(helpRow("impact <query|entity-id> [--json]", "Trace likely impact paths"));
82
87
  console.log(helpRow("rules [--json]", "List active context rules"));
83
88
  console.log(helpRow("explain <query|entity-id> [--json]", "Show search score evidence"));
89
+ console.log(helpRow("pattern-evidence <file|entity-id> [--query <text>] [--top-k <n>] [--json]", "Collect cited repo-local pattern evidence"));
84
90
  console.log(helpRow("dashboard [--interval <sec>]", "Live local dashboard"));
85
91
  console.log(helpRow("memory-compile [--dry-run] [--verbose]", "Compile memory artifacts"));
86
92
  console.log(helpRow("memory-lint [--verbose] [--json]", "Lint compiled memory"));
87
93
  console.log(helpRow("watch [start|stop|status|run|once]", "Background sync (--interval, --debounce, --mode)"));
88
94
 
89
95
  console.log(helpSection("GOVERNANCE"));
90
- console.log(helpRow("enterprise <api-key>", "Install enforcement + hooks + daemon (sudo)"));
96
+ console.log(helpRow("enterprise install --api-key-stdin", "Install enforcement + hooks + daemon (sudo)"));
91
97
  console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
92
98
  console.log(helpRow("enterprise status", "Show local enforcement state"));
93
99
  console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
94
100
  console.log(helpRow("enterprise uninstall", "Remove enforcement (sudo, --break-glass --reason)"));
95
101
  console.log(helpRow("enterprise repair", "Verify managed paths, clear tamper-lock (sudo)"));
96
102
  console.log(helpRow("run <claude|codex|copilot> [args...]", "Wrap an AI CLI in cortex enforcement"));
97
- console.log(helpRow("daemon [start|stop|status]", "Local supervisor daemon"));
103
+ console.log(helpRow("daemon [start|stop|restart|status]", "Local supervisor daemon"));
98
104
  console.log(helpRow("hooks [install|uninstall|status] [--project]", "Claude Code hooks"));
99
105
  console.log(helpRow("telemetry test", "Smoke-test the push pipeline"));
100
106
 
@@ -198,81 +204,7 @@ function ensureScaffoldExists() {
198
204
  // Files that should never be overwritten if they already exist in the target.
199
205
  // These contain user-specific configuration that would be lost on re-init.
200
206
  const PRESERVE_FILES = new Set(["config.yaml", "rules.yaml", "enterprise.yml", "enterprise.yaml", "CLAUDE.md", "AGENTS.md"]);
201
- const DEFAULT_SOURCE_PATHS = [
202
- "src",
203
- "docs",
204
- "design",
205
- ".context/notes",
206
- ".context/decisions",
207
- "README.md"
208
- ];
209
- const INIT_SKIP_DIRECTORIES = new Set([
210
- ".git",
211
- ".idea",
212
- ".vscode",
213
- "node_modules",
214
- "dist",
215
- "build",
216
- "coverage",
217
- ".next",
218
- ".cache",
219
- ".context",
220
- "scripts",
221
- ".githooks",
222
- "bin",
223
- "obj"
224
- ]);
225
- const INIT_SOURCE_EXTENSIONS = new Set([
226
- ".md",
227
- ".mdx",
228
- ".txt",
229
- ".adoc",
230
- ".rst",
231
- ".yaml",
232
- ".yml",
233
- ".json",
234
- ".toml",
235
- ".csv",
236
- ".ts",
237
- ".tsx",
238
- ".mts",
239
- ".cts",
240
- ".js",
241
- ".jsx",
242
- ".mjs",
243
- ".cjs",
244
- ".py",
245
- ".go",
246
- ".java",
247
- ".cs",
248
- ".vb",
249
- ".sln",
250
- ".vbproj",
251
- ".csproj",
252
- ".fsproj",
253
- ".props",
254
- ".targets",
255
- ".config",
256
- ".resx",
257
- ".settings",
258
- ".rb",
259
- ".rs",
260
- ".php",
261
- ".swift",
262
- ".kt",
263
- ".sql",
264
- ".sh",
265
- ".bash",
266
- ".zsh",
267
- ".ps1",
268
- ".c",
269
- ".h",
270
- ".cpp",
271
- ".hpp",
272
- ".cc",
273
- ".hh"
274
- ]);
275
- const ROOT_DOC_PATHS = new Set(["docs", "design"]);
207
+ const DEFAULT_SOURCE_PATHS = ["."];
276
208
 
277
209
  function copyDirectory(sourceDir, targetDir) {
278
210
  fs.mkdirSync(targetDir, { recursive: true });
@@ -318,92 +250,8 @@ function slugifyRepoId(value) {
318
250
  return dashed || "cortex";
319
251
  }
320
252
 
321
- function isInterestingSourceFile(fileName) {
322
- const base = fileName.toLowerCase();
323
- const ext = path.extname(fileName).toLowerCase();
324
- return INIT_SOURCE_EXTENSIONS.has(ext) || base === "readme" || base.startsWith("readme.");
325
- }
326
-
327
- function directoryContainsInterestingFiles(directoryPath) {
328
- const stack = [directoryPath];
329
- while (stack.length > 0) {
330
- const current = stack.pop();
331
- let entries = [];
332
- try {
333
- entries = fs.readdirSync(current, { withFileTypes: true });
334
- } catch {
335
- continue;
336
- }
337
-
338
- for (const entry of entries) {
339
- const absolutePath = path.join(current, entry.name);
340
- if (entry.isDirectory()) {
341
- if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
342
- continue;
343
- }
344
- stack.push(absolutePath);
345
- continue;
346
- }
347
-
348
- if (entry.isFile() && isInterestingSourceFile(entry.name)) {
349
- return true;
350
- }
351
- }
352
- }
353
-
354
- return false;
355
- }
356
-
357
253
  function detectInitialSourcePaths(targetDir) {
358
- if (!fs.existsSync(targetDir)) {
359
- return [...DEFAULT_SOURCE_PATHS];
360
- }
361
-
362
- let entries = [];
363
- try {
364
- entries = fs.readdirSync(targetDir, { withFileTypes: true });
365
- } catch {
366
- return [...DEFAULT_SOURCE_PATHS];
367
- }
368
-
369
- const codeDirs = [];
370
- const docDirs = [];
371
- const rootFiles = [];
372
-
373
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
374
- const absolutePath = path.join(targetDir, entry.name);
375
-
376
- if (entry.isDirectory()) {
377
- if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
378
- continue;
379
- }
380
- if (!directoryContainsInterestingFiles(absolutePath)) {
381
- continue;
382
- }
383
-
384
- const bucket = ROOT_DOC_PATHS.has(entry.name) ? docDirs : codeDirs;
385
- bucket.push(toPosixPath(entry.name));
386
- continue;
387
- }
388
-
389
- if (entry.isFile() && isInterestingSourceFile(entry.name)) {
390
- rootFiles.push(toPosixPath(entry.name));
391
- }
392
- }
393
-
394
- const readmeFiles = rootFiles.filter((filePath) => /^readme(\.|$)/i.test(path.basename(filePath)));
395
- const nonReadmeRootFiles = rootFiles.filter((filePath) => !readmeFiles.includes(filePath));
396
- const detected = [
397
- ...codeDirs,
398
- ...nonReadmeRootFiles,
399
- ...docDirs,
400
- ".context/notes",
401
- ".context/decisions",
402
- ...readmeFiles
403
- ];
404
- const uniqueDetected = [...new Set(detected)];
405
- const hasConcreteRepoContent = uniqueDetected.some((value) => !value.startsWith(".context/"));
406
- return hasConcreteRepoContent ? uniqueDetected : [...DEFAULT_SOURCE_PATHS];
254
+ return [...DEFAULT_SOURCE_PATHS];
407
255
  }
408
256
 
409
257
  function buildInitialConfig(targetDir) {
@@ -418,12 +266,10 @@ function buildInitialConfig(targetDir) {
418
266
  " - RULE",
419
267
  " - CODE",
420
268
  " - WIKI",
421
- "# graph weight is low because graph degree mostly measures how many rules",
422
- "# constrain an entity (docs are hubs, leaf code is not). Tuned as a pair with",
423
- "# the midrank-percentile graph_score.",
269
+ "# Tuned together with the midrank-percentile graph_score in searchResults.ts.",
424
270
  "ranking:",
425
- " semantic: 0.55",
426
- " graph: 0.10",
271
+ " semantic: 0.40",
272
+ " graph: 0.25",
427
273
  " trust: 0.20",
428
274
  " recency: 0.15",
429
275
  "runtime:",
@@ -443,6 +289,31 @@ function initializeScaffold(targetDir, force) {
443
289
  }
444
290
  }
445
291
 
292
+ function hardenEnterpriseConfigPermissions(targetDir) {
293
+ const contextDir = path.join(targetDir, ".context");
294
+ for (const filename of ["enterprise.yml", "enterprise.yaml"]) {
295
+ const configPath = path.join(contextDir, filename);
296
+ let stat;
297
+ try {
298
+ stat = fs.lstatSync(configPath);
299
+ } catch (err) {
300
+ if (err && typeof err === "object" && err.code === "ENOENT") continue;
301
+ throw err;
302
+ }
303
+ if (stat.isSymbolicLink()) {
304
+ throw new Error(
305
+ `Refusing symlinked Enterprise configuration: ${configPath}`,
306
+ );
307
+ }
308
+ if (!stat.isFile()) {
309
+ throw new Error(
310
+ `Enterprise configuration is not a regular file: ${configPath}`,
311
+ );
312
+ }
313
+ fs.chmodSync(configPath, 0o600);
314
+ }
315
+ }
316
+
446
317
  function ensurePathWritable(targetPath, force) {
447
318
  if (!fs.existsSync(targetPath)) {
448
319
  return;
@@ -645,6 +516,10 @@ function installCodexAgentsSection(targetDir) {
645
516
  const startMarker = "<!-- cortex:auto:start -->";
646
517
  const endMarker = "<!-- cortex:auto:end -->";
647
518
  const section = `## Cortex Auto Workflow
519
+ - Use the \`using-cortex\` skill if available; otherwise follow the commands below.
520
+ - Search before answering code questions: \`cortex search "<query>" --json\`; never answer from memory.
521
+ - Check \`cortex rules --json\` before suggesting changes and \`cortex impact "<query>" --json\` before refactors.
522
+ - Review changed files with \`cortex pattern-evidence <file> --json\` before finalizing.
648
523
  - Run \`cortex update\` before completing substantial code changes.
649
524
  - If background sync is enabled, check with \`cortex watch status\`.`;
650
525
  const changed = upsertSectionByMarkers(agentsPath, startMarker, endMarker, section);
@@ -956,12 +831,31 @@ async function ensureProjectInitializedForMcp(targetDir) {
956
831
  }
957
832
  }
958
833
 
834
+ // The session bootstrap hook caches index status for 10 minutes; drop the
835
+ // cache whenever a command may have changed the index so new sessions never
836
+ // see a stale "no index"/"index is old" verdict.
837
+ const INDEX_MUTATING_COMMANDS = new Set(["bootstrap", "update", "refresh", "ingest", "embed", "graph-load"]);
838
+
839
+ function invalidateSessionStatusCache(cwd) {
840
+ try {
841
+ fs.rmSync(path.join(cwd, ".context", "cache", "session-status.json"), { force: true });
842
+ } catch {
843
+ // best effort: a stale cache only delays the status refresh
844
+ }
845
+ }
846
+
959
847
  async function runContextCommand(cwd, contextArgs) {
960
848
  const contextScript = path.join(cwd, CONTEXT_SCRIPTS_REL, "context.sh");
961
849
  if (!fs.existsSync(contextScript)) {
962
850
  throw new Error(`Missing ${contextScript}. Run 'cortex init' first.`);
963
851
  }
964
- await runCommand("bash", [contextScript, ...contextArgs], cwd);
852
+ try {
853
+ await runCommand("bash", [contextScript, ...contextArgs], cwd);
854
+ } finally {
855
+ if (INDEX_MUTATING_COMMANDS.has(contextArgs[0])) {
856
+ invalidateSessionStatusCache(cwd);
857
+ }
858
+ }
965
859
  }
966
860
 
967
861
  async function run() {
@@ -987,6 +881,7 @@ async function run() {
987
881
  printBanner("Cortex initializes repo-scoped context for AI coding agents.");
988
882
  fs.mkdirSync(target, { recursive: true });
989
883
  initializeScaffold(target, force);
884
+ hardenEnterpriseConfigPermissions(target);
990
885
  const helpers = installAssistantHelpers(target);
991
886
  await maybeInstallGitHooks(target);
992
887
 
@@ -1027,6 +922,7 @@ async function run() {
1027
922
 
1028
923
  if (bootstrap) {
1029
924
  await runContextCommand(target, ["bootstrap"]);
925
+ await restartDaemonAfterRuntimeUpgrade(target);
1030
926
  }
1031
927
 
1032
928
  if (connect) {
@@ -1117,23 +1013,34 @@ async function run() {
1117
1013
  }
1118
1014
 
1119
1015
  await maybeMigrateScaffold(process.cwd(), command);
1016
+ if (command === "bootstrap") {
1017
+ hardenEnterpriseConfigPermissions(process.cwd());
1018
+ }
1120
1019
  await runContextCommand(process.cwd(), [command, ...rest]);
1020
+ if (command === "bootstrap") {
1021
+ await restartDaemonAfterRuntimeUpgrade(process.cwd());
1022
+ }
1121
1023
  }
1122
1024
 
1123
1025
  // ---------------------------------------------------------------------------
1124
1026
  // v2.0.0: daemon + hooks commands
1125
1027
  // ---------------------------------------------------------------------------
1126
1028
 
1127
- const DAEMON_DIR = path.join(process.env.HOME || "", ".cortex");
1128
- const PID_FILE = path.join(DAEMON_DIR, "daemon.pid");
1029
+ function daemonDirPath() {
1030
+ return path.join(process.env.HOME || os.homedir(), ".cortex");
1031
+ }
1032
+
1033
+ function daemonPidFilePath() {
1034
+ return path.join(daemonDirPath(), "daemon.pid");
1035
+ }
1129
1036
 
1130
1037
  function pidFileExists() {
1131
- return fs.existsSync(PID_FILE);
1038
+ return fs.existsSync(daemonPidFilePath());
1132
1039
  }
1133
1040
 
1134
1041
  function readPid() {
1135
1042
  try {
1136
- const raw = fs.readFileSync(PID_FILE, "utf8").trim();
1043
+ const raw = fs.readFileSync(daemonPidFilePath(), "utf8").trim();
1137
1044
  const pid = Number.parseInt(raw, 10);
1138
1045
  return Number.isFinite(pid) && pid > 0 ? pid : null;
1139
1046
  } catch {
@@ -1154,20 +1061,23 @@ function isPidAlive(pid) {
1154
1061
  }
1155
1062
  }
1156
1063
 
1157
- function resolveProjectRuntimeDist() {
1064
+ function resolveProjectRuntimeDist(projectRoot) {
1158
1065
  // v2.0.5: project layout was moved from <cwd>/mcp/ to <cwd>/.context/mcp/.
1159
1066
  // The runtime still lives there for compatibility, but CLI commands now
1160
1067
  // treat it as the local context runtime rather than an MCP-only surface.
1161
- const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
1068
+ const target =
1069
+ projectRoot ||
1070
+ process.env.CORTEX_PROJECT_ROOT?.trim() ||
1071
+ process.cwd();
1162
1072
  return path.join(target, CONTEXT_RUNTIME_REL, "dist");
1163
1073
  }
1164
1074
 
1165
- function resolveProjectMcpDist() {
1166
- return resolveProjectRuntimeDist();
1075
+ function resolveProjectMcpDist(projectRoot) {
1076
+ return resolveProjectRuntimeDist(projectRoot);
1167
1077
  }
1168
1078
 
1169
- function resolveDaemonEntry() {
1170
- return path.join(resolveProjectRuntimeDist(), "daemon", "main.js");
1079
+ function resolveDaemonEntry(projectRoot) {
1080
+ return path.join(resolveProjectRuntimeDist(projectRoot), "daemon", "main.js");
1171
1081
  }
1172
1082
 
1173
1083
  function resolveHookEntry(name) {
@@ -1178,54 +1088,116 @@ function resolveCliEntry(name) {
1178
1088
  return path.join(resolveProjectRuntimeDist(), "cli", `${name}.js`);
1179
1089
  }
1180
1090
 
1181
- async function runDaemonCommand(args) {
1091
+ function resolveTrustedCliEntry(name) {
1092
+ return path.join(SCAFFOLD_ROOT, "mcp", "dist", "cli", `${name}.js`);
1093
+ }
1094
+
1095
+ async function daemonControlDeps() {
1096
+ return {
1097
+ readPid,
1098
+ isPidAlive,
1099
+ call: callDaemon,
1100
+ };
1101
+ }
1102
+
1103
+ async function waitForVerifiedDaemon(deps, timeoutMs = 5_000) {
1104
+ const deadline = Date.now() + timeoutMs;
1105
+ while (Date.now() < deadline) {
1106
+ const probe = await probeVerifiedDaemon(deps, 250);
1107
+ if (probe.verified) return probe;
1108
+ await new Promise((resolve) => setTimeout(resolve, 50));
1109
+ }
1110
+ return probeVerifiedDaemon(deps, 250);
1111
+ }
1112
+
1113
+ async function runDaemonCommand(args, options = {}) {
1182
1114
  const sub = args[0] || "status";
1115
+ const projectRoot = options.projectRoot || process.cwd();
1116
+ const deps = await daemonControlDeps();
1183
1117
  if (sub === "start") {
1184
- if (isPidAlive(readPid())) {
1118
+ const existing = await probeVerifiedDaemon(deps);
1119
+ if (existing.verified) {
1185
1120
  console.log("Daemon already running.");
1186
1121
  return;
1187
1122
  }
1188
- fs.mkdirSync(DAEMON_DIR, { recursive: true });
1189
- const entry = resolveDaemonEntry();
1123
+ if (existing.running) {
1124
+ throw new Error(
1125
+ `Refusing to replace unverified live pid ${existing.pid} (${existing.reason}).`,
1126
+ );
1127
+ }
1128
+ const daemonDir = daemonDirPath();
1129
+ fs.mkdirSync(daemonDir, { recursive: true });
1130
+ const entry = resolveDaemonEntry(projectRoot);
1190
1131
  if (!fs.existsSync(entry)) {
1191
1132
  throw new Error(`Daemon entry not found: ${entry}. Build cortex first.`);
1192
1133
  }
1193
- const logFd = fs.openSync(path.join(DAEMON_DIR, "daemon.log"), "a");
1134
+ const logFd = fs.openSync(path.join(daemonDir, "daemon.log"), "a");
1194
1135
  const child = spawn(process.execPath, [entry], {
1195
1136
  detached: true,
1196
1137
  stdio: ["ignore", logFd, logFd],
1138
+ cwd: projectRoot,
1139
+ env: { ...process.env, CORTEX_PROJECT_ROOT: projectRoot },
1197
1140
  });
1198
1141
  child.unref();
1199
- console.log(`Daemon started (pid=${child.pid}). Log: ${path.join(DAEMON_DIR, "daemon.log")}`);
1142
+ fs.closeSync(logFd);
1143
+ const started = await waitForVerifiedDaemon(deps);
1144
+ if (!started.verified) {
1145
+ throw new Error(
1146
+ `Daemon did not complete its verified socket handshake (${started.reason}).`,
1147
+ );
1148
+ }
1149
+ console.log(`Daemon started (pid=${started.pid}). Log: ${path.join(daemonDir, "daemon.log")}`);
1200
1150
  return;
1201
1151
  }
1202
1152
  if (sub === "stop") {
1203
- const pid = readPid();
1204
- if (!isPidAlive(pid)) {
1153
+ const result = await stopVerifiedDaemon(deps);
1154
+ if (!result.stopped) {
1205
1155
  console.log("Daemon not running.");
1206
1156
  return;
1207
1157
  }
1208
- try {
1209
- process.kill(pid, "SIGTERM");
1210
- console.log(`Sent SIGTERM to pid ${pid}`);
1211
- } catch (err) {
1212
- throw new Error(`Failed to stop daemon: ${err instanceof Error ? err.message : String(err)}`);
1213
- }
1158
+ console.log(`Daemon stopped (verified pid=${result.pid}).`);
1159
+ return;
1160
+ }
1161
+ if (sub === "restart") {
1162
+ await stopVerifiedDaemon(deps);
1163
+ await runDaemonCommand(["start"], { projectRoot });
1214
1164
  return;
1215
1165
  }
1216
1166
  if (sub === "status") {
1217
- const pid = readPid();
1218
- if (isPidAlive(pid)) {
1219
- console.log(`Daemon running (pid=${pid})`);
1167
+ const probe = await probeVerifiedDaemon(deps);
1168
+ if (probe.verified) {
1169
+ console.log(`Daemon running (verified pid=${probe.pid})`);
1170
+ } else if (probe.running) {
1171
+ console.log(
1172
+ `Daemon state unsafe: live pid=${probe.pid}, identity not verified (${probe.reason}).`,
1173
+ );
1220
1174
  } else {
1221
1175
  console.log("Daemon not running.");
1222
1176
  if (pidFileExists()) {
1223
- console.log(`(stale pid file at ${PID_FILE})`);
1177
+ console.log(`(stale pid file at ${daemonPidFilePath()})`);
1224
1178
  }
1225
1179
  }
1226
1180
  return;
1227
1181
  }
1228
- throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|status`);
1182
+ throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|restart|status`);
1183
+ }
1184
+
1185
+ async function restartDaemonAfterRuntimeUpgrade(projectRoot) {
1186
+ const daemonEntry = resolveDaemonEntry(projectRoot);
1187
+ if (!fs.existsSync(daemonEntry)) return;
1188
+ const deps = await daemonControlDeps();
1189
+ const probe = await probeVerifiedDaemon(deps);
1190
+ if (!probe.running) return;
1191
+ if (!probe.verified) {
1192
+ throw new Error(
1193
+ `Runtime upgraded, but live pid ${probe.pid} could not be verified. ` +
1194
+ "Refusing to signal it; stop it manually after confirming ownership.",
1195
+ );
1196
+ }
1197
+ console.log(
1198
+ `[cortex] restarting verified daemon pid=${probe.pid} to activate the upgraded runtime`,
1199
+ );
1200
+ await runDaemonCommand(["restart"], { projectRoot });
1229
1201
  }
1230
1202
 
1231
1203
  async function runHookShim(args) {
@@ -1318,7 +1290,8 @@ function writeJson(file, data) {
1318
1290
  }
1319
1291
 
1320
1292
  // Enterprise == govern. One command, sudo-elevated, hard-fail without it.
1321
- // `cortex enterprise <api-key>` does the full install. Subcommands status/sync/uninstall
1293
+ // `cortex enterprise install --api-key-stdin` does the full install.
1294
+ // Subcommands status/sync/uninstall
1322
1295
  // dispatch to scaffold/mcp/dist/cli/govern.js.
1323
1296
 
1324
1297
  function requireSudoElevation() {
@@ -1356,11 +1329,58 @@ function dropPrivileges(sudo) {
1356
1329
  return sudoInfo.homedir;
1357
1330
  }
1358
1331
 
1332
+ async function runAsSudoUser(sudo, operation) {
1333
+ const required = [
1334
+ "geteuid",
1335
+ "getegid",
1336
+ "seteuid",
1337
+ "setegid",
1338
+ "getgroups",
1339
+ "setgroups",
1340
+ ];
1341
+ for (const name of required) {
1342
+ if (typeof process[name] !== "function") {
1343
+ throw new Error(
1344
+ `Secure Enterprise installation requires process.${name} support.`,
1345
+ );
1346
+ }
1347
+ }
1348
+
1349
+ const previous = {
1350
+ euid: process.geteuid(),
1351
+ egid: process.getegid(),
1352
+ groups: process.getgroups(),
1353
+ home: process.env.HOME,
1354
+ user: process.env.USER,
1355
+ logname: process.env.LOGNAME,
1356
+ };
1357
+ const sudoInfo = os.userInfo({ uid: sudo.uid });
1358
+ try {
1359
+ process.setgroups([sudo.gid]);
1360
+ process.setegid(sudo.gid);
1361
+ process.seteuid(sudo.uid);
1362
+ process.env.HOME = sudoInfo.homedir;
1363
+ process.env.USER = sudo.user;
1364
+ process.env.LOGNAME = sudo.user;
1365
+ return await operation();
1366
+ } finally {
1367
+ process.seteuid(previous.euid);
1368
+ process.setegid(previous.egid);
1369
+ process.setgroups(previous.groups);
1370
+ if (previous.home === undefined) delete process.env.HOME;
1371
+ else process.env.HOME = previous.home;
1372
+ if (previous.user === undefined) delete process.env.USER;
1373
+ else process.env.USER = previous.user;
1374
+ if (previous.logname === undefined) delete process.env.LOGNAME;
1375
+ else process.env.LOGNAME = previous.logname;
1376
+ }
1377
+ }
1378
+
1359
1379
  function loadGovernModule() {
1360
- const entry = resolveCliEntry("govern");
1380
+ const entry = resolveTrustedCliEntry("govern");
1361
1381
  if (!fs.existsSync(entry)) {
1362
1382
  throw new Error(
1363
- `Build the project's context runtime first (missing ${entry}). Run 'cortex bootstrap' in the project root.`
1383
+ `The installed Cortex package is missing its trusted Enterprise runtime (${entry}). Reinstall Cortex.`
1364
1384
  );
1365
1385
  }
1366
1386
  return import(pathToFileURL(entry).href);
@@ -1369,10 +1389,15 @@ function loadGovernModule() {
1369
1389
  const ENTERPRISE_SUBCOMMANDS = new Set(["status", "sync", "uninstall", "repair", "help", "--help", "-h"]);
1370
1390
 
1371
1391
  async function runEnterpriseCommand(args) {
1392
+ if (args[0] === "install") {
1393
+ return runEnterpriseInstall(args.slice(1));
1394
+ }
1372
1395
  if (args.length === 0 || ENTERPRISE_SUBCOMMANDS.has(args[0])) {
1373
1396
  return runEnterpriseSubcommand(args);
1374
1397
  }
1375
- return runEnterpriseInstall(args);
1398
+ throw new Error(
1399
+ "Positional enterprise API keys are not accepted. Use 'cortex enterprise install --api-key-stdin'.",
1400
+ );
1376
1401
  }
1377
1402
 
1378
1403
  async function runEnterpriseSubcommand(args) {
@@ -1380,13 +1405,14 @@ async function runEnterpriseSubcommand(args) {
1380
1405
 
1381
1406
  if (sub === "help" || sub === "--help" || sub === "-h" || !sub) {
1382
1407
  console.log(gradient("cortex enterprise") + muted(" · governance, armed."));
1383
- console.log(helpRow("enterprise <api-key>", "Install (sudo). Managed enforcement + hooks + daemon."));
1408
+ console.log(helpRow("enterprise install --api-key-stdin", "Install (sudo). Managed enforcement + hooks + daemon."));
1384
1409
  console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
1385
1410
  console.log(helpRow("enterprise status [--verbose|--json]", "Show local enforcement state"));
1386
1411
  console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
1387
1412
  console.log(helpRow("enterprise uninstall", "Remove. [--break-glass --reason \"<text>\"] in enforced mode (sudo)"));
1388
1413
  console.log(helpRow("enterprise repair", "Verify managed paths, clear .cortex-tamper.lock (sudo)"));
1389
1414
  console.log("");
1415
+ console.log(muted("Example: printf '%s\\n' \"$CORTEX_API_KEY\" | sudo cortex enterprise install --api-key-stdin"));
1390
1416
  console.log(muted("Default endpoint: https://cortex-web-rho.vercel.app"));
1391
1417
  return;
1392
1418
  }
@@ -1407,9 +1433,14 @@ async function runEnterpriseSubcommand(args) {
1407
1433
  }
1408
1434
 
1409
1435
  if (sub === "sync") {
1410
- requireSudoElevation();
1436
+ const sudo = requireSudoElevation();
1411
1437
  const mod = await loadGovernModule();
1412
- await mod.runGovernSync({ cwd: process.cwd() });
1438
+ const projectOperation = (operation) => runAsSudoUser(sudo, operation);
1439
+ await mod.runGovernSync({
1440
+ cwd: process.cwd(),
1441
+ projectOperation,
1442
+ });
1443
+ dropPrivileges(sudo);
1413
1444
  return;
1414
1445
  }
1415
1446
 
@@ -1425,14 +1456,17 @@ async function runEnterpriseSubcommand(args) {
1425
1456
  throw new Error(`Unknown enterprise uninstall option: ${args[i]}`);
1426
1457
  }
1427
1458
  }
1428
- requireSudoElevation();
1459
+ const sudo = requireSudoElevation();
1429
1460
  const mod = await loadGovernModule();
1461
+ const projectOperation = (operation) => runAsSudoUser(sudo, operation);
1430
1462
  const result = await mod.runGovernUninstall({
1431
1463
  cli: "all",
1432
1464
  breakGlass,
1433
1465
  reason,
1434
1466
  cwd: process.cwd(),
1467
+ projectOperation,
1435
1468
  });
1469
+ dropPrivileges(sudo);
1436
1470
  if (!result.ok) {
1437
1471
  printBullet("fail", result.message, process.stderr);
1438
1472
  process.exit(1);
@@ -1451,9 +1485,15 @@ async function runEnterpriseSubcommand(args) {
1451
1485
  throw new Error(`Unknown enterprise repair option: ${args[i]}`);
1452
1486
  }
1453
1487
  }
1454
- requireSudoElevation();
1488
+ const sudo = requireSudoElevation();
1455
1489
  const mod = await loadGovernModule();
1456
- const result = await mod.runGovernRepair({ cwd: process.cwd(), reason });
1490
+ const projectOperation = (operation) => runAsSudoUser(sudo, operation);
1491
+ const result = await mod.runGovernRepair({
1492
+ cwd: process.cwd(),
1493
+ reason,
1494
+ projectOperation,
1495
+ });
1496
+ dropPrivileges(sudo);
1457
1497
  if (!result.ok) {
1458
1498
  printBullet("fail", result.message, process.stderr);
1459
1499
  process.exit(1);
@@ -1465,14 +1505,19 @@ async function runEnterpriseSubcommand(args) {
1465
1505
  throw new Error(`Unknown enterprise subcommand: ${sub}`);
1466
1506
  }
1467
1507
 
1468
- async function runEnterpriseInstall(args) {
1469
- const apiKey = args[0];
1508
+ async function runEnterpriseInstall(args, injected = {}) {
1509
+ if (args.includes("--help") || args.includes("-h")) {
1510
+ return runEnterpriseSubcommand(["help"]);
1511
+ }
1512
+ let readKeyFromStdin = false;
1470
1513
  let endpoint;
1471
1514
  let frameworks;
1472
1515
  let installHooks = true;
1473
1516
  let startDaemon = true;
1474
- for (let i = 1; i < args.length; i++) {
1475
- if (args[i] === "--endpoint" && args[i + 1]) {
1517
+ for (let i = 0; i < args.length; i++) {
1518
+ if (args[i] === "--api-key-stdin") {
1519
+ readKeyFromStdin = true;
1520
+ } else if (args[i] === "--endpoint" && args[i + 1]) {
1476
1521
  endpoint = args[i + 1];
1477
1522
  i++;
1478
1523
  } else if (args[i] === "--frameworks" && args[i + 1]) {
@@ -1484,23 +1529,61 @@ async function runEnterpriseInstall(args) {
1484
1529
  startDaemon = false;
1485
1530
  } else if (args[i].startsWith("-")) {
1486
1531
  throw new Error(`Unknown enterprise install option: ${args[i]}`);
1532
+ } else {
1533
+ throw new Error(
1534
+ "Positional enterprise API keys are not accepted. Use --api-key-stdin.",
1535
+ );
1536
+ }
1537
+ }
1538
+
1539
+ if (!readKeyFromStdin) {
1540
+ throw new Error(
1541
+ "Enterprise installation requires --api-key-stdin so the key is not exposed in process arguments.",
1542
+ );
1543
+ }
1544
+ const secretInput = injected.stdin ?? process.stdin;
1545
+ if (secretInput.isTTY) {
1546
+ throw new Error(
1547
+ "Read the enterprise API key from a pipe or redirected stdin; interactive echo is disabled for secrets.",
1548
+ );
1549
+ }
1550
+ let apiKey = "";
1551
+ for await (const chunk of secretInput) {
1552
+ apiKey += chunk.toString();
1553
+ if (apiKey.length > 4096) {
1554
+ throw new Error("Enterprise API key input is too large.");
1487
1555
  }
1488
1556
  }
1557
+ apiKey = apiKey.trim();
1558
+ if (!apiKey || apiKey.includes("\n") || apiKey.includes("\r")) {
1559
+ throw new Error("Expected exactly one enterprise API key on stdin.");
1560
+ }
1489
1561
 
1490
- const sudo = requireSudoElevation();
1562
+ const sudo = (injected.requireSudoElevation ?? requireSudoElevation)();
1563
+ const projectOperation = (operation) =>
1564
+ (injected.runAsSudoUser ?? runAsSudoUser)(sudo, operation);
1491
1565
 
1492
1566
  process.stdout.write(headerBanner({ tagline: " Cortex enterprise — activating governance" }));
1493
1567
 
1494
- const enterpriseEntry = resolveCliEntry("enterprise-setup");
1495
- if (!fs.existsSync(enterpriseEntry)) {
1496
- printBullet("fail", `Build the project's context runtime first (missing ${enterpriseEntry}). Run 'cortex bootstrap' in the project root.`);
1497
- process.exit(1);
1568
+ let enterpriseMod = injected.enterpriseMod;
1569
+ if (!enterpriseMod) {
1570
+ const enterpriseEntry = resolveTrustedCliEntry("enterprise-setup");
1571
+ if (!fs.existsSync(enterpriseEntry)) {
1572
+ printBullet("fail", `The installed Cortex package is missing its trusted Enterprise runtime (${enterpriseEntry}). Reinstall Cortex.`);
1573
+ process.exit(1);
1574
+ }
1575
+ enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
1498
1576
  }
1499
- const enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
1500
1577
 
1501
1578
  // Step 1 — Initializing Cortex core (license validation + enterprise.yml).
1502
1579
  const step1 = spinner("Initializing Cortex core");
1503
- const setupResult = await enterpriseMod.runEnterpriseSetup({ apiKey, endpoint, cwd: process.cwd() });
1580
+ const setupResult = await projectOperation(
1581
+ () => enterpriseMod.runEnterpriseSetup({
1582
+ apiKey,
1583
+ endpoint,
1584
+ cwd: process.cwd(),
1585
+ }),
1586
+ );
1504
1587
  if (!setupResult.ok) {
1505
1588
  step1.stop("fail", `Initializing Cortex core — ${setupResult.message}`);
1506
1589
  process.exit(1);
@@ -1508,24 +1591,40 @@ async function runEnterpriseInstall(args) {
1508
1591
  step1.stop("ok", `Initializing Cortex core — license ${setupResult.edition}, expires ${setupResult.expiresAt}`);
1509
1592
  printBullet("info", muted(`config: ${setupResult.configPath}`));
1510
1593
 
1511
- // enterprise.yml was just written as root; transfer ownership before we drop privs.
1512
- try {
1513
- fs.chownSync(setupResult.configPath, sudo.uid, sudo.gid);
1514
- } catch (err) {
1515
- printBullet("warn", `Could not chown ${setupResult.configPath}: ${err instanceof Error ? err.message : String(err)}`);
1594
+ const sudoHome =
1595
+ injected.sudoHome ??
1596
+ os.userInfo({ uid: sudo.uid }).homedir;
1597
+ const bindEnterpriseIdentity =
1598
+ injected.bindEnterpriseIdentity ??
1599
+ enterpriseMod.bindEnterpriseIdentity;
1600
+ if (
1601
+ typeof bindEnterpriseIdentity !== "function" ||
1602
+ !await projectOperation(
1603
+ () => bindEnterpriseIdentity({
1604
+ apiKey,
1605
+ endpoint,
1606
+ homeDir: sudoHome,
1607
+ }),
1608
+ )
1609
+ ) {
1610
+ throw new Error(
1611
+ "Could not bind this user profile to the verified Enterprise identity. " +
1612
+ "A different endpoint is already enrolled; use a separate OS user boundary.",
1613
+ );
1516
1614
  }
1517
1615
 
1518
1616
  // Step 2 — Loading policy engine (govern install: managed config + frameworks).
1519
1617
  const baseUrl = (endpoint ?? "https://cortex-web-rho.vercel.app").replace(/\/$/, "");
1520
1618
  const step2 = spinner("Loading policy engine");
1521
- const governMod = await loadGovernModule();
1619
+ const governMod = injected.governMod ?? await loadGovernModule();
1522
1620
  const governResult = await governMod.runGovernInstall({
1523
1621
  cli: "all",
1524
1622
  mode: "enforced",
1525
1623
  cwd: process.cwd(),
1526
1624
  apiKey,
1527
1625
  baseUrl,
1528
- frameworks,
1626
+ frameworks: frameworks ?? ["iso27001", "iso42001", "soc2"],
1627
+ projectOperation,
1529
1628
  });
1530
1629
  if (!governResult.ok) {
1531
1630
  step2.stop("fail", `Loading policy engine — ${governResult.message}`);
@@ -1533,31 +1632,21 @@ async function runEnterpriseInstall(args) {
1533
1632
  }
1534
1633
  step2.stop("ok", "Loading policy engine — policies armed");
1535
1634
 
1536
- // govern.local.json was written as root in cwd/.context. chown it back.
1537
- const governStatePath = path.join(process.cwd(), ".context", "govern.local.json");
1538
- if (fs.existsSync(governStatePath)) {
1539
- try {
1540
- fs.chownSync(governStatePath, sudo.uid, sudo.gid);
1541
- } catch (err) {
1542
- printBullet("warn", `Could not chown ${governStatePath}: ${err instanceof Error ? err.message : String(err)}`);
1543
- }
1544
- }
1545
-
1546
1635
  // Step 3 — Connecting audit pipeline (telemetry endpoint already wired by govern install).
1547
1636
  const step3 = spinner("Connecting audit pipeline");
1548
1637
  step3.stop("ok", `Connecting audit pipeline — endpoint ${baseUrl}`);
1549
1638
 
1550
1639
  // Drop privileges before user-scope writes (Claude Code hooks in $HOME) and daemon spawn.
1551
- dropPrivileges(sudo);
1640
+ (injected.dropPrivileges ?? dropPrivileges)(sudo);
1552
1641
 
1553
1642
  // Step 4 — Preparing MCP gateway (Claude Code hooks bind the MCP surface).
1554
1643
  if (installHooks) {
1555
1644
  const step4 = spinner("Preparing MCP gateway");
1556
1645
  try {
1557
- if (hasManagedClaudeHooks()) {
1646
+ if ((injected.hasManagedClaudeHooks ?? hasManagedClaudeHooks)()) {
1558
1647
  step4.stop("ok", "Preparing MCP gateway — managed Claude hooks active");
1559
1648
  } else {
1560
- await runHooksCommand(["install"]);
1649
+ await (injected.runHooksCommand ?? runHooksCommand)(["install"]);
1561
1650
  step4.stop("ok", "Preparing MCP gateway — hooks installed");
1562
1651
  }
1563
1652
  } catch (err) {
@@ -1571,10 +1660,11 @@ async function runEnterpriseInstall(args) {
1571
1660
  if (startDaemon) {
1572
1661
  const step5 = spinner("Installing guardrails");
1573
1662
  try {
1574
- await runDaemonCommand(["start"]);
1663
+ await (injected.runDaemonCommand ?? runDaemonCommand)(["restart"]);
1575
1664
  step5.stop("ok", "Installing guardrails — daemon online");
1576
1665
  } catch (err) {
1577
1666
  step5.stop("fail", `Installing guardrails — ${err instanceof Error ? err.message : String(err)}`);
1667
+ throw err;
1578
1668
  }
1579
1669
  } else {
1580
1670
  printBullet("warn", "Installing guardrails — skipped (--no-daemon)");
@@ -1588,7 +1678,7 @@ async function runEnterpriseInstall(args) {
1588
1678
  }
1589
1679
 
1590
1680
  const RUN_CLIS = new Set(["claude", "codex", "copilot"]);
1591
- const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
1681
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
1592
1682
 
1593
1683
  async function runRunCommand(args) {
1594
1684
  const sub = args[0];
@@ -1598,7 +1688,7 @@ async function runRunCommand(args) {
1598
1688
  console.log("");
1599
1689
  console.log("Wraps the named AI CLI in cortex enforcement:");
1600
1690
  console.log(" claude/codex: passthrough — their own managed-config + sandbox");
1601
- console.log(" cover Tier 1 enforcement after 'cortex enterprise <key>'.");
1691
+ console.log(" cover Tier 1 enforcement after 'cortex enterprise install --api-key-stdin'.");
1602
1692
  console.log(" copilot: Tier 2 — OS-level sandbox (sandbox-exec on macOS,");
1603
1693
  console.log(" bwrap on Linux). Denies writes to ~/.copilot/,");
1604
1694
  console.log(" ~/.copilot.local/, /etc/copilot* so AI cannot");
@@ -1762,4 +1852,11 @@ if (invokedAsScript) {
1762
1852
  });
1763
1853
  }
1764
1854
 
1765
- export { buildInitialConfig, detectInitialSourcePaths, isScaffoldOutOfDate, slugifyRepoId };
1855
+ export {
1856
+ buildInitialConfig,
1857
+ detectInitialSourcePaths,
1858
+ hardenEnterpriseConfigPermissions,
1859
+ isScaffoldOutOfDate,
1860
+ runEnterpriseInstall,
1861
+ slugifyRepoId,
1862
+ };