@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,66 @@
1
+ /**
2
+ * Policy enforcement layer.
3
+ *
4
+ * Bridges the injection scanner with the policy store — only runs the
5
+ * scanner when the `prompt-injection-defense` policy is active.
6
+ */
7
+ import { scanForInjection, sanitizeContent } from "./injection.js";
8
+ const RULE_ID = "prompt-injection-defense";
9
+ /**
10
+ * Check whether the prompt-injection-defense policy is active.
11
+ */
12
+ export function isInjectionDefenseActive(policies) {
13
+ return policies.some((p) => p.id === RULE_ID && p.enforce);
14
+ }
15
+ /**
16
+ * Enforce the prompt-injection-defense policy against a piece of text.
17
+ *
18
+ * If the policy is not active the text is always allowed.
19
+ *
20
+ * @param text The text to check
21
+ * @param policies The current merged policy list
22
+ * @param options Optional: pass `sanitize: true` to get a sanitised version of the text
23
+ * @returns EnforcementResult
24
+ */
25
+ export function enforceInjectionPolicy(text, policies, options) {
26
+ if (!isInjectionDefenseActive(policies)) {
27
+ return {
28
+ allowed: true,
29
+ ruleId: RULE_ID,
30
+ scan: { score: 0, flagged: false, matches: [] },
31
+ };
32
+ }
33
+ const scan = scanForInjection(text);
34
+ const result = {
35
+ allowed: !scan.flagged,
36
+ ruleId: RULE_ID,
37
+ scan,
38
+ };
39
+ if (options?.sanitize && scan.flagged) {
40
+ result.sanitized = sanitizeContent(text);
41
+ }
42
+ return result;
43
+ }
44
+ /**
45
+ * Build a violation payload compatible with the cortex-web
46
+ * `POST /api/v1/violations/push` endpoint.
47
+ */
48
+ export function buildViolationPayload(matches, context) {
49
+ const topMatch = matches[0];
50
+ const score = matches.reduce((s, m) => s + m.weight, 0).toFixed(2);
51
+ const message = `Prompt injection detected: ${topMatch.category} (score ${score})`;
52
+ return {
53
+ rule_id: RULE_ID,
54
+ severity: "warning",
55
+ message: message.slice(0, 2000),
56
+ file_path: context.filePath?.slice(0, 500),
57
+ metadata: JSON.stringify({
58
+ query_present: Boolean(context.query),
59
+ query_length: context.query?.length ?? 0,
60
+ match_count: matches.length,
61
+ categories: [...new Set(matches.map((m) => m.category))],
62
+ patterns: matches.map((m) => m.pattern),
63
+ }).slice(0, 5000),
64
+ occurred_at: new Date().toISOString(),
65
+ };
66
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Prompt injection scanner.
3
+ *
4
+ * Detects common prompt injection patterns in text and returns a risk
5
+ * score with categorised matches. Designed to run on every piece of
6
+ * content that flows between the file system and an LLM.
7
+ */
8
+ const PATTERNS = [
9
+ // ── Instruction overrides ──
10
+ {
11
+ regex: /ignore\s+(all\s+)?previous\s+instructions/gi,
12
+ category: "instruction_override",
13
+ pattern: "ignore_previous_instructions",
14
+ weight: 0.5,
15
+ },
16
+ {
17
+ regex: /disregard\s+(all\s+)?(above|previous|prior|earlier)/gi,
18
+ category: "instruction_override",
19
+ pattern: "disregard_previous",
20
+ weight: 0.5,
21
+ },
22
+ {
23
+ regex: /forget\s+(all\s+)?(above|previous|prior|earlier)\s+(instructions|context|rules)/gi,
24
+ category: "instruction_override",
25
+ pattern: "forget_previous",
26
+ weight: 0.5,
27
+ },
28
+ {
29
+ regex: /new\s+instructions?\s*:/gi,
30
+ category: "instruction_override",
31
+ pattern: "new_instructions",
32
+ weight: 0.4,
33
+ },
34
+ {
35
+ regex: /you\s+are\s+now\s+/gi,
36
+ category: "instruction_override",
37
+ pattern: "you_are_now",
38
+ weight: 0.4,
39
+ },
40
+ {
41
+ regex: /from\s+now\s+on\s*,?\s+(you|ignore|do\s+not)/gi,
42
+ category: "instruction_override",
43
+ pattern: "from_now_on",
44
+ weight: 0.4,
45
+ },
46
+ {
47
+ regex: /override\s+(all\s+)?(system|safety|previous)\s+(prompt|instructions|rules)/gi,
48
+ category: "instruction_override",
49
+ pattern: "override_system",
50
+ weight: 0.5,
51
+ },
52
+ // ── Role-play / persona attacks ──
53
+ {
54
+ regex: /act\s+as\s+(an?\s+)?(unrestricted|unfiltered|evil|malicious|jailbroken)/gi,
55
+ category: "role_play",
56
+ pattern: "act_as_unrestricted",
57
+ weight: 0.5,
58
+ },
59
+ {
60
+ regex: /pretend\s+you\s+are\s+(an?\s+)?(different|new|unrestricted)/gi,
61
+ category: "role_play",
62
+ pattern: "pretend_you_are",
63
+ weight: 0.4,
64
+ },
65
+ {
66
+ regex: /enter\s+(DAN|developer|god|sudo|admin)\s+mode/gi,
67
+ category: "role_play",
68
+ pattern: "special_mode",
69
+ weight: 0.5,
70
+ },
71
+ {
72
+ regex: /jailbreak/gi,
73
+ category: "role_play",
74
+ pattern: "jailbreak_keyword",
75
+ weight: 0.4,
76
+ },
77
+ // ── Delimiter escapes ──
78
+ {
79
+ regex: /<\/?(system|assistant|user|tool_result|human|function_call|antml:)[\s>]/gi,
80
+ category: "delimiter_escape",
81
+ pattern: "xml_tag_escape",
82
+ weight: 0.5,
83
+ },
84
+ {
85
+ regex: /\[INST\]|\[\/INST\]|\[SYS\]|\[\/SYS\]/gi,
86
+ category: "delimiter_escape",
87
+ pattern: "inst_tag_escape",
88
+ weight: 0.5,
89
+ },
90
+ {
91
+ regex: /```\s*(system|prompt|instructions)/gi,
92
+ category: "delimiter_escape",
93
+ pattern: "code_block_escape",
94
+ weight: 0.3,
95
+ },
96
+ // ── Instruction markers ──
97
+ {
98
+ regex: /^\s*(IMPORTANT|SYSTEM|ADMIN|OVERRIDE|INSTRUCTION)\s*:/gim,
99
+ category: "instruction_marker",
100
+ pattern: "authority_marker",
101
+ weight: 0.3,
102
+ },
103
+ {
104
+ regex: /\bdo\s+not\s+follow\s+(any\s+)?(previous|prior|above)\s+(rules|instructions|guidelines)/gi,
105
+ category: "instruction_marker",
106
+ pattern: "do_not_follow",
107
+ weight: 0.5,
108
+ },
109
+ // ── Encoded payloads ──
110
+ {
111
+ regex: /(?:eval|atob|Buffer\.from)\s*\(\s*["'`][A-Za-z0-9+/=]{20,}["'`]\s*\)/g,
112
+ category: "encoded_payload",
113
+ pattern: "encoded_eval",
114
+ weight: 0.4,
115
+ },
116
+ {
117
+ regex: /&#x[0-9a-f]{2,4};/gi,
118
+ category: "encoded_payload",
119
+ pattern: "html_entity_encode",
120
+ weight: 0.2,
121
+ },
122
+ ];
123
+ // ── Public API ─────────────────────────────────────────────────────
124
+ const DEFAULT_THRESHOLD = 0.3;
125
+ /**
126
+ * Scan a text string for prompt injection patterns.
127
+ *
128
+ * @param text The text to scan
129
+ * @param threshold Risk score threshold (0–1) above which `flagged` is true
130
+ * @returns ScanResult with score, flagged boolean, and matches
131
+ */
132
+ export function scanForInjection(text, threshold = DEFAULT_THRESHOLD) {
133
+ if (!text) {
134
+ return { score: 0, flagged: false, matches: [] };
135
+ }
136
+ const matches = [];
137
+ for (const def of PATTERNS) {
138
+ // Reset lastIndex for global regexes
139
+ def.regex.lastIndex = 0;
140
+ let m;
141
+ while ((m = def.regex.exec(text)) !== null) {
142
+ matches.push({
143
+ pattern: def.pattern,
144
+ category: def.category,
145
+ matched: m[0],
146
+ position: m.index,
147
+ weight: def.weight,
148
+ });
149
+ }
150
+ }
151
+ const score = Math.min(1, matches.reduce((sum, m) => sum + m.weight, 0));
152
+ return {
153
+ score,
154
+ flagged: score >= threshold,
155
+ matches,
156
+ };
157
+ }
158
+ /**
159
+ * Sanitize text by wrapping it in safe delimiters and neutralising
160
+ * known injection patterns. Use this on content returned to the LLM
161
+ * when you want to keep the content but reduce injection risk.
162
+ */
163
+ export function sanitizeContent(text) {
164
+ if (!text)
165
+ return text;
166
+ let out = text;
167
+ // Neutralise XML-like tags that mimic system boundaries
168
+ out = out.replace(/<\/?(system|assistant|user|tool_result|human|function_call|antml:)/gi, (m) => m.replace(/</g, "&lt;").replace(/>/g, "&gt;"));
169
+ // Neutralise [INST] / [SYS] tags
170
+ out = out.replace(/\[(\/?)(?:INST|SYS)\]/gi, "[$1_$&_]");
171
+ return out;
172
+ }
@@ -0,0 +1,179 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Parse the simple YAML rules format used by Cortex:
5
+ *
6
+ * rules:
7
+ * - id: rule.foo
8
+ * description: "..."
9
+ * priority: 100
10
+ * enforce: true
11
+ */
12
+ function parseRulesYaml(text, source) {
13
+ const policies = [];
14
+ let current = null;
15
+ for (const line of text.split("\n")) {
16
+ const trimmed = line.trim();
17
+ // New rule entry
18
+ if (trimmed.startsWith("- id:")) {
19
+ if (current?.id) {
20
+ policies.push(finalize(current, source));
21
+ }
22
+ current = { id: trimmed.slice(5).trim() };
23
+ continue;
24
+ }
25
+ if (!current)
26
+ continue;
27
+ if (trimmed.startsWith("title:")) {
28
+ current.title = trimmed.slice(6).trim().replace(/^["']|["']$/g, "");
29
+ }
30
+ else if (trimmed.startsWith("kind:")) {
31
+ const kind = trimmed.slice(5).trim();
32
+ if (kind === "predefined" || kind === "custom")
33
+ current.kind = kind;
34
+ }
35
+ else if (trimmed.startsWith("status:")) {
36
+ const status = trimmed.slice(7).trim();
37
+ if (status === "draft" || status === "active" || status === "disabled" || status === "archived") {
38
+ current.status = status;
39
+ }
40
+ }
41
+ else if (trimmed.startsWith("severity:")) {
42
+ const severity = trimmed.slice(9).trim();
43
+ if (severity === "info" || severity === "warning" || severity === "error" || severity === "block") {
44
+ current.severity = severity;
45
+ }
46
+ }
47
+ else if (trimmed.startsWith("description:")) {
48
+ current.description = trimmed.slice(12).trim().replace(/^["']|["']$/g, "");
49
+ }
50
+ else if (trimmed.startsWith("priority:")) {
51
+ current.priority = parseInt(trimmed.slice(9).trim(), 10) || 50;
52
+ }
53
+ else if (trimmed.startsWith("scope:")) {
54
+ current.scope = trimmed.slice(6).trim();
55
+ }
56
+ else if (trimmed.startsWith("enforce:")) {
57
+ current.enforce = trimmed.slice(8).trim() === "true";
58
+ }
59
+ else if (trimmed.startsWith("type:")) {
60
+ const raw = trimmed.slice(5).trim();
61
+ current.type = raw === "null" || raw === "" ? null : raw;
62
+ }
63
+ else if (trimmed.startsWith("config:")) {
64
+ // Config is JSON-encoded on a single line so the line-based parser
65
+ // doesn't need to understand nested YAML. `config: null` or an
66
+ // unparseable value leaves it null.
67
+ const raw = trimmed.slice(7).trim();
68
+ if (raw && raw !== "null") {
69
+ try {
70
+ const parsed = JSON.parse(raw);
71
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
72
+ current.config = parsed;
73
+ }
74
+ }
75
+ catch {
76
+ // ignore malformed config; evaluator will see null and fail
77
+ // gracefully with its own validation message.
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (current?.id) {
83
+ policies.push(finalize(current, source));
84
+ }
85
+ return policies;
86
+ }
87
+ function finalize(partial, source) {
88
+ return {
89
+ id: partial.id ?? "",
90
+ title: partial.title ?? partial.id ?? "",
91
+ kind: partial.kind ?? null,
92
+ status: partial.status ?? "active",
93
+ severity: partial.severity ?? "block",
94
+ description: partial.description ?? "",
95
+ priority: partial.priority ?? 50,
96
+ scope: partial.scope ?? "global",
97
+ enforce: partial.enforce ?? true,
98
+ source,
99
+ type: partial.type ?? null,
100
+ config: partial.config ?? null,
101
+ };
102
+ }
103
+ function policiesToYaml(policies) {
104
+ const lines = ["rules:"];
105
+ for (const p of policies) {
106
+ lines.push(` - id: ${p.id}`);
107
+ if (p.title)
108
+ lines.push(` title: "${p.title.replace(/"/g, '\\"')}"`);
109
+ if (p.kind)
110
+ lines.push(` kind: ${p.kind}`);
111
+ if (p.status)
112
+ lines.push(` status: ${p.status}`);
113
+ if (p.severity)
114
+ lines.push(` severity: ${p.severity}`);
115
+ lines.push(` description: "${p.description.replace(/"/g, '\\"')}"`);
116
+ lines.push(` priority: ${p.priority}`);
117
+ lines.push(` scope: ${p.scope}`);
118
+ lines.push(` enforce: ${p.enforce}`);
119
+ if (p.type) {
120
+ lines.push(` type: ${p.type}`);
121
+ }
122
+ if (p.config) {
123
+ lines.push(` config: ${JSON.stringify(p.config)}`);
124
+ }
125
+ lines.push("");
126
+ }
127
+ return lines.join("\n");
128
+ }
129
+ export class PolicyStore {
130
+ contextDir;
131
+ orgRulesPath;
132
+ localRulesPath;
133
+ constructor(contextDir) {
134
+ this.contextDir = contextDir;
135
+ this.orgRulesPath = join(contextDir, "policies", "org-rules.yaml");
136
+ this.localRulesPath = join(contextDir, "rules.yaml");
137
+ }
138
+ loadOrgPolicies() {
139
+ try {
140
+ const raw = readFileSync(this.orgRulesPath, "utf8");
141
+ return parseRulesYaml(raw, "org");
142
+ }
143
+ catch {
144
+ return [];
145
+ }
146
+ }
147
+ loadLocalPolicies() {
148
+ try {
149
+ const raw = readFileSync(this.localRulesPath, "utf8");
150
+ return parseRulesYaml(raw, "local");
151
+ }
152
+ catch {
153
+ return [];
154
+ }
155
+ }
156
+ /**
157
+ * Merge org + local policies. Org rules override local rules with same ID.
158
+ * Result is sorted by priority (highest first).
159
+ */
160
+ getMergedPolicies() {
161
+ const orgPolicies = this.loadOrgPolicies();
162
+ const localPolicies = this.loadLocalPolicies();
163
+ const merged = new Map();
164
+ // Local first
165
+ for (const p of localPolicies) {
166
+ merged.set(p.id, p);
167
+ }
168
+ // Org overrides
169
+ for (const p of orgPolicies) {
170
+ merged.set(p.id, p);
171
+ }
172
+ return [...merged.values()].sort((a, b) => b.priority - a.priority);
173
+ }
174
+ writeOrgPolicies(policies) {
175
+ const dir = join(this.contextDir, "policies");
176
+ mkdirSync(dir, { recursive: true });
177
+ writeFileSync(this.orgRulesPath, policiesToYaml(policies));
178
+ }
179
+ }
@@ -0,0 +1,30 @@
1
+ const PERMISSIONS = {
2
+ // Admin-only actions
3
+ "policy.write": ["admin"],
4
+ "policy.sync": ["admin"],
5
+ "telemetry.configure": ["admin"],
6
+ // Admin + developer
7
+ "audit.query": ["admin", "developer"],
8
+ "policy.list": ["admin", "developer"],
9
+ "telemetry.status": ["admin", "developer"],
10
+ "context.review": ["admin", "developer"],
11
+ "workflow.plan": ["admin", "developer"],
12
+ "workflow.review_plan": ["admin", "developer"],
13
+ "workflow.start": ["admin", "developer"],
14
+ "workflow.update": ["admin", "developer"],
15
+ "workflow.note": ["admin", "developer"],
16
+ "workflow.todo": ["admin", "developer"],
17
+ "workflow.approve": ["admin", "developer"],
18
+ // All roles
19
+ "enterprise.status": ["admin", "developer", "readonly"],
20
+ "workflow.status": ["admin", "developer", "readonly"],
21
+ };
22
+ export function checkAccess(role, action) {
23
+ const allowed = PERMISSIONS[action];
24
+ if (!allowed)
25
+ return false; // deny unknown actions by default
26
+ return allowed.includes(role);
27
+ }
28
+ export function getAccessDeniedMessage(role, action) {
29
+ return `Access denied: role '${role}' cannot perform '${action}'`;
30
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Enterprise bearer credentials may travel only over TLS. Loopback HTTP is
3
+ * retained for local development because the connection cannot leave the
4
+ * host. Embedded URL credentials are never accepted.
5
+ */
6
+ export function isAllowedEnterpriseEndpoint(endpoint) {
7
+ let parsed;
8
+ try {
9
+ parsed = new URL(endpoint);
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ if (parsed.username || parsed.password)
15
+ return false;
16
+ if (parsed.protocol === "https:")
17
+ return true;
18
+ if (parsed.protocol !== "http:")
19
+ return false;
20
+ const host = parsed.hostname.toLowerCase();
21
+ return (host === "localhost" ||
22
+ host === "127.0.0.1" ||
23
+ host === "::1" ||
24
+ host === "[::1]");
25
+ }