@danielblomma/cortex-mcp 2.4.0 → 2.4.2

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 (214) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +24 -3
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +12 -1607
  23. package/bin/daemon-control.mjs +162 -0
  24. package/mcp-registry-submission.json +1 -1
  25. package/package.json +8 -3
  26. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  27. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  28. package/scaffold/mcp/dist/cli/govern.js +937 -0
  29. package/scaffold/mcp/dist/cli/query.js +409 -0
  30. package/scaffold/mcp/dist/cli/run.js +295 -0
  31. package/scaffold/mcp/dist/cli/stage.js +308 -0
  32. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  33. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  34. package/scaffold/mcp/dist/contextEntities.js +282 -0
  35. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  36. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  37. package/scaffold/mcp/dist/core/config.js +235 -0
  38. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  39. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  40. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  41. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  42. package/scaffold/mcp/dist/core/index.js +14 -0
  43. package/scaffold/mcp/dist/core/license.js +278 -0
  44. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  45. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  46. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  47. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  48. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  49. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  50. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  51. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  52. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  53. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  54. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  55. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  56. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  57. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  58. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  59. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  60. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  61. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  62. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  63. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  64. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  65. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  66. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  67. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  68. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  69. package/scaffold/mcp/dist/daemon/client.js +130 -0
  70. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  71. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  72. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  73. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  74. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  75. package/scaffold/mcp/dist/daemon/main.js +449 -0
  76. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  77. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  78. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  79. package/scaffold/mcp/dist/daemon/server.js +180 -0
  80. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  81. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  82. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  83. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  84. package/scaffold/mcp/dist/defaults.js +6 -0
  85. package/scaffold/mcp/dist/embed.js +627 -0
  86. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  87. package/scaffold/mcp/dist/embeddings.js +167 -0
  88. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  89. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  90. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  91. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  92. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  93. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  94. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  95. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  96. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  97. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  98. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  99. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  100. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  101. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  102. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  103. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  104. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  105. package/scaffold/mcp/dist/frontmatter.js +33 -0
  106. package/scaffold/mcp/dist/graph.js +769 -0
  107. package/scaffold/mcp/dist/graphCsv.js +55 -0
  108. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  109. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  110. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  111. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  112. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  113. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  114. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  115. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  116. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  117. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  118. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  119. package/scaffold/mcp/dist/impactRanking.js +191 -0
  120. package/scaffold/mcp/dist/impactResponse.js +30 -0
  121. package/scaffold/mcp/dist/impactResults.js +105 -0
  122. package/scaffold/mcp/dist/impactSeed.js +20 -0
  123. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  124. package/scaffold/mcp/dist/jsonl.js +77 -0
  125. package/scaffold/mcp/dist/loadGraph.js +759 -0
  126. package/scaffold/mcp/dist/lruCache.js +38 -0
  127. package/scaffold/mcp/dist/paths.js +97 -0
  128. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  129. package/scaffold/mcp/dist/plugin.js +81 -0
  130. package/scaffold/mcp/dist/presets.js +78 -0
  131. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  132. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  133. package/scaffold/mcp/dist/rules.js +23 -0
  134. package/scaffold/mcp/dist/search.js +212 -0
  135. package/scaffold/mcp/dist/searchCore.js +457 -0
  136. package/scaffold/mcp/dist/searchResults.js +230 -0
  137. package/scaffold/mcp/dist/server.js +317 -0
  138. package/scaffold/mcp/dist/types.js +1 -0
  139. package/scaffold/mcp/package-lock.json +321 -200
  140. package/scaffold/mcp/package.json +15 -6
  141. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  142. package/scaffold/mcp/src/cli/govern.ts +137 -52
  143. package/scaffold/mcp/src/cli/run.ts +53 -19
  144. package/scaffold/mcp/src/cli/stage.ts +8 -3
  145. package/scaffold/mcp/src/core/config.ts +9 -2
  146. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  147. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  148. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  149. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  150. package/scaffold/mcp/src/core/license.ts +186 -17
  151. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  152. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  153. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  154. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  155. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  156. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  157. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  158. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  159. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  160. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  161. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  162. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  163. package/scaffold/mcp/src/daemon/main.ts +99 -25
  164. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  165. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  166. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  167. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  168. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  169. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  170. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  171. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  172. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  173. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  174. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  175. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  176. package/scaffold/mcp/src/plugin.ts +20 -0
  177. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  178. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  179. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  180. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  181. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  182. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  183. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  184. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  185. package/scaffold/mcp/tests/license.test.mjs +367 -0
  186. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  187. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  188. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  189. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  190. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  191. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  192. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  193. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  194. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  195. package/scaffold/ownership/current.json +4 -0
  196. package/scaffold/ownership/v1.json +456 -0
  197. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  198. package/scaffold/scripts/ingest-worker.mjs +4 -1
  199. package/scaffold/scripts/ingest.mjs +5 -3899
  200. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  201. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  202. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  203. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  204. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  205. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  206. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  207. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  208. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  209. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  210. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  211. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  212. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  213. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  214. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,557 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { homedir, hostname } from "node:os";
4
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { loadEnterpriseConfig } from "../core/config.js";
6
+ import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
7
+ import { enterpriseCredentialId, isAllowedLicenseEndpoint, } from "../core/license.js";
8
+ import { writeHostAuditEvent } from "./ungoverned-scanner.js";
9
+ import { daemonDir } from "./paths.js";
10
+ /**
11
+ * Skills v3 sync flow — daemon side.
12
+ *
13
+ * The daemon polls cortex-web /api/v1/govern/skills/manifest each tick to
14
+ * learn what skills the org has authored. It diffs against a local state
15
+ * file, then for each new/changed skill it fetches the assembled SKILL.md
16
+ * and writes it to the appropriate per-CLI skills directory. Removed
17
+ * skills are unlinked. Unlike govern-config sync, this does NOT need
18
+ * root: SKILL.md files live in user-owned directories the daemon can
19
+ * write to directly.
20
+ *
21
+ * Three audit outcomes per tick:
22
+ * - skills_unchanged — manifest matches local state
23
+ * - skills_synced — at least one skill was written or removed
24
+ * (metadata: added/changed/removed counts)
25
+ * - skills_sync_failed — network / auth / disk error
26
+ *
27
+ * When something changes, a notification file is written so
28
+ * 'cortex enterprise status' can prompt the user to restart Claude
29
+ * Code / Codex CLI to pick up the new skills.
30
+ */
31
+ const STATE_FILENAME = "skills.local.json";
32
+ const NOTIFICATION_FILENAME = ".skills-update-applied.json";
33
+ const OWNERSHIP_FILENAME = ".cortex-managed.json";
34
+ const OWNERSHIP_VERSION = 1;
35
+ const SUPPORTED_CLIS = ["claude", "codex"];
36
+ const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
37
+ function stateFilePath() {
38
+ return join(daemonDir(), STATE_FILENAME);
39
+ }
40
+ function notificationFilePath() {
41
+ return join(daemonDir(), NOTIFICATION_FILENAME);
42
+ }
43
+ function readState() {
44
+ const path = stateFilePath();
45
+ if (!existsSync(path))
46
+ return { skills: {} };
47
+ try {
48
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
49
+ const normalizedSkills = {};
50
+ for (const [key, record] of Object.entries(parsed.skills ?? {})) {
51
+ if (!record || typeof record !== "object")
52
+ continue;
53
+ const inferredCli = record.path?.includes("/.codex/skills/")
54
+ ? "codex"
55
+ : "claude";
56
+ const cli = record.cli === "codex" || record.cli === "claude"
57
+ ? record.cli
58
+ : inferredCli;
59
+ const normalizedKey = key.includes(":") ? key : `${cli}:${key}`;
60
+ normalizedSkills[normalizedKey] = {
61
+ credential_id: typeof record.credential_id === "string"
62
+ ? record.credential_id
63
+ : undefined,
64
+ cli,
65
+ scope: String(record.scope ?? "global"),
66
+ updated_at: String(record.updated_at ?? ""),
67
+ path: String(record.path ?? ""),
68
+ };
69
+ }
70
+ return {
71
+ credential_id: typeof parsed.credential_id === "string"
72
+ ? parsed.credential_id
73
+ : undefined,
74
+ skills: normalizedSkills,
75
+ last_synced_at: parsed.last_synced_at,
76
+ };
77
+ }
78
+ catch {
79
+ return { skills: {} };
80
+ }
81
+ }
82
+ function writeState(state) {
83
+ writeFileSync(stateFilePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
84
+ }
85
+ /**
86
+ * Resolve the on-disk SKILL.md path for a skill install target. Global
87
+ * skills are installed once per CLI, so the destination root depends on the
88
+ * active sync target rather than just the stored scope.
89
+ */
90
+ function isSafeSkillName(name) {
91
+ return typeof name === "string" && SKILL_NAME_RE.test(name);
92
+ }
93
+ function assertPathContained(root, target) {
94
+ const rel = relative(root, target);
95
+ if (rel === ".." ||
96
+ rel.startsWith(`..${sep}`) ||
97
+ isAbsolute(rel)) {
98
+ throw new Error("skill target escapes managed root");
99
+ }
100
+ }
101
+ function configuredSkillRoot(cli) {
102
+ return join(realpathSync(homedir()), cli === "codex" ? ".codex" : ".claude", "skills");
103
+ }
104
+ function assertNotSymlink(path, description) {
105
+ if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
106
+ throw new Error(`${description} must not be a symbolic link`);
107
+ }
108
+ }
109
+ function resolveSkillRoot(cli, create) {
110
+ const root = configuredSkillRoot(cli);
111
+ const cliRoot = resolve(root, "..");
112
+ assertNotSymlink(cliRoot, `${cli} configuration directory`);
113
+ if (!existsSync(cliRoot)) {
114
+ if (!create)
115
+ return null;
116
+ mkdirSync(cliRoot, { mode: 0o700 });
117
+ }
118
+ if (!lstatSync(cliRoot).isDirectory()) {
119
+ throw new Error(`${cli} configuration path is not a directory`);
120
+ }
121
+ assertNotSymlink(root, `${cli} skills root`);
122
+ if (!existsSync(root)) {
123
+ if (!create)
124
+ return null;
125
+ mkdirSync(root, { mode: 0o700 });
126
+ }
127
+ if (!lstatSync(root).isDirectory()) {
128
+ throw new Error(`${cli} skills root is not a directory`);
129
+ }
130
+ return realpathSync(root);
131
+ }
132
+ function skillFilePath(root, name) {
133
+ if (!isSafeSkillName(name)) {
134
+ throw new Error("invalid skill name");
135
+ }
136
+ const target = resolve(root, name, "SKILL.md");
137
+ assertPathContained(resolve(root), target);
138
+ return target;
139
+ }
140
+ function stateSkillKey(cli, name) {
141
+ return `${cli}:${name}`;
142
+ }
143
+ function shouldSyncForCli(scope, cli) {
144
+ if (scope === "global")
145
+ return true;
146
+ return scope === `cli:${cli}`;
147
+ }
148
+ async function fetchManifest(baseUrl, apiKey, cli) {
149
+ const url = new URL(baseUrl.replace(/\/$/, "") + "/api/v1/govern/skills/manifest");
150
+ url.searchParams.set("cli", cli);
151
+ const res = await fetch(url, {
152
+ headers: { Authorization: `Bearer ${apiKey}` },
153
+ });
154
+ if (!res.ok) {
155
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
156
+ }
157
+ const body = (await res.json());
158
+ if (body.skills === undefined)
159
+ return [];
160
+ if (!Array.isArray(body.skills)) {
161
+ throw new Error("invalid skill manifest: skills must be an array");
162
+ }
163
+ return body.skills;
164
+ }
165
+ async function fetchSkillBody(baseUrl, apiKey, name) {
166
+ const url = new URL(baseUrl.replace(/\/$/, "") +
167
+ "/api/v1/govern/skills/" +
168
+ encodeURIComponent(name));
169
+ const res = await fetch(url, {
170
+ headers: { Authorization: `Bearer ${apiKey}` },
171
+ });
172
+ if (!res.ok) {
173
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
174
+ }
175
+ return res.text();
176
+ }
177
+ function validateManifest(manifest, cli) {
178
+ const validated = [];
179
+ const names = new Set();
180
+ for (let index = 0; index < manifest.length; index += 1) {
181
+ const raw = manifest[index];
182
+ if (!raw || typeof raw !== "object") {
183
+ throw new Error(`invalid skill manifest entry at index ${index}`);
184
+ }
185
+ const entry = raw;
186
+ if (!isSafeSkillName(entry.name)) {
187
+ throw new Error(`invalid skill name at manifest index ${index}`);
188
+ }
189
+ if (entry.scope !== "global" &&
190
+ entry.scope !== "cli:claude" &&
191
+ entry.scope !== "cli:codex") {
192
+ throw new Error(`invalid skill scope at manifest index ${index}`);
193
+ }
194
+ if (typeof entry.updated_at !== "string" || !entry.updated_at.trim()) {
195
+ throw new Error(`invalid skill timestamp at manifest index ${index}`);
196
+ }
197
+ if (shouldSyncForCli(entry.scope, cli)) {
198
+ if (names.has(entry.name)) {
199
+ throw new Error(`duplicate skill name at manifest index ${index}`);
200
+ }
201
+ names.add(entry.name);
202
+ }
203
+ validated.push({
204
+ name: entry.name,
205
+ scope: entry.scope,
206
+ updated_at: entry.updated_at,
207
+ });
208
+ }
209
+ return validated;
210
+ }
211
+ function ownershipMarker(cli, name) {
212
+ return {
213
+ version: OWNERSHIP_VERSION,
214
+ manager: "cortex",
215
+ cli,
216
+ name,
217
+ };
218
+ }
219
+ function assertCortexOwnedSkillDir(skillDir, cli, name) {
220
+ const markerPath = join(skillDir, OWNERSHIP_FILENAME);
221
+ if (!existsSync(markerPath) || lstatSync(markerPath).isSymbolicLink()) {
222
+ throw new Error("skill directory is not owned by Cortex");
223
+ }
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(readFileSync(markerPath, "utf8"));
227
+ }
228
+ catch {
229
+ throw new Error("skill directory has an invalid Cortex ownership marker");
230
+ }
231
+ if (parsed.version !== OWNERSHIP_VERSION ||
232
+ parsed.manager !== "cortex" ||
233
+ parsed.cli !== cli ||
234
+ parsed.name !== name) {
235
+ throw new Error("skill directory has an invalid Cortex ownership marker");
236
+ }
237
+ }
238
+ function preflightSkillWrite(cli, name) {
239
+ const root = resolveSkillRoot(cli, true);
240
+ if (!root)
241
+ throw new Error("skill root could not be created");
242
+ const target = skillFilePath(root, name);
243
+ const skillDir = resolve(root, name);
244
+ assertPathContained(root, skillDir);
245
+ if (!existsSync(skillDir))
246
+ return { root, skillDir, target };
247
+ const stat = lstatSync(skillDir);
248
+ if (stat.isSymbolicLink()) {
249
+ throw new Error("skill directory must not be a symbolic link");
250
+ }
251
+ if (!stat.isDirectory()) {
252
+ throw new Error("managed skill target is not a directory");
253
+ }
254
+ assertCortexOwnedSkillDir(skillDir, cli, name);
255
+ const existingSkillFile = join(skillDir, "SKILL.md");
256
+ if (existsSync(existingSkillFile) &&
257
+ lstatSync(existingSkillFile).isSymbolicLink()) {
258
+ throw new Error("skill file must not be a symbolic link");
259
+ }
260
+ return { root, skillDir, target };
261
+ }
262
+ function writeSkillFile(cli, name, content) {
263
+ const { root, skillDir, target: lexicalTarget, } = preflightSkillWrite(cli, name);
264
+ let created = false;
265
+ if (existsSync(skillDir)) {
266
+ const stat = lstatSync(skillDir);
267
+ if (stat.isSymbolicLink()) {
268
+ throw new Error("skill directory must not be a symbolic link");
269
+ }
270
+ if (!stat.isDirectory()) {
271
+ throw new Error("managed skill target is not a directory");
272
+ }
273
+ assertCortexOwnedSkillDir(skillDir, cli, name);
274
+ }
275
+ else {
276
+ mkdirSync(skillDir, { mode: 0o700 });
277
+ created = true;
278
+ writeFileSync(join(skillDir, OWNERSHIP_FILENAME), JSON.stringify(ownershipMarker(cli, name), null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
279
+ }
280
+ const realSkillDir = realpathSync(skillDir);
281
+ assertPathContained(root, realSkillDir);
282
+ const target = join(realSkillDir, "SKILL.md");
283
+ assertPathContained(root, target);
284
+ if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
285
+ throw new Error("skill file must not be a symbolic link");
286
+ }
287
+ const temporaryTarget = join(realSkillDir, `.SKILL.md.${process.pid}.${randomUUID()}.tmp`);
288
+ try {
289
+ writeFileSync(temporaryTarget, content, {
290
+ encoding: "utf8",
291
+ mode: 0o600,
292
+ flag: "wx",
293
+ });
294
+ renameSync(temporaryTarget, target);
295
+ }
296
+ catch (err) {
297
+ if (existsSync(temporaryTarget))
298
+ unlinkSync(temporaryTarget);
299
+ if (created) {
300
+ const entries = readdirSync(skillDir);
301
+ if (entries.length === 1 &&
302
+ entries[0] === OWNERSHIP_FILENAME) {
303
+ unlinkSync(join(skillDir, OWNERSHIP_FILENAME));
304
+ rmdirSync(skillDir);
305
+ }
306
+ }
307
+ throw err;
308
+ }
309
+ return lexicalTarget;
310
+ }
311
+ function removeSkillFile(cli, name) {
312
+ if (!isSafeSkillName(name)) {
313
+ throw new Error("invalid skill name");
314
+ }
315
+ const root = resolveSkillRoot(cli, false);
316
+ if (!root)
317
+ return;
318
+ const skillDir = resolve(root, name);
319
+ assertPathContained(root, skillDir);
320
+ if (!existsSync(skillDir))
321
+ return;
322
+ const stat = lstatSync(skillDir);
323
+ if (stat.isSymbolicLink()) {
324
+ throw new Error("skill directory must not be a symbolic link");
325
+ }
326
+ if (!stat.isDirectory()) {
327
+ throw new Error("managed skill target is not a directory");
328
+ }
329
+ assertPathContained(root, realpathSync(skillDir));
330
+ assertCortexOwnedSkillDir(skillDir, cli, name);
331
+ const entries = readdirSync(skillDir);
332
+ const unexpected = entries.filter((entry) => entry !== OWNERSHIP_FILENAME && entry !== "SKILL.md");
333
+ if (unexpected.length > 0) {
334
+ throw new Error("managed skill directory contains unowned files");
335
+ }
336
+ const skillPath = join(skillDir, "SKILL.md");
337
+ if (existsSync(skillPath)) {
338
+ if (lstatSync(skillPath).isSymbolicLink()) {
339
+ throw new Error("skill file must not be a symbolic link");
340
+ }
341
+ unlinkSync(skillPath);
342
+ }
343
+ unlinkSync(join(skillDir, OWNERSHIP_FILENAME));
344
+ rmdirSync(skillDir);
345
+ }
346
+ function writeNotification(data) {
347
+ writeFileSync(notificationFilePath(), JSON.stringify(data, null, 2) + "\n", "utf8");
348
+ }
349
+ export async function runSkillSyncForCli(cwd, cli) {
350
+ const config = loadEnterpriseConfig(join(cwd, ".context"));
351
+ const apiKey = config.enterprise.api_key.trim();
352
+ const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
353
+ if (!apiKey || !baseUrl) {
354
+ return { kind: "failed", cli, error: "enterprise not configured" };
355
+ }
356
+ if (!isAllowedLicenseEndpoint(baseUrl)) {
357
+ return { kind: "failed", cli, error: "insecure or invalid enterprise endpoint" };
358
+ }
359
+ const credentialId = enterpriseCredentialId(baseUrl, apiKey);
360
+ if (!matchesEnterpriseHostIdentity(credentialId)) {
361
+ return {
362
+ kind: "failed",
363
+ cli,
364
+ error: "enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
365
+ };
366
+ }
367
+ const state = readState();
368
+ const identityChanged = state.credential_id !== credentialId;
369
+ let stateSanitized = identityChanged;
370
+ state.credential_id = credentialId;
371
+ let manifest;
372
+ try {
373
+ manifest = validateManifest(await fetchManifest(baseUrl, apiKey, cli), cli);
374
+ }
375
+ catch (err) {
376
+ return {
377
+ kind: "failed",
378
+ cli,
379
+ error: err instanceof Error ? err.message : String(err),
380
+ };
381
+ }
382
+ const relevantManifest = manifest.filter((entry) => shouldSyncForCli(entry.scope, cli));
383
+ const remoteByName = new Map(relevantManifest.map((e) => [e.name, e]));
384
+ const added = [];
385
+ const changed = [];
386
+ const removed = [];
387
+ // Detect adds + changes
388
+ for (const entry of relevantManifest) {
389
+ const skillKey = stateSkillKey(cli, entry.name);
390
+ const local = state.skills[skillKey];
391
+ const isNew = !local;
392
+ const isChanged = Boolean(local) &&
393
+ (identityChanged ||
394
+ local.credential_id !== credentialId ||
395
+ local.updated_at !== entry.updated_at ||
396
+ local.scope !== entry.scope);
397
+ if (!isNew && !isChanged)
398
+ continue;
399
+ try {
400
+ preflightSkillWrite(cli, entry.name);
401
+ }
402
+ catch (err) {
403
+ return {
404
+ kind: "failed",
405
+ cli,
406
+ error: err instanceof Error
407
+ ? `preflight ${entry.name}: ${err.message}`
408
+ : `preflight ${entry.name}: ${String(err)}`,
409
+ };
410
+ }
411
+ let body;
412
+ try {
413
+ body = await fetchSkillBody(baseUrl, apiKey, entry.name);
414
+ }
415
+ catch (err) {
416
+ return {
417
+ kind: "failed",
418
+ cli,
419
+ error: err instanceof Error
420
+ ? `fetch ${entry.name}: ${err.message}`
421
+ : `fetch ${entry.name}: ${String(err)}`,
422
+ };
423
+ }
424
+ let path;
425
+ try {
426
+ path = writeSkillFile(cli, entry.name, body);
427
+ }
428
+ catch (err) {
429
+ return {
430
+ kind: "failed",
431
+ cli,
432
+ error: err instanceof Error
433
+ ? `write ${entry.name}: ${err.message}`
434
+ : `write ${entry.name}: ${String(err)}`,
435
+ };
436
+ }
437
+ state.skills[skillKey] = {
438
+ credential_id: credentialId,
439
+ cli,
440
+ scope: entry.scope,
441
+ updated_at: entry.updated_at,
442
+ path,
443
+ };
444
+ (isNew ? added : changed).push(entry.name);
445
+ }
446
+ // Detect removes — entries we have locally for this cli but the manifest
447
+ // dropped (or disabled). We only consider state entries whose scope
448
+ // matches this cli, so we don't accidentally remove the other CLI's
449
+ // skills when running a per-cli tick.
450
+ for (const [skillKey, record] of Object.entries(state.skills)) {
451
+ if (record.cli !== cli)
452
+ continue;
453
+ const [, name] = skillKey.split(":", 2);
454
+ if (!name)
455
+ continue;
456
+ if (!isSafeSkillName(name)) {
457
+ // State from an older vulnerable version is not deletion authority.
458
+ // Drop the record without touching its persisted absolute path.
459
+ delete state.skills[skillKey];
460
+ stateSanitized = true;
461
+ continue;
462
+ }
463
+ if (!shouldSyncForCli(record.scope, cli))
464
+ continue;
465
+ if (remoteByName.has(name))
466
+ continue;
467
+ try {
468
+ removeSkillFile(cli, name);
469
+ }
470
+ catch (err) {
471
+ return {
472
+ kind: "failed",
473
+ cli,
474
+ error: err instanceof Error
475
+ ? `remove ${name}: ${err.message}`
476
+ : `remove ${name}: ${String(err)}`,
477
+ };
478
+ }
479
+ delete state.skills[skillKey];
480
+ removed.push(name);
481
+ }
482
+ const totalChanged = added.length + changed.length + removed.length;
483
+ if (totalChanged === 0) {
484
+ if (stateSanitized) {
485
+ state.last_synced_at = new Date().toISOString();
486
+ writeState(state);
487
+ }
488
+ return { kind: "unchanged", cli, count: relevantManifest.length };
489
+ }
490
+ state.last_synced_at = new Date().toISOString();
491
+ writeState(state);
492
+ return { kind: "synced", cli, added, changed, removed };
493
+ }
494
+ export async function runSkillSyncOnce(cwd, clis = SUPPORTED_CLIS) {
495
+ const outcomes = [];
496
+ const now = new Date().toISOString();
497
+ for (const cli of clis) {
498
+ const outcome = await runSkillSyncForCli(cwd, cli);
499
+ outcomes.push(outcome);
500
+ const eventBase = {
501
+ timestamp: now,
502
+ host_id: hostname(),
503
+ cli,
504
+ };
505
+ if (outcome.kind === "unchanged") {
506
+ await writeHostAuditEvent(cwd, {
507
+ ...eventBase,
508
+ event_type: "skills_unchanged",
509
+ count: outcome.count,
510
+ }).catch(() => undefined);
511
+ }
512
+ else if (outcome.kind === "synced") {
513
+ await writeHostAuditEvent(cwd, {
514
+ ...eventBase,
515
+ event_type: "skills_synced",
516
+ added: outcome.added,
517
+ changed: outcome.changed,
518
+ removed: outcome.removed,
519
+ }).catch(() => undefined);
520
+ writeNotification({
521
+ added: outcome.added.length,
522
+ changed: outcome.changed.length,
523
+ removed: outcome.removed.length,
524
+ cli,
525
+ detected_at: now,
526
+ });
527
+ }
528
+ else {
529
+ await writeHostAuditEvent(cwd, {
530
+ ...eventBase,
531
+ event_type: "skills_sync_failed",
532
+ error: outcome.error,
533
+ }).catch(() => undefined);
534
+ }
535
+ }
536
+ // We deliberately leave the notification file in place when this tick
537
+ // had no changes — it represents "restart pending" from a prior sync,
538
+ // not current drift. `cortex enterprise status --acknowledge-skills`
539
+ // (future CLI) will be the explicit clear path.
540
+ return outcomes;
541
+ }
542
+ export function startSkillSyncTimer(cwd, intervalMs) {
543
+ const tick = () => {
544
+ void runSkillSyncOnce(cwd).catch((err) => {
545
+ process.stderr.write(`[cortex-daemon] skill sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
546
+ });
547
+ };
548
+ void Promise.resolve().then(tick);
549
+ const handle = setInterval(tick, intervalMs);
550
+ if (typeof handle.unref === "function")
551
+ handle.unref();
552
+ return {
553
+ stop() {
554
+ clearInterval(handle);
555
+ },
556
+ };
557
+ }