@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,449 @@
1
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { CortexDaemon } from "./server.js";
5
+ import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
6
+ import { configuredEnterpriseCredentialId } from "../core/enterprise-identity.js";
7
+ import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
8
+ import { startGlobalUngovernedEventsPusher } from "./global-host-events.js";
9
+ import { pushMetrics } from "../enterprise/telemetry/sync.js";
10
+ import { TelemetryCollector } from "../core/telemetry/collector.js";
11
+ import { resolveTelemetryStateDir, telemetryStatePath } from "../core/telemetry/state-dir.js";
12
+ import { AuditWriter } from "../core/audit/writer.js";
13
+ import { PolicyStore } from "../core/policy/store.js";
14
+ import { enforceInjectionPolicy, isInjectionDefenseActive, } from "../core/policy/enforce.js";
15
+ import { startUngovernedScanner } from "./ungoverned-scanner.js";
16
+ import { HeartbeatTracker, writeTamperLock, emitTamperAudit, } from "./heartbeat-tracker.js";
17
+ import { startSyncTimer } from "./sync-checker.js";
18
+ import { startSkillSyncTimer } from "./skill-sync-checker.js";
19
+ import { startWorkflowSyncTimer } from "./workflow-sync-checker.js";
20
+ import { startCapabilitySyncTimer } from "./capability-sync-checker.js";
21
+ import { startHostEventsPusher } from "./host-events-pusher.js";
22
+ import { startEgressProxy } from "./egress-proxy.js";
23
+ import { startHeartbeatPusher } from "./heartbeat-pusher.js";
24
+ import { normalizeProjectCwd, ProjectServiceRegistry, } from "./project-service-registry.js";
25
+ /**
26
+ * Daemon entry point. Run by `cortex daemon start` (or auto-spawned by
27
+ * the first hook that needs it).
28
+ *
29
+ * v2.0.0: policy.check is currently a stub allowing all calls (real policy
30
+ * evaluation in subsequent commit). telemetry.flush is fully wired — the
31
+ * Stop hook now reliably pushes metrics.json even if MCP died abruptly.
32
+ */
33
+ function extractStringFields(value, out = []) {
34
+ if (typeof value === "string") {
35
+ out.push(value);
36
+ }
37
+ else if (Array.isArray(value)) {
38
+ for (const v of value)
39
+ extractStringFields(v, out);
40
+ }
41
+ else if (value && typeof value === "object") {
42
+ for (const v of Object.values(value)) {
43
+ extractStringFields(v, out);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ async function policyCheck(payload) {
49
+ if (!payload.cwd)
50
+ return { allow: true };
51
+ const contextDir = join(payload.cwd, ".context");
52
+ if (!existsSync(contextDir))
53
+ return { allow: true };
54
+ const store = new PolicyStore(contextDir);
55
+ const policies = store.getMergedPolicies();
56
+ if (!isInjectionDefenseActive(policies)) {
57
+ return { allow: true };
58
+ }
59
+ const haystack = extractStringFields(payload.input).join("\n");
60
+ if (!haystack)
61
+ return { allow: true };
62
+ const result = enforceInjectionPolicy(haystack, policies);
63
+ if (result.allowed)
64
+ return { allow: true };
65
+ const topMatch = result.scan.matches[0];
66
+ const reason = topMatch
67
+ ? `prompt-injection-defense: ${topMatch.category} (${topMatch.matched.slice(0, 80)})`
68
+ : "prompt-injection-defense: flagged";
69
+ return { allow: false, reason };
70
+ }
71
+ function readMetrics(contextDir) {
72
+ const path = telemetryStatePath(contextDir, "metrics.json");
73
+ if (!existsSync(path))
74
+ return null;
75
+ try {
76
+ return JSON.parse(readFileSync(path, "utf8"));
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function pendingPushPath(contextDir) {
83
+ return telemetryStatePath(contextDir, "pending-push.json");
84
+ }
85
+ function readPendingPush(contextDir) {
86
+ const path = pendingPushPath(contextDir);
87
+ if (!existsSync(path))
88
+ return null;
89
+ try {
90
+ return JSON.parse(readFileSync(path, "utf8"));
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ function writePendingPush(contextDir, pending) {
97
+ const path = pendingPushPath(contextDir);
98
+ mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
99
+ writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
100
+ }
101
+ function deletePendingPush(contextDir) {
102
+ const path = pendingPushPath(contextDir);
103
+ try {
104
+ rmSync(path, { force: true });
105
+ }
106
+ catch {
107
+ // best effort
108
+ }
109
+ }
110
+ function ackOnDisk(contextDir, pushed) {
111
+ const collector = new TelemetryCollector(contextDir, pushed.client_version || "unknown");
112
+ collector.acknowledgePush(pushed);
113
+ collector.flush();
114
+ }
115
+ const telemetryBackoff = new Map();
116
+ const TELEMETRY_BACKOFF_BASE_MS = 60_000;
117
+ const TELEMETRY_BACKOFF_CAP_MS = 30 * 60_000;
118
+ function shouldSkipTelemetryPush(cwd, now = Date.now()) {
119
+ const state = telemetryBackoff.get(cwd);
120
+ return state ? now < state.nextPushAt : false;
121
+ }
122
+ function recordTelemetryPushOutcome(cwd, success, now = Date.now()) {
123
+ if (success) {
124
+ telemetryBackoff.delete(cwd);
125
+ return;
126
+ }
127
+ const prev = telemetryBackoff.get(cwd) ?? { nextPushAt: 0, consecutiveFailures: 0 };
128
+ const failures = prev.consecutiveFailures + 1;
129
+ const delay = Math.min(TELEMETRY_BACKOFF_BASE_MS * 2 ** (failures - 1), TELEMETRY_BACKOFF_CAP_MS);
130
+ telemetryBackoff.set(cwd, {
131
+ consecutiveFailures: failures,
132
+ nextPushAt: now + delay,
133
+ });
134
+ }
135
+ async function telemetryFlush(payload) {
136
+ const cwd = payload.cwd ?? process.cwd();
137
+ const contextDir = join(cwd, ".context");
138
+ if (!existsSync(contextDir)) {
139
+ return { flushed: false, events_pushed: 0 };
140
+ }
141
+ const config = loadEnterpriseConfig(contextDir);
142
+ const activation = resolveEnterpriseActivation(config);
143
+ if (!activation.active || !config.telemetry.enabled) {
144
+ // Community mode or telemetry disabled → nothing to push.
145
+ return { flushed: false, events_pushed: 0 };
146
+ }
147
+ if (!config.telemetry.endpoint || !config.telemetry.api_key) {
148
+ return { flushed: false, events_pushed: 0 };
149
+ }
150
+ if (shouldSkipTelemetryPush(cwd)) {
151
+ return { flushed: false, events_pushed: 0 };
152
+ }
153
+ const repo = basename(cwd);
154
+ const endpoint = config.telemetry.endpoint;
155
+ const apiKey = config.telemetry.api_key;
156
+ // Recovery: if a pending push exists, retry it first with the same
157
+ // push_id so the server can deduplicate against an earlier in-flight
158
+ // attempt that may have crashed before delete.
159
+ const pending = readPendingPush(contextDir);
160
+ if (pending) {
161
+ const result = await pushMetrics(pending.snapshot, endpoint, apiKey, {
162
+ repo,
163
+ session_id: payload.session_id,
164
+ push_id: pending.push_id,
165
+ });
166
+ recordTelemetryPushOutcome(cwd, result.success);
167
+ if (!result.success) {
168
+ process.stderr.write(`[cortex-daemon] pending telemetry push retry failed: ${result.error ?? "unknown"}\n`);
169
+ return { flushed: false, events_pushed: 0 };
170
+ }
171
+ ackOnDisk(contextDir, pending.snapshot);
172
+ deletePendingPush(contextDir);
173
+ return { flushed: true, events_pushed: pending.snapshot.total_tool_calls };
174
+ }
175
+ const metrics = readMetrics(contextDir);
176
+ if (!metrics) {
177
+ // No metrics on disk yet — MCP hasn't flushed. Nothing to push.
178
+ return { flushed: false, events_pushed: 0 };
179
+ }
180
+ const push_id = randomUUID();
181
+ writePendingPush(contextDir, {
182
+ snapshot: metrics,
183
+ push_id,
184
+ written_at: new Date().toISOString(),
185
+ });
186
+ const result = await pushMetrics(metrics, endpoint, apiKey, {
187
+ repo,
188
+ session_id: payload.session_id,
189
+ push_id,
190
+ });
191
+ recordTelemetryPushOutcome(cwd, result.success);
192
+ if (!result.success) {
193
+ process.stderr.write(`[cortex-daemon] telemetry push failed: ${result.error ?? "unknown"}\n`);
194
+ // Pending stays on disk; next tick (after backoff) will retry.
195
+ return { flushed: false, events_pushed: 0 };
196
+ }
197
+ ackOnDisk(contextDir, metrics);
198
+ deletePendingPush(contextDir);
199
+ return {
200
+ flushed: true,
201
+ events_pushed: metrics.total_tool_calls,
202
+ };
203
+ }
204
+ // Per-cwd AuditWriter cache. Daemon serves multiple projects so we don't
205
+ // want to instantiate (and lose buffered state) on every audit.log call.
206
+ const auditWriters = new Map();
207
+ function getAuditWriter(cwd) {
208
+ const contextDir = join(cwd, ".context");
209
+ let writer = auditWriters.get(contextDir);
210
+ if (!writer) {
211
+ writer = new AuditWriter(contextDir);
212
+ auditWriters.set(contextDir, writer);
213
+ }
214
+ return writer;
215
+ }
216
+ async function auditLog(payload) {
217
+ if (!payload.cwd || !payload.entry) {
218
+ return { written: false };
219
+ }
220
+ const contextDir = join(payload.cwd, ".context");
221
+ if (!existsSync(contextDir)) {
222
+ return { written: false };
223
+ }
224
+ const writer = getAuditWriter(payload.cwd);
225
+ const entry = {
226
+ timestamp: payload.entry.timestamp,
227
+ tool: payload.entry.tool,
228
+ input: payload.entry.input,
229
+ result_count: payload.entry.result_count ?? 0,
230
+ entities_returned: [],
231
+ rules_applied: [],
232
+ duration_ms: payload.entry.duration_ms ?? 0,
233
+ status: payload.entry.status,
234
+ event_type: payload.entry.event_type,
235
+ evidence_level: payload.entry.evidence_level,
236
+ resource_type: payload.entry.resource_type,
237
+ session_id: payload.entry.session_id,
238
+ metadata: payload.entry.metadata,
239
+ };
240
+ writer.log(entry);
241
+ return { written: true };
242
+ }
243
+ async function main() {
244
+ // Phase 6: hook heartbeat tracker (per-session activity record + tamper detect).
245
+ const tracker = new HeartbeatTracker();
246
+ let projectServices = null;
247
+ function registerEnterpriseProject(cwd) {
248
+ const config = loadEnterpriseConfig(join(cwd, ".context"));
249
+ if (!resolveEnterpriseActivation(config).active)
250
+ return true;
251
+ if (projectServices?.has(cwd))
252
+ return true;
253
+ const credentialId = configuredEnterpriseCredentialId(cwd);
254
+ if (!credentialId || !matchesEnterpriseHostIdentity(credentialId)) {
255
+ process.stderr.write(`[cortex-daemon] rejected Enterprise project ${cwd}: ` +
256
+ "this user profile is enrolled to another endpoint or API key\n");
257
+ return false;
258
+ }
259
+ if (!projectServices?.register(cwd)) {
260
+ process.stderr.write(`[cortex-daemon] rejected Enterprise project ${cwd}: ` +
261
+ "this user daemon is already bound to another endpoint or API key\n");
262
+ return false;
263
+ }
264
+ return true;
265
+ }
266
+ async function heartbeat(payload) {
267
+ const cwd = normalizeProjectCwd(payload.cwd);
268
+ if (!cwd) {
269
+ return { recorded: false, tamper_lock_active: false };
270
+ }
271
+ if (!registerEnterpriseProject(cwd)) {
272
+ return { recorded: false, tamper_lock_active: false };
273
+ }
274
+ return tracker.recordHeartbeat({ ...payload, cwd });
275
+ }
276
+ const daemon = new CortexDaemon({
277
+ onPolicyCheck: policyCheck,
278
+ onTelemetryFlush: telemetryFlush,
279
+ onAuditLog: auditLog,
280
+ onHeartbeat: heartbeat,
281
+ });
282
+ // Phase 5: Tier 3 ungoverned-session detection. Periodic process scan, audit
283
+ // emit per finding, optional SIGTERM in enforced mode (same-user only).
284
+ const scanInterval = parseInt(process.env.CORTEX_UNGOVERNED_SCAN_MS ?? "", 10);
285
+ const intervalMs = Number.isFinite(scanInterval) && scanInterval > 0 ? scanInterval : 60_000;
286
+ // Phase 6: periodic tamper-checker. For each active session that had at
287
+ // least one tool-fired hook then went silent past missing_threshold_seconds,
288
+ // write .cortex-tamper.lock + audit event. The next SessionStart in
289
+ // enforced mode will refuse to register tools until 'cortex enterprise
290
+ // repair' clears the lock.
291
+ const tamperThreshold = parseInt(process.env.CORTEX_TAMPER_MISSING_THRESHOLD_S ?? "", 10);
292
+ const missingThresholdSeconds = Number.isFinite(tamperThreshold) && tamperThreshold > 0 ? tamperThreshold : 300;
293
+ const tamperCheckInterval = parseInt(process.env.CORTEX_TAMPER_CHECK_MS ?? "", 10);
294
+ const tamperCheckMs = Number.isFinite(tamperCheckInterval) && tamperCheckInterval > 0 ? tamperCheckInterval : 60_000;
295
+ // Phase 7: periodic sync-version-check + host-events push to cortex-web.
296
+ // Daemon runs as the user post-Fas-3 privilege drop, so sync only checks
297
+ // version availability (writes a notification + audit). Re-applying
298
+ // managed-settings still requires 'sudo cortex enterprise sync'.
299
+ const syncIntervalRaw = parseInt(process.env.CORTEX_SYNC_CHECK_MS ?? "", 10);
300
+ const syncIntervalMs = Number.isFinite(syncIntervalRaw) && syncIntervalRaw > 0 ? syncIntervalRaw : 60 * 60 * 1000;
301
+ const pushIntervalRaw = parseInt(process.env.CORTEX_HOST_EVENTS_PUSH_MS ?? "", 10);
302
+ const pushIntervalMs = Number.isFinite(pushIntervalRaw) && pushIntervalRaw > 0 ? pushIntervalRaw : 5 * 60 * 1000;
303
+ // Skills v3: poll cortex-web for org-authored skills, write SKILL.md
304
+ // files into per-CLI user-scope directories. Runs at the same cadence
305
+ // as the govern-config sync check by default but is independently
306
+ // configurable.
307
+ const skillSyncRaw = parseInt(process.env.CORTEX_SKILL_SYNC_MS ?? "", 10);
308
+ const skillSyncMs = Number.isFinite(skillSyncRaw) && skillSyncRaw > 0 ? skillSyncRaw : syncIntervalMs;
309
+ // Harness Phase 2: poll cortex-web for org-authored workflows, cache
310
+ // their definitions locally so cortex.workflow.start can resolve
311
+ // org-specific workflow_ids ahead of bundled defaults. Same cadence
312
+ // as the skill sync by default; independently configurable via
313
+ // CORTEX_WORKFLOW_SYNC_MS / CORTEX_DISABLE_WORKFLOW_SYNC.
314
+ const workflowSyncRaw = parseInt(process.env.CORTEX_WORKFLOW_SYNC_MS ?? "", 10);
315
+ const workflowSyncMs = Number.isFinite(workflowSyncRaw) && workflowSyncRaw > 0
316
+ ? workflowSyncRaw
317
+ : skillSyncMs;
318
+ // Harness Phase 2: poll cortex-web for org-authored capabilities and
319
+ // cache definitions locally so evaluateToolCall can merge them over
320
+ // bundled DEFAULT_CAPABILITIES on the pre-tool-use path. Same cadence
321
+ // as the workflow sync by default; independently configurable via
322
+ // CORTEX_CAPABILITY_SYNC_MS / CORTEX_DISABLE_CAPABILITY_SYNC.
323
+ const capabilitySyncRaw = parseInt(process.env.CORTEX_CAPABILITY_SYNC_MS ?? "", 10);
324
+ const capabilitySyncMs = Number.isFinite(capabilitySyncRaw) && capabilitySyncRaw > 0
325
+ ? capabilitySyncRaw
326
+ : workflowSyncMs;
327
+ // Govern host heartbeat — fills host_enrollment on cortex-web so the
328
+ // dashboard at /dashboard/govern actually shows this host.
329
+ const heartbeatRaw = parseInt(process.env.CORTEX_HEARTBEAT_PUSH_MS ?? "", 10);
330
+ const heartbeatMs = Number.isFinite(heartbeatRaw) && heartbeatRaw > 0 ? heartbeatRaw : 5 * 60 * 1000;
331
+ projectServices = new ProjectServiceRegistry((cwd) => {
332
+ const handles = [];
333
+ if (process.env.CORTEX_DISABLE_SYNC_CHECK !== "1") {
334
+ handles.push(startSyncTimer(cwd, syncIntervalMs));
335
+ }
336
+ if (process.env.CORTEX_DISABLE_HOST_EVENTS_PUSH !== "1") {
337
+ handles.push(startHostEventsPusher(cwd, pushIntervalMs));
338
+ }
339
+ return handles;
340
+ }, {
341
+ credentialIdForCwd: configuredEnterpriseCredentialId,
342
+ hostFactory: (cwd, credentialId) => {
343
+ const handles = [];
344
+ // These services operate on user-global process or artifact state.
345
+ // One explicit host governance project owns them; projects carrying
346
+ // another Enterprise identity are rejected by the registry.
347
+ if (process.env.CORTEX_DISABLE_UNGOVERNED_SCAN !== "1") {
348
+ // Process snapshots do not carry a trustworthy project cwd. Keep the
349
+ // single host-wide scanner detection-only so an enforced project can
350
+ // never terminate a process belonging to an advisory project.
351
+ handles.push(startUngovernedScanner({
352
+ cwd,
353
+ credentialId,
354
+ intervalMs,
355
+ mode: "advisory",
356
+ }));
357
+ }
358
+ if (process.env.CORTEX_DISABLE_HOST_EVENTS_PUSH !== "1") {
359
+ handles.push(startGlobalUngovernedEventsPusher(cwd, credentialId, pushIntervalMs));
360
+ }
361
+ if (process.env.CORTEX_DISABLE_SKILL_SYNC !== "1") {
362
+ handles.push(startSkillSyncTimer(cwd, skillSyncMs));
363
+ }
364
+ if (process.env.CORTEX_DISABLE_WORKFLOW_SYNC !== "1") {
365
+ handles.push(startWorkflowSyncTimer(cwd, workflowSyncMs));
366
+ }
367
+ if (process.env.CORTEX_DISABLE_CAPABILITY_SYNC !== "1") {
368
+ handles.push(startCapabilitySyncTimer(cwd, capabilitySyncMs));
369
+ }
370
+ if (process.env.CORTEX_DISABLE_HEARTBEAT_PUSH !== "1") {
371
+ handles.push(startHeartbeatPusher(cwd, heartbeatMs));
372
+ }
373
+ return handles;
374
+ },
375
+ });
376
+ await daemon.start();
377
+ const startupCwd = normalizeProjectCwd(process.cwd());
378
+ if (startupCwd)
379
+ registerEnterpriseProject(startupCwd);
380
+ // Phase 4 task 19: cortex egress proxy. Logs SNI + destination per
381
+ // outbound connection (no TLS termination). cortex run sets
382
+ // HTTPS_PROXY/HTTP_PROXY for the Copilot wrap; other AI CLIs respect
383
+ // these env vars too if a developer wires them in.
384
+ const proxyPortRaw = parseInt(process.env.CORTEX_EGRESS_PROXY_PORT ?? "", 10);
385
+ const proxyPort = Number.isFinite(proxyPortRaw) && proxyPortRaw > 0 ? proxyPortRaw : 18888;
386
+ if (process.env.CORTEX_DISABLE_EGRESS_PROXY !== "1") {
387
+ // A shared proxy connection has no trustworthy project identity. Keep
388
+ // transport support, but suppress project audit emission until callers
389
+ // use an attributable per-project channel.
390
+ startEgressProxy({ port: proxyPort })
391
+ .then((handle) => {
392
+ process.stderr.write(`[cortex-daemon] egress proxy listening on 127.0.0.1:${handle.port}\n`);
393
+ })
394
+ .catch((err) => {
395
+ process.stderr.write(`[cortex-daemon] egress proxy failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
396
+ });
397
+ }
398
+ // Periodic telemetry push. Daemon owns the network call so MCP doesn't
399
+ // race with itself or with this loop. Walks active sessions, dedupes
400
+ // cwds, and runs the existing per-cwd flush handler.
401
+ const telemetryPushRaw = parseInt(process.env.CORTEX_TELEMETRY_PUSH_MS ?? "", 10);
402
+ const telemetryPushMs = Number.isFinite(telemetryPushRaw) && telemetryPushRaw > 0
403
+ ? telemetryPushRaw
404
+ : 5 * 60 * 1000;
405
+ if (process.env.CORTEX_DISABLE_TELEMETRY_PUSH !== "1") {
406
+ const telemetryTimer = setInterval(async () => {
407
+ const cwds = new Set();
408
+ for (const [, state] of tracker.getActiveSessions()) {
409
+ if (state.cwd)
410
+ cwds.add(state.cwd);
411
+ }
412
+ for (const cwd of cwds) {
413
+ try {
414
+ await telemetryFlush({ reason: "interval", cwd });
415
+ }
416
+ catch (err) {
417
+ process.stderr.write(`[cortex-daemon] telemetry push failed for ${cwd}: ${err instanceof Error ? err.message : String(err)}\n`);
418
+ }
419
+ }
420
+ }, telemetryPushMs);
421
+ if (typeof telemetryTimer.unref === "function")
422
+ telemetryTimer.unref();
423
+ }
424
+ if (process.env.CORTEX_DISABLE_TAMPER_CHECK !== "1") {
425
+ const checkTimer = setInterval(() => {
426
+ const detected = tracker.detectTamper({
427
+ cwds: projectServices?.projectCwds() ?? [],
428
+ missingThresholdSeconds,
429
+ });
430
+ for (const entry of detected) {
431
+ try {
432
+ writeTamperLock(entry.cwd, entry);
433
+ }
434
+ catch (err) {
435
+ process.stderr.write(`[cortex-daemon] failed to write tamper lock: ${err instanceof Error ? err.message : String(err)}\n`);
436
+ }
437
+ void emitTamperAudit(entry.cwd, entry).catch((err) => {
438
+ process.stderr.write(`[cortex-daemon] failed to emit tamper audit: ${err instanceof Error ? err.message : String(err)}\n`);
439
+ });
440
+ }
441
+ }, tamperCheckMs);
442
+ if (typeof checkTimer.unref === "function")
443
+ checkTimer.unref();
444
+ }
445
+ }
446
+ main().catch((err) => {
447
+ process.stderr.write(`[cortex-daemon] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
448
+ process.exit(1);
449
+ });
@@ -0,0 +1,36 @@
1
+ import { join } from "node:path";
2
+ import { homedir, tmpdir, userInfo } from "node:os";
3
+ import { mkdirSync } from "node:fs";
4
+ /**
5
+ * Resolves filesystem locations the daemon and hooks share.
6
+ * Per-user, not per-project — one daemon serves all projects so warm graph
7
+ * + embeddings stay loaded across switches.
8
+ */
9
+ function safeUid() {
10
+ try {
11
+ const info = userInfo();
12
+ if (typeof info.uid === "number" && info.uid >= 0) {
13
+ return String(info.uid);
14
+ }
15
+ return info.username || "anon";
16
+ }
17
+ catch {
18
+ return "anon";
19
+ }
20
+ }
21
+ export function daemonDir() {
22
+ const dir = join(homedir(), ".cortex");
23
+ mkdirSync(dir, { recursive: true });
24
+ return dir;
25
+ }
26
+ export function pidFilePath() {
27
+ return join(daemonDir(), "daemon.pid");
28
+ }
29
+ export function logFilePath() {
30
+ return join(daemonDir(), "daemon.log");
31
+ }
32
+ export function socketPath() {
33
+ // Keep socket in tmpdir per-user — Linux has 108-char path limit on
34
+ // sockaddr_un.sun_path so we avoid putting it under $HOME.
35
+ return join(tmpdir(), `cortex-${safeUid()}.sock`);
36
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync, realpathSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ export function normalizeProjectCwd(cwd) {
4
+ if (!cwd.trim())
5
+ return null;
6
+ const candidate = resolve(cwd);
7
+ try {
8
+ if (!existsSync(candidate) || !statSync(candidate).isDirectory()) {
9
+ return null;
10
+ }
11
+ if (!existsSync(join(candidate, ".context"))) {
12
+ return null;
13
+ }
14
+ return realpathSync(candidate);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /**
21
+ * Owns background services for every project observed by the per-user daemon.
22
+ * Request-time handlers already carry cwd in their payloads; this registry
23
+ * gives the periodic services the same project isolation.
24
+ */
25
+ export class ProjectServiceRegistry {
26
+ factory;
27
+ options;
28
+ projects = new Map();
29
+ hostCredentialId = null;
30
+ hostHandles = [];
31
+ constructor(factory, options = {}) {
32
+ this.factory = factory;
33
+ this.options = options;
34
+ }
35
+ register(cwd) {
36
+ const normalized = normalizeProjectCwd(cwd);
37
+ if (!normalized || this.projects.has(normalized))
38
+ return false;
39
+ const credentialId = this.options.credentialIdForCwd?.(normalized) ?? "project-local";
40
+ if (!credentialId)
41
+ return false;
42
+ if (this.hostCredentialId &&
43
+ this.hostCredentialId !== credentialId) {
44
+ return false;
45
+ }
46
+ if (!this.hostCredentialId) {
47
+ this.hostCredentialId = credentialId;
48
+ this.hostHandles =
49
+ this.options.hostFactory?.(normalized, credentialId) ?? [];
50
+ }
51
+ const handles = this.factory(normalized);
52
+ this.projects.set(normalized, handles);
53
+ return true;
54
+ }
55
+ has(cwd) {
56
+ const normalized = normalizeProjectCwd(cwd);
57
+ return normalized ? this.projects.has(normalized) : false;
58
+ }
59
+ projectCwds() {
60
+ return Array.from(this.projects.keys());
61
+ }
62
+ async stopAll() {
63
+ const handles = [
64
+ ...Array.from(this.projects.values()).flat(),
65
+ ...this.hostHandles,
66
+ ];
67
+ this.projects.clear();
68
+ this.hostHandles = [];
69
+ this.hostCredentialId = null;
70
+ await Promise.allSettled(handles.map((handle) => Promise.resolve(handle.stop())));
71
+ }
72
+ size() {
73
+ return this.projects.size;
74
+ }
75
+ activeCredentialId() {
76
+ return this.hostCredentialId;
77
+ }
78
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wire protocol between cortex hooks and the cortex daemon.
3
+ * Newline-delimited JSON over a Unix socket.
4
+ *
5
+ * Each request: { id, type, payload }
6
+ * Each response: { id, ok, result?, error? }
7
+ */
8
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 5000;