@danielblomma/cortex-mcp 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -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
- package/scaffold/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { composeStageEnvelope, createRun, advanceStage, getRunState, } from "../core/workflow/index.js";
|
|
4
|
+
import { resolveWorkflowDefinition } from "../core/workflow/resolution.js";
|
|
5
|
+
import { isEnforcedMode, isEnterpriseProject } from "../hooks/shared.js";
|
|
6
|
+
/**
|
|
7
|
+
* `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
|
|
8
|
+
* around the workflow primitives, mirroring the cortex.workflow.* MCP
|
|
9
|
+
* tools so shell scripts and CI can drive the harness without an MCP
|
|
10
|
+
* client.
|
|
11
|
+
*
|
|
12
|
+
* Subcommands:
|
|
13
|
+
* start --task-id <id> --description "..." [--workflow <id>]
|
|
14
|
+
* status --task-id <id>
|
|
15
|
+
* envelope --task-id <id> [--stage <name>]
|
|
16
|
+
* advance --task-id <id> --stage <name> --body-file <path>
|
|
17
|
+
* [--frontmatter-file <path>] [--status <complete|blocked|failed>]
|
|
18
|
+
* run --task-id <id> -- <command> [args...]
|
|
19
|
+
* Sets CORTEX_ACTIVE_TASK_ID and execs the command. Use this to
|
|
20
|
+
* spawn an agent that runs under the harness's pre-tool-use gate.
|
|
21
|
+
*
|
|
22
|
+
* All commands resolve cwd from CORTEX_PROJECT_ROOT (preferred) or
|
|
23
|
+
* process.cwd() so they work both inside an MCP server context and as
|
|
24
|
+
* standalone shell calls.
|
|
25
|
+
*/
|
|
26
|
+
const ENTERPRISE_REQUIRED_MESSAGE = "cortex stage is part of the Cortex Harness — an enterprise-only feature. " +
|
|
27
|
+
"This project does not have an enterprise license configured (no enterprise.api_key " +
|
|
28
|
+
"in .context/enterprise.yml). Run 'cortex enterprise install --api-key-stdin' to enable it, or " +
|
|
29
|
+
"contact your org admin.";
|
|
30
|
+
export async function runStageCommand(args) {
|
|
31
|
+
const sub = args[0] ?? "help";
|
|
32
|
+
const rest = args.slice(1);
|
|
33
|
+
// help is allowed in community mode so users can discover the feature exists.
|
|
34
|
+
if (sub !== "help" && sub !== "--help" && sub !== "-h") {
|
|
35
|
+
if (!isEnterpriseProject(projectRoot())) {
|
|
36
|
+
throw new Error(ENTERPRISE_REQUIRED_MESSAGE);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
switch (sub) {
|
|
40
|
+
case "start":
|
|
41
|
+
return runStart(rest);
|
|
42
|
+
case "status":
|
|
43
|
+
return runStatus(rest);
|
|
44
|
+
case "envelope":
|
|
45
|
+
return runEnvelope(rest);
|
|
46
|
+
case "advance":
|
|
47
|
+
return runAdvance(rest);
|
|
48
|
+
case "run":
|
|
49
|
+
return runRun(rest);
|
|
50
|
+
case "help":
|
|
51
|
+
case "--help":
|
|
52
|
+
case "-h":
|
|
53
|
+
printHelp();
|
|
54
|
+
return;
|
|
55
|
+
default:
|
|
56
|
+
printHelp();
|
|
57
|
+
throw new Error(`Unknown stage subcommand: ${sub}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function printHelp() {
|
|
61
|
+
const lines = [
|
|
62
|
+
"Usage:",
|
|
63
|
+
" cortex stage start --task-id <id> --description \"...\" [--workflow <id>]",
|
|
64
|
+
" cortex stage status --task-id <id>",
|
|
65
|
+
" cortex stage envelope --task-id <id> [--stage <name>]",
|
|
66
|
+
" cortex stage advance --task-id <id> --stage <name> --body-file <path>",
|
|
67
|
+
" [--frontmatter-file <path>] [--status <s>] [--outcome-file <path>]",
|
|
68
|
+
" [--validators-passed <id1,id2,...>]",
|
|
69
|
+
" [--override-reason \"...\"] [--override-skipped-validators <id1,id2>]",
|
|
70
|
+
" cortex stage run --task-id <id> -- <command> [args...]",
|
|
71
|
+
"",
|
|
72
|
+
"Status values: complete (default) | blocked | failed",
|
|
73
|
+
"All commands operate on .agents/<task-id>/ in the current project root.",
|
|
74
|
+
];
|
|
75
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
76
|
+
}
|
|
77
|
+
function projectRoot() {
|
|
78
|
+
return process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
79
|
+
}
|
|
80
|
+
function parseFlags(args) {
|
|
81
|
+
const flags = {};
|
|
82
|
+
const rest = [];
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < args.length) {
|
|
85
|
+
const arg = args[i];
|
|
86
|
+
if (arg === "--") {
|
|
87
|
+
rest.push(...args.slice(i + 1));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (arg.startsWith("--")) {
|
|
91
|
+
const next = args[i + 1];
|
|
92
|
+
if (next === undefined || next.startsWith("--")) {
|
|
93
|
+
flags[arg.slice(2)] = true;
|
|
94
|
+
i += 1;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
flags[arg.slice(2)] = next;
|
|
98
|
+
i += 2;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
rest.push(arg);
|
|
103
|
+
i += 1;
|
|
104
|
+
}
|
|
105
|
+
return { flags, rest };
|
|
106
|
+
}
|
|
107
|
+
function requireFlag(flags, name) {
|
|
108
|
+
const value = flags[name];
|
|
109
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
110
|
+
throw new Error(`Missing required flag: --${name}`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function emitJson(data) {
|
|
115
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
116
|
+
}
|
|
117
|
+
async function runStart(args) {
|
|
118
|
+
const { flags } = parseFlags(args);
|
|
119
|
+
const taskId = requireFlag(flags, "task-id");
|
|
120
|
+
const description = requireFlag(flags, "description");
|
|
121
|
+
const workflowId = typeof flags.workflow === "string" ? flags.workflow : "secure-build";
|
|
122
|
+
const resolved = resolveWorkflowDefinition(workflowId, {
|
|
123
|
+
cwd: projectRoot(),
|
|
124
|
+
emitBundledFallbackWarning: true,
|
|
125
|
+
bundledFallbackPolicy: isEnforcedMode(projectRoot()) ? "block" : "warn",
|
|
126
|
+
});
|
|
127
|
+
const workflow = resolved.workflow;
|
|
128
|
+
const state = createRun({
|
|
129
|
+
cwd: projectRoot(),
|
|
130
|
+
taskId,
|
|
131
|
+
workflow,
|
|
132
|
+
taskDescription: description,
|
|
133
|
+
});
|
|
134
|
+
const envelope = composeStageEnvelope({
|
|
135
|
+
cwd: projectRoot(),
|
|
136
|
+
taskId,
|
|
137
|
+
workflow,
|
|
138
|
+
});
|
|
139
|
+
emitJson({
|
|
140
|
+
state,
|
|
141
|
+
envelope,
|
|
142
|
+
workflow_source: resolved.source,
|
|
143
|
+
warnings: resolved.warnings,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function runStatus(args) {
|
|
147
|
+
const { flags } = parseFlags(args);
|
|
148
|
+
const taskId = requireFlag(flags, "task-id");
|
|
149
|
+
const state = getRunState(projectRoot(), taskId);
|
|
150
|
+
emitJson({ state });
|
|
151
|
+
}
|
|
152
|
+
async function runEnvelope(args) {
|
|
153
|
+
const { flags } = parseFlags(args);
|
|
154
|
+
const taskId = requireFlag(flags, "task-id");
|
|
155
|
+
const stageName = typeof flags.stage === "string" ? flags.stage : undefined;
|
|
156
|
+
const state = getRunState(projectRoot(), taskId);
|
|
157
|
+
if (!state) {
|
|
158
|
+
throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
|
|
159
|
+
}
|
|
160
|
+
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
161
|
+
cwd: projectRoot(),
|
|
162
|
+
}).workflow;
|
|
163
|
+
const envelope = composeStageEnvelope({
|
|
164
|
+
cwd: projectRoot(),
|
|
165
|
+
taskId,
|
|
166
|
+
workflow,
|
|
167
|
+
stageName,
|
|
168
|
+
});
|
|
169
|
+
emitJson({ envelope });
|
|
170
|
+
}
|
|
171
|
+
async function runAdvance(args) {
|
|
172
|
+
const { flags } = parseFlags(args);
|
|
173
|
+
const taskId = requireFlag(flags, "task-id");
|
|
174
|
+
const stageName = requireFlag(flags, "stage");
|
|
175
|
+
const bodyPath = requireFlag(flags, "body-file");
|
|
176
|
+
const frontmatterPath = typeof flags["frontmatter-file"] === "string"
|
|
177
|
+
? flags["frontmatter-file"]
|
|
178
|
+
: null;
|
|
179
|
+
const outcomePath = typeof flags["outcome-file"] === "string" ? flags["outcome-file"] : null;
|
|
180
|
+
const statusFlag = typeof flags.status === "string" ? flags.status : undefined;
|
|
181
|
+
const validatorsRaw = typeof flags["validators-passed"] === "string"
|
|
182
|
+
? flags["validators-passed"]
|
|
183
|
+
: null;
|
|
184
|
+
const overrideReason = typeof flags["override-reason"] === "string"
|
|
185
|
+
? flags["override-reason"]
|
|
186
|
+
: null;
|
|
187
|
+
const overrideSkippedRaw = typeof flags["override-skipped-validators"] === "string"
|
|
188
|
+
? flags["override-skipped-validators"]
|
|
189
|
+
: null;
|
|
190
|
+
const body = readFileSync(bodyPath, "utf8");
|
|
191
|
+
const frontmatter = frontmatterPath
|
|
192
|
+
? parseJsonObject(frontmatterPath)
|
|
193
|
+
: {};
|
|
194
|
+
const outcome = outcomePath ? parseJsonObject(outcomePath) : undefined;
|
|
195
|
+
const validatorsPassed = validatorsRaw
|
|
196
|
+
? validatorsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
197
|
+
: undefined;
|
|
198
|
+
const override = overrideReason
|
|
199
|
+
? {
|
|
200
|
+
reason: overrideReason,
|
|
201
|
+
skipped_validators: overrideSkippedRaw
|
|
202
|
+
? overrideSkippedRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
203
|
+
: [],
|
|
204
|
+
skipped_requirements: [],
|
|
205
|
+
}
|
|
206
|
+
: undefined;
|
|
207
|
+
const state = getRunState(projectRoot(), taskId);
|
|
208
|
+
if (!state) {
|
|
209
|
+
throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
|
|
210
|
+
}
|
|
211
|
+
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
212
|
+
cwd: projectRoot(),
|
|
213
|
+
}).workflow;
|
|
214
|
+
const stage = workflow.stages.find((s) => s.name === stageName);
|
|
215
|
+
if (!stage) {
|
|
216
|
+
throw new Error(`Stage ${stageName} is not defined in workflow ${workflow.id}`);
|
|
217
|
+
}
|
|
218
|
+
const finalStatus = statusFlag ?? "complete";
|
|
219
|
+
const next = advanceStage({
|
|
220
|
+
cwd: projectRoot(),
|
|
221
|
+
taskId,
|
|
222
|
+
workflow,
|
|
223
|
+
stageName,
|
|
224
|
+
artifactName: stage.artifact,
|
|
225
|
+
frontmatter: {
|
|
226
|
+
...frontmatter,
|
|
227
|
+
stage: stageName,
|
|
228
|
+
status: finalStatus,
|
|
229
|
+
references: (Array.isArray(frontmatter.references)
|
|
230
|
+
? frontmatter.references
|
|
231
|
+
.filter((v) => typeof v === "string")
|
|
232
|
+
: null) ?? deriveReferencesFromReads(stage.reads, workflow),
|
|
233
|
+
},
|
|
234
|
+
body,
|
|
235
|
+
status: finalStatus,
|
|
236
|
+
outcome,
|
|
237
|
+
validatorsPassed,
|
|
238
|
+
override,
|
|
239
|
+
});
|
|
240
|
+
let nextEnvelope = null;
|
|
241
|
+
if (next.outcome === "in_progress" && next.current_stage) {
|
|
242
|
+
nextEnvelope = composeStageEnvelope({
|
|
243
|
+
cwd: projectRoot(),
|
|
244
|
+
taskId,
|
|
245
|
+
workflow,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
emitJson({ state: next, next_envelope: nextEnvelope });
|
|
249
|
+
}
|
|
250
|
+
function parseJsonObject(filePath) {
|
|
251
|
+
const raw = readFileSync(filePath, "utf8");
|
|
252
|
+
let value;
|
|
253
|
+
try {
|
|
254
|
+
value = JSON.parse(raw);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
throw new Error(`Failed to parse JSON in ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
}
|
|
259
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
260
|
+
throw new Error(`Expected JSON object in ${filePath}, got ${typeof value}`);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
function deriveReferencesFromReads(reads, workflow) {
|
|
265
|
+
const refs = [];
|
|
266
|
+
for (const readName of reads) {
|
|
267
|
+
const stage = workflow.stages.find((s) => s.name === readName);
|
|
268
|
+
if (stage)
|
|
269
|
+
refs.push(stage.artifact);
|
|
270
|
+
}
|
|
271
|
+
return refs;
|
|
272
|
+
}
|
|
273
|
+
async function runRun(args) {
|
|
274
|
+
const { flags, rest } = parseFlags(args);
|
|
275
|
+
const taskId = requireFlag(flags, "task-id");
|
|
276
|
+
if (rest.length === 0) {
|
|
277
|
+
throw new Error("cortex stage run requires a command after --, e.g. 'cortex stage run --task-id task-1 -- claude'");
|
|
278
|
+
}
|
|
279
|
+
const state = getRunState(projectRoot(), taskId);
|
|
280
|
+
if (!state) {
|
|
281
|
+
throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
|
|
282
|
+
}
|
|
283
|
+
if (state.outcome !== "in_progress" || !state.current_stage) {
|
|
284
|
+
throw new Error(`Run ${taskId} is not in progress (outcome=${state.outcome}). Cannot spawn agent.`);
|
|
285
|
+
}
|
|
286
|
+
const [command, ...commandArgs] = rest;
|
|
287
|
+
const child = spawn(command, commandArgs, {
|
|
288
|
+
stdio: "inherit",
|
|
289
|
+
env: {
|
|
290
|
+
...process.env,
|
|
291
|
+
CORTEX_ACTIVE_TASK_ID: taskId,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
await new Promise((resolve, reject) => {
|
|
295
|
+
child.on("error", reject);
|
|
296
|
+
child.on("exit", (code, signal) => {
|
|
297
|
+
if (signal) {
|
|
298
|
+
reject(new Error(`spawned process terminated by signal ${signal}`));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (code !== 0) {
|
|
302
|
+
reject(new Error(`spawned process exited with code ${code}`));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
resolve();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { hostname, platform, arch } from "node:os";
|
|
5
|
+
import { loadEnterpriseConfig, resolveEnterpriseActivation, } from "../core/config.js";
|
|
6
|
+
import { telemetryStatePath } from "../core/telemetry/state-dir.js";
|
|
7
|
+
import { pushMetrics } from "../enterprise/telemetry/sync.js";
|
|
8
|
+
/**
|
|
9
|
+
* Smoke-test the telemetry pipeline end-to-end.
|
|
10
|
+
*
|
|
11
|
+
* $ cortex telemetry test
|
|
12
|
+
*
|
|
13
|
+
* Reads .context/enterprise.yml, builds a synthetic-but-valid metrics
|
|
14
|
+
* payload, sends it to the configured endpoint, and reports the result
|
|
15
|
+
* with actionable diagnostics on failure.
|
|
16
|
+
*
|
|
17
|
+
* This exists because the silent-failure path (push fails, dashboard
|
|
18
|
+
* stays empty, no error surfaces anywhere) was the original v2.0.0
|
|
19
|
+
* motivator. The smoke-test gives users a single command to verify
|
|
20
|
+
* their entire pipeline.
|
|
21
|
+
*/
|
|
22
|
+
function readMachineId(contextDir) {
|
|
23
|
+
const path = telemetryStatePath(contextDir, "machine_id");
|
|
24
|
+
if (existsSync(path)) {
|
|
25
|
+
try {
|
|
26
|
+
const id = readFileSync(path, "utf8").trim();
|
|
27
|
+
if (id)
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// fall through to compute one
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return createHash("sha256")
|
|
35
|
+
.update(`${hostname()}|${platform()}|${arch()}|test`)
|
|
36
|
+
.digest("hex")
|
|
37
|
+
.slice(0, 16);
|
|
38
|
+
}
|
|
39
|
+
function buildSyntheticMetrics(instanceId, version) {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const periodStart = new Date(now.getTime() - 60_000).toISOString();
|
|
42
|
+
const periodEnd = now.toISOString();
|
|
43
|
+
return {
|
|
44
|
+
period_start: periodStart,
|
|
45
|
+
period_end: periodEnd,
|
|
46
|
+
total_tool_calls: 1,
|
|
47
|
+
successful_tool_calls: 1,
|
|
48
|
+
failed_tool_calls: 0,
|
|
49
|
+
total_duration_ms: 100,
|
|
50
|
+
session_starts: 1,
|
|
51
|
+
session_ends: 1,
|
|
52
|
+
session_duration_ms_total: 100,
|
|
53
|
+
searches: 1,
|
|
54
|
+
related_lookups: 0,
|
|
55
|
+
caller_lookups: 0,
|
|
56
|
+
trace_lookups: 0,
|
|
57
|
+
impact_analyses: 0,
|
|
58
|
+
rule_lookups: 0,
|
|
59
|
+
reloads: 0,
|
|
60
|
+
total_results_returned: 1,
|
|
61
|
+
estimated_tokens_saved: 100,
|
|
62
|
+
estimated_tokens_total: 500,
|
|
63
|
+
client_version: version,
|
|
64
|
+
instance_id: instanceId,
|
|
65
|
+
tool_metrics: {
|
|
66
|
+
"telemetry.test": {
|
|
67
|
+
calls: 1,
|
|
68
|
+
failures: 0,
|
|
69
|
+
total_duration_ms: 100,
|
|
70
|
+
total_results_returned: 1,
|
|
71
|
+
estimated_tokens_saved: 100,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function runTelemetryTest() {
|
|
77
|
+
const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
78
|
+
const contextDir = join(projectRoot, ".context");
|
|
79
|
+
if (!existsSync(contextDir)) {
|
|
80
|
+
console.error(`No .context directory at ${projectRoot}`);
|
|
81
|
+
console.error(`Run 'cortex init --bootstrap' first.`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const config = loadEnterpriseConfig(contextDir);
|
|
85
|
+
const activation = resolveEnterpriseActivation(config);
|
|
86
|
+
console.log(`Project: ${projectRoot}`);
|
|
87
|
+
console.log(`Enterprise active: ${activation.active} (${activation.reason})`);
|
|
88
|
+
if (!activation.active) {
|
|
89
|
+
console.error("");
|
|
90
|
+
console.error("Cannot run telemetry test in community mode.");
|
|
91
|
+
console.error("Configure .context/enterprise.yml with valid api_key + endpoint.");
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (!config.telemetry.enabled) {
|
|
95
|
+
console.error("");
|
|
96
|
+
console.error("telemetry.enabled is false in enterprise.yml");
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
console.log(`Endpoint: ${config.telemetry.endpoint}`);
|
|
100
|
+
console.log(`API key prefix: ${config.telemetry.api_key.slice(0, 8)}...`);
|
|
101
|
+
const instanceId = readMachineId(contextDir);
|
|
102
|
+
const version = process.env.CORTEX_VERSION ?? "telemetry-test";
|
|
103
|
+
const metrics = buildSyntheticMetrics(instanceId, version);
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log("Sending synthetic metrics...");
|
|
106
|
+
const start = Date.now();
|
|
107
|
+
const result = await pushMetrics(metrics, config.telemetry.endpoint, config.telemetry.api_key, { repo: basename(projectRoot) });
|
|
108
|
+
const elapsed = Date.now() - start;
|
|
109
|
+
if (result.success) {
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(`✓ Push succeeded in ${elapsed}ms (HTTP ${result.status})`);
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log("Next: open the dashboard. The synthetic event should appear within 60s.");
|
|
114
|
+
console.log("If it does not, the issue is on the server (ingest or read path),");
|
|
115
|
+
console.log("not the client.");
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
console.error("");
|
|
119
|
+
console.error(`✗ Push failed after ${elapsed}ms: ${result.error ?? "unknown"}`);
|
|
120
|
+
console.error("");
|
|
121
|
+
if (result.status === 401) {
|
|
122
|
+
console.error(" → API key was rejected. Check that the key in enterprise.yml");
|
|
123
|
+
console.error(" matches a row in cortex-web's api_keys table and is not");
|
|
124
|
+
console.error(" revoked or expired.");
|
|
125
|
+
}
|
|
126
|
+
else if (result.status === 403) {
|
|
127
|
+
console.error(" → API key lacks 'telemetry' scope. Add the scope in cortex-web.");
|
|
128
|
+
}
|
|
129
|
+
else if (result.status === 400) {
|
|
130
|
+
console.error(" → Payload schema rejected. Likely a client/server version skew.");
|
|
131
|
+
console.error(" Check telemetryPushSchema vs TelemetryMetrics.");
|
|
132
|
+
}
|
|
133
|
+
else if (result.status && result.status >= 500) {
|
|
134
|
+
console.error(" → Server error. Check cortex-web logs and DB connectivity.");
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.error(" → Network error. Verify endpoint is reachable from this machine.");
|
|
138
|
+
console.error(` curl -I ${config.telemetry.endpoint}`);
|
|
139
|
+
}
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { hostname, userInfo } from "node:os";
|
|
4
|
+
export const DEFAULT_AI_BINARIES = [
|
|
5
|
+
"claude",
|
|
6
|
+
"codex",
|
|
7
|
+
"copilot",
|
|
8
|
+
"gemini-cli",
|
|
9
|
+
"gemini",
|
|
10
|
+
"aider",
|
|
11
|
+
"cursor",
|
|
12
|
+
];
|
|
13
|
+
const PS_LINE_RE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s*(.*)$/;
|
|
14
|
+
export function listProcesses() {
|
|
15
|
+
try {
|
|
16
|
+
const out = execSync("ps -axo pid,ppid,user,comm,args", {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
timeout: 5000,
|
|
19
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
20
|
+
});
|
|
21
|
+
const lines = out.trim().split("\n").slice(1);
|
|
22
|
+
return lines
|
|
23
|
+
.map(parseProcessLine)
|
|
24
|
+
.filter((p) => p !== null);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function parseProcessLine(line) {
|
|
31
|
+
const m = line.match(PS_LINE_RE);
|
|
32
|
+
if (!m)
|
|
33
|
+
return null;
|
|
34
|
+
const pid = parseInt(m[1], 10);
|
|
35
|
+
const ppid = parseInt(m[2], 10);
|
|
36
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid))
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
pid,
|
|
40
|
+
ppid,
|
|
41
|
+
user: m[3],
|
|
42
|
+
comm: m[4],
|
|
43
|
+
args: m[5] ?? "",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function isCortexAncestor(args) {
|
|
47
|
+
if (!args)
|
|
48
|
+
return false;
|
|
49
|
+
// Accept invocations whose command line shows cortex orchestration:
|
|
50
|
+
// - `cortex run ...` → wrapper spawned the AI CLI (Tier 2 governed)
|
|
51
|
+
// - `cortex enterprise` → install-time orchestration
|
|
52
|
+
// - `cortex daemon` → daemon-supervised
|
|
53
|
+
// - any path ending in `cortex.mjs` or named `cortex` followed by space/EOL
|
|
54
|
+
if (/\bcortex\s+run\b/.test(args))
|
|
55
|
+
return true;
|
|
56
|
+
if (/\bcortex\s+enterprise\b/.test(args))
|
|
57
|
+
return true;
|
|
58
|
+
if (/\bcortex\s+daemon\b/.test(args))
|
|
59
|
+
return true;
|
|
60
|
+
if (/\bcortex\s+hook\b/.test(args))
|
|
61
|
+
return true;
|
|
62
|
+
if (/\bcortex(\s|$)/.test(args))
|
|
63
|
+
return true;
|
|
64
|
+
if (/cortex\.mjs/.test(args))
|
|
65
|
+
return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function findCli(comm, knownBinaries) {
|
|
69
|
+
const base = basename(comm);
|
|
70
|
+
return knownBinaries.includes(base) ? base : null;
|
|
71
|
+
}
|
|
72
|
+
export function detectUngoverned(options = {}) {
|
|
73
|
+
const known = options.knownBinaries ?? DEFAULT_AI_BINARIES;
|
|
74
|
+
const procs = options.processes ?? listProcesses();
|
|
75
|
+
const byPid = new Map();
|
|
76
|
+
for (const p of procs)
|
|
77
|
+
byPid.set(p.pid, p);
|
|
78
|
+
const hostId = options.hostId ?? hostname();
|
|
79
|
+
const findings = [];
|
|
80
|
+
for (const proc of procs) {
|
|
81
|
+
const cli = findCli(proc.comm, known);
|
|
82
|
+
if (!cli)
|
|
83
|
+
continue;
|
|
84
|
+
if (isCortexAncestor(proc.args))
|
|
85
|
+
continue;
|
|
86
|
+
const chain = [proc.pid];
|
|
87
|
+
let current = byPid.get(proc.ppid);
|
|
88
|
+
let governed = false;
|
|
89
|
+
let depth = 0;
|
|
90
|
+
while (current && current.pid > 1 && depth < 32) {
|
|
91
|
+
chain.push(current.pid);
|
|
92
|
+
if (isCortexAncestor(current.args)) {
|
|
93
|
+
governed = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const next = byPid.get(current.ppid);
|
|
97
|
+
if (!next || next.pid === current.pid)
|
|
98
|
+
break;
|
|
99
|
+
current = next;
|
|
100
|
+
depth += 1;
|
|
101
|
+
}
|
|
102
|
+
if (!governed) {
|
|
103
|
+
findings.push({
|
|
104
|
+
pid: proc.pid,
|
|
105
|
+
ppid: proc.ppid,
|
|
106
|
+
user: proc.user,
|
|
107
|
+
cli,
|
|
108
|
+
binary: proc.comm,
|
|
109
|
+
args: proc.args,
|
|
110
|
+
parent_chain: chain,
|
|
111
|
+
detected_at: new Date().toISOString(),
|
|
112
|
+
host_id: hostId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
export function enforceFinding(finding, options) {
|
|
119
|
+
if (options.mode !== "enforced")
|
|
120
|
+
return "logged";
|
|
121
|
+
const me = options.currentUser ?? userInfo().username;
|
|
122
|
+
if (finding.user !== me) {
|
|
123
|
+
return "skipped_cross_user";
|
|
124
|
+
}
|
|
125
|
+
const send = options.sendSignal ?? ((pid, sig) => process.kill(pid, sig));
|
|
126
|
+
try {
|
|
127
|
+
send(finding.pid, "SIGTERM");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// process exited between detection and signal — best-effort
|
|
131
|
+
}
|
|
132
|
+
return "sigterm";
|
|
133
|
+
}
|