@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,826 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { walkProjectFiles } from "./walk.js";
|
|
6
|
+
import { resolveChangedReviewFiles } from "../reviews/changed-files.js";
|
|
7
|
+
import { enforceInjectionPolicy, buildViolationPayload } from "../../core/policy/enforce.js";
|
|
8
|
+
import { getLastPush } from "../telemetry/sync.js";
|
|
9
|
+
import { syncFromCloud, syncFromLocal, getLastSync } from "../policy/sync.js";
|
|
10
|
+
import { queueViolation } from "../violations/push.js";
|
|
11
|
+
import { getReviewPushContext, queueReviewResult } from "../reviews/push.js";
|
|
12
|
+
import { partitionReviewPolicies } from "../reviews/policy-selection.js";
|
|
13
|
+
import { recordQueuedReviewTrustState } from "../reviews/trust-state.js";
|
|
14
|
+
import { buildPatternReviewContext } from "../reviews/pattern-context.js";
|
|
15
|
+
import { pushWorkflowSnapshot } from "../workflow/push.js";
|
|
16
|
+
import { OUTBOUND_DATA_BOUNDARY } from "../privacy/boundary.js";
|
|
17
|
+
import { addWorkflowNote, addWorkflowTodo, approveWorkflow, completeWorkflowTodo, loadWorkflowState, recordWorkflowReview, recordWorkflowUpdate, reviewWorkflowPlan, setWorkflowPlan, startWorkflowImplementation, } from "../workflow/state.js";
|
|
18
|
+
import { queryAuditLog } from "../../core/audit/query.js";
|
|
19
|
+
import { checkAccess, getAccessDeniedMessage } from "../../core/rbac/check.js";
|
|
20
|
+
import { runValidators } from "../../core/validators/engine.js";
|
|
21
|
+
import "../../core/validators/builtins.js";
|
|
22
|
+
import { recordToolActivity } from "../index.js";
|
|
23
|
+
const VALID_ROLES = new Set(["admin", "developer", "readonly"]);
|
|
24
|
+
export function buildContextReviewAuditInput(input) {
|
|
25
|
+
return {
|
|
26
|
+
scope: input.scope,
|
|
27
|
+
include_passed: input.include_passed,
|
|
28
|
+
include_pattern_evidence: input.include_pattern_evidence,
|
|
29
|
+
pattern_query_present: Boolean(input.pattern_query),
|
|
30
|
+
pattern_query_length: input.pattern_query?.length ?? 0,
|
|
31
|
+
pattern_top_k: input.pattern_top_k,
|
|
32
|
+
pattern_limit: input.pattern_limit,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function snapshotReviewedFiles(projectRoot, changedFiles) {
|
|
36
|
+
if (!changedFiles)
|
|
37
|
+
return null;
|
|
38
|
+
return [...new Set(changedFiles)]
|
|
39
|
+
.sort()
|
|
40
|
+
.map((file) => {
|
|
41
|
+
const abs = join(projectRoot, file);
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(abs);
|
|
44
|
+
if (!stat.isFile()) {
|
|
45
|
+
return { path: file, exists: false, hash: null };
|
|
46
|
+
}
|
|
47
|
+
const hash = createHash("sha256")
|
|
48
|
+
.update(readFileSync(abs))
|
|
49
|
+
.digest("hex");
|
|
50
|
+
return { path: file, exists: true, hash };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { path: file, exists: false, hash: null };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function buildToolResult(data) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: JSON.stringify(data, null, 2),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
structuredContent: data,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function accessDenied(role, action) {
|
|
69
|
+
return buildToolResult({
|
|
70
|
+
error: getAccessDeniedMessage(role, action),
|
|
71
|
+
role,
|
|
72
|
+
action,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function pushWorkflowStateIfConfigured(config, state) {
|
|
76
|
+
const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
77
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
78
|
+
if (!baseUrl || !apiKey)
|
|
79
|
+
return;
|
|
80
|
+
const result = await pushWorkflowSnapshot(baseUrl, apiKey, state);
|
|
81
|
+
if (!result.success) {
|
|
82
|
+
process.stderr.write(`[cortex-enterprise] Workflow snapshot push failed: ${result.error ?? "unknown error"}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function registerEnterpriseTools(server, collector, auditWriter, config, contextDir, policyStore, version) {
|
|
86
|
+
const roleCandidate = config.rbac.enabled ? config.rbac.default_role : "admin";
|
|
87
|
+
const role = VALID_ROLES.has(roleCandidate)
|
|
88
|
+
? roleCandidate
|
|
89
|
+
: "readonly";
|
|
90
|
+
if (!VALID_ROLES.has(roleCandidate) && config.rbac.enabled) {
|
|
91
|
+
process.stderr.write(`[cortex-enterprise] Invalid RBAC role '${roleCandidate}', falling back to 'readonly'\n`);
|
|
92
|
+
}
|
|
93
|
+
// ── telemetry.status ──
|
|
94
|
+
server.registerTool("telemetry.status", {
|
|
95
|
+
description: "Return telemetry configuration and current aggregated metrics.",
|
|
96
|
+
inputSchema: z.object({}),
|
|
97
|
+
}, async () => {
|
|
98
|
+
if (config.rbac.enabled && !checkAccess(role, "telemetry.status")) {
|
|
99
|
+
return accessDenied(role, "telemetry.status");
|
|
100
|
+
}
|
|
101
|
+
const metrics = collector.getMetrics();
|
|
102
|
+
const lastPush = getLastPush();
|
|
103
|
+
recordToolActivity({
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
tool: "telemetry.status",
|
|
106
|
+
input: {},
|
|
107
|
+
result_count: 1,
|
|
108
|
+
entities_returned: [],
|
|
109
|
+
rules_applied: [],
|
|
110
|
+
duration_ms: 0,
|
|
111
|
+
event_type: "tool_call",
|
|
112
|
+
evidence_level: "diagnostic",
|
|
113
|
+
resource_type: "telemetry",
|
|
114
|
+
});
|
|
115
|
+
return buildToolResult({
|
|
116
|
+
enabled: config.telemetry.enabled,
|
|
117
|
+
endpoint: config.telemetry.endpoint || null,
|
|
118
|
+
interval_minutes: config.telemetry.interval_minutes,
|
|
119
|
+
metrics,
|
|
120
|
+
last_push: lastPush,
|
|
121
|
+
boundary: OUTBOUND_DATA_BOUNDARY,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// ── audit.query ──
|
|
125
|
+
server.registerTool("audit.query", {
|
|
126
|
+
description: "Search the enterprise audit log by date range, tool name, and limit.",
|
|
127
|
+
inputSchema: z.object({
|
|
128
|
+
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
129
|
+
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
130
|
+
tool: z.string().optional().describe("Filter by tool name"),
|
|
131
|
+
event_type: z.string().optional().describe("Filter by audit event type"),
|
|
132
|
+
evidence_level: z.string().optional().describe("Filter by evidence level"),
|
|
133
|
+
status: z.enum(["success", "error"]).optional().describe("Filter by status"),
|
|
134
|
+
session_id: z.string().optional().describe("Filter by session id"),
|
|
135
|
+
limit: z.number().int().positive().max(500).default(50),
|
|
136
|
+
}),
|
|
137
|
+
}, async (input) => {
|
|
138
|
+
if (config.rbac.enabled && !checkAccess(role, "audit.query")) {
|
|
139
|
+
return accessDenied(role, "audit.query");
|
|
140
|
+
}
|
|
141
|
+
const parsed = z.object({
|
|
142
|
+
from: z.string().optional(),
|
|
143
|
+
to: z.string().optional(),
|
|
144
|
+
tool: z.string().optional(),
|
|
145
|
+
event_type: z.string().optional(),
|
|
146
|
+
evidence_level: z.string().optional(),
|
|
147
|
+
status: z.enum(["success", "error"]).optional(),
|
|
148
|
+
session_id: z.string().optional(),
|
|
149
|
+
limit: z.number().int().positive().max(500).default(50),
|
|
150
|
+
}).parse(input ?? {});
|
|
151
|
+
const entries = queryAuditLog(contextDir, parsed);
|
|
152
|
+
recordToolActivity({
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
tool: "audit.query",
|
|
155
|
+
input: parsed,
|
|
156
|
+
result_count: entries.length,
|
|
157
|
+
entities_returned: [],
|
|
158
|
+
rules_applied: [],
|
|
159
|
+
duration_ms: 0,
|
|
160
|
+
event_type: "tool_call",
|
|
161
|
+
evidence_level: "diagnostic",
|
|
162
|
+
resource_type: "audit",
|
|
163
|
+
});
|
|
164
|
+
return buildToolResult({
|
|
165
|
+
count: entries.length,
|
|
166
|
+
entries,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ── policy.list ──
|
|
170
|
+
server.registerTool("policy.list", {
|
|
171
|
+
description: "List all active policies (org + local merged). Org rules override local rules with same ID.",
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
source: z.enum(["all", "org", "local"]).default("all").describe("Filter by policy source"),
|
|
174
|
+
}),
|
|
175
|
+
}, async (input) => {
|
|
176
|
+
if (config.rbac.enabled && !checkAccess(role, "policy.list")) {
|
|
177
|
+
return accessDenied(role, "policy.list");
|
|
178
|
+
}
|
|
179
|
+
const parsed = z.object({
|
|
180
|
+
source: z.enum(["all", "org", "local"]).default("all"),
|
|
181
|
+
}).parse(input ?? {});
|
|
182
|
+
let policies = policyStore.getMergedPolicies();
|
|
183
|
+
if (parsed.source !== "all") {
|
|
184
|
+
policies = policies.filter(p => p.source === parsed.source);
|
|
185
|
+
}
|
|
186
|
+
recordToolActivity({
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
tool: "policy.list",
|
|
189
|
+
input: parsed,
|
|
190
|
+
result_count: policies.length,
|
|
191
|
+
entities_returned: policies.map(p => p.id),
|
|
192
|
+
rules_applied: [],
|
|
193
|
+
duration_ms: 0,
|
|
194
|
+
event_type: "tool_call",
|
|
195
|
+
evidence_level: "diagnostic",
|
|
196
|
+
resource_type: "policy",
|
|
197
|
+
});
|
|
198
|
+
return buildToolResult({
|
|
199
|
+
count: policies.length,
|
|
200
|
+
policies: policies.map(p => ({
|
|
201
|
+
id: p.id,
|
|
202
|
+
description: p.description,
|
|
203
|
+
priority: p.priority,
|
|
204
|
+
scope: p.scope,
|
|
205
|
+
enforce: p.enforce,
|
|
206
|
+
source: p.source,
|
|
207
|
+
})),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
// ── policy.sync ──
|
|
211
|
+
server.registerTool("policy.sync", {
|
|
212
|
+
description: "Trigger manual policy sync. Connected: pulls from cloud API. Air-gapped: reloads local org-rules.yaml.",
|
|
213
|
+
inputSchema: z.object({}),
|
|
214
|
+
}, async () => {
|
|
215
|
+
if (config.rbac.enabled && !checkAccess(role, "policy.sync")) {
|
|
216
|
+
return accessDenied(role, "policy.sync");
|
|
217
|
+
}
|
|
218
|
+
let result;
|
|
219
|
+
if (config.policy.endpoint && config.policy.api_key) {
|
|
220
|
+
result = await syncFromCloud(config.policy.endpoint, config.policy.api_key, policyStore);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
result = syncFromLocal(policyStore);
|
|
224
|
+
}
|
|
225
|
+
recordToolActivity({
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
tool: "policy.sync",
|
|
228
|
+
input: {},
|
|
229
|
+
result_count: result.synced,
|
|
230
|
+
entities_returned: [],
|
|
231
|
+
rules_applied: [],
|
|
232
|
+
duration_ms: 0,
|
|
233
|
+
event_type: "policy_sync",
|
|
234
|
+
evidence_level: "required",
|
|
235
|
+
resource_type: "policy",
|
|
236
|
+
metadata: { synced: result.synced },
|
|
237
|
+
});
|
|
238
|
+
return buildToolResult(result);
|
|
239
|
+
});
|
|
240
|
+
// ── enterprise.status ──
|
|
241
|
+
server.registerTool("enterprise.status", {
|
|
242
|
+
description: "Return Cortex Enterprise overview: version, feature status, and policy health.",
|
|
243
|
+
inputSchema: z.object({}),
|
|
244
|
+
}, async () => {
|
|
245
|
+
if (config.rbac.enabled && !checkAccess(role, "enterprise.status")) {
|
|
246
|
+
return accessDenied(role, "enterprise.status");
|
|
247
|
+
}
|
|
248
|
+
const lastSyncResult = getLastSync();
|
|
249
|
+
const policies = policyStore.getMergedPolicies();
|
|
250
|
+
recordToolActivity({
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
tool: "enterprise.status",
|
|
253
|
+
input: {},
|
|
254
|
+
result_count: policies.length,
|
|
255
|
+
entities_returned: [],
|
|
256
|
+
rules_applied: [],
|
|
257
|
+
duration_ms: 0,
|
|
258
|
+
event_type: "tool_call",
|
|
259
|
+
evidence_level: "diagnostic",
|
|
260
|
+
resource_type: "policy",
|
|
261
|
+
});
|
|
262
|
+
return buildToolResult({
|
|
263
|
+
edition: "enterprise",
|
|
264
|
+
version,
|
|
265
|
+
features: {
|
|
266
|
+
telemetry: config.telemetry.enabled ? "active" : "disabled",
|
|
267
|
+
policy_sync: config.policy.enabled ? "active" : "disabled",
|
|
268
|
+
audit_log: config.audit.enabled ? "active" : "disabled",
|
|
269
|
+
rbac: config.rbac.enabled ? `active (role: ${role})` : "disabled",
|
|
270
|
+
},
|
|
271
|
+
policies: {
|
|
272
|
+
total: policies.length,
|
|
273
|
+
org: policies.filter(p => p.source === "org").length,
|
|
274
|
+
local: policies.filter(p => p.source === "local").length,
|
|
275
|
+
last_sync: lastSyncResult,
|
|
276
|
+
},
|
|
277
|
+
workflow: loadWorkflowState(contextDir),
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
// ── workflow.status ──
|
|
281
|
+
server.registerTool("workflow.status", {
|
|
282
|
+
description: "Return the governed workflow state persisted in .context/workflow/state.json.",
|
|
283
|
+
inputSchema: z.object({}),
|
|
284
|
+
}, async () => {
|
|
285
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.status")) {
|
|
286
|
+
return accessDenied(role, "workflow.status");
|
|
287
|
+
}
|
|
288
|
+
const state = loadWorkflowState(contextDir);
|
|
289
|
+
recordToolActivity({
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
tool: "workflow.status",
|
|
292
|
+
input: {},
|
|
293
|
+
result_count: 1,
|
|
294
|
+
entities_returned: [],
|
|
295
|
+
rules_applied: [],
|
|
296
|
+
duration_ms: 0,
|
|
297
|
+
event_type: "tool_call",
|
|
298
|
+
evidence_level: "diagnostic",
|
|
299
|
+
resource_type: "workflow",
|
|
300
|
+
metadata: {
|
|
301
|
+
phase: state.phase,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
return buildToolResult(state);
|
|
305
|
+
});
|
|
306
|
+
// ── workflow.plan ──
|
|
307
|
+
server.registerTool("workflow.plan", {
|
|
308
|
+
description: "Create or update the implementation plan. Resets approval until the plan is reviewed again.",
|
|
309
|
+
inputSchema: z.object({
|
|
310
|
+
title: z.string().min(1).max(200),
|
|
311
|
+
summary: z.string().min(1).max(5000),
|
|
312
|
+
tasks: z.array(z.string().min(1).max(500)).max(50).default([]),
|
|
313
|
+
}),
|
|
314
|
+
}, async (input) => {
|
|
315
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.plan")) {
|
|
316
|
+
return accessDenied(role, "workflow.plan");
|
|
317
|
+
}
|
|
318
|
+
const parsed = z.object({
|
|
319
|
+
title: z.string().min(1).max(200),
|
|
320
|
+
summary: z.string().min(1).max(5000),
|
|
321
|
+
tasks: z.array(z.string().min(1).max(500)).max(50).default([]),
|
|
322
|
+
}).parse(input ?? {});
|
|
323
|
+
const state = setWorkflowPlan(contextDir, parsed);
|
|
324
|
+
await pushWorkflowStateIfConfigured(config, state);
|
|
325
|
+
recordToolActivity({
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
tool: "workflow.plan",
|
|
328
|
+
input: parsed,
|
|
329
|
+
result_count: state.plan.tasks.length,
|
|
330
|
+
entities_returned: [],
|
|
331
|
+
rules_applied: [],
|
|
332
|
+
duration_ms: 0,
|
|
333
|
+
event_type: "workflow_transition",
|
|
334
|
+
evidence_level: "required",
|
|
335
|
+
resource_type: "workflow",
|
|
336
|
+
metadata: {
|
|
337
|
+
phase: state.phase,
|
|
338
|
+
plan_status: state.plan.status,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return buildToolResult(state);
|
|
342
|
+
});
|
|
343
|
+
// ── workflow.review_plan ──
|
|
344
|
+
server.registerTool("workflow.review_plan", {
|
|
345
|
+
description: "Review and approve or reject the current plan before implementation starts.",
|
|
346
|
+
inputSchema: z.object({
|
|
347
|
+
approved: z.boolean(),
|
|
348
|
+
notes: z.string().max(5000).optional(),
|
|
349
|
+
}),
|
|
350
|
+
}, async (input) => {
|
|
351
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.review_plan")) {
|
|
352
|
+
return accessDenied(role, "workflow.review_plan");
|
|
353
|
+
}
|
|
354
|
+
const parsed = z.object({
|
|
355
|
+
approved: z.boolean(),
|
|
356
|
+
notes: z.string().max(5000).optional(),
|
|
357
|
+
}).parse(input ?? {});
|
|
358
|
+
const result = reviewWorkflowPlan(contextDir, parsed);
|
|
359
|
+
if (!result.ok) {
|
|
360
|
+
return buildToolResult({
|
|
361
|
+
error: result.error,
|
|
362
|
+
workflow: result.state,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
await pushWorkflowStateIfConfigured(config, result.state);
|
|
366
|
+
recordToolActivity({
|
|
367
|
+
timestamp: new Date().toISOString(),
|
|
368
|
+
tool: "workflow.review_plan",
|
|
369
|
+
input: parsed,
|
|
370
|
+
result_count: 1,
|
|
371
|
+
entities_returned: [],
|
|
372
|
+
rules_applied: [],
|
|
373
|
+
duration_ms: 0,
|
|
374
|
+
event_type: "workflow_transition",
|
|
375
|
+
evidence_level: "required",
|
|
376
|
+
resource_type: "workflow",
|
|
377
|
+
metadata: {
|
|
378
|
+
approved: parsed.approved,
|
|
379
|
+
phase: result.state.phase,
|
|
380
|
+
plan_status: result.state.plan.status,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return buildToolResult(result.state);
|
|
384
|
+
});
|
|
385
|
+
// ── workflow.start ──
|
|
386
|
+
server.registerTool("workflow.start", {
|
|
387
|
+
description: "Mark the workflow as actively implementing after the plan has been approved.",
|
|
388
|
+
inputSchema: z.object({}),
|
|
389
|
+
}, async () => {
|
|
390
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.start")) {
|
|
391
|
+
return accessDenied(role, "workflow.start");
|
|
392
|
+
}
|
|
393
|
+
const result = startWorkflowImplementation(contextDir);
|
|
394
|
+
if (!result.ok) {
|
|
395
|
+
return buildToolResult({
|
|
396
|
+
error: result.error,
|
|
397
|
+
workflow: result.state,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
await pushWorkflowStateIfConfigured(config, result.state);
|
|
401
|
+
recordToolActivity({
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
tool: "workflow.start",
|
|
404
|
+
input: {},
|
|
405
|
+
result_count: 1,
|
|
406
|
+
entities_returned: [],
|
|
407
|
+
rules_applied: [],
|
|
408
|
+
duration_ms: 0,
|
|
409
|
+
event_type: "workflow_transition",
|
|
410
|
+
evidence_level: "required",
|
|
411
|
+
resource_type: "workflow",
|
|
412
|
+
metadata: {
|
|
413
|
+
phase: result.state.phase,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
return buildToolResult(result.state);
|
|
417
|
+
});
|
|
418
|
+
// ── workflow.update ──
|
|
419
|
+
server.registerTool("workflow.update", {
|
|
420
|
+
description: "Record an implementation or iteration update without mutating the plan itself.",
|
|
421
|
+
inputSchema: z.object({
|
|
422
|
+
summary: z.string().min(1).max(5000),
|
|
423
|
+
phase: z.enum(["implementation", "iterating", "plan_review"]).optional(),
|
|
424
|
+
}),
|
|
425
|
+
}, async (input) => {
|
|
426
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.update")) {
|
|
427
|
+
return accessDenied(role, "workflow.update");
|
|
428
|
+
}
|
|
429
|
+
const parsed = z.object({
|
|
430
|
+
summary: z.string().min(1).max(5000),
|
|
431
|
+
phase: z.enum(["implementation", "iterating", "plan_review"]).optional(),
|
|
432
|
+
}).parse(input ?? {});
|
|
433
|
+
const state = recordWorkflowUpdate(contextDir, parsed);
|
|
434
|
+
await pushWorkflowStateIfConfigured(config, state);
|
|
435
|
+
recordToolActivity({
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
tool: "workflow.update",
|
|
438
|
+
input: parsed,
|
|
439
|
+
result_count: 1,
|
|
440
|
+
entities_returned: [],
|
|
441
|
+
rules_applied: [],
|
|
442
|
+
duration_ms: 0,
|
|
443
|
+
event_type: "workflow_transition",
|
|
444
|
+
evidence_level: "required",
|
|
445
|
+
resource_type: "workflow",
|
|
446
|
+
metadata: {
|
|
447
|
+
phase: state.phase,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
return buildToolResult(state);
|
|
451
|
+
});
|
|
452
|
+
// ── workflow.note ──
|
|
453
|
+
server.registerTool("workflow.note", {
|
|
454
|
+
description: "Persist a durable workflow note in .context/workflow/state.json.",
|
|
455
|
+
inputSchema: z.object({
|
|
456
|
+
title: z.string().min(1).max(200),
|
|
457
|
+
details: z.string().min(1).max(5000),
|
|
458
|
+
}),
|
|
459
|
+
}, async (input) => {
|
|
460
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.note")) {
|
|
461
|
+
return accessDenied(role, "workflow.note");
|
|
462
|
+
}
|
|
463
|
+
const parsed = z.object({
|
|
464
|
+
title: z.string().min(1).max(200),
|
|
465
|
+
details: z.string().min(1).max(5000),
|
|
466
|
+
}).parse(input ?? {});
|
|
467
|
+
const state = addWorkflowNote(contextDir, parsed);
|
|
468
|
+
await pushWorkflowStateIfConfigured(config, state);
|
|
469
|
+
recordToolActivity({
|
|
470
|
+
timestamp: new Date().toISOString(),
|
|
471
|
+
tool: "workflow.note",
|
|
472
|
+
input: parsed,
|
|
473
|
+
result_count: state.notes.length,
|
|
474
|
+
entities_returned: [],
|
|
475
|
+
rules_applied: [],
|
|
476
|
+
duration_ms: 0,
|
|
477
|
+
event_type: "workflow_transition",
|
|
478
|
+
evidence_level: "required",
|
|
479
|
+
resource_type: "workflow",
|
|
480
|
+
metadata: {
|
|
481
|
+
note_count: state.notes.length,
|
|
482
|
+
phase: state.phase,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
return buildToolResult(state);
|
|
486
|
+
});
|
|
487
|
+
// ── workflow.todo ──
|
|
488
|
+
server.registerTool("workflow.todo", {
|
|
489
|
+
description: "Add or complete workflow TODOs persisted in .context.",
|
|
490
|
+
inputSchema: z.object({
|
|
491
|
+
action: z.enum(["add", "complete"]),
|
|
492
|
+
id: z.number().int().positive().optional(),
|
|
493
|
+
title: z.string().min(1).max(200).optional(),
|
|
494
|
+
details: z.string().max(5000).optional(),
|
|
495
|
+
}),
|
|
496
|
+
}, async (input) => {
|
|
497
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.todo")) {
|
|
498
|
+
return accessDenied(role, "workflow.todo");
|
|
499
|
+
}
|
|
500
|
+
const parsed = z.object({
|
|
501
|
+
action: z.enum(["add", "complete"]),
|
|
502
|
+
id: z.number().int().positive().optional(),
|
|
503
|
+
title: z.string().min(1).max(200).optional(),
|
|
504
|
+
details: z.string().max(5000).optional(),
|
|
505
|
+
}).parse(input ?? {});
|
|
506
|
+
if (parsed.action === "add") {
|
|
507
|
+
if (!parsed.title) {
|
|
508
|
+
return buildToolResult({ error: "title is required when action=add" });
|
|
509
|
+
}
|
|
510
|
+
const state = addWorkflowTodo(contextDir, {
|
|
511
|
+
title: parsed.title,
|
|
512
|
+
details: parsed.details,
|
|
513
|
+
});
|
|
514
|
+
await pushWorkflowStateIfConfigured(config, state);
|
|
515
|
+
recordToolActivity({
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
tool: "workflow.todo",
|
|
518
|
+
input: parsed,
|
|
519
|
+
result_count: state.todos.length,
|
|
520
|
+
entities_returned: [],
|
|
521
|
+
rules_applied: [],
|
|
522
|
+
duration_ms: 0,
|
|
523
|
+
event_type: "workflow_transition",
|
|
524
|
+
evidence_level: "required",
|
|
525
|
+
resource_type: "workflow",
|
|
526
|
+
metadata: {
|
|
527
|
+
action: "add",
|
|
528
|
+
open_todos: state.todos.filter((todo) => todo.status === "open").length,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
return buildToolResult(state);
|
|
532
|
+
}
|
|
533
|
+
if (!parsed.id) {
|
|
534
|
+
return buildToolResult({ error: "id is required when action=complete" });
|
|
535
|
+
}
|
|
536
|
+
const result = completeWorkflowTodo(contextDir, parsed.id);
|
|
537
|
+
if (!result.ok) {
|
|
538
|
+
return buildToolResult({
|
|
539
|
+
error: result.error,
|
|
540
|
+
workflow: result.state,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
await pushWorkflowStateIfConfigured(config, result.state);
|
|
544
|
+
recordToolActivity({
|
|
545
|
+
timestamp: new Date().toISOString(),
|
|
546
|
+
tool: "workflow.todo",
|
|
547
|
+
input: parsed,
|
|
548
|
+
result_count: result.state.todos.length,
|
|
549
|
+
entities_returned: [],
|
|
550
|
+
rules_applied: [],
|
|
551
|
+
duration_ms: 0,
|
|
552
|
+
event_type: "workflow_transition",
|
|
553
|
+
evidence_level: "required",
|
|
554
|
+
resource_type: "workflow",
|
|
555
|
+
metadata: {
|
|
556
|
+
action: "complete",
|
|
557
|
+
completed_id: parsed.id,
|
|
558
|
+
open_todos: result.state.todos.filter((todo) => todo.status === "open").length,
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
return buildToolResult(result.state);
|
|
562
|
+
});
|
|
563
|
+
// ── workflow.approve ──
|
|
564
|
+
server.registerTool("workflow.approve", {
|
|
565
|
+
description: "Approve the workflow only after the plan is approved and the latest code review passes.",
|
|
566
|
+
inputSchema: z.object({
|
|
567
|
+
notes: z.string().max(5000).optional(),
|
|
568
|
+
}),
|
|
569
|
+
}, async (input) => {
|
|
570
|
+
if (config.rbac.enabled && !checkAccess(role, "workflow.approve")) {
|
|
571
|
+
return accessDenied(role, "workflow.approve");
|
|
572
|
+
}
|
|
573
|
+
const parsed = z.object({
|
|
574
|
+
notes: z.string().max(5000).optional(),
|
|
575
|
+
}).parse(input ?? {});
|
|
576
|
+
const result = approveWorkflow(contextDir, parsed.notes);
|
|
577
|
+
if (!result.ok) {
|
|
578
|
+
return buildToolResult({
|
|
579
|
+
error: result.error,
|
|
580
|
+
workflow: result.state,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
await pushWorkflowStateIfConfigured(config, result.state);
|
|
584
|
+
recordToolActivity({
|
|
585
|
+
timestamp: new Date().toISOString(),
|
|
586
|
+
tool: "workflow.approve",
|
|
587
|
+
input: parsed,
|
|
588
|
+
result_count: 1,
|
|
589
|
+
entities_returned: [],
|
|
590
|
+
rules_applied: [],
|
|
591
|
+
duration_ms: 0,
|
|
592
|
+
event_type: "approval",
|
|
593
|
+
evidence_level: "required",
|
|
594
|
+
resource_type: "workflow",
|
|
595
|
+
metadata: {
|
|
596
|
+
approval_status: result.state.approval.status,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
return buildToolResult(result.state);
|
|
600
|
+
});
|
|
601
|
+
// ── security.scan ──
|
|
602
|
+
server.registerTool("security.scan", {
|
|
603
|
+
description: "Scan text for prompt injection attempts. Returns a risk score and matched patterns. " +
|
|
604
|
+
"Only active when the prompt-injection-defense policy is enforced.",
|
|
605
|
+
inputSchema: z.object({
|
|
606
|
+
text: z.string().min(1).max(50_000).describe("Text to scan for prompt injection"),
|
|
607
|
+
file_path: z.string().max(500).optional().describe("Source file path (for violation reporting)"),
|
|
608
|
+
}),
|
|
609
|
+
}, async (input) => {
|
|
610
|
+
if (config.rbac.enabled && !checkAccess(role, "policy.list")) {
|
|
611
|
+
return accessDenied(role, "security.scan");
|
|
612
|
+
}
|
|
613
|
+
const parsed = z.object({
|
|
614
|
+
text: z.string().min(1).max(50_000),
|
|
615
|
+
file_path: z.string().max(500).optional(),
|
|
616
|
+
}).parse(input ?? {});
|
|
617
|
+
const policies = policyStore.getMergedPolicies();
|
|
618
|
+
const result = enforceInjectionPolicy(parsed.text, policies, { sanitize: true });
|
|
619
|
+
// Queue violation for push to cortex-web
|
|
620
|
+
if (!result.allowed && result.scan.matches.length > 0) {
|
|
621
|
+
const violation = buildViolationPayload(result.scan.matches, {
|
|
622
|
+
filePath: parsed.file_path,
|
|
623
|
+
});
|
|
624
|
+
queueViolation(violation);
|
|
625
|
+
}
|
|
626
|
+
const rulesApplied = result.allowed ? [] : [result.ruleId];
|
|
627
|
+
recordToolActivity({
|
|
628
|
+
timestamp: new Date().toISOString(),
|
|
629
|
+
tool: "security.scan",
|
|
630
|
+
input: { text_length: parsed.text.length, file_path: parsed.file_path },
|
|
631
|
+
result_count: result.scan.matches.length,
|
|
632
|
+
entities_returned: [],
|
|
633
|
+
rules_applied: rulesApplied,
|
|
634
|
+
duration_ms: 0,
|
|
635
|
+
event_type: "security_scan",
|
|
636
|
+
evidence_level: result.allowed ? "diagnostic" : "required",
|
|
637
|
+
resource_type: "policy",
|
|
638
|
+
metadata: {
|
|
639
|
+
flagged: result.scan.flagged,
|
|
640
|
+
score: result.scan.score,
|
|
641
|
+
allowed: result.allowed,
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
return buildToolResult({
|
|
645
|
+
flagged: result.scan.flagged,
|
|
646
|
+
score: result.scan.score,
|
|
647
|
+
allowed: result.allowed,
|
|
648
|
+
policy_active: !result.allowed || result.scan.score > 0 ? true : policies.some(p => p.id === "prompt-injection-defense" && p.enforce),
|
|
649
|
+
matches: result.scan.matches.map((m) => ({
|
|
650
|
+
pattern: m.pattern,
|
|
651
|
+
category: m.category,
|
|
652
|
+
matched: m.matched,
|
|
653
|
+
position: m.position,
|
|
654
|
+
weight: m.weight,
|
|
655
|
+
})),
|
|
656
|
+
sanitized: result.sanitized ?? null,
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
// ── context.review ──
|
|
660
|
+
server.registerTool("context.review", {
|
|
661
|
+
description: "Run enterprise policy validators against the current project. " +
|
|
662
|
+
"Checks enforced policies (test coverage, file size, external API calls, code review) " +
|
|
663
|
+
"and returns pass/fail results plus non-blocking repo-local pattern evidence.",
|
|
664
|
+
inputSchema: z.object({
|
|
665
|
+
scope: z.enum(["all", "changed"]).default("changed")
|
|
666
|
+
.describe("'changed' validates only git-modified files; 'all' validates everything"),
|
|
667
|
+
include_passed: z.boolean().default(true)
|
|
668
|
+
.describe("Include passing validators in results"),
|
|
669
|
+
include_pattern_evidence: z.boolean().default(true)
|
|
670
|
+
.describe("Include bounded, local-only repo pattern context without changing pass/fail"),
|
|
671
|
+
pattern_query: z.string().trim().min(1).max(1000).optional()
|
|
672
|
+
.describe("Optional shared pattern query; omitted queries are derived per target"),
|
|
673
|
+
pattern_top_k: z.number().int().min(1).max(5).default(2)
|
|
674
|
+
.describe("Maximum evidence items returned per locality tier and target"),
|
|
675
|
+
pattern_limit: z.number().int().min(1).max(25).default(10)
|
|
676
|
+
.describe("Maximum review targets analyzed for pattern evidence"),
|
|
677
|
+
}),
|
|
678
|
+
}, async (input) => {
|
|
679
|
+
if (config.rbac.enabled && !checkAccess(role, "context.review")) {
|
|
680
|
+
return accessDenied(role, "context.review");
|
|
681
|
+
}
|
|
682
|
+
const parsed = z.object({
|
|
683
|
+
scope: z.enum(["all", "changed"]).default("changed"),
|
|
684
|
+
include_passed: z.boolean().default(true),
|
|
685
|
+
include_pattern_evidence: z.boolean().default(true),
|
|
686
|
+
pattern_query: z.string().trim().min(1).max(1000).optional(),
|
|
687
|
+
pattern_top_k: z.number().int().min(1).max(5).default(2),
|
|
688
|
+
pattern_limit: z.number().int().min(1).max(25).default(10),
|
|
689
|
+
}).parse(input ?? {});
|
|
690
|
+
const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
691
|
+
// Resolve the file set for this review.
|
|
692
|
+
//
|
|
693
|
+
// scope=changed (default): ask git for the diff. If the working copy
|
|
694
|
+
// is not a git repo — or git otherwise fails — fall back to
|
|
695
|
+
// walking the project so the review doesn't silently pass
|
|
696
|
+
// everything with an empty file list (pre-0.9.1 regression).
|
|
697
|
+
//
|
|
698
|
+
// scope=all: always walk the project. Explicit opt-in for whole-
|
|
699
|
+
// project review; no git dependency.
|
|
700
|
+
let changedFiles;
|
|
701
|
+
if (parsed.scope === "changed") {
|
|
702
|
+
changedFiles = resolveChangedReviewFiles(projectRoot) ?? walkProjectFiles(projectRoot);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
changedFiles = walkProjectFiles(projectRoot);
|
|
706
|
+
}
|
|
707
|
+
// Build enforced policies list, carrying type + config so the
|
|
708
|
+
// engine can dispatch to generic evaluators for cortex-web custom
|
|
709
|
+
// rules. Predefined rules leave type/config null and route to the
|
|
710
|
+
// name-based validator registry.
|
|
711
|
+
const policies = policyStore.getMergedPolicies();
|
|
712
|
+
const { enforced, skipped: skippedPolicies, } = partitionReviewPolicies(policies);
|
|
713
|
+
const now = new Date().toISOString();
|
|
714
|
+
const output = await runValidators(enforced, {
|
|
715
|
+
contextDir,
|
|
716
|
+
projectRoot,
|
|
717
|
+
changedFiles,
|
|
718
|
+
}, config.validators);
|
|
719
|
+
const patternReview = await buildPatternReviewContext({
|
|
720
|
+
files: changedFiles ?? [],
|
|
721
|
+
enabled: parsed.include_pattern_evidence,
|
|
722
|
+
query: parsed.pattern_query,
|
|
723
|
+
topK: parsed.pattern_top_k,
|
|
724
|
+
limit: parsed.pattern_limit,
|
|
725
|
+
});
|
|
726
|
+
const patternSummary = patternReview.summary;
|
|
727
|
+
// Filter out passed if requested
|
|
728
|
+
const results = parsed.include_passed
|
|
729
|
+
? output.results
|
|
730
|
+
: output.results.filter((r) => !r.pass);
|
|
731
|
+
// Queue failures as violations
|
|
732
|
+
for (const r of output.results) {
|
|
733
|
+
if (!r.pass) {
|
|
734
|
+
queueViolation({
|
|
735
|
+
rule_id: r.policy_id,
|
|
736
|
+
severity: r.severity,
|
|
737
|
+
message: r.message.slice(0, 2000),
|
|
738
|
+
metadata: r.detail ? JSON.stringify({ detail: r.detail }).slice(0, 5000) : undefined,
|
|
739
|
+
occurred_at: now,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
queueReviewResult({
|
|
743
|
+
policy_id: r.policy_id,
|
|
744
|
+
pass: r.pass,
|
|
745
|
+
severity: r.severity,
|
|
746
|
+
message: r.message,
|
|
747
|
+
detail: r.detail,
|
|
748
|
+
reviewed_at: now,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
recordToolActivity({
|
|
752
|
+
timestamp: now,
|
|
753
|
+
tool: "context.review",
|
|
754
|
+
input: buildContextReviewAuditInput(parsed),
|
|
755
|
+
result_count: output.results.length,
|
|
756
|
+
entities_returned: output.results.map((r) => r.policy_id),
|
|
757
|
+
rules_applied: output.results.filter((r) => !r.pass).map((r) => r.policy_id),
|
|
758
|
+
duration_ms: 0,
|
|
759
|
+
event_type: "review_result",
|
|
760
|
+
evidence_level: "required",
|
|
761
|
+
resource_type: "review",
|
|
762
|
+
metadata: {
|
|
763
|
+
scope: parsed.scope,
|
|
764
|
+
passed: output.summary.passed,
|
|
765
|
+
failed: output.summary.failed,
|
|
766
|
+
warnings: output.results.filter((r) => !r.pass && r.severity === "warning").length,
|
|
767
|
+
skipped: skippedPolicies.length,
|
|
768
|
+
pattern_analyzed: patternSummary.analyzed ?? 0,
|
|
769
|
+
pattern_local_evidence: patternSummary.local_evidence ?? 0,
|
|
770
|
+
pattern_repo_fallback: patternSummary.repo_fallback ?? 0,
|
|
771
|
+
pattern_omitted: patternSummary.omitted ?? 0,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
const reviewedFiles = snapshotReviewedFiles(projectRoot, changedFiles);
|
|
775
|
+
const workflowState = recordWorkflowReview(contextDir, {
|
|
776
|
+
scope: parsed.scope,
|
|
777
|
+
output,
|
|
778
|
+
reviewed_files: reviewedFiles,
|
|
779
|
+
});
|
|
780
|
+
const lastReview = workflowState.last_review;
|
|
781
|
+
const reviewedAt = lastReview?.reviewed_at ?? now;
|
|
782
|
+
if (lastReview) {
|
|
783
|
+
writeFileSync(join(contextDir, "review-status.json"), `${JSON.stringify({
|
|
784
|
+
reviewed: lastReview.status === "passed",
|
|
785
|
+
reviewer: "context.review",
|
|
786
|
+
timestamp: reviewedAt,
|
|
787
|
+
scope: parsed.scope,
|
|
788
|
+
reviewed_files: reviewedFiles,
|
|
789
|
+
}, null, 2)}\n`, "utf8");
|
|
790
|
+
}
|
|
791
|
+
const trustState = recordQueuedReviewTrustState({
|
|
792
|
+
contextDir,
|
|
793
|
+
projectRoot,
|
|
794
|
+
repo: getReviewPushContext().repo,
|
|
795
|
+
instance_id: getReviewPushContext().instance_id,
|
|
796
|
+
session_id: getReviewPushContext().session_id,
|
|
797
|
+
task_id: process.env.CORTEX_ACTIVE_TASK_ID?.trim() || undefined,
|
|
798
|
+
}, {
|
|
799
|
+
reviewedAt,
|
|
800
|
+
summary: {
|
|
801
|
+
total: output.summary.total,
|
|
802
|
+
passed: output.summary.passed,
|
|
803
|
+
failed: output.summary.failed,
|
|
804
|
+
warnings: output.summary.warnings,
|
|
805
|
+
skipped: skippedPolicies.length,
|
|
806
|
+
},
|
|
807
|
+
skippedPolicies: skippedPolicies,
|
|
808
|
+
});
|
|
809
|
+
const workflowWithTrust = loadWorkflowState(contextDir);
|
|
810
|
+
await pushWorkflowStateIfConfigured(config, workflowWithTrust);
|
|
811
|
+
return buildToolResult({
|
|
812
|
+
scope: parsed.scope,
|
|
813
|
+
results,
|
|
814
|
+
skipped_policies: skippedPolicies,
|
|
815
|
+
summary: {
|
|
816
|
+
...output.summary,
|
|
817
|
+
skipped: skippedPolicies.length,
|
|
818
|
+
},
|
|
819
|
+
workflow: workflowWithTrust,
|
|
820
|
+
workflow_source: trustState.workflow.source,
|
|
821
|
+
delivery: trustState.delivery,
|
|
822
|
+
trust_warnings: trustState.trust_warnings,
|
|
823
|
+
pattern_review: patternReview,
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|