@danielblomma/cortex-mcp 2.4.0 → 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 (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  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 +321 -200
  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/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
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,
@@ -88,14 +93,14 @@ function printHelp() {
88
93
  console.log(helpRow("watch [start|stop|status|run|once]", "Background sync (--interval, --debounce, --mode)"));
89
94
 
90
95
  console.log(helpSection("GOVERNANCE"));
91
- 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)"));
92
97
  console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
93
98
  console.log(helpRow("enterprise status", "Show local enforcement state"));
94
99
  console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
95
100
  console.log(helpRow("enterprise uninstall", "Remove enforcement (sudo, --break-glass --reason)"));
96
101
  console.log(helpRow("enterprise repair", "Verify managed paths, clear tamper-lock (sudo)"));
97
102
  console.log(helpRow("run <claude|codex|copilot> [args...]", "Wrap an AI CLI in cortex enforcement"));
98
- console.log(helpRow("daemon [start|stop|status]", "Local supervisor daemon"));
103
+ console.log(helpRow("daemon [start|stop|restart|status]", "Local supervisor daemon"));
99
104
  console.log(helpRow("hooks [install|uninstall|status] [--project]", "Claude Code hooks"));
100
105
  console.log(helpRow("telemetry test", "Smoke-test the push pipeline"));
101
106
 
@@ -284,6 +289,31 @@ function initializeScaffold(targetDir, force) {
284
289
  }
285
290
  }
286
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
+
287
317
  function ensurePathWritable(targetPath, force) {
288
318
  if (!fs.existsSync(targetPath)) {
289
319
  return;
@@ -851,6 +881,7 @@ async function run() {
851
881
  printBanner("Cortex initializes repo-scoped context for AI coding agents.");
852
882
  fs.mkdirSync(target, { recursive: true });
853
883
  initializeScaffold(target, force);
884
+ hardenEnterpriseConfigPermissions(target);
854
885
  const helpers = installAssistantHelpers(target);
855
886
  await maybeInstallGitHooks(target);
856
887
 
@@ -891,6 +922,7 @@ async function run() {
891
922
 
892
923
  if (bootstrap) {
893
924
  await runContextCommand(target, ["bootstrap"]);
925
+ await restartDaemonAfterRuntimeUpgrade(target);
894
926
  }
895
927
 
896
928
  if (connect) {
@@ -981,23 +1013,34 @@ async function run() {
981
1013
  }
982
1014
 
983
1015
  await maybeMigrateScaffold(process.cwd(), command);
1016
+ if (command === "bootstrap") {
1017
+ hardenEnterpriseConfigPermissions(process.cwd());
1018
+ }
984
1019
  await runContextCommand(process.cwd(), [command, ...rest]);
1020
+ if (command === "bootstrap") {
1021
+ await restartDaemonAfterRuntimeUpgrade(process.cwd());
1022
+ }
985
1023
  }
986
1024
 
987
1025
  // ---------------------------------------------------------------------------
988
1026
  // v2.0.0: daemon + hooks commands
989
1027
  // ---------------------------------------------------------------------------
990
1028
 
991
- const DAEMON_DIR = path.join(process.env.HOME || "", ".cortex");
992
- 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
+ }
993
1036
 
994
1037
  function pidFileExists() {
995
- return fs.existsSync(PID_FILE);
1038
+ return fs.existsSync(daemonPidFilePath());
996
1039
  }
997
1040
 
998
1041
  function readPid() {
999
1042
  try {
1000
- const raw = fs.readFileSync(PID_FILE, "utf8").trim();
1043
+ const raw = fs.readFileSync(daemonPidFilePath(), "utf8").trim();
1001
1044
  const pid = Number.parseInt(raw, 10);
1002
1045
  return Number.isFinite(pid) && pid > 0 ? pid : null;
1003
1046
  } catch {
@@ -1018,20 +1061,23 @@ function isPidAlive(pid) {
1018
1061
  }
1019
1062
  }
1020
1063
 
1021
- function resolveProjectRuntimeDist() {
1064
+ function resolveProjectRuntimeDist(projectRoot) {
1022
1065
  // v2.0.5: project layout was moved from <cwd>/mcp/ to <cwd>/.context/mcp/.
1023
1066
  // The runtime still lives there for compatibility, but CLI commands now
1024
1067
  // treat it as the local context runtime rather than an MCP-only surface.
1025
- 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();
1026
1072
  return path.join(target, CONTEXT_RUNTIME_REL, "dist");
1027
1073
  }
1028
1074
 
1029
- function resolveProjectMcpDist() {
1030
- return resolveProjectRuntimeDist();
1075
+ function resolveProjectMcpDist(projectRoot) {
1076
+ return resolveProjectRuntimeDist(projectRoot);
1031
1077
  }
1032
1078
 
1033
- function resolveDaemonEntry() {
1034
- return path.join(resolveProjectRuntimeDist(), "daemon", "main.js");
1079
+ function resolveDaemonEntry(projectRoot) {
1080
+ return path.join(resolveProjectRuntimeDist(projectRoot), "daemon", "main.js");
1035
1081
  }
1036
1082
 
1037
1083
  function resolveHookEntry(name) {
@@ -1042,54 +1088,116 @@ function resolveCliEntry(name) {
1042
1088
  return path.join(resolveProjectRuntimeDist(), "cli", `${name}.js`);
1043
1089
  }
1044
1090
 
1045
- 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 = {}) {
1046
1114
  const sub = args[0] || "status";
1115
+ const projectRoot = options.projectRoot || process.cwd();
1116
+ const deps = await daemonControlDeps();
1047
1117
  if (sub === "start") {
1048
- if (isPidAlive(readPid())) {
1118
+ const existing = await probeVerifiedDaemon(deps);
1119
+ if (existing.verified) {
1049
1120
  console.log("Daemon already running.");
1050
1121
  return;
1051
1122
  }
1052
- fs.mkdirSync(DAEMON_DIR, { recursive: true });
1053
- 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);
1054
1131
  if (!fs.existsSync(entry)) {
1055
1132
  throw new Error(`Daemon entry not found: ${entry}. Build cortex first.`);
1056
1133
  }
1057
- const logFd = fs.openSync(path.join(DAEMON_DIR, "daemon.log"), "a");
1134
+ const logFd = fs.openSync(path.join(daemonDir, "daemon.log"), "a");
1058
1135
  const child = spawn(process.execPath, [entry], {
1059
1136
  detached: true,
1060
1137
  stdio: ["ignore", logFd, logFd],
1138
+ cwd: projectRoot,
1139
+ env: { ...process.env, CORTEX_PROJECT_ROOT: projectRoot },
1061
1140
  });
1062
1141
  child.unref();
1063
- 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")}`);
1064
1150
  return;
1065
1151
  }
1066
1152
  if (sub === "stop") {
1067
- const pid = readPid();
1068
- if (!isPidAlive(pid)) {
1153
+ const result = await stopVerifiedDaemon(deps);
1154
+ if (!result.stopped) {
1069
1155
  console.log("Daemon not running.");
1070
1156
  return;
1071
1157
  }
1072
- try {
1073
- process.kill(pid, "SIGTERM");
1074
- console.log(`Sent SIGTERM to pid ${pid}`);
1075
- } catch (err) {
1076
- throw new Error(`Failed to stop daemon: ${err instanceof Error ? err.message : String(err)}`);
1077
- }
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 });
1078
1164
  return;
1079
1165
  }
1080
1166
  if (sub === "status") {
1081
- const pid = readPid();
1082
- if (isPidAlive(pid)) {
1083
- 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
+ );
1084
1174
  } else {
1085
1175
  console.log("Daemon not running.");
1086
1176
  if (pidFileExists()) {
1087
- console.log(`(stale pid file at ${PID_FILE})`);
1177
+ console.log(`(stale pid file at ${daemonPidFilePath()})`);
1088
1178
  }
1089
1179
  }
1090
1180
  return;
1091
1181
  }
1092
- 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 });
1093
1201
  }
1094
1202
 
1095
1203
  async function runHookShim(args) {
@@ -1182,7 +1290,8 @@ function writeJson(file, data) {
1182
1290
  }
1183
1291
 
1184
1292
  // Enterprise == govern. One command, sudo-elevated, hard-fail without it.
1185
- // `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
1186
1295
  // dispatch to scaffold/mcp/dist/cli/govern.js.
1187
1296
 
1188
1297
  function requireSudoElevation() {
@@ -1220,11 +1329,58 @@ function dropPrivileges(sudo) {
1220
1329
  return sudoInfo.homedir;
1221
1330
  }
1222
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
+
1223
1379
  function loadGovernModule() {
1224
- const entry = resolveCliEntry("govern");
1380
+ const entry = resolveTrustedCliEntry("govern");
1225
1381
  if (!fs.existsSync(entry)) {
1226
1382
  throw new Error(
1227
- `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.`
1228
1384
  );
1229
1385
  }
1230
1386
  return import(pathToFileURL(entry).href);
@@ -1233,10 +1389,15 @@ function loadGovernModule() {
1233
1389
  const ENTERPRISE_SUBCOMMANDS = new Set(["status", "sync", "uninstall", "repair", "help", "--help", "-h"]);
1234
1390
 
1235
1391
  async function runEnterpriseCommand(args) {
1392
+ if (args[0] === "install") {
1393
+ return runEnterpriseInstall(args.slice(1));
1394
+ }
1236
1395
  if (args.length === 0 || ENTERPRISE_SUBCOMMANDS.has(args[0])) {
1237
1396
  return runEnterpriseSubcommand(args);
1238
1397
  }
1239
- return runEnterpriseInstall(args);
1398
+ throw new Error(
1399
+ "Positional enterprise API keys are not accepted. Use 'cortex enterprise install --api-key-stdin'.",
1400
+ );
1240
1401
  }
1241
1402
 
1242
1403
  async function runEnterpriseSubcommand(args) {
@@ -1244,13 +1405,14 @@ async function runEnterpriseSubcommand(args) {
1244
1405
 
1245
1406
  if (sub === "help" || sub === "--help" || sub === "-h" || !sub) {
1246
1407
  console.log(gradient("cortex enterprise") + muted(" · governance, armed."));
1247
- 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."));
1248
1409
  console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
1249
1410
  console.log(helpRow("enterprise status [--verbose|--json]", "Show local enforcement state"));
1250
1411
  console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
1251
1412
  console.log(helpRow("enterprise uninstall", "Remove. [--break-glass --reason \"<text>\"] in enforced mode (sudo)"));
1252
1413
  console.log(helpRow("enterprise repair", "Verify managed paths, clear .cortex-tamper.lock (sudo)"));
1253
1414
  console.log("");
1415
+ console.log(muted("Example: printf '%s\\n' \"$CORTEX_API_KEY\" | sudo cortex enterprise install --api-key-stdin"));
1254
1416
  console.log(muted("Default endpoint: https://cortex-web-rho.vercel.app"));
1255
1417
  return;
1256
1418
  }
@@ -1271,9 +1433,14 @@ async function runEnterpriseSubcommand(args) {
1271
1433
  }
1272
1434
 
1273
1435
  if (sub === "sync") {
1274
- requireSudoElevation();
1436
+ const sudo = requireSudoElevation();
1275
1437
  const mod = await loadGovernModule();
1276
- 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);
1277
1444
  return;
1278
1445
  }
1279
1446
 
@@ -1289,14 +1456,17 @@ async function runEnterpriseSubcommand(args) {
1289
1456
  throw new Error(`Unknown enterprise uninstall option: ${args[i]}`);
1290
1457
  }
1291
1458
  }
1292
- requireSudoElevation();
1459
+ const sudo = requireSudoElevation();
1293
1460
  const mod = await loadGovernModule();
1461
+ const projectOperation = (operation) => runAsSudoUser(sudo, operation);
1294
1462
  const result = await mod.runGovernUninstall({
1295
1463
  cli: "all",
1296
1464
  breakGlass,
1297
1465
  reason,
1298
1466
  cwd: process.cwd(),
1467
+ projectOperation,
1299
1468
  });
1469
+ dropPrivileges(sudo);
1300
1470
  if (!result.ok) {
1301
1471
  printBullet("fail", result.message, process.stderr);
1302
1472
  process.exit(1);
@@ -1315,9 +1485,15 @@ async function runEnterpriseSubcommand(args) {
1315
1485
  throw new Error(`Unknown enterprise repair option: ${args[i]}`);
1316
1486
  }
1317
1487
  }
1318
- requireSudoElevation();
1488
+ const sudo = requireSudoElevation();
1319
1489
  const mod = await loadGovernModule();
1320
- 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);
1321
1497
  if (!result.ok) {
1322
1498
  printBullet("fail", result.message, process.stderr);
1323
1499
  process.exit(1);
@@ -1329,14 +1505,19 @@ async function runEnterpriseSubcommand(args) {
1329
1505
  throw new Error(`Unknown enterprise subcommand: ${sub}`);
1330
1506
  }
1331
1507
 
1332
- async function runEnterpriseInstall(args) {
1333
- 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;
1334
1513
  let endpoint;
1335
1514
  let frameworks;
1336
1515
  let installHooks = true;
1337
1516
  let startDaemon = true;
1338
- for (let i = 1; i < args.length; i++) {
1339
- 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]) {
1340
1521
  endpoint = args[i + 1];
1341
1522
  i++;
1342
1523
  } else if (args[i] === "--frameworks" && args[i + 1]) {
@@ -1348,23 +1529,61 @@ async function runEnterpriseInstall(args) {
1348
1529
  startDaemon = false;
1349
1530
  } else if (args[i].startsWith("-")) {
1350
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.");
1351
1555
  }
1352
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
+ }
1353
1561
 
1354
- const sudo = requireSudoElevation();
1562
+ const sudo = (injected.requireSudoElevation ?? requireSudoElevation)();
1563
+ const projectOperation = (operation) =>
1564
+ (injected.runAsSudoUser ?? runAsSudoUser)(sudo, operation);
1355
1565
 
1356
1566
  process.stdout.write(headerBanner({ tagline: " Cortex enterprise — activating governance" }));
1357
1567
 
1358
- const enterpriseEntry = resolveCliEntry("enterprise-setup");
1359
- if (!fs.existsSync(enterpriseEntry)) {
1360
- printBullet("fail", `Build the project's context runtime first (missing ${enterpriseEntry}). Run 'cortex bootstrap' in the project root.`);
1361
- 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);
1362
1576
  }
1363
- const enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
1364
1577
 
1365
1578
  // Step 1 — Initializing Cortex core (license validation + enterprise.yml).
1366
1579
  const step1 = spinner("Initializing Cortex core");
1367
- 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
+ );
1368
1587
  if (!setupResult.ok) {
1369
1588
  step1.stop("fail", `Initializing Cortex core — ${setupResult.message}`);
1370
1589
  process.exit(1);
@@ -1372,24 +1591,40 @@ async function runEnterpriseInstall(args) {
1372
1591
  step1.stop("ok", `Initializing Cortex core — license ${setupResult.edition}, expires ${setupResult.expiresAt}`);
1373
1592
  printBullet("info", muted(`config: ${setupResult.configPath}`));
1374
1593
 
1375
- // enterprise.yml was just written as root; transfer ownership before we drop privs.
1376
- try {
1377
- fs.chownSync(setupResult.configPath, sudo.uid, sudo.gid);
1378
- } catch (err) {
1379
- 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
+ );
1380
1614
  }
1381
1615
 
1382
1616
  // Step 2 — Loading policy engine (govern install: managed config + frameworks).
1383
1617
  const baseUrl = (endpoint ?? "https://cortex-web-rho.vercel.app").replace(/\/$/, "");
1384
1618
  const step2 = spinner("Loading policy engine");
1385
- const governMod = await loadGovernModule();
1619
+ const governMod = injected.governMod ?? await loadGovernModule();
1386
1620
  const governResult = await governMod.runGovernInstall({
1387
1621
  cli: "all",
1388
1622
  mode: "enforced",
1389
1623
  cwd: process.cwd(),
1390
1624
  apiKey,
1391
1625
  baseUrl,
1392
- frameworks,
1626
+ frameworks: frameworks ?? ["iso27001", "iso42001", "soc2"],
1627
+ projectOperation,
1393
1628
  });
1394
1629
  if (!governResult.ok) {
1395
1630
  step2.stop("fail", `Loading policy engine — ${governResult.message}`);
@@ -1397,31 +1632,21 @@ async function runEnterpriseInstall(args) {
1397
1632
  }
1398
1633
  step2.stop("ok", "Loading policy engine — policies armed");
1399
1634
 
1400
- // govern.local.json was written as root in cwd/.context. chown it back.
1401
- const governStatePath = path.join(process.cwd(), ".context", "govern.local.json");
1402
- if (fs.existsSync(governStatePath)) {
1403
- try {
1404
- fs.chownSync(governStatePath, sudo.uid, sudo.gid);
1405
- } catch (err) {
1406
- printBullet("warn", `Could not chown ${governStatePath}: ${err instanceof Error ? err.message : String(err)}`);
1407
- }
1408
- }
1409
-
1410
1635
  // Step 3 — Connecting audit pipeline (telemetry endpoint already wired by govern install).
1411
1636
  const step3 = spinner("Connecting audit pipeline");
1412
1637
  step3.stop("ok", `Connecting audit pipeline — endpoint ${baseUrl}`);
1413
1638
 
1414
1639
  // Drop privileges before user-scope writes (Claude Code hooks in $HOME) and daemon spawn.
1415
- dropPrivileges(sudo);
1640
+ (injected.dropPrivileges ?? dropPrivileges)(sudo);
1416
1641
 
1417
1642
  // Step 4 — Preparing MCP gateway (Claude Code hooks bind the MCP surface).
1418
1643
  if (installHooks) {
1419
1644
  const step4 = spinner("Preparing MCP gateway");
1420
1645
  try {
1421
- if (hasManagedClaudeHooks()) {
1646
+ if ((injected.hasManagedClaudeHooks ?? hasManagedClaudeHooks)()) {
1422
1647
  step4.stop("ok", "Preparing MCP gateway — managed Claude hooks active");
1423
1648
  } else {
1424
- await runHooksCommand(["install"]);
1649
+ await (injected.runHooksCommand ?? runHooksCommand)(["install"]);
1425
1650
  step4.stop("ok", "Preparing MCP gateway — hooks installed");
1426
1651
  }
1427
1652
  } catch (err) {
@@ -1435,10 +1660,11 @@ async function runEnterpriseInstall(args) {
1435
1660
  if (startDaemon) {
1436
1661
  const step5 = spinner("Installing guardrails");
1437
1662
  try {
1438
- await runDaemonCommand(["start"]);
1663
+ await (injected.runDaemonCommand ?? runDaemonCommand)(["restart"]);
1439
1664
  step5.stop("ok", "Installing guardrails — daemon online");
1440
1665
  } catch (err) {
1441
1666
  step5.stop("fail", `Installing guardrails — ${err instanceof Error ? err.message : String(err)}`);
1667
+ throw err;
1442
1668
  }
1443
1669
  } else {
1444
1670
  printBullet("warn", "Installing guardrails — skipped (--no-daemon)");
@@ -1462,7 +1688,7 @@ async function runRunCommand(args) {
1462
1688
  console.log("");
1463
1689
  console.log("Wraps the named AI CLI in cortex enforcement:");
1464
1690
  console.log(" claude/codex: passthrough — their own managed-config + sandbox");
1465
- console.log(" cover Tier 1 enforcement after 'cortex enterprise <key>'.");
1691
+ console.log(" cover Tier 1 enforcement after 'cortex enterprise install --api-key-stdin'.");
1466
1692
  console.log(" copilot: Tier 2 — OS-level sandbox (sandbox-exec on macOS,");
1467
1693
  console.log(" bwrap on Linux). Denies writes to ~/.copilot/,");
1468
1694
  console.log(" ~/.copilot.local/, /etc/copilot* so AI cannot");
@@ -1626,4 +1852,11 @@ if (invokedAsScript) {
1626
1852
  });
1627
1853
  }
1628
1854
 
1629
- export { buildInitialConfig, detectInitialSourcePaths, isScaffoldOutOfDate, slugifyRepoId };
1855
+ export {
1856
+ buildInitialConfig,
1857
+ detectInitialSourcePaths,
1858
+ hardenEnterpriseConfigPermissions,
1859
+ isScaffoldOutOfDate,
1860
+ runEnterpriseInstall,
1861
+ slugifyRepoId,
1862
+ };