@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,102 @@
1
+ /**
2
+ * The default secure-build workflow that ships with Cortex. Organizations
3
+ * can override this from cortex-web later (Phase 2 of the harness rollout);
4
+ * until then this is the workflow every project gets out of the box.
5
+ */
6
+ export const SECURE_BUILD_WORKFLOW = {
7
+ id: "secure-build",
8
+ description: "Plan → Review → Build → Review → Mutation Tests → Security Review → Human Approval. " +
9
+ "The default Cortex Harness workflow for AI-driven development on production code.",
10
+ version: 1,
11
+ stages: [
12
+ {
13
+ name: "plan",
14
+ artifact: "plan.md",
15
+ reads: [],
16
+ required_fields: ["files_targeted", "constraints"],
17
+ validators: [],
18
+ capability: "planner",
19
+ description: "Produce a step-by-step implementation plan grounded in the repo's rules and memory.",
20
+ },
21
+ {
22
+ name: "plan-review",
23
+ artifact: "plan-review.md",
24
+ reads: ["plan"],
25
+ required_fields: ["approved", "blocking_comments"],
26
+ validators: [],
27
+ capability: "reviewer",
28
+ description: "Review the plan for architectural fit and rule compliance before any code is written.",
29
+ },
30
+ {
31
+ name: "build",
32
+ artifact: "changes.md",
33
+ reads: ["plan", "plan-review"],
34
+ required_fields: ["files_changed"],
35
+ validators: [
36
+ {
37
+ id: "build-passes",
38
+ description: "The project's build / typecheck command must succeed on the produced diff.",
39
+ },
40
+ {
41
+ id: "tests-pass",
42
+ description: "The project's existing test suite must pass on the produced diff.",
43
+ },
44
+ ],
45
+ capability: "builder",
46
+ description: "Implement the approved plan. Produces the diff manifest used by the downstream reviewers.",
47
+ },
48
+ {
49
+ name: "build-review",
50
+ artifact: "build-review.md",
51
+ reads: ["plan", "changes"],
52
+ required_fields: ["approved", "blocking_comments"],
53
+ validators: [],
54
+ capability: "reviewer",
55
+ description: "Review the implementation against the plan and the project's rules.",
56
+ },
57
+ {
58
+ name: "mutation",
59
+ artifact: "mutation-report.md",
60
+ reads: ["changes"],
61
+ required_fields: ["score", "survived"],
62
+ validators: [
63
+ {
64
+ id: "mutation-score",
65
+ description: "Mutation testing tool of the agent's choice (e.g. stryker, mutmut) must run and report a score against the changed files.",
66
+ },
67
+ ],
68
+ capability: "tester",
69
+ description: "Run mutation tests on the changed files. Report score + surviving mutants.",
70
+ },
71
+ {
72
+ name: "security",
73
+ artifact: "security-report.md",
74
+ reads: ["changes"],
75
+ required_fields: ["findings", "severity_summary"],
76
+ validators: [
77
+ {
78
+ id: "secret-scan",
79
+ description: "Scan the diff for newly committed secrets (API keys, tokens, credentials).",
80
+ },
81
+ {
82
+ id: "dependency-audit",
83
+ description: "Audit any added or upgraded dependencies for known vulnerabilities.",
84
+ },
85
+ ],
86
+ capability: "security-reviewer",
87
+ description: "Security review focused on the diff: injection, authn/authz, secrets, dependency risk.",
88
+ },
89
+ {
90
+ name: "approval",
91
+ artifact: "approval.md",
92
+ reads: ["plan", "changes", "build-review", "mutation", "security"],
93
+ required_fields: ["approved", "approver"],
94
+ validators: [],
95
+ capability: "human",
96
+ description: "Human sign-off. Pulls every prior artifact for the approver to read; the approver writes the artifact.",
97
+ },
98
+ ],
99
+ };
100
+ export const DEFAULT_WORKFLOWS = {
101
+ [SECURE_BUILD_WORKFLOW.id]: SECURE_BUILD_WORKFLOW,
102
+ };
@@ -0,0 +1,164 @@
1
+ import { isAbsolute, relative } from "node:path";
2
+ import { minimatch } from "minimatch";
3
+ import { readRunState } from "./artifact-io.js";
4
+ import { DEFAULT_CAPABILITIES } from "./capabilities.js";
5
+ import { workflowDefinitionSchema } from "./schemas.js";
6
+ import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
7
+ import { loadSyncedCapabilities } from "./synced-capability-registry.js";
8
+ import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
9
+ /**
10
+ * Tool names that are pure mutations of the file system. Edits and writes
11
+ * gate against `write_globs`. Bash is treated as a mutation by default
12
+ * because we cannot reliably extract paths from arbitrary shell — agents
13
+ * running in restricted-write capabilities lose Bash unless the
14
+ * capability explicitly allow-lists it.
15
+ */
16
+ const MUTATING_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
17
+ /**
18
+ * Tool names that read but do not mutate. Gate against `read_globs`.
19
+ */
20
+ const READING_TOOLS = new Set(["Read", "Grep", "Glob", "NotebookRead"]);
21
+ export function evaluateToolCall(options) {
22
+ const state = readRunState(options.cwd, options.taskId);
23
+ if (!state) {
24
+ return { allowed: true, reason: "no run state — harness not active" };
25
+ }
26
+ if (state.outcome !== "in_progress" || !state.current_stage) {
27
+ return {
28
+ allowed: true,
29
+ reason: `run not in progress (outcome=${state.outcome}) — no capability gate to apply`,
30
+ };
31
+ }
32
+ const workflows = options.workflows ?? DEFAULT_WORKFLOWS;
33
+ const workflow = workflows[state.workflow_id];
34
+ if (!workflow) {
35
+ return {
36
+ allowed: false,
37
+ reason: `unknown workflow_id ${state.workflow_id}; cannot resolve capability for current stage`,
38
+ };
39
+ }
40
+ // Validate so corrupt input doesn't slip through.
41
+ workflowDefinitionSchema.parse(workflow);
42
+ const stage = workflow.stages.find((s) => s.name === state.current_stage);
43
+ if (!stage) {
44
+ return {
45
+ allowed: false,
46
+ reason: `current stage ${state.current_stage} is not defined in workflow ${workflow.id}`,
47
+ };
48
+ }
49
+ if (!stage.capability) {
50
+ return { allowed: true, reason: "stage has no capability declared" };
51
+ }
52
+ // When the caller passes an explicit registry, use it as-is (tests).
53
+ // Otherwise merge bundled defaults with the daemon-synced org-authored
54
+ // capabilities, with synced ones taking precedence on name collisions
55
+ // so org overrides actually override.
56
+ const capabilities = options.capabilities ?? {
57
+ ...DEFAULT_CAPABILITIES,
58
+ ...loadSyncedCapabilities(undefined, configuredEnterpriseCredentialId(options.cwd) ?? undefined),
59
+ };
60
+ const capability = capabilities[stage.capability];
61
+ if (!capability) {
62
+ return {
63
+ allowed: false,
64
+ reason: `capability ${stage.capability} (referenced by stage ${stage.name}) is not in the registry`,
65
+ };
66
+ }
67
+ return evaluateAgainstCapability(capability, options.call, options.cwd);
68
+ }
69
+ function evaluateAgainstCapability(capability, call, cwd) {
70
+ // 1. tools_allowed: empty = no restriction; otherwise tool must be in the list.
71
+ if (capability.tools_allowed.length > 0 &&
72
+ !capability.tools_allowed.includes(call.toolName)) {
73
+ return {
74
+ allowed: false,
75
+ reason: `capability ${capability.name} does not allow tool ${call.toolName}`,
76
+ };
77
+ }
78
+ const isMutation = MUTATING_TOOLS.has(call.toolName);
79
+ const isRead = READING_TOOLS.has(call.toolName);
80
+ // Bash is special: with restricted write_globs we have to assume the
81
+ // worst (since the shell can write anywhere). Block unless capability
82
+ // explicitly allow-lists Bash via tools_allowed.
83
+ if (call.toolName === "Bash") {
84
+ const isAllowedViaToolList = capability.tools_allowed.includes("Bash");
85
+ const writesUnrestricted = capability.write_globs.length === 0;
86
+ if (writesUnrestricted && !isAllowedViaToolList) {
87
+ return {
88
+ allowed: false,
89
+ reason: `capability ${capability.name} is read-only; Bash can mutate the filesystem and is not allow-listed`,
90
+ };
91
+ }
92
+ return { allowed: true };
93
+ }
94
+ if (isMutation) {
95
+ if (capability.write_globs.length === 0) {
96
+ return {
97
+ allowed: false,
98
+ reason: `capability ${capability.name} is read-only; ${call.toolName} cannot run`,
99
+ };
100
+ }
101
+ const targetPath = extractFilePath(call.toolInput);
102
+ if (!targetPath) {
103
+ return {
104
+ allowed: false,
105
+ reason: `${call.toolName} did not include a file_path; cannot verify against capability ${capability.name}`,
106
+ };
107
+ }
108
+ const relPath = toRepoRelative(cwd, targetPath);
109
+ if (!matchesAnyGlob(relPath, capability.write_globs)) {
110
+ return {
111
+ allowed: false,
112
+ reason: `path ${relPath} is outside capability ${capability.name}'s write_globs (${capability.write_globs.join(", ")})`,
113
+ };
114
+ }
115
+ return { allowed: true };
116
+ }
117
+ if (isRead) {
118
+ if (capability.read_globs.length === 0) {
119
+ // No reads allowed at all — only the human capability lands here.
120
+ return {
121
+ allowed: false,
122
+ reason: `capability ${capability.name} does not permit any read operations`,
123
+ };
124
+ }
125
+ const targetPath = extractFilePath(call.toolInput);
126
+ if (!targetPath) {
127
+ // Some read tools (Grep, Glob) operate on the whole repo; allow
128
+ // through if the capability has any read access at all.
129
+ return { allowed: true };
130
+ }
131
+ const relPath = toRepoRelative(cwd, targetPath);
132
+ if (!matchesAnyGlob(relPath, capability.read_globs)) {
133
+ return {
134
+ allowed: false,
135
+ reason: `path ${relPath} is outside capability ${capability.name}'s read_globs (${capability.read_globs.join(", ")})`,
136
+ };
137
+ }
138
+ return { allowed: true };
139
+ }
140
+ // Unknown tool — fall through to allow if not explicitly restricted.
141
+ return { allowed: true };
142
+ }
143
+ function extractFilePath(toolInput) {
144
+ const candidates = ["file_path", "path", "notebook_path"];
145
+ for (const key of candidates) {
146
+ const value = toolInput[key];
147
+ if (typeof value === "string" && value.length > 0)
148
+ return value;
149
+ }
150
+ return null;
151
+ }
152
+ function toRepoRelative(cwd, targetPath) {
153
+ if (!isAbsolute(targetPath))
154
+ return targetPath;
155
+ const rel = relative(cwd, targetPath);
156
+ // If the path is outside the repo, return the absolute form so glob
157
+ // matches (which expect repo-relative) reliably miss.
158
+ if (rel.startsWith(".."))
159
+ return targetPath;
160
+ return rel;
161
+ }
162
+ function matchesAnyGlob(path, globs) {
163
+ return globs.some((pattern) => minimatch(path, pattern, { dot: true, nocase: false }));
164
+ }
@@ -0,0 +1,132 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { artifactPath, readRunState } from "./artifact-io.js";
3
+ import { workflowDefinitionSchema } from "./schemas.js";
4
+ export function composeStageEnvelope(options) {
5
+ const workflow = workflowDefinitionSchema.parse(options.workflow);
6
+ const state = readRunState(options.cwd, options.taskId);
7
+ if (!state) {
8
+ throw new Error(`No run state found for task ${options.taskId}. Call createRun() first.`);
9
+ }
10
+ if (state.workflow_id !== workflow.id) {
11
+ throw new Error(`Workflow mismatch: run was started with ${state.workflow_id}, envelope was composed with ${workflow.id}`);
12
+ }
13
+ const stageName = options.stageName ?? state.current_stage;
14
+ if (!stageName) {
15
+ throw new Error(`Run ${options.taskId} is not at any stage (outcome=${state.outcome}). Cannot compose envelope.`);
16
+ }
17
+ const stage = workflow.stages.find((s) => s.name === stageName);
18
+ if (!stage) {
19
+ throw new Error(`Stage ${stageName} is not defined in workflow ${workflow.id}`);
20
+ }
21
+ const handoffs = [];
22
+ for (const readName of stage.reads) {
23
+ const priorStage = workflow.stages.find((s) => s.name === readName);
24
+ if (!priorStage) {
25
+ throw new Error(`Stage ${stageName} declares reads from unknown stage ${readName}`);
26
+ }
27
+ const priorRecord = state.stages.find((r) => r.name === readName);
28
+ if (!priorRecord || priorRecord.status === "pending" || !priorRecord.artifact) {
29
+ throw new Error(`Stage ${stageName} requires artifact from ${readName}, but it has not been produced yet`);
30
+ }
31
+ const path = artifactPath(options.cwd, options.taskId, priorRecord.artifact);
32
+ let raw;
33
+ try {
34
+ raw = readFileSync(path, "utf8");
35
+ }
36
+ catch (err) {
37
+ throw new Error(`Failed to read handoff artifact for ${readName} at ${path}: ${err instanceof Error ? err.message : String(err)}`);
38
+ }
39
+ handoffs.push(renderHandoff(readName, priorRecord.artifact, raw));
40
+ }
41
+ const requiredFields = stage.required_fields;
42
+ const capability = stage.capability ?? null;
43
+ const validators = stage.validators;
44
+ const prompt = renderPrompt({
45
+ taskDescription: state.task_description,
46
+ workflowId: workflow.id,
47
+ workflowDescription: workflow.description,
48
+ stageName: stage.name,
49
+ stageDescription: stage.description,
50
+ expectedArtifact: stage.artifact,
51
+ requiredFields,
52
+ capability,
53
+ handoffs,
54
+ validators,
55
+ });
56
+ return {
57
+ prompt,
58
+ expectedArtifact: stage.artifact,
59
+ requiredFields,
60
+ capability,
61
+ validators,
62
+ };
63
+ }
64
+ function renderHandoff(stageName, artifactName, rawArtifact) {
65
+ return [
66
+ `--- handoff:${stageName} (${artifactName}) ---`,
67
+ rawArtifact.trim(),
68
+ `--- end handoff:${stageName} ---`,
69
+ ].join("\n");
70
+ }
71
+ function renderPrompt(o) {
72
+ const sections = [];
73
+ sections.push([
74
+ `# TASK`,
75
+ ``,
76
+ o.taskDescription.trim(),
77
+ ``,
78
+ `Workflow: ${o.workflowId} — ${o.workflowDescription}`,
79
+ ].join("\n"));
80
+ sections.push([
81
+ `# STAGE: ${o.stageName}`,
82
+ ``,
83
+ o.stageDescription.trim(),
84
+ ``,
85
+ o.capability
86
+ ? `Running under capability: \`${o.capability}\` (file and tool restrictions are enforced by Cortex hooks at tool-use time, not by you).`
87
+ : `No capability constraint declared for this stage.`,
88
+ ].join("\n"));
89
+ if (o.handoffs.length === 0) {
90
+ sections.push([`# HANDOFFS`, ``, `_No prior-stage artifacts; this is the first stage._`].join("\n"));
91
+ }
92
+ else {
93
+ sections.push([
94
+ `# HANDOFFS`,
95
+ ``,
96
+ `The following stages have already run. Each artifact below is the complete file as it lives on disk; use the frontmatter for structured outcomes and the body for reasoning.`,
97
+ ``,
98
+ ...o.handoffs,
99
+ ].join("\n"));
100
+ }
101
+ const requiredLines = o.requiredFields.length === 0
102
+ ? `_No additional required fields beyond the harness defaults._`
103
+ : o.requiredFields.map((f) => `- \`${f}\``).join("\n");
104
+ const validatorLines = o.validators.length === 0
105
+ ? `_No validators required for this stage._`
106
+ : o.validators
107
+ .map((v) => `- \`${v.id}\` — ${v.description}`)
108
+ .join("\n");
109
+ sections.push([
110
+ `# VALIDATORS`,
111
+ ``,
112
+ `Cortex defines what must be validated; you (the agent) pick the concrete tooling and run it in this environment, then report back.`,
113
+ ``,
114
+ validatorLines,
115
+ ``,
116
+ `When you call \`cortex.workflow.advance\`, set \`validators_passed: [<id1>, <id2>, ...]\` listing every validator you successfully ran. The harness blocks the advance if a required validator is missing — unless you explicitly pass \`override\` with a reason and the list of skipped validator ids.`,
117
+ ].join("\n"));
118
+ sections.push([
119
+ `# OUTPUT`,
120
+ ``,
121
+ `Produce a single markdown file named \`${o.expectedArtifact}\` with YAML frontmatter on top.`,
122
+ ``,
123
+ `Required frontmatter fields (in addition to \`stage\`, \`status\`, \`references\`, \`validators_passed\`, \`written_at\` which the harness manages):`,
124
+ ``,
125
+ requiredLines,
126
+ ``,
127
+ `Body: clear, well-structured markdown explaining your reasoning. Cite handoff artifacts by stage name when relevant.`,
128
+ ``,
129
+ `If you cannot complete this stage (missing context, blocking concern, conflicting prior decisions), set \`status: blocked\` in frontmatter and explain why in the body — do not fabricate work.`,
130
+ ].join("\n"));
131
+ return sections.join("\n\n");
132
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./schemas.js";
2
+ export * from "./artifact-io.js";
3
+ export * from "./run-lifecycle.js";
4
+ export * from "./envelope.js";
5
+ export * from "./default-workflows.js";
6
+ export * from "./mcp-tools.js";
7
+ export * from "./capabilities.js";
8
+ export * from "./enforcement.js";
9
+ export * from "./synced-registry.js";
10
+ export * from "./synced-capability-registry.js";
11
+ export * from "./resolution.js";
@@ -0,0 +1,175 @@
1
+ import { z } from "zod";
2
+ import { advanceStage, createRun, getRunState } from "./run-lifecycle.js";
3
+ import { composeStageEnvelope } from "./envelope.js";
4
+ import { resolveWorkflowDefinition } from "./resolution.js";
5
+ import { stageOverrideSchema, stageStatusSchema, } from "./schemas.js";
6
+ /**
7
+ * Pure runner functions that back the cortex.workflow.* MCP tools.
8
+ * Kept separate from server.ts so they can be unit-tested without spinning
9
+ * up an MCP server. server.ts is a thin shim that registers each runner
10
+ * under its tool name and serializes the result through buildToolResult.
11
+ */
12
+ const slugSchema = z
13
+ .string()
14
+ .min(1)
15
+ .max(80)
16
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/);
17
+ export const WorkflowStartInput = z.object({
18
+ task_id: slugSchema,
19
+ task_description: z.string().min(1).max(2000),
20
+ workflow_id: slugSchema.default("secure-build"),
21
+ });
22
+ export const WorkflowAdvanceInput = z.object({
23
+ task_id: slugSchema,
24
+ /** Required for safety: must equal the run's current_stage. */
25
+ stage: slugSchema,
26
+ /**
27
+ * Stage frontmatter as a free-form object. Stage / status / references /
28
+ * written_at are managed by the harness and may be omitted (or, if set,
29
+ * are overridden). Stage-specific fields like `approved` or `score` are
30
+ * passed through.
31
+ */
32
+ frontmatter: z.record(z.string(), z.unknown()).default({}),
33
+ body: z.string().min(1),
34
+ /** Final stage status. Defaults to "complete". Use "blocked" or "failed" to halt the run. */
35
+ status: stageStatusSchema.optional(),
36
+ /** Optional structured outcome surfaced into state.json for fast lookup by later stages. */
37
+ outcome: z.record(z.string(), z.unknown()).optional(),
38
+ /**
39
+ * Validators the agent reports having run for this stage. Compared
40
+ * against the stage definition's required validators on advance —
41
+ * missing entries block the call unless an override is supplied.
42
+ */
43
+ validators_passed: z.array(z.string().min(1)).default([]),
44
+ /**
45
+ * Process override. Required when validators_passed does not cover
46
+ * every validator the stage declares. Logged as a high-evidence
47
+ * audit event and stamped into the artifact's frontmatter so the
48
+ * deviation is visible in the evidence trail.
49
+ */
50
+ override: stageOverrideSchema.optional(),
51
+ });
52
+ export const WorkflowStatusInput = z.object({
53
+ task_id: slugSchema,
54
+ });
55
+ export const WorkflowEnvelopeInput = z.object({
56
+ task_id: slugSchema,
57
+ /** Defaults to the run's current stage. */
58
+ stage: slugSchema.optional(),
59
+ });
60
+ /**
61
+ * Resolves the project root. The MCP server is started with cwd =
62
+ * project root and CORTEX_PROJECT_ROOT set to the same value (see
63
+ * bin/cortex.mjs `mcp` command). Tests pass cwd explicitly.
64
+ */
65
+ export function resolveProjectRoot() {
66
+ const fromEnv = process.env.CORTEX_PROJECT_ROOT?.trim();
67
+ if (fromEnv)
68
+ return fromEnv;
69
+ return process.cwd();
70
+ }
71
+ export function runWorkflowStart(input, ctx) {
72
+ const resolved = resolveWorkflowDefinition(input.workflow_id, {
73
+ registry: ctx.workflows,
74
+ cwd: ctx.cwd,
75
+ bundledFallbackPolicy: ctx.bundledFallbackPolicy,
76
+ });
77
+ const workflow = resolved.workflow;
78
+ const state = createRun({
79
+ cwd: ctx.cwd,
80
+ taskId: input.task_id,
81
+ workflow,
82
+ taskDescription: input.task_description,
83
+ });
84
+ const envelope = composeStageEnvelope({
85
+ cwd: ctx.cwd,
86
+ taskId: input.task_id,
87
+ workflow,
88
+ });
89
+ return {
90
+ state,
91
+ envelope,
92
+ workflow_source: resolved.source,
93
+ warnings: resolved.warnings,
94
+ };
95
+ }
96
+ export function runWorkflowAdvance(input, ctx) {
97
+ const state = getRunState(ctx.cwd, input.task_id);
98
+ if (!state) {
99
+ throw new Error(`No run state found for task ${input.task_id}. Call cortex.workflow.start first.`);
100
+ }
101
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
102
+ registry: ctx.workflows,
103
+ cwd: ctx.cwd,
104
+ }).workflow;
105
+ const stage = workflow.stages.find((s) => s.name === input.stage);
106
+ if (!stage) {
107
+ throw new Error(`Stage ${input.stage} is not defined in workflow ${workflow.id}`);
108
+ }
109
+ const finalStatus = input.status ?? "complete";
110
+ const nextState = advanceStage({
111
+ cwd: ctx.cwd,
112
+ taskId: input.task_id,
113
+ workflow,
114
+ stageName: input.stage,
115
+ artifactName: stage.artifact,
116
+ frontmatter: {
117
+ ...input.frontmatter,
118
+ stage: input.stage,
119
+ status: finalStatus,
120
+ references: (Array.isArray(input.frontmatter.references)
121
+ ? input.frontmatter.references
122
+ .filter((v) => typeof v === "string")
123
+ : null) ?? deriveReferencesFromReads(stage.reads, workflow),
124
+ },
125
+ body: input.body,
126
+ outcome: input.outcome,
127
+ status: finalStatus,
128
+ validatorsPassed: input.validators_passed,
129
+ override: input.override,
130
+ });
131
+ // If the run is still going, also return the next envelope so the caller
132
+ // can immediately know what comes next without a follow-up status round-trip.
133
+ let nextEnvelope = null;
134
+ if (nextState.outcome === "in_progress" && nextState.current_stage) {
135
+ nextEnvelope = composeStageEnvelope({
136
+ cwd: ctx.cwd,
137
+ taskId: input.task_id,
138
+ workflow,
139
+ });
140
+ }
141
+ return {
142
+ state: nextState,
143
+ next_envelope: nextEnvelope,
144
+ };
145
+ }
146
+ function deriveReferencesFromReads(reads, workflow) {
147
+ const refs = [];
148
+ for (const readName of reads) {
149
+ const stage = workflow.stages.find((s) => s.name === readName);
150
+ if (stage)
151
+ refs.push(stage.artifact);
152
+ }
153
+ return refs;
154
+ }
155
+ export function runWorkflowStatus(input, ctx) {
156
+ const state = getRunState(ctx.cwd, input.task_id);
157
+ return { state };
158
+ }
159
+ export function runWorkflowEnvelope(input, ctx) {
160
+ const state = getRunState(ctx.cwd, input.task_id);
161
+ if (!state) {
162
+ throw new Error(`No run state found for task ${input.task_id}. Call cortex.workflow.start first.`);
163
+ }
164
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
165
+ registry: ctx.workflows,
166
+ cwd: ctx.cwd,
167
+ }).workflow;
168
+ const envelope = composeStageEnvelope({
169
+ cwd: ctx.cwd,
170
+ taskId: input.task_id,
171
+ workflow,
172
+ stageName: input.stage,
173
+ });
174
+ return { envelope };
175
+ }
@@ -0,0 +1,69 @@
1
+ import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
2
+ import { loadSyncedWorkflows } from "./synced-registry.js";
3
+ import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
4
+ function sortIds(ids) {
5
+ return [...ids].sort((left, right) => left.localeCompare(right));
6
+ }
7
+ function formatIds(ids) {
8
+ return ids.length > 0 ? ids.join(", ") : "<none>";
9
+ }
10
+ export function resolveWorkflowDefinition(workflowId, options = {}) {
11
+ if (options.registry) {
12
+ const availableIds = sortIds(Object.keys(options.registry));
13
+ const workflow = options.registry[workflowId];
14
+ if (!workflow) {
15
+ throw new Error(`Unknown workflow_id: ${workflowId}. Available injected: ${formatIds(availableIds)}`);
16
+ }
17
+ return {
18
+ workflow,
19
+ source: "injected",
20
+ warnings: [],
21
+ bundled_ids: [],
22
+ synced_ids: [],
23
+ available_ids: availableIds,
24
+ };
25
+ }
26
+ const bundled = DEFAULT_WORKFLOWS;
27
+ const expectedCredentialId = options.syncedDir
28
+ ? undefined
29
+ : configuredEnterpriseCredentialId(options.cwd ?? process.cwd()) ?? undefined;
30
+ const synced = loadSyncedWorkflows(options.syncedDir, expectedCredentialId);
31
+ const bundledIds = sortIds(Object.keys(bundled));
32
+ const syncedIds = sortIds(Object.keys(synced));
33
+ const merged = { ...bundled, ...synced };
34
+ const availableIds = sortIds(Object.keys(merged));
35
+ const workflow = merged[workflowId];
36
+ if (!workflow) {
37
+ throw new Error(`Unknown workflow_id: ${workflowId}. Available bundled: ${formatIds(bundledIds)}. ` +
38
+ `Available synced: ${formatIds(syncedIds)}. Available all: ${formatIds(availableIds)}`);
39
+ }
40
+ const source = Object.prototype.hasOwnProperty.call(synced, workflowId)
41
+ ? "synced"
42
+ : "bundled";
43
+ const warnings = [];
44
+ const fallbackPolicy = options.bundledFallbackPolicy ?? "allow";
45
+ if (source === "bundled" && fallbackPolicy === "block") {
46
+ throw new Error(`Workflow "${workflowId}" is only available from the bundled registry, ` +
47
+ "but enforced govern mode requires a synced org workflow. " +
48
+ `Available synced: ${formatIds(syncedIds)}.`);
49
+ }
50
+ if (source === "bundled" &&
51
+ (options.emitBundledFallbackWarning || fallbackPolicy === "warn")) {
52
+ warnings.push({
53
+ code: "bundled-workflow-fallback",
54
+ workflow_id: workflowId,
55
+ bundled_available: true,
56
+ synced_available: syncedIds.length > 0,
57
+ message: `Workflow "${workflowId}" was resolved from the bundled registry because ` +
58
+ "no synced org workflow with that id exists in the local cache.",
59
+ });
60
+ }
61
+ return {
62
+ workflow,
63
+ source,
64
+ warnings,
65
+ bundled_ids: bundledIds,
66
+ synced_ids: syncedIds,
67
+ available_ids: availableIds,
68
+ };
69
+ }