@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
@@ -0,0 +1,193 @@
1
+ import { chownSync, chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ const HOST_IDENTITY_FILENAME = "enterprise-host-identity.json";
6
+ const HOST_IDENTITY_VERSION = 2;
7
+ function configuredHomeDir(homeDir) {
8
+ return homeDir ?? process.env.HOME?.trim() ?? homedir();
9
+ }
10
+ function hostIdentityDir(options = {}, create = false) {
11
+ try {
12
+ const home = realpathSync(configuredHomeDir(options.homeDir));
13
+ const dir = join(home, ".cortex");
14
+ if (existsSync(dir)) {
15
+ const stat = lstatSync(dir);
16
+ if (!stat.isDirectory() || stat.isSymbolicLink())
17
+ return null;
18
+ if (realpathSync(dir) !== dir)
19
+ return null;
20
+ if (options.owner) {
21
+ if (stat.uid !== options.owner.uid && stat.uid !== 0)
22
+ return null;
23
+ chownSync(dir, options.owner.uid, options.owner.gid);
24
+ chmodSync(dir, 0o700);
25
+ }
26
+ return dir;
27
+ }
28
+ if (!create)
29
+ return null;
30
+ mkdirSync(dir, { mode: 0o700 });
31
+ if (options.owner) {
32
+ chownSync(dir, options.owner.uid, options.owner.gid);
33
+ }
34
+ chmodSync(dir, 0o700);
35
+ return dir;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function hostIdentityPath(options = {}, createDir = false) {
42
+ const dir = hostIdentityDir(options, createDir);
43
+ return dir ? join(dir, HOST_IDENTITY_FILENAME) : null;
44
+ }
45
+ function readHostIdentity(options = {}) {
46
+ const path = hostIdentityPath(options);
47
+ if (!path)
48
+ return null;
49
+ if (!existsSync(path))
50
+ return null;
51
+ try {
52
+ const stat = lstatSync(path);
53
+ if (!stat.isFile() || stat.isSymbolicLink())
54
+ return null;
55
+ if (options.owner &&
56
+ stat.uid !== options.owner.uid &&
57
+ stat.uid !== 0) {
58
+ return null;
59
+ }
60
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
61
+ if (parsed.version !== HOST_IDENTITY_VERSION ||
62
+ typeof parsed.credential_id !== "string" ||
63
+ !/^[a-f0-9]{64}$/.test(parsed.credential_id) ||
64
+ typeof parsed.endpoint_sha256 !== "string" ||
65
+ !/^[a-f0-9]{64}$/.test(parsed.endpoint_sha256)) {
66
+ return null;
67
+ }
68
+ return parsed;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ /**
75
+ * Atomically bind user-global Enterprise services and artifacts to one
76
+ * endpoint/API-key identity. Malformed existing enrollment fails closed.
77
+ */
78
+ export function claimEnterpriseHostIdentity(credentialId, endpoint, options = {}) {
79
+ if (!/^[a-f0-9]{64}$/.test(credentialId))
80
+ return false;
81
+ const endpointSha256 = createHash("sha256")
82
+ .update(endpoint.trim().replace(/\/+$/, ""), "utf8")
83
+ .digest("hex");
84
+ const path = hostIdentityPath(options, true);
85
+ if (!path)
86
+ return false;
87
+ if (existsSync(path)) {
88
+ const existing = readHostIdentity(options);
89
+ if (!existing || existing.endpoint_sha256 !== endpointSha256) {
90
+ return false;
91
+ }
92
+ if (existing.credential_id === credentialId) {
93
+ try {
94
+ if (options.owner) {
95
+ chownSync(path, options.owner.uid, options.owner.gid);
96
+ }
97
+ chmodSync(path, 0o600);
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ return replaceHostIdentity(path, {
105
+ version: HOST_IDENTITY_VERSION,
106
+ credential_id: credentialId,
107
+ endpoint_sha256: endpointSha256,
108
+ }, options.owner);
109
+ }
110
+ try {
111
+ writeFileSync(path, JSON.stringify({
112
+ version: HOST_IDENTITY_VERSION,
113
+ credential_id: credentialId,
114
+ endpoint_sha256: endpointSha256,
115
+ }, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
116
+ if (options.owner) {
117
+ chownSync(path, options.owner.uid, options.owner.gid);
118
+ }
119
+ chmodSync(path, 0o600);
120
+ return true;
121
+ }
122
+ catch {
123
+ // Another process may have won the create race. Only the same identity
124
+ // is accepted after re-reading the completed enrollment record.
125
+ const existing = readHostIdentity(options);
126
+ return (existing?.credential_id === credentialId &&
127
+ existing.endpoint_sha256 === endpointSha256);
128
+ }
129
+ }
130
+ function replaceHostIdentity(path, identity, owner) {
131
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
132
+ try {
133
+ writeFileSync(temporaryPath, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
134
+ if (owner) {
135
+ chownSync(temporaryPath, owner.uid, owner.gid);
136
+ }
137
+ chmodSync(temporaryPath, 0o600);
138
+ renameSync(temporaryPath, path);
139
+ chmodSync(path, 0o600);
140
+ return true;
141
+ }
142
+ catch {
143
+ try {
144
+ unlinkSync(temporaryPath);
145
+ }
146
+ catch {
147
+ // Best effort.
148
+ }
149
+ return false;
150
+ }
151
+ }
152
+ export function matchesEnterpriseHostIdentity(credentialId, options = {}) {
153
+ if (!/^[a-f0-9]{64}$/.test(credentialId))
154
+ return false;
155
+ return readHostIdentity(options)?.credential_id === credentialId;
156
+ }
157
+ export function inspectEnterpriseHostIdentity(credentialId, endpoint, options = {}) {
158
+ if (!/^[a-f0-9]{64}$/.test(credentialId))
159
+ return "invalid";
160
+ let home;
161
+ try {
162
+ home = realpathSync(configuredHomeDir(options.homeDir));
163
+ }
164
+ catch {
165
+ return "invalid";
166
+ }
167
+ const dir = join(home, ".cortex");
168
+ if (!existsSync(dir))
169
+ return "unbound";
170
+ try {
171
+ const dirStat = lstatSync(dir);
172
+ if (!dirStat.isDirectory() ||
173
+ dirStat.isSymbolicLink() ||
174
+ realpathSync(dir) !== dir) {
175
+ return "invalid";
176
+ }
177
+ }
178
+ catch {
179
+ return "invalid";
180
+ }
181
+ const path = join(dir, HOST_IDENTITY_FILENAME);
182
+ if (!existsSync(path))
183
+ return "unbound";
184
+ const existing = readHostIdentity(options);
185
+ if (!existing)
186
+ return "invalid";
187
+ const endpointSha256 = createHash("sha256")
188
+ .update(endpoint.trim().replace(/\/+$/, ""), "utf8")
189
+ .digest("hex");
190
+ if (existing.endpoint_sha256 !== endpointSha256)
191
+ return "conflict";
192
+ return existing.credential_id === credentialId ? "match" : "rotation";
193
+ }
@@ -0,0 +1,18 @@
1
+ import { join } from "node:path";
2
+ import { loadEnterpriseConfig } from "./config.js";
3
+ import { enterpriseCredentialId } from "./license.js";
4
+ /**
5
+ * Resolve the opaque per-user Enterprise identity for a project.
6
+ *
7
+ * The identifier binds the normalized endpoint and API key without
8
+ * persisting either credential in shared daemon metadata.
9
+ */
10
+ export function configuredEnterpriseCredentialId(cwd) {
11
+ const config = loadEnterpriseConfig(join(cwd, ".context"));
12
+ const apiKey = config.enterprise.api_key.trim();
13
+ const endpoint = (config.enterprise.base_url ||
14
+ config.enterprise.endpoint).trim();
15
+ if (!apiKey || !endpoint)
16
+ return null;
17
+ return enterpriseCredentialId(endpoint, apiKey);
18
+ }
@@ -0,0 +1,158 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmdirSync, unlinkSync, } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
5
+ const OWNERSHIP_FILENAME = ".cortex-managed.json";
6
+ const STATE_FILES = [
7
+ "skills.local.json",
8
+ "workflows.local.json",
9
+ "capabilities.local.json",
10
+ ".skills-update-applied.json",
11
+ ];
12
+ function configuredHome(homeDir) {
13
+ return homeDir ?? process.env.HOME?.trim() ?? homedir();
14
+ }
15
+ function safeDirectory(path) {
16
+ if (!existsSync(path))
17
+ return null;
18
+ const stat = lstatSync(path);
19
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
20
+ throw new Error(`${path} is not a safe directory`);
21
+ }
22
+ const real = realpathSync(path);
23
+ if (real !== path)
24
+ throw new Error(`${path} is not a canonical directory`);
25
+ return real;
26
+ }
27
+ function inspectManagedSkill(root, cli, name) {
28
+ if (!SKILL_NAME_RE.test(name)) {
29
+ throw new Error("invalid managed skill name");
30
+ }
31
+ const dir = resolve(root, name);
32
+ if (!dir.startsWith(`${root}/`)) {
33
+ throw new Error("managed skill escapes its root");
34
+ }
35
+ const stat = lstatSync(dir);
36
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
37
+ throw new Error("managed skill target is not a safe directory");
38
+ }
39
+ if (realpathSync(dir) !== dir) {
40
+ throw new Error("managed skill target is not canonical");
41
+ }
42
+ const marker = join(dir, OWNERSHIP_FILENAME);
43
+ if (!existsSync(marker))
44
+ return null;
45
+ const markerStat = lstatSync(marker);
46
+ if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
47
+ throw new Error("managed skill ownership marker is unsafe");
48
+ }
49
+ const parsed = JSON.parse(readFileSync(marker, "utf8"));
50
+ if (parsed.version !== 1 ||
51
+ parsed.manager !== "cortex" ||
52
+ parsed.cli !== cli ||
53
+ parsed.name !== name) {
54
+ throw new Error("managed skill ownership marker is invalid");
55
+ }
56
+ const entries = readdirSync(dir);
57
+ const unexpected = entries.filter((entry) => entry !== OWNERSHIP_FILENAME && entry !== "SKILL.md");
58
+ if (unexpected.length > 0) {
59
+ throw new Error("managed skill directory contains unowned files");
60
+ }
61
+ const skill = join(dir, "SKILL.md");
62
+ if (existsSync(skill)) {
63
+ const skillStat = lstatSync(skill);
64
+ if (!skillStat.isFile() || skillStat.isSymbolicLink()) {
65
+ throw new Error("managed skill file is unsafe");
66
+ }
67
+ }
68
+ return { dir, marker, skill: existsSync(skill) ? skill : null };
69
+ }
70
+ function recordedSkills(home) {
71
+ const statePath = join(home, ".cortex", "skills.local.json");
72
+ if (!existsSync(statePath))
73
+ return [];
74
+ const stat = lstatSync(statePath);
75
+ if (!stat.isFile() || stat.isSymbolicLink()) {
76
+ throw new Error("skills state is unsafe");
77
+ }
78
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
79
+ if (!parsed.skills || typeof parsed.skills !== "object")
80
+ return [];
81
+ const records = [];
82
+ for (const [key, value] of Object.entries(parsed.skills)) {
83
+ const [keyCli, name] = key.includes(":")
84
+ ? key.split(":", 2)
85
+ : [String(value?.cli ?? ""), key];
86
+ if ((keyCli !== "claude" && keyCli !== "codex") ||
87
+ !SKILL_NAME_RE.test(name)) {
88
+ throw new Error("skills state contains an unsafe record");
89
+ }
90
+ records.push({ cli: keyCli, name });
91
+ }
92
+ return records;
93
+ }
94
+ /**
95
+ * Purge credential-specific artifacts before an explicit API-key rotation.
96
+ * Every path is derived from the target user's home and every skill deletion
97
+ * requires the Cortex ownership marker. Any ambiguity aborts rotation.
98
+ */
99
+ export function prepareEnterpriseCredentialRotation(homeDir) {
100
+ try {
101
+ const home = realpathSync(configuredHome(homeDir));
102
+ const cortexDir = safeDirectory(join(home, ".cortex"));
103
+ if (!cortexDir)
104
+ return true;
105
+ const targets = new Map();
106
+ const recorded = recordedSkills(home);
107
+ for (const cli of ["claude", "codex"]) {
108
+ const cliDir = safeDirectory(join(home, cli === "codex" ? ".codex" : ".claude"));
109
+ if (!cliDir) {
110
+ if (recorded.some((record) => record.cli === cli))
111
+ return false;
112
+ continue;
113
+ }
114
+ const root = safeDirectory(join(cliDir, "skills"));
115
+ if (!root) {
116
+ if (recorded.some((record) => record.cli === cli))
117
+ return false;
118
+ continue;
119
+ }
120
+ for (const name of readdirSync(root)) {
121
+ const candidate = join(root, name);
122
+ const stat = lstatSync(candidate);
123
+ if (!stat.isDirectory() || stat.isSymbolicLink())
124
+ continue;
125
+ const target = inspectManagedSkill(root, cli, name);
126
+ if (target)
127
+ targets.set(target.dir, target);
128
+ }
129
+ for (const record of recorded.filter((entry) => entry.cli === cli)) {
130
+ const candidate = join(root, record.name);
131
+ if (!existsSync(candidate))
132
+ continue;
133
+ const target = inspectManagedSkill(root, cli, record.name);
134
+ if (!target) {
135
+ // Legacy state without an ownership marker is not deletion
136
+ // authority. Abort and require the operator to resolve it.
137
+ return false;
138
+ }
139
+ targets.set(target.dir, target);
140
+ }
141
+ }
142
+ for (const target of targets.values()) {
143
+ if (target.skill)
144
+ unlinkSync(target.skill);
145
+ unlinkSync(target.marker);
146
+ rmdirSync(target.dir);
147
+ }
148
+ for (const filename of STATE_FILES) {
149
+ const path = join(cortexDir, filename);
150
+ if (existsSync(path))
151
+ unlinkSync(path);
152
+ }
153
+ return true;
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
@@ -0,0 +1,21 @@
1
+ const MANAGED_PATHS = {
2
+ claude: {
3
+ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
4
+ linux: "/etc/claude-code/managed-settings.json",
5
+ },
6
+ codex: {
7
+ darwin: "/Library/Application Support/Codex/requirements.toml",
8
+ linux: "/etc/codex/requirements.toml",
9
+ },
10
+ copilot: {
11
+ darwin: "/usr/local/bin/copilot",
12
+ linux: "/usr/local/bin/copilot",
13
+ },
14
+ };
15
+ export function getGovernManagedPath(cli, os) {
16
+ const path = MANAGED_PATHS[cli][os];
17
+ if (!path) {
18
+ throw new Error(`govern install for ${cli} not yet supported on ${os}`);
19
+ }
20
+ return path;
21
+ }
@@ -0,0 +1,14 @@
1
+ // Config
2
+ export { loadEnterpriseConfig, resolveEnterpriseActivation } from "./config.js";
3
+ // Telemetry
4
+ export { TelemetryCollector } from "./telemetry/collector.js";
5
+ // Audit
6
+ export { AuditWriter } from "./audit/writer.js";
7
+ export { queryAuditLog } from "./audit/query.js";
8
+ // RBAC
9
+ export { checkAccess, getAccessDeniedMessage } from "./rbac/check.js";
10
+ // Policy
11
+ export { PolicyStore } from "./policy/store.js";
12
+ // Prompt injection
13
+ export { scanForInjection, sanitizeContent } from "./policy/injection.js";
14
+ export { enforceInjectionPolicy, isInjectionDefenseActive, buildViolationPayload } from "./policy/enforce.js";
@@ -0,0 +1,278 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, } from "node:fs";
3
+ import { resolveTelemetryStateDir, telemetryStatePath } from "./telemetry/state-dir.js";
4
+ import { isAllowedEnterpriseEndpoint } from "./secure-endpoint.js";
5
+ export { isAllowedEnterpriseEndpoint as isAllowedLicenseEndpoint };
6
+ const CACHE_FILE = "license_cache.json";
7
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h fresh
8
+ const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7d grace if endpoint unreachable
9
+ const REQUEST_TIMEOUT_MS = 5000;
10
+ function cachePath(contextDir) {
11
+ return telemetryStatePath(contextDir, CACHE_FILE);
12
+ }
13
+ function isIsoTimestamp(value) {
14
+ if (typeof value !== "string")
15
+ return false;
16
+ const timestamp = new Date(value).getTime();
17
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
18
+ }
19
+ function readCache(contextDir) {
20
+ const path = cachePath(contextDir);
21
+ if (!existsSync(path))
22
+ return null;
23
+ try {
24
+ const raw = readFileSync(path, "utf8");
25
+ const parsed = JSON.parse(raw);
26
+ if (!parsed ||
27
+ typeof parsed !== "object" ||
28
+ parsed.version !== 2 ||
29
+ typeof parsed.endpoint !== "string" ||
30
+ !parsed.endpoint ||
31
+ typeof parsed.api_key_sha256 !== "string" ||
32
+ !/^[a-f0-9]{64}$/.test(parsed.api_key_sha256) ||
33
+ !parsed.result ||
34
+ typeof parsed.result !== "object" ||
35
+ typeof parsed.result.valid !== "boolean" ||
36
+ !isIsoTimestamp(parsed.cached_at)) {
37
+ deleteCache(contextDir);
38
+ return null;
39
+ }
40
+ if (parsed.result.valid) {
41
+ if (typeof parsed.result.edition !== "string" ||
42
+ !Array.isArray(parsed.result.features) ||
43
+ !parsed.result.features.every((feature) => typeof feature === "string") ||
44
+ !isIsoTimestamp(parsed.result.expires_at) ||
45
+ typeof parsed.result.max_repos !== "number" ||
46
+ !Number.isFinite(parsed.result.max_repos) ||
47
+ parsed.result.max_repos < 0 ||
48
+ !isIsoTimestamp(parsed.result.verified_at) ||
49
+ !["remote", "cache"].includes(parsed.result.source)) {
50
+ deleteCache(contextDir);
51
+ return null;
52
+ }
53
+ }
54
+ else if (typeof parsed.result.reason !== "string" ||
55
+ !isIsoTimestamp(parsed.result.verified_at) ||
56
+ !["remote", "cache", "grace_expired"].includes(parsed.result.source)) {
57
+ deleteCache(contextDir);
58
+ return null;
59
+ }
60
+ return parsed;
61
+ }
62
+ catch {
63
+ deleteCache(contextDir);
64
+ return null;
65
+ }
66
+ }
67
+ function writeCache(contextDir, entry) {
68
+ const path = cachePath(contextDir);
69
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
70
+ try {
71
+ mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
72
+ writeFileSync(temporaryPath, JSON.stringify(entry, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
73
+ chmodSync(temporaryPath, 0o600);
74
+ renameSync(temporaryPath, path);
75
+ chmodSync(path, 0o600);
76
+ }
77
+ catch {
78
+ try {
79
+ unlinkSync(temporaryPath);
80
+ }
81
+ catch {
82
+ // Best effort.
83
+ }
84
+ // Cache failures are non-fatal — license check just won't be cached.
85
+ }
86
+ }
87
+ function deleteCache(contextDir) {
88
+ const path = cachePath(contextDir);
89
+ if (!existsSync(path))
90
+ return;
91
+ try {
92
+ unlinkSync(path);
93
+ }
94
+ catch {
95
+ // ignore — best-effort
96
+ }
97
+ }
98
+ function ageMs(isoTimestamp) {
99
+ return Date.now() - new Date(isoTimestamp).getTime();
100
+ }
101
+ export function normalizeLicenseEndpoint(endpoint) {
102
+ return endpoint.trim().replace(/\/+$/, "");
103
+ }
104
+ function apiKeyFingerprint(apiKey) {
105
+ return createHash("sha256").update(apiKey, "utf8").digest("hex");
106
+ }
107
+ function cacheMatchesCredential(cached, endpoint, apiKey) {
108
+ return (cached.version === 2 &&
109
+ cached.endpoint === normalizeLicenseEndpoint(endpoint) &&
110
+ cached.api_key_sha256 === apiKeyFingerprint(apiKey));
111
+ }
112
+ export function enterpriseCredentialId(endpoint, apiKey) {
113
+ return createHash("sha256")
114
+ .update(normalizeLicenseEndpoint(endpoint), "utf8")
115
+ .update("\0", "utf8")
116
+ .update(apiKey, "utf8")
117
+ .digest("hex");
118
+ }
119
+ function cacheResultIsUnexpired(cached) {
120
+ if (!cached.result.valid)
121
+ return false;
122
+ const expiresAt = new Date(cached.result.expires_at).getTime();
123
+ return Number.isFinite(expiresAt) && expiresAt > Date.now();
124
+ }
125
+ function cacheAgeIsWithin(cached, limitMs) {
126
+ const age = ageMs(cached.cached_at);
127
+ return Number.isFinite(age) && age >= 0 && age < limitMs;
128
+ }
129
+ async function fetchLicense(endpoint, apiKey, instanceId, clientVersion) {
130
+ const url = `${endpoint.replace(/\/$/, "")}/api/v1/license/verify`;
131
+ const controller = new AbortController();
132
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
133
+ try {
134
+ const res = await fetch(url, {
135
+ method: "POST",
136
+ headers: {
137
+ "content-type": "application/json",
138
+ authorization: `Bearer ${apiKey}`,
139
+ },
140
+ body: JSON.stringify({
141
+ instance_id: instanceId,
142
+ client_version: clientVersion,
143
+ }),
144
+ signal: controller.signal,
145
+ });
146
+ if (res.status === 401 || res.status === 403) {
147
+ return {
148
+ valid: false,
149
+ reason: "authentication_rejected",
150
+ verified_at: new Date().toISOString(),
151
+ source: "remote",
152
+ };
153
+ }
154
+ if (!res.ok) {
155
+ // Rate limiting, server failures, and other transport-level responses
156
+ // may use an identity-matched positive cache during the grace period.
157
+ return null;
158
+ }
159
+ const json = (await res.json());
160
+ const verifiedAt = new Date().toISOString();
161
+ if (json.valid === true &&
162
+ typeof json.edition === "string" &&
163
+ Array.isArray(json.features) &&
164
+ json.features.every((feature) => typeof feature === "string") &&
165
+ isIsoTimestamp(json.expires_at) &&
166
+ new Date(json.expires_at).getTime() > Date.now() &&
167
+ typeof json.max_repos === "number" &&
168
+ Number.isFinite(json.max_repos) &&
169
+ json.max_repos >= 0) {
170
+ return {
171
+ valid: true,
172
+ edition: json.edition,
173
+ features: json.features,
174
+ expires_at: json.expires_at,
175
+ max_repos: json.max_repos,
176
+ verified_at: verifiedAt,
177
+ source: "remote",
178
+ };
179
+ }
180
+ return {
181
+ valid: false,
182
+ reason: json.valid === true
183
+ ? "malformed_license_response"
184
+ : String(json.reason ?? "unknown"),
185
+ verified_at: verifiedAt,
186
+ source: "remote",
187
+ };
188
+ }
189
+ catch {
190
+ // Network error, timeout, JSON parse error — treat as transient.
191
+ return null;
192
+ }
193
+ finally {
194
+ clearTimeout(timeout);
195
+ }
196
+ }
197
+ /**
198
+ * Verify the license for the given api_key. Layered fallback:
199
+ * 1. If a positive cache (valid:true) is fresh (<24h), unexpired, and
200
+ * bound to this endpoint + API-key fingerprint → use cache
201
+ * Negative cache entries are never trusted; if one is encountered
202
+ * it's deleted on the spot so a since-fixed remote can heal.
203
+ * 2. Otherwise try remote endpoint
204
+ * - On valid:true → write positive cache, return result
205
+ * - On valid:false (authoritative fail) → DELETE any positive
206
+ * cache (so a revoked/expired key doesn't keep masquerading as
207
+ * valid past its remote-side fail), return result, do NOT
208
+ * cache the negative.
209
+ * - On transient failure → fall back to the same identity-bound positive
210
+ * cache if within the grace period (7d). Authentication rejection is
211
+ * authoritative and never uses grace.
212
+ * 3. If no usable cache and endpoint unreachable → return invalid
213
+ * (grace_expired).
214
+ *
215
+ * The caller decides what to do based on the result. Typically:
216
+ * - valid:true → activate enterprise hooks
217
+ * - valid:false → community mode (no enterprise)
218
+ */
219
+ export async function verifyLicense(contextDir, endpoint, apiKey, options = {}) {
220
+ if (!isAllowedEnterpriseEndpoint(endpoint)) {
221
+ deleteCache(contextDir);
222
+ return {
223
+ valid: false,
224
+ reason: "insecure_or_invalid_endpoint",
225
+ verified_at: new Date().toISOString(),
226
+ source: "remote",
227
+ };
228
+ }
229
+ let cached = readCache(contextDir);
230
+ // Defensive: a previous version of this code wrote negative results
231
+ // into the cache. Refuse to honour them and clean them up so a
232
+ // since-deployed fix on the remote can be observed.
233
+ if (cached && cached.result.valid === false) {
234
+ deleteCache(contextDir);
235
+ cached = null;
236
+ }
237
+ const matchingCache = cached && cacheMatchesCredential(cached, endpoint, apiKey)
238
+ ? cached
239
+ : null;
240
+ // Fresh positive cache: skip remote.
241
+ if (matchingCache &&
242
+ matchingCache.result.valid === true &&
243
+ cacheResultIsUnexpired(matchingCache) &&
244
+ cacheAgeIsWithin(matchingCache, CACHE_TTL_MS)) {
245
+ return { ...matchingCache.result, source: "cache" };
246
+ }
247
+ const remote = await fetchLicense(endpoint, apiKey, options.instance_id, options.client_version);
248
+ if (remote) {
249
+ if (remote.valid) {
250
+ writeCache(contextDir, {
251
+ version: 2,
252
+ endpoint: normalizeLicenseEndpoint(endpoint),
253
+ api_key_sha256: apiKeyFingerprint(apiKey),
254
+ result: remote,
255
+ cached_at: new Date().toISOString(),
256
+ });
257
+ }
258
+ else {
259
+ // Authoritative fail from remote — drop any stale positive cache
260
+ // so we don't bounce back to "valid" on the next call.
261
+ deleteCache(contextDir);
262
+ }
263
+ return remote;
264
+ }
265
+ // Remote unreachable. Fall back to positive cache if within grace.
266
+ if (matchingCache &&
267
+ matchingCache.result.valid === true &&
268
+ cacheResultIsUnexpired(matchingCache) &&
269
+ cacheAgeIsWithin(matchingCache, GRACE_PERIOD_MS)) {
270
+ return { ...matchingCache.result, source: "cache" };
271
+ }
272
+ return {
273
+ valid: false,
274
+ reason: "endpoint_unreachable_grace_expired",
275
+ verified_at: new Date().toISOString(),
276
+ source: "grace_expired",
277
+ };
278
+ }