@danielblomma/cortex-mcp 2.4.0 → 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 (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  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 +321 -200
  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/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
@@ -0,0 +1,123 @@
1
+ import { readRunState, writeRunState, writeStageArtifact, } from "./artifact-io.js";
2
+ import { runStateSchema, stageArtifactFrontmatterSchema, stageOverrideSchema, workflowDefinitionSchema, } from "./schemas.js";
3
+ export function createRun(options) {
4
+ const workflow = workflowDefinitionSchema.parse(options.workflow);
5
+ const now = (options.now ?? (() => new Date()))();
6
+ const startedAt = now.toISOString();
7
+ const stages = workflow.stages.map((stage) => ({
8
+ name: stage.name,
9
+ status: "pending",
10
+ validators_passed: [],
11
+ }));
12
+ const state = {
13
+ schema_version: 1,
14
+ task_id: options.taskId,
15
+ workflow_id: workflow.id,
16
+ workflow_version: workflow.version,
17
+ task_description: options.taskDescription,
18
+ current_stage: workflow.stages[0].name,
19
+ outcome: "in_progress",
20
+ started_at: startedAt,
21
+ completed_at: null,
22
+ stages,
23
+ };
24
+ // Validate before write so a malformed input never reaches disk.
25
+ const validated = runStateSchema.parse(state);
26
+ writeRunState(options.cwd, validated);
27
+ return validated;
28
+ }
29
+ export function getRunState(cwd, taskId) {
30
+ return readRunState(cwd, taskId);
31
+ }
32
+ /**
33
+ * Marks `stageName` as finished, writes its artifact under .agents/<task-id>/,
34
+ * and advances `current_stage` to the next stage (or marks the run complete
35
+ * if this was the final stage). Idempotent only at the artifact layer —
36
+ * calling twice for the same stage will overwrite the artifact and the
37
+ * state.json record.
38
+ */
39
+ export function advanceStage(options) {
40
+ const workflow = workflowDefinitionSchema.parse(options.workflow);
41
+ const state = readRunState(options.cwd, options.taskId);
42
+ if (!state) {
43
+ throw new Error(`No run state found for task ${options.taskId}. Call createRun() first.`);
44
+ }
45
+ if (state.workflow_id !== workflow.id) {
46
+ throw new Error(`Workflow mismatch: run was started with ${state.workflow_id}, advance was called with ${workflow.id}`);
47
+ }
48
+ if (state.current_stage !== options.stageName) {
49
+ throw new Error(`Cannot advance stage ${options.stageName}: run is currently at ${state.current_stage ?? "<finished>"}`);
50
+ }
51
+ const stageIndex = workflow.stages.findIndex((s) => s.name === options.stageName);
52
+ const stageDef = workflow.stages[stageIndex];
53
+ // Validator coverage check: every validator the stage declares must
54
+ // appear in validators_passed unless the override explicitly skips it.
55
+ // Process is enforced here even though the validators themselves run
56
+ // in the agent's environment.
57
+ const validatorsPassed = options.validatorsPassed ?? [];
58
+ const override = options.override
59
+ ? stageOverrideSchema.parse(options.override)
60
+ : undefined;
61
+ const requiredValidators = stageDef.validators.map((v) => v.id);
62
+ const declaredSkipped = new Set(override?.skipped_validators ?? []);
63
+ const missingValidators = requiredValidators.filter((id) => !validatorsPassed.includes(id) && !declaredSkipped.has(id));
64
+ // blocked / failed stages are exempt from validator coverage — the
65
+ // stage is explicitly halting before completion, so the validators
66
+ // logically cannot have run.
67
+ const finalStatus = options.status ?? "complete";
68
+ const exemptStatus = finalStatus === "blocked" || finalStatus === "failed";
69
+ if (missingValidators.length > 0 && !exemptStatus) {
70
+ throw new Error(`Stage ${options.stageName} requires validators ${requiredValidators.join(", ")} ` +
71
+ `but artifact reported only ${validatorsPassed.join(", ") || "<none>"}. ` +
72
+ `Missing: ${missingValidators.join(", ")}. ` +
73
+ `Pass override.skipped_validators with a reason to advance anyway.`);
74
+ }
75
+ const now = (options.now ?? (() => new Date()))();
76
+ const completedAt = now.toISOString();
77
+ const frontmatter = stageArtifactFrontmatterSchema.parse({
78
+ ...options.frontmatter,
79
+ stage: options.stageName,
80
+ validators_passed: validatorsPassed,
81
+ ...(override
82
+ ? {
83
+ override: {
84
+ reason: override.reason,
85
+ skipped_validators: override.skipped_validators,
86
+ skipped_requirements: override.skipped_requirements,
87
+ },
88
+ }
89
+ : {}),
90
+ written_at: options.frontmatter.written_at ?? completedAt,
91
+ });
92
+ writeStageArtifact(options.cwd, options.taskId, options.artifactName, frontmatter, options.body);
93
+ const nextStage = workflow.stages[stageIndex + 1] ?? null;
94
+ const updatedStages = state.stages.map((record) => {
95
+ if (record.name !== options.stageName)
96
+ return record;
97
+ return {
98
+ ...record,
99
+ status: finalStatus,
100
+ artifact: options.artifactName,
101
+ started_at: record.started_at ?? state.started_at,
102
+ completed_at: completedAt,
103
+ outcome: options.outcome,
104
+ validators_passed: validatorsPassed,
105
+ override,
106
+ };
107
+ });
108
+ const runOutcome = finalStatus === "blocked" || finalStatus === "failed"
109
+ ? finalStatus
110
+ : nextStage
111
+ ? "in_progress"
112
+ : "complete";
113
+ const next = {
114
+ ...state,
115
+ current_stage: runOutcome === "in_progress" && nextStage ? nextStage.name : null,
116
+ outcome: runOutcome,
117
+ completed_at: runOutcome === "in_progress" ? null : completedAt,
118
+ stages: updatedStages,
119
+ };
120
+ const validated = runStateSchema.parse(next);
121
+ writeRunState(options.cwd, validated);
122
+ return validated;
123
+ }
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schemas for the Cortex Harness workflow engine.
4
+ *
5
+ * See docs/harness-vision.md for the design. In short:
6
+ *
7
+ * .agents/<task-id>/
8
+ * plan.md # frontmatter + body
9
+ * review.md
10
+ * changes.md
11
+ * mutation-report.md
12
+ * security-report.md
13
+ * state.json # current run state
14
+ *
15
+ * All artifacts are markdown with YAML frontmatter; state.json is the only
16
+ * JSON file. Both are tracked in git so a PR carries the evidence trail.
17
+ */
18
+ const slugSchema = z
19
+ .string()
20
+ .min(1)
21
+ .max(80)
22
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, "Must be lowercase alphanumeric with hyphens (no leading/trailing hyphen)");
23
+ /**
24
+ * Validator requirement declared by a workflow stage. Cortex enforces
25
+ * that the agent reports having run each declared validator (via the
26
+ * artifact's `validators_passed` frontmatter), but never executes the
27
+ * validator itself — the agent picks the concrete tooling.
28
+ *
29
+ * id is a stable identifier (e.g. "mutation-score") that the agent
30
+ * echoes back; description is human-readable context for the agent
31
+ * rendered into the stage envelope.
32
+ */
33
+ export const validatorRequirementSchema = z.object({
34
+ id: slugSchema,
35
+ description: z.string().min(1).max(500),
36
+ });
37
+ /**
38
+ * Static definition of a single stage in a workflow. Authored at the
39
+ * organization level (in cortex-web later) and synced down to projects.
40
+ */
41
+ export const stageDefinitionSchema = z.object({
42
+ name: slugSchema,
43
+ artifact: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*\.md$/),
44
+ /** Stage names this stage may read artifacts from. Empty = no inputs. */
45
+ reads: z.array(slugSchema).default([]),
46
+ /** Required frontmatter fields the produced artifact must populate. */
47
+ required_fields: z.array(z.string().min(1)).default([]),
48
+ /**
49
+ * Validators the stage requires the agent to have run. The agent
50
+ * picks the actual tooling (e.g. stryker for mutation testing) and
51
+ * reports the result by listing each validator's id under
52
+ * `validators_passed` in the artifact frontmatter. Cortex enforces
53
+ * the list-coverage contract on advance unless the call carries an
54
+ * explicit override.
55
+ */
56
+ validators: z.array(validatorRequirementSchema).default([]),
57
+ /** Capability key the stage runs under. References a separate capability registry. */
58
+ capability: z.string().min(1).optional(),
59
+ /** Human-readable summary surfaced in dashboards and audit. */
60
+ description: z.string().min(1).max(500),
61
+ });
62
+ /**
63
+ * A complete workflow: ordered stages plus a stable identifier.
64
+ */
65
+ export const workflowDefinitionSchema = z.object({
66
+ id: slugSchema,
67
+ description: z.string().min(1).max(500),
68
+ version: z.number().int().min(1),
69
+ stages: z.array(stageDefinitionSchema).min(1),
70
+ });
71
+ /**
72
+ * Status of a single stage inside a run.
73
+ */
74
+ export const stageStatusSchema = z.enum([
75
+ "pending",
76
+ "in_progress",
77
+ "complete",
78
+ "blocked",
79
+ "failed",
80
+ ]);
81
+ /**
82
+ * Process-override record. When a stage advances despite missing or
83
+ * failed validators, the caller must pass an override with a free-text
84
+ * reason. The override is recorded on the StageRecord, stamped into
85
+ * the artifact's frontmatter, and emitted as a high-evidence audit
86
+ * event so reviewers can see the deviation in the evidence trail.
87
+ */
88
+ export const stageOverrideSchema = z.object({
89
+ reason: z.string().min(1).max(2000),
90
+ skipped_validators: z.array(z.string().min(1)).default([]),
91
+ skipped_requirements: z.array(z.string().min(1)).default([]),
92
+ });
93
+ /**
94
+ * Per-stage record inside state.json. Holds outcome metadata that the next
95
+ * stage's envelope composer needs without re-parsing every artifact.
96
+ */
97
+ export const stageRecordSchema = z.object({
98
+ name: slugSchema,
99
+ status: stageStatusSchema,
100
+ artifact: z.string().min(1).optional(),
101
+ started_at: z.string().datetime().optional(),
102
+ completed_at: z.string().datetime().optional(),
103
+ /** Frontmatter outcome surfaced for fast lookup (e.g. approved=true on review). */
104
+ outcome: z.record(z.string(), z.unknown()).optional(),
105
+ /** Validators the agent reported having run for this stage. */
106
+ validators_passed: z.array(z.string().min(1)).default([]),
107
+ /** Override record if the stage advanced despite missing/failed requirements. */
108
+ override: stageOverrideSchema.optional(),
109
+ });
110
+ /**
111
+ * The full state of one workflow run, persisted as
112
+ * .agents/<task-id>/state.json. Written only on stage boundaries so it
113
+ * never churns mid-tick.
114
+ */
115
+ export const runStateSchema = z.object({
116
+ schema_version: z.literal(1),
117
+ task_id: slugSchema,
118
+ workflow_id: slugSchema,
119
+ workflow_version: z.number().int().min(1),
120
+ task_description: z.string().min(1).max(2000),
121
+ current_stage: slugSchema.nullable(),
122
+ outcome: z.enum(["in_progress", "complete", "failed", "blocked"]),
123
+ started_at: z.string().datetime(),
124
+ completed_at: z.string().datetime().nullable(),
125
+ stages: z.array(stageRecordSchema).min(1),
126
+ });
127
+ /**
128
+ * The required-by-convention frontmatter shape every stage artifact carries.
129
+ * Stages may add additional structured fields; these four are the ones the
130
+ * harness itself relies on.
131
+ */
132
+ export const stageArtifactFrontmatterSchema = z
133
+ .object({
134
+ stage: slugSchema,
135
+ status: stageStatusSchema,
136
+ /** Sister-artifacts this artifact references (relative filenames). */
137
+ references: z.array(z.string().min(1)).default([]),
138
+ /** ISO 8601; injected by the harness, not the agent. */
139
+ written_at: z.string().datetime(),
140
+ })
141
+ .passthrough();
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
5
+ import { capabilityDefinitionSchema, } from "./capabilities.js";
6
+ /**
7
+ * Read side of the org-capability sync cache. The daemon's
8
+ * capability-sync-checker writes ~/.cortex/capabilities.local.json;
9
+ * this module reads it. Kept in core/workflow/ rather than daemon/ so
10
+ * enforcement.ts can consult the cache without depending on daemon code.
11
+ *
12
+ * Each entry is validated against capabilityDefinitionSchema before being
13
+ * surfaced — if the cache file is corrupt or contains stale shapes from
14
+ * an older daemon, those entries are silently dropped rather than
15
+ * crashing the read.
16
+ */
17
+ export const SYNCED_CAPABILITIES_FILENAME = "capabilities.local.json";
18
+ export function syncedCapabilitiesCachePath(dir) {
19
+ return join(dir ?? join(homedir(), ".cortex"), SYNCED_CAPABILITIES_FILENAME);
20
+ }
21
+ /**
22
+ * Returns the synced org-authored capabilities keyed by capability name.
23
+ * Empty object when the cache is missing, unreadable, malformed, or
24
+ * contains no valid entries. The optional `dir` argument is for tests;
25
+ * production callers leave it unset.
26
+ */
27
+ export function loadSyncedCapabilities(dir, expectedCredentialId) {
28
+ const path = syncedCapabilitiesCachePath(dir);
29
+ if (!existsSync(path))
30
+ return {};
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ const requiredCredentialId = expectedCredentialId ??
39
+ (dir ? undefined : configuredEnterpriseCredentialId(process.cwd()));
40
+ if (!dir && !requiredCredentialId) {
41
+ return {};
42
+ }
43
+ if (requiredCredentialId &&
44
+ parsed.credential_id !== requiredCredentialId) {
45
+ return {};
46
+ }
47
+ const records = parsed.capabilities;
48
+ if (!records || typeof records !== "object")
49
+ return {};
50
+ const out = {};
51
+ for (const [name, record] of Object.entries(records)) {
52
+ if (!record || typeof record !== "object")
53
+ continue;
54
+ const result = capabilityDefinitionSchema.safeParse(record.definition);
55
+ if (!result.success)
56
+ continue;
57
+ out[name] = result.data;
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { configuredEnterpriseCredentialId } from "../enterprise-identity.js";
5
+ import { workflowDefinitionSchema } from "./schemas.js";
6
+ /**
7
+ * Read side of the org-workflow sync cache. The daemon's
8
+ * workflow-sync-checker writes ~/.cortex/workflows.local.json; this
9
+ * module reads it. Kept in core/workflow/ rather than daemon/ so
10
+ * mcp-tools.ts can consult the cache without depending on daemon code.
11
+ *
12
+ * Each entry is validated against workflowDefinitionSchema before being
13
+ * surfaced — if the cache file is corrupt or contains stale shapes from
14
+ * an older daemon, those entries are silently dropped rather than
15
+ * crashing the read.
16
+ */
17
+ export const SYNCED_WORKFLOWS_FILENAME = "workflows.local.json";
18
+ export function syncedWorkflowsCachePath(dir) {
19
+ return join(dir ?? join(homedir(), ".cortex"), SYNCED_WORKFLOWS_FILENAME);
20
+ }
21
+ /**
22
+ * Returns the synced org-authored workflows keyed by `workflow_id`.
23
+ * Empty object when the cache is missing, unreadable, malformed, or
24
+ * contains no valid entries. The optional `dir` argument is for tests;
25
+ * production callers leave it unset.
26
+ */
27
+ export function loadSyncedWorkflows(dir, expectedCredentialId) {
28
+ const path = syncedWorkflowsCachePath(dir);
29
+ if (!existsSync(path))
30
+ return {};
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ const requiredCredentialId = expectedCredentialId ??
39
+ (dir ? undefined : configuredEnterpriseCredentialId(process.cwd()));
40
+ if (!dir && !requiredCredentialId) {
41
+ return {};
42
+ }
43
+ if (requiredCredentialId &&
44
+ parsed.credential_id !== requiredCredentialId) {
45
+ return {};
46
+ }
47
+ const records = parsed.workflows;
48
+ if (!records || typeof records !== "object")
49
+ return {};
50
+ const out = {};
51
+ for (const [id, record] of Object.entries(records)) {
52
+ if (!record || typeof record !== "object")
53
+ continue;
54
+ const result = workflowDefinitionSchema.safeParse(record.definition);
55
+ if (!result.success)
56
+ continue;
57
+ out[id] = result.data;
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,242 @@
1
+ import { existsSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { hostname } from "node:os";
3
+ import { join } from "node:path";
4
+ import { loadEnterpriseConfig } from "../core/config.js";
5
+ import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
6
+ import { enterpriseCredentialId, isAllowedLicenseEndpoint, } from "../core/license.js";
7
+ import { capabilityDefinitionSchema, } from "../core/workflow/capabilities.js";
8
+ import { writeHostAuditEvent } from "./ungoverned-scanner.js";
9
+ import { daemonDir } from "./paths.js";
10
+ /**
11
+ * Org-capability sync flow — daemon side.
12
+ *
13
+ * The daemon polls cortex-web /api/v1/govern/capabilities/manifest each
14
+ * tick to learn what capabilities the org has authored. It diffs against
15
+ * a local state file, fetches changed full definitions, and caches them
16
+ * locally. The pre-tool-use hook's evaluateToolCall consults the merged
17
+ * registry via loadSyncedCapabilities() with synced taking precedence
18
+ * over bundled DEFAULT_CAPABILITIES on name collisions.
19
+ *
20
+ * Three audit outcomes per tick:
21
+ * - capabilities_unchanged — manifest matches local state
22
+ * - capabilities_synced — at least one capability was added /
23
+ * changed / removed (metadata: counts)
24
+ * - capabilities_sync_failed — network / auth / parse error
25
+ */
26
+ const STATE_FILENAME = "capabilities.local.json";
27
+ function stateFilePath() {
28
+ return join(daemonDir(), STATE_FILENAME);
29
+ }
30
+ function readSyncedCapabilitiesState() {
31
+ const path = stateFilePath();
32
+ if (!existsSync(path))
33
+ return { capabilities: {} };
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
36
+ return {
37
+ credential_id: typeof parsed.credential_id === "string"
38
+ ? parsed.credential_id
39
+ : undefined,
40
+ capabilities: parsed.capabilities ?? {},
41
+ last_synced_at: parsed.last_synced_at,
42
+ };
43
+ }
44
+ catch {
45
+ return { capabilities: {} };
46
+ }
47
+ }
48
+ function writeSyncedCapabilitiesState(state) {
49
+ writeFileSync(stateFilePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
50
+ }
51
+ async function fetchManifest(baseUrl, apiKey) {
52
+ const url = new URL(baseUrl.replace(/\/$/, "") + "/api/v1/govern/capabilities/manifest");
53
+ const res = await fetch(url, {
54
+ headers: { Authorization: `Bearer ${apiKey}` },
55
+ });
56
+ if (!res.ok) {
57
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
58
+ }
59
+ const body = (await res.json());
60
+ return body.capabilities ?? [];
61
+ }
62
+ async function fetchCapability(baseUrl, apiKey, capabilityName) {
63
+ const url = new URL(baseUrl.replace(/\/$/, "") +
64
+ "/api/v1/govern/capabilities/" +
65
+ encodeURIComponent(capabilityName));
66
+ const res = await fetch(url, {
67
+ headers: { Authorization: `Bearer ${apiKey}` },
68
+ });
69
+ if (!res.ok) {
70
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
71
+ }
72
+ const body = (await res.json());
73
+ if (!body.capability) {
74
+ throw new Error(`Response for ${capabilityName} missing 'capability' field`);
75
+ }
76
+ return body.capability;
77
+ }
78
+ export async function runCapabilitySyncOnce(cwd) {
79
+ const config = loadEnterpriseConfig(join(cwd, ".context"));
80
+ const apiKey = config.enterprise.api_key.trim();
81
+ const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
82
+ if (!apiKey || !baseUrl) {
83
+ const outcome = {
84
+ kind: "failed",
85
+ error: "enterprise not configured",
86
+ };
87
+ await writeAudit(cwd, outcome);
88
+ return outcome;
89
+ }
90
+ if (!isAllowedLicenseEndpoint(baseUrl)) {
91
+ const outcome = {
92
+ kind: "failed",
93
+ error: "insecure or invalid enterprise endpoint",
94
+ };
95
+ await writeAudit(cwd, outcome);
96
+ return outcome;
97
+ }
98
+ const credentialId = enterpriseCredentialId(baseUrl, apiKey);
99
+ if (!matchesEnterpriseHostIdentity(credentialId)) {
100
+ const outcome = {
101
+ kind: "failed",
102
+ error: "enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
103
+ };
104
+ await writeAudit(cwd, outcome);
105
+ return outcome;
106
+ }
107
+ const state = readSyncedCapabilitiesState();
108
+ const identityChanged = state.credential_id !== credentialId;
109
+ state.credential_id = credentialId;
110
+ let manifest;
111
+ try {
112
+ manifest = await fetchManifest(baseUrl, apiKey);
113
+ }
114
+ catch (err) {
115
+ const outcome = {
116
+ kind: "failed",
117
+ error: err instanceof Error ? err.message : String(err),
118
+ };
119
+ await writeAudit(cwd, outcome);
120
+ return outcome;
121
+ }
122
+ const remoteByName = new Map(manifest.map((e) => [e.capability_name, e]));
123
+ const added = [];
124
+ const changed = [];
125
+ const removed = [];
126
+ for (const entry of manifest) {
127
+ const local = state.capabilities[entry.capability_name];
128
+ const isNew = !local;
129
+ const isChanged = Boolean(local) &&
130
+ (identityChanged || local.updated_at !== entry.updated_at);
131
+ if (!isNew && !isChanged)
132
+ continue;
133
+ let fetched;
134
+ try {
135
+ fetched = await fetchCapability(baseUrl, apiKey, entry.capability_name);
136
+ }
137
+ catch (err) {
138
+ const outcome = {
139
+ kind: "failed",
140
+ error: err instanceof Error
141
+ ? `fetch ${entry.capability_name}: ${err.message}`
142
+ : `fetch ${entry.capability_name}: ${String(err)}`,
143
+ };
144
+ await writeAudit(cwd, outcome);
145
+ return outcome;
146
+ }
147
+ let validated;
148
+ try {
149
+ validated = capabilityDefinitionSchema.parse(fetched.definition);
150
+ }
151
+ catch (err) {
152
+ const outcome = {
153
+ kind: "failed",
154
+ error: err instanceof Error
155
+ ? `validate ${entry.capability_name}: ${err.message}`
156
+ : `validate ${entry.capability_name}: ${String(err)}`,
157
+ };
158
+ await writeAudit(cwd, outcome);
159
+ return outcome;
160
+ }
161
+ state.capabilities[entry.capability_name] = {
162
+ capability_name: entry.capability_name,
163
+ updated_at: fetched.updated_at,
164
+ definition: validated,
165
+ };
166
+ (isNew ? added : changed).push(entry.capability_name);
167
+ }
168
+ for (const name of Object.keys(state.capabilities)) {
169
+ if (remoteByName.has(name))
170
+ continue;
171
+ delete state.capabilities[name];
172
+ removed.push(name);
173
+ }
174
+ const totalChanged = added.length + changed.length + removed.length;
175
+ if (totalChanged === 0) {
176
+ if (identityChanged) {
177
+ state.last_synced_at = new Date().toISOString();
178
+ writeSyncedCapabilitiesState(state);
179
+ }
180
+ const outcome = {
181
+ kind: "unchanged",
182
+ count: manifest.length,
183
+ };
184
+ await writeAudit(cwd, outcome);
185
+ return outcome;
186
+ }
187
+ state.last_synced_at = new Date().toISOString();
188
+ writeSyncedCapabilitiesState(state);
189
+ const outcome = {
190
+ kind: "synced",
191
+ added,
192
+ changed,
193
+ removed,
194
+ };
195
+ await writeAudit(cwd, outcome);
196
+ return outcome;
197
+ }
198
+ async function writeAudit(cwd, outcome) {
199
+ const eventBase = {
200
+ timestamp: new Date().toISOString(),
201
+ host_id: hostname(),
202
+ };
203
+ if (outcome.kind === "unchanged") {
204
+ await writeHostAuditEvent(cwd, {
205
+ ...eventBase,
206
+ event_type: "capabilities_unchanged",
207
+ count: outcome.count,
208
+ }).catch(() => undefined);
209
+ }
210
+ else if (outcome.kind === "synced") {
211
+ await writeHostAuditEvent(cwd, {
212
+ ...eventBase,
213
+ event_type: "capabilities_synced",
214
+ added: outcome.added,
215
+ changed: outcome.changed,
216
+ removed: outcome.removed,
217
+ }).catch(() => undefined);
218
+ }
219
+ else {
220
+ await writeHostAuditEvent(cwd, {
221
+ ...eventBase,
222
+ event_type: "capabilities_sync_failed",
223
+ error: outcome.error,
224
+ }).catch(() => undefined);
225
+ }
226
+ }
227
+ export function startCapabilitySyncTimer(cwd, intervalMs) {
228
+ const tick = () => {
229
+ void runCapabilitySyncOnce(cwd).catch((err) => {
230
+ process.stderr.write(`[cortex-daemon] capability sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
231
+ });
232
+ };
233
+ void Promise.resolve().then(tick);
234
+ const handle = setInterval(tick, intervalMs);
235
+ if (typeof handle.unref === "function")
236
+ handle.unref();
237
+ return {
238
+ stop() {
239
+ clearInterval(handle);
240
+ },
241
+ };
242
+ }