@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
@@ -14,23 +14,32 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@huggingface/transformers": "^4.1.0",
17
- "@modelcontextprotocol/sdk": "^1.27.1",
17
+ "@modelcontextprotocol/sdk": "^1.30.0",
18
18
  "@types/js-yaml": "^4.0.9",
19
- "js-yaml": "^4.1.1",
20
- "minimatch": "^10.2.5",
19
+ "js-yaml": "^4.3.0",
20
+ "minimatch": "^10.2.6",
21
21
  "ryugraph": "^25.9.1",
22
22
  "zod": "^3.24.1"
23
23
  },
24
24
  "overrides": {
25
+ "@hono/node-server": "^2.0.12",
26
+ "adm-zip": "^0.6.0",
27
+ "body-parser": "^2.3.0",
28
+ "brace-expansion": "^5.0.8",
25
29
  "cmake-js": "^8.0.0",
26
30
  "express-rate-limit": "^8.5.1",
27
- "hono": "^4.12.23",
28
- "@hono/node-server": "^1.19.13",
29
- "tar": "^7.5.11"
31
+ "fast-uri": "^3.1.4",
32
+ "hono": "^4.12.32",
33
+ "protobufjs": "^7.6.5",
34
+ "sharp": "^0.35.3",
35
+ "tar": "^7.5.22"
30
36
  },
31
37
  "devDependencies": {
32
38
  "@types/node": "^22.10.1",
33
39
  "ts-node": "^10.9.2",
34
40
  "typescript": "^5.7.2"
41
+ },
42
+ "engines": {
43
+ "node": ">=20.9.0"
35
44
  }
36
45
  }
@@ -1,11 +1,28 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { join } from "node:path";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
- import { verifyLicense } from "../core/license.js";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ renameSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import {
12
+ enterpriseCredentialId,
13
+ isAllowedLicenseEndpoint,
14
+ verifyLicense,
15
+ } from "../core/license.js";
16
+ import {
17
+ claimEnterpriseHostIdentity,
18
+ inspectEnterpriseHostIdentity,
19
+ } from "../core/enterprise-host-identity.js";
20
+ import { prepareEnterpriseCredentialRotation } from "../core/enterprise-rotation.js";
4
21
 
5
22
  /**
6
23
  * One-liner enterprise onboarding.
7
24
  *
8
- * $ cortex enterprise <api-key> [--endpoint <url>]
25
+ * $ printenv CORTEX_API_KEY | cortex enterprise install --api-key-stdin
9
26
  *
10
27
  * Replaces the manual `.context/enterprise.yml` editing flow that's been
11
28
  * the friction point for new users. Validates the key against the
@@ -33,7 +50,7 @@ const API_KEY_RE = /^(?:ctx|ent)_[A-Za-z0-9._-]{8,}$/;
33
50
 
34
51
  function buildEnterpriseYaml(baseUrl: string, apiKey: string): string {
35
52
  const lines = [
36
- "# Cortex enterprise configuration. Generated by `cortex enterprise <key>`.",
53
+ "# Cortex enterprise configuration. Generated by `cortex enterprise install --api-key-stdin`.",
37
54
  "# Single api_key is used for telemetry, policy, audit and govern services.",
38
55
  "enterprise:",
39
56
  ` api_key: ${apiKey}`,
@@ -75,10 +92,11 @@ export async function runEnterpriseSetup(
75
92
  };
76
93
  }
77
94
 
78
- if (!/^https?:\/\//.test(endpoint)) {
95
+ if (!isAllowedLicenseEndpoint(endpoint)) {
79
96
  return {
80
97
  ok: false,
81
- message: `Endpoint must be http(s) URL: ${endpoint}`,
98
+ message:
99
+ "Endpoint must use HTTPS. Plain HTTP is allowed only for localhost loopback development.",
82
100
  };
83
101
  }
84
102
 
@@ -101,13 +119,33 @@ export async function runEnterpriseSetup(
101
119
  message: `License rejected: ${license.reason} (source=${license.source}). Verify the API key and endpoint are correct.`,
102
120
  };
103
121
  }
104
-
105
122
  // Write enterprise.yml.
106
123
  const configPath = join(contextDir, "enterprise.yml");
124
+ const temporaryConfigPath = join(
125
+ contextDir,
126
+ `.enterprise.yml.${process.pid}.${randomUUID()}.tmp`,
127
+ );
107
128
  try {
108
129
  mkdirSync(contextDir, { recursive: true });
109
- writeFileSync(configPath, buildEnterpriseYaml(endpoint, apiKey), "utf8");
130
+ writeFileSync(
131
+ temporaryConfigPath,
132
+ buildEnterpriseYaml(endpoint, apiKey),
133
+ {
134
+ encoding: "utf8",
135
+ mode: 0o600,
136
+ flag: "wx",
137
+ },
138
+ );
139
+ chmodSync(temporaryConfigPath, 0o600);
140
+ renameSync(temporaryConfigPath, configPath);
141
+ // Preserve restrictive permissions even when replacing an existing file.
142
+ chmodSync(configPath, 0o600);
110
143
  } catch (err) {
144
+ try {
145
+ unlinkSync(temporaryConfigPath);
146
+ } catch {
147
+ // The rename may already have completed, or the temp file was not made.
148
+ }
111
149
  return {
112
150
  ok: false,
113
151
  message: `Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
@@ -122,3 +160,39 @@ export async function runEnterpriseSetup(
122
160
  expiresAt: license.expires_at,
123
161
  };
124
162
  }
163
+
164
+ export function bindEnterpriseIdentity(options: {
165
+ apiKey: string;
166
+ endpoint?: string;
167
+ homeDir?: string;
168
+ owner?: { uid: number; gid: number };
169
+ }): boolean {
170
+ const endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
171
+ const apiKey = options.apiKey.trim();
172
+ if (!API_KEY_RE.test(apiKey) || !isAllowedLicenseEndpoint(endpoint)) {
173
+ return false;
174
+ }
175
+ const credentialId = enterpriseCredentialId(endpoint, apiKey);
176
+ const identityStatus = inspectEnterpriseHostIdentity(
177
+ credentialId,
178
+ endpoint,
179
+ { homeDir: options.homeDir, owner: options.owner },
180
+ );
181
+ if (identityStatus === "conflict" || identityStatus === "invalid") {
182
+ return false;
183
+ }
184
+ if (
185
+ identityStatus === "rotation" &&
186
+ !prepareEnterpriseCredentialRotation(options.homeDir)
187
+ ) {
188
+ return false;
189
+ }
190
+ return claimEnterpriseHostIdentity(
191
+ credentialId,
192
+ endpoint,
193
+ {
194
+ homeDir: options.homeDir,
195
+ owner: options.owner,
196
+ },
197
+ );
198
+ }
@@ -14,7 +14,15 @@ import { join, dirname } from "node:path";
14
14
  import { platform, hostname } from "node:os";
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { loadEnterpriseConfig, type ComplianceFramework } from "../core/config.js";
17
- import { installCopilotShim, uninstallCopilotShim } from "./run.js";
17
+ import { isAllowedEnterpriseEndpoint } from "../core/secure-endpoint.js";
18
+ import { getGovernManagedPath } from "../core/govern-paths.js";
19
+ import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
20
+ import { enterpriseCredentialId } from "../core/license.js";
21
+ import {
22
+ getDefaultCopilotShimPath,
23
+ installCopilotShim,
24
+ uninstallCopilotShim,
25
+ } from "./run.js";
18
26
  import {
19
27
  readTamperLock,
20
28
  removeTamperLock,
@@ -28,21 +36,14 @@ const TIER1_CLIS: GovernCli[] = ["claude", "codex"];
28
36
 
29
37
  export type ManagedSettingsPaths = Partial<Record<GovernCli, Partial<Record<NodeJS.Platform, string>>>>;
30
38
 
31
- const DEFAULT_PATHS: ManagedSettingsPaths = {
32
- claude: {
33
- darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
34
- linux: "/etc/claude-code/managed-settings.json",
35
- },
36
- codex: {
37
- darwin: "/Library/Application Support/Codex/requirements.toml",
38
- linux: "/etc/codex/requirements.toml",
39
- },
40
- };
41
-
42
39
  export type GovernState = {
43
40
  installs: Partial<Record<GovernCli, GovernInstallRecord>>;
44
41
  };
45
42
 
43
+ export type ProjectOperationRunner = <T>(
44
+ operation: () => T | Promise<T>,
45
+ ) => T | Promise<T>;
46
+
46
47
  export type GovernInstallRecord = {
47
48
  path: string;
48
49
  version: string;
@@ -84,11 +85,10 @@ const SUPPORTED_CODEX_HOOK_EVENTS = new Set([
84
85
  ]);
85
86
 
86
87
  export function getManagedSettingsPath(cli: GovernCli, os: NodeJS.Platform): string {
87
- const path = DEFAULT_PATHS[cli]?.[os];
88
- if (!path) {
88
+ if (cli === "copilot") {
89
89
  throw new Error(`govern install for ${cli} not yet supported on ${os}`);
90
90
  }
91
- return path;
91
+ return getGovernManagedPath(cli, os);
92
92
  }
93
93
 
94
94
  export function requireRoot(): void {
@@ -346,8 +346,33 @@ function loadState(cwd: string): GovernState {
346
346
 
347
347
  function saveState(cwd: string, state: GovernState): void {
348
348
  const path = getStatePath(cwd);
349
- mkdirSync(dirname(path), { recursive: true });
350
- writeFileSync(path, JSON.stringify(state, null, 2), "utf8");
349
+ writeAtomic(path, JSON.stringify(state, null, 2) + "\n", 0o600);
350
+ }
351
+
352
+ async function runProjectOperation<T>(
353
+ runner: ProjectOperationRunner | undefined,
354
+ operation: () => T | Promise<T>,
355
+ ): Promise<T> {
356
+ return runner ? await runner(operation) : await operation();
357
+ }
358
+
359
+ function managedPathForCli(
360
+ cli: GovernCli,
361
+ options: {
362
+ skipRoot?: boolean;
363
+ pathOverride?: Partial<Record<GovernCli, string>>;
364
+ },
365
+ ): string {
366
+ const override = options.pathOverride?.[cli];
367
+ if (override) {
368
+ if (!options.skipRoot) {
369
+ throw new Error("Managed path overrides are allowed only in non-root tests.");
370
+ }
371
+ return override;
372
+ }
373
+ return cli === "copilot"
374
+ ? getDefaultCopilotShimPath(platform())
375
+ : getManagedSettingsPath(cli, platform());
351
376
  }
352
377
 
353
378
  async function fetchGovernConfig(
@@ -356,6 +381,9 @@ async function fetchGovernConfig(
356
381
  cli: GovernCli,
357
382
  frameworks: string[],
358
383
  ): Promise<{ config: FetchedConfig; etag: string | null }> {
384
+ if (!isAllowedEnterpriseEndpoint(baseUrl)) {
385
+ throw new Error("insecure or invalid enterprise endpoint");
386
+ }
359
387
  const url = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/govern/config`);
360
388
  url.searchParams.set("cli", cli);
361
389
  url.searchParams.set("frameworks", frameworks.join(","));
@@ -383,6 +411,9 @@ async function postApplied(
383
411
  instance_id?: string;
384
412
  },
385
413
  ): Promise<void> {
414
+ if (!isAllowedEnterpriseEndpoint(baseUrl)) {
415
+ throw new Error("insecure or invalid enterprise endpoint");
416
+ }
386
417
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/govern/applied`, {
387
418
  method: "POST",
388
419
  headers: {
@@ -406,6 +437,7 @@ export type GovernInstallOptions = {
406
437
  skipRoot?: boolean;
407
438
  apiKey?: string;
408
439
  baseUrl?: string;
440
+ projectOperation?: ProjectOperationRunner;
409
441
  };
410
442
 
411
443
  export type GovernInstallResult = {
@@ -432,7 +464,10 @@ export async function runGovernInstall(
432
464
  let frameworks = options.frameworks ?? [];
433
465
 
434
466
  if (!apiKey || !baseUrl || frameworks.length === 0) {
435
- const config = loadEnterpriseConfig(contextDir);
467
+ const config = await runProjectOperation(
468
+ options.projectOperation,
469
+ () => loadEnterpriseConfig(contextDir),
470
+ );
436
471
  if (!apiKey) apiKey = config.enterprise.api_key.trim();
437
472
  if (!baseUrl) baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
438
473
  if (frameworks.length === 0) {
@@ -468,16 +503,17 @@ export async function runGovernInstall(
468
503
  }
469
504
 
470
505
  const mode = options.mode ?? "advisory";
471
- const state = loadState(cwd);
506
+ const state = await runProjectOperation(
507
+ options.projectOperation,
508
+ () => loadState(cwd),
509
+ );
472
510
  const installed: GovernCli[] = [];
473
511
 
474
512
  for (const cli of targets) {
475
513
  if (cli === "copilot") {
476
514
  if (!options.skipRoot) requireRoot();
477
- const shimPath = options.pathOverride?.copilot;
478
- const shimResult = installCopilotShim(
479
- shimPath ? { shimPath } : {},
480
- );
515
+ const shimPath = managedPathForCli(cli, options);
516
+ const shimResult = installCopilotShim({ shimPath });
481
517
  if (!shimResult.ok) {
482
518
  console.log(`! ${cli}: ${shimResult.message}`);
483
519
  continue;
@@ -495,7 +531,7 @@ export async function runGovernInstall(
495
531
  }
496
532
  if (!options.skipRoot) requireRoot();
497
533
 
498
- const path = options.pathOverride?.[cli] ?? getManagedSettingsPath(cli, platform());
534
+ const path = managedPathForCli(cli, options);
499
535
 
500
536
  let merged: FetchedConfig;
501
537
  let version: string;
@@ -564,7 +600,10 @@ export async function runGovernInstall(
564
600
  console.log(`✓ ${cli}: managed-settings written to ${path} (version ${shortVersion}, mode=${mode})`);
565
601
  }
566
602
 
567
- saveState(cwd, state);
603
+ await runProjectOperation(
604
+ options.projectOperation,
605
+ () => saveState(cwd, state),
606
+ );
568
607
  return {
569
608
  ok: true,
570
609
  message: `Installed govern for ${installed.join(", ") || "(none)"}.`,
@@ -578,13 +617,18 @@ export type GovernUninstallOptions = {
578
617
  reason?: string;
579
618
  cwd?: string;
580
619
  skipRoot?: boolean;
620
+ pathOverride?: Partial<Record<GovernCli, string>>;
621
+ projectOperation?: ProjectOperationRunner;
581
622
  };
582
623
 
583
624
  export async function runGovernUninstall(
584
625
  options: GovernUninstallOptions,
585
626
  ): Promise<{ ok: boolean; message: string; uninstalled: GovernCli[] }> {
586
627
  const cwd = options.cwd ?? process.cwd();
587
- const state = loadState(cwd);
628
+ const state = await runProjectOperation(
629
+ options.projectOperation,
630
+ () => loadState(cwd),
631
+ );
588
632
 
589
633
  // Filter out unknown CLI keys defensively — govern.local.json is a
590
634
  // user-writable file and a corrupted/forward-compatible entry must not
@@ -620,8 +664,9 @@ export async function runGovernUninstall(
620
664
  const inst = state.installs[cli];
621
665
  if (!inst) continue;
622
666
  if (!options.skipRoot) requireRoot();
667
+ const managedPath = managedPathForCli(cli, options);
623
668
  if (cli === "copilot") {
624
- const shimResult = uninstallCopilotShim(inst.path);
669
+ const shimResult = uninstallCopilotShim(managedPath);
625
670
  if (!shimResult.ok) {
626
671
  console.log(`! ${cli}: ${shimResult.message}`);
627
672
  continue;
@@ -635,19 +680,22 @@ export async function runGovernUninstall(
635
680
  continue;
636
681
  }
637
682
  try {
638
- unlinkSync(inst.path);
683
+ unlinkSync(managedPath);
639
684
  } catch {
640
685
  // file already gone — proceed
641
686
  }
642
687
  delete state.installs[cli];
643
688
  uninstalled.push(cli);
644
689
  console.log(
645
- `✓ ${cli}: managed-settings removed from ${inst.path}` +
690
+ `✓ ${cli}: managed-settings removed from ${managedPath}` +
646
691
  (options.breakGlass ? ` (break-glass: ${options.reason})` : ""),
647
692
  );
648
693
  }
649
694
 
650
- saveState(cwd, state);
695
+ await runProjectOperation(
696
+ options.projectOperation,
697
+ () => saveState(cwd, state),
698
+ );
651
699
  return {
652
700
  ok: true,
653
701
  message: `Uninstalled govern for ${uninstalled.join(", ") || "(none)"}.`,
@@ -685,6 +733,7 @@ export type GovernStatusReport = {
685
733
  generated_at: string;
686
734
  enterprise: {
687
735
  api_key_set: boolean;
736
+ host_identity_bound: boolean;
688
737
  base_url: string;
689
738
  frameworks_configured: ComplianceFramework[];
690
739
  govern_mode_config: GovernConfigModeFromConfig;
@@ -933,14 +982,23 @@ export function buildGovernStatus(options: { cwd?: string; now?: Date } = {}): G
933
982
  }
934
983
 
935
984
  const { counts, sample } = readRecentEvents(cwd, 24 * 60 * 60 * 1000, now);
985
+ const enterpriseBaseUrl =
986
+ config.enterprise.base_url || config.enterprise.endpoint;
987
+ const enterpriseApiKey = config.enterprise.api_key.trim();
936
988
 
937
989
  return {
938
990
  cwd,
939
991
  host_id: hostname(),
940
992
  generated_at: now.toISOString(),
941
993
  enterprise: {
942
- api_key_set: config.enterprise.api_key.trim() !== "",
943
- base_url: config.enterprise.base_url || config.enterprise.endpoint,
994
+ api_key_set: enterpriseApiKey !== "",
995
+ host_identity_bound:
996
+ enterpriseApiKey !== "" &&
997
+ enterpriseBaseUrl !== "" &&
998
+ matchesEnterpriseHostIdentity(
999
+ enterpriseCredentialId(enterpriseBaseUrl, enterpriseApiKey),
1000
+ ),
1001
+ base_url: enterpriseBaseUrl,
944
1002
  frameworks_configured: config.compliance.frameworks,
945
1003
  govern_mode_config: config.govern.mode,
946
1004
  },
@@ -964,7 +1022,10 @@ function formatCompact(report: GovernStatusReport): string {
964
1022
  );
965
1023
  lines.push(`Endpoint: ${report.enterprise.base_url || "(not set)"}`);
966
1024
  lines.push(
967
- `API key: ${report.enterprise.api_key_set ? "configured" : "NOT SET (run 'sudo cortex enterprise <key>')"}`,
1025
+ `API key: ${report.enterprise.api_key_set ? "configured" : "NOT SET (run 'sudo cortex enterprise install --api-key-stdin')"}`,
1026
+ );
1027
+ lines.push(
1028
+ `Host identity: ${report.enterprise.host_identity_bound ? "verified" : "NOT ENROLLED (re-run stdin install)"}`,
968
1029
  );
969
1030
  lines.push("");
970
1031
  if (report.tamper_lock) {
@@ -986,7 +1047,7 @@ function formatCompact(report: GovernStatusReport): string {
986
1047
  }
987
1048
  if (report.installs.length === 0) {
988
1049
  lines.push("No CLIs governed on this host.");
989
- lines.push("Run: sudo cortex enterprise <api-key>");
1050
+ lines.push("Run: sudo cortex enterprise install --api-key-stdin");
990
1051
  return lines.join("\n");
991
1052
  }
992
1053
  lines.push("AI CLIs on this host:");
@@ -1065,6 +1126,8 @@ export type GovernRepairOptions = {
1065
1126
  cwd?: string;
1066
1127
  skipRoot?: boolean;
1067
1128
  reason?: string;
1129
+ pathOverride?: Partial<Record<GovernCli, string>>;
1130
+ projectOperation?: ProjectOperationRunner;
1068
1131
  };
1069
1132
 
1070
1133
  export type GovernRepairResult = {
@@ -1087,7 +1150,10 @@ export async function runGovernRepair(
1087
1150
  options: GovernRepairOptions = {},
1088
1151
  ): Promise<GovernRepairResult> {
1089
1152
  const cwd = options.cwd ?? process.cwd();
1090
- const state = loadState(cwd);
1153
+ const state = await runProjectOperation(
1154
+ options.projectOperation,
1155
+ () => loadState(cwd),
1156
+ );
1091
1157
  // Filter to known CLIs so a corrupt or forward-compatible install
1092
1158
  // record doesn't crash the repair walk.
1093
1159
  const installed: Array<[GovernCli, GovernInstallRecord]> = Object.entries(
@@ -1100,7 +1166,7 @@ export async function runGovernRepair(
1100
1166
  return {
1101
1167
  ok: false,
1102
1168
  message:
1103
- "No CLIs governed on this host — nothing to repair. Run 'cortex enterprise <key>' first.",
1169
+ "No CLIs governed on this host — nothing to repair. Run 'cortex enterprise install --api-key-stdin' first.",
1104
1170
  reverified: [],
1105
1171
  };
1106
1172
  }
@@ -1109,21 +1175,22 @@ export async function runGovernRepair(
1109
1175
 
1110
1176
  const verified: GovernCli[] = [];
1111
1177
  const missing: string[] = [];
1112
- for (const [cli, record] of installed) {
1113
- if (!existsSync(record.path)) {
1114
- missing.push(`${cli}: ${record.path} is missing`);
1178
+ for (const [cli] of installed) {
1179
+ const managedPath = managedPathForCli(cli, options);
1180
+ if (!existsSync(managedPath)) {
1181
+ missing.push(`${cli}: ${managedPath} is missing`);
1115
1182
  continue;
1116
1183
  }
1117
1184
  if (cli === "copilot") {
1118
1185
  // Verify the file is still our shim (not replaced by a real binary).
1119
1186
  try {
1120
- const raw = readFileSync(record.path, "utf8");
1187
+ const raw = readFileSync(managedPath, "utf8");
1121
1188
  if (!raw.includes("# cortex-shim-v1")) {
1122
- missing.push(`${cli}: ${record.path} is no longer a cortex shim`);
1189
+ missing.push(`${cli}: ${managedPath} is no longer a cortex shim`);
1123
1190
  continue;
1124
1191
  }
1125
1192
  } catch {
1126
- missing.push(`${cli}: ${record.path} could not be read`);
1193
+ missing.push(`${cli}: ${managedPath} could not be read`);
1127
1194
  continue;
1128
1195
  }
1129
1196
  }
@@ -1141,7 +1208,10 @@ export async function runGovernRepair(
1141
1208
  };
1142
1209
  }
1143
1210
 
1144
- const lock = readTamperLock(cwd);
1211
+ const lock = await runProjectOperation(
1212
+ options.projectOperation,
1213
+ () => readTamperLock(cwd),
1214
+ );
1145
1215
  if (!lock) {
1146
1216
  return {
1147
1217
  ok: true,
@@ -1152,14 +1222,20 @@ export async function runGovernRepair(
1152
1222
  };
1153
1223
  }
1154
1224
 
1155
- const removed = removeTamperLock(cwd);
1225
+ const removed = await runProjectOperation(
1226
+ options.projectOperation,
1227
+ () => removeTamperLock(cwd),
1228
+ );
1156
1229
  if (removed) {
1157
- await emitTamperAudit(cwd, {
1158
- ...lock,
1159
- detected_at: new Date().toISOString(),
1160
- hook_name: "tamper_repaired",
1161
- missing_seconds: 0,
1162
- }).catch(() => undefined);
1230
+ await runProjectOperation(
1231
+ options.projectOperation,
1232
+ () => emitTamperAudit(cwd, {
1233
+ ...lock,
1234
+ detected_at: new Date().toISOString(),
1235
+ hook_name: "tamper_repaired",
1236
+ missing_seconds: 0,
1237
+ }).catch(() => undefined),
1238
+ );
1163
1239
  }
1164
1240
 
1165
1241
  return {
@@ -1172,9 +1248,17 @@ export async function runGovernRepair(
1172
1248
  };
1173
1249
  }
1174
1250
 
1175
- export async function runGovernSync(options: { cwd?: string } = {}): Promise<void> {
1251
+ export async function runGovernSync(
1252
+ options: {
1253
+ cwd?: string;
1254
+ projectOperation?: ProjectOperationRunner;
1255
+ } = {},
1256
+ ): Promise<void> {
1176
1257
  const cwd = options.cwd ?? process.cwd();
1177
- const state = loadState(cwd);
1258
+ const state = await runProjectOperation(
1259
+ options.projectOperation,
1260
+ () => loadState(cwd),
1261
+ );
1178
1262
  // Drop unknown CLI keys: runGovernInstall would throw on an unsupported
1179
1263
  // cli, but silently skipping is the right behaviour when sync runs in
1180
1264
  // the daemon — it's not a user typo to surface, just stale/forward
@@ -1192,6 +1276,7 @@ export async function runGovernSync(options: { cwd?: string } = {}): Promise<voi
1192
1276
  cli,
1193
1277
  cwd,
1194
1278
  mode: previous?.mode,
1279
+ projectOperation: options.projectOperation,
1195
1280
  });
1196
1281
  if (!result.ok) {
1197
1282
  console.log(`! sync ${cli} failed: ${result.message}`);
@@ -1,7 +1,9 @@
1
1
  import { runContextRules } from "../rules.js";
2
+ import { runPatternEvidence } from "../patternEvidence.js";
2
3
  import { runContextImpact, runContextRelated, runContextSearch } from "../search.js";
3
4
  import type {
4
5
  ImpactParams,
6
+ PatternEvidenceParams,
5
7
  RelatedParams,
6
8
  RelationType,
7
9
  RulesParams,
@@ -29,7 +31,7 @@ type JsonEnvelope = {
29
31
  };
30
32
  };
31
33
 
32
- const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
34
+ const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
33
35
 
34
36
  const ENTITY_ID_PREFIXES = [
35
37
  "file:",
@@ -66,6 +68,8 @@ export async function runQueryCommand(args: string[]): Promise<void> {
66
68
  return await runRules(rest);
67
69
  case "explain":
68
70
  return await runExplain(rest);
71
+ case "pattern-evidence":
72
+ return await runPatternEvidenceCommand(rest);
69
73
  default:
70
74
  throw new Error(`Unknown query command: ${command}`);
71
75
  }
@@ -93,6 +97,7 @@ function printHelp(): void {
93
97
  " cortex impact <query-or-entity-id> [--entity-id <id>] [--query <q>] [--depth <n>] [--top-k <n>] [--json]",
94
98
  " cortex rules [--scope <scope>] [--include-inactive] [--json]",
95
99
  " cortex explain <query-or-entity-id> [--top-k <n>] [--json]",
100
+ " cortex pattern-evidence <file-path|entity-id> [--query <text>] [--top-k <n>] [--json]",
96
101
  "",
97
102
  "These commands read the local Cortex graph and emit MCP-equivalent data with --json.",
98
103
  ];
@@ -157,7 +162,11 @@ function parsePositiveIntFlag(
157
162
  if (typeof raw !== "string" || raw.trim().length === 0) {
158
163
  throw new Error(`--${name} requires a numeric value`);
159
164
  }
160
- const parsed = Number.parseInt(raw, 10);
165
+ const normalized = raw.trim();
166
+ if (!/^[1-9]\d*$/.test(normalized)) {
167
+ throw new Error(`--${name} must be an integer from 1 to ${max}`);
168
+ }
169
+ const parsed = Number(normalized);
161
170
  if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) {
162
171
  throw new Error(`--${name} must be an integer from 1 to ${max}`);
163
172
  }
@@ -453,3 +462,28 @@ async function runExplain(args: string[]): Promise<void> {
453
462
  }
454
463
  printSummary("explain", data);
455
464
  }
465
+
466
+ async function runPatternEvidenceCommand(args: string[]): Promise<void> {
467
+ const { flags, rest } = parseArgs(args);
468
+ const target = optionalString(flags, "target") ?? positionalText(rest, "file-path-or-entity-id");
469
+ const input: PatternEvidenceParams = {
470
+ target,
471
+ query: optionalString(flags, "query"),
472
+ top_k: parsePositiveIntFlag(flags, "top-k", 3, 10),
473
+ include_deprecated: isFlagEnabled(flags, "include-deprecated"),
474
+ };
475
+ const data = await runPatternEvidence(input);
476
+ if (isFlagEnabled(flags, "json")) {
477
+ emitEnvelope("pattern-evidence", input, data);
478
+ return;
479
+ }
480
+
481
+ const tiers = Array.isArray(data.tiers) ? data.tiers : [];
482
+ process.stdout.write(`pattern-evidence: local_pattern_found=${String(data.local_pattern_found)}\n`);
483
+ for (const value of tiers) {
484
+ if (!value || typeof value !== "object") continue;
485
+ const tier = value as Record<string, unknown>;
486
+ const evidence = Array.isArray(tier.evidence) ? tier.evidence : [];
487
+ process.stdout.write(`- ${String(tier.name ?? "")}: ${evidence.length}\n`);
488
+ }
489
+ }