@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.
- package/CHANGELOG.md +201 -0
- package/README.md +19 -3
- package/bin/cortex.mjs +308 -75
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- 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
|
+
}
|