@danielblomma/cortex-mcp 2.0.16 → 2.0.18
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/README.md +5 -3
- package/package.json +1 -1
- package/scaffold/mcp/package-lock.json +59 -60
- package/scaffold/mcp/package.json +2 -2
- package/scaffold/mcp/src/cli/stage.ts +15 -18
- package/scaffold/mcp/src/core/validators/builtins.ts +24 -4
- package/scaffold/mcp/src/core/workflow/index.ts +1 -0
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
- package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
- package/scaffold/mcp/src/enterprise/index.ts +16 -1
- package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
- package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
- package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
- package/scaffold/mcp/tests/enterprise-review-trust.test.mjs +318 -0
- package/scaffold/mcp/tests/fixtures/org-skillz-contract.json +59 -0
- package/scaffold/mcp/tests/fixtures/review-trust-contract.json +111 -0
- package/scaffold/mcp/tests/review-policy-selection.test.mjs +85 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +167 -0
- package/scaffold/mcp/tests/review-validator-source.test.mjs +52 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +277 -0
- package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
- package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
- package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { OrgPolicy } from "../../core/policy/store.js";
|
|
2
|
+
import {
|
|
3
|
+
getGenericEvaluator,
|
|
4
|
+
getValidator,
|
|
5
|
+
type EnforcedPolicy,
|
|
6
|
+
} from "../../core/validators/engine.js";
|
|
7
|
+
import type { ReviewSkippedPolicy } from "./trust-state.js";
|
|
8
|
+
|
|
9
|
+
export const DEFERRED_CODE_REVIEW_REASON =
|
|
10
|
+
"Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.";
|
|
11
|
+
|
|
12
|
+
export type PartitionedReviewPolicies = {
|
|
13
|
+
enforced: EnforcedPolicy[];
|
|
14
|
+
skipped: ReviewSkippedPolicy[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function partitionReviewPolicies(
|
|
18
|
+
policies: OrgPolicy[],
|
|
19
|
+
): PartitionedReviewPolicies {
|
|
20
|
+
const enforced: EnforcedPolicy[] = [];
|
|
21
|
+
const skipped: ReviewSkippedPolicy[] = [];
|
|
22
|
+
|
|
23
|
+
for (const policy of policies) {
|
|
24
|
+
if (!policy.enforce) continue;
|
|
25
|
+
|
|
26
|
+
if (policy.id === "require-code-review") {
|
|
27
|
+
skipped.push({
|
|
28
|
+
policy_id: policy.id,
|
|
29
|
+
kind: policy.kind ?? null,
|
|
30
|
+
type: policy.type ?? null,
|
|
31
|
+
reason: DEFERRED_CODE_REVIEW_REASON,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (policy.type) {
|
|
37
|
+
if (!getGenericEvaluator(policy.type)) {
|
|
38
|
+
skipped.push({
|
|
39
|
+
policy_id: policy.id,
|
|
40
|
+
kind: policy.kind ?? null,
|
|
41
|
+
type: policy.type,
|
|
42
|
+
reason: `No evaluator registered for type "${policy.type}"`,
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
} else if (!getValidator(policy.id)) {
|
|
47
|
+
skipped.push({
|
|
48
|
+
policy_id: policy.id,
|
|
49
|
+
kind: policy.kind ?? null,
|
|
50
|
+
type: null,
|
|
51
|
+
reason: "No executable validator registered for this policy",
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
enforced.push({
|
|
57
|
+
id: policy.id,
|
|
58
|
+
type: policy.type ?? null,
|
|
59
|
+
config: policy.config ?? null,
|
|
60
|
+
severity: policy.severity ?? "block",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { enforced, skipped };
|
|
65
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recordReviewDeliveryStatus } from "./trust-state.js";
|
|
2
|
+
|
|
1
3
|
export type ReviewPushItem = {
|
|
2
4
|
policy_id: string;
|
|
3
5
|
pass: boolean;
|
|
@@ -11,12 +13,16 @@ export type ReviewPushContext = {
|
|
|
11
13
|
repo?: string;
|
|
12
14
|
instance_id?: string;
|
|
13
15
|
session_id?: string;
|
|
16
|
+
context_dir?: string;
|
|
17
|
+
project_root?: string;
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export type ReviewPushResult = {
|
|
17
21
|
success: boolean;
|
|
18
22
|
count: number;
|
|
19
23
|
error?: string;
|
|
24
|
+
attempted?: boolean;
|
|
25
|
+
delivery_status?: "accepted" | "failed";
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
const pending: ReviewPushItem[] = [];
|
|
@@ -26,6 +32,10 @@ export function setReviewPushContext(context: ReviewPushContext): void {
|
|
|
26
32
|
activeContext = { ...context };
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
export function getReviewPushContext(): ReviewPushContext {
|
|
36
|
+
return { ...activeContext };
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export function queueReviewResult(item: ReviewPushItem): void {
|
|
30
40
|
pending.push(item);
|
|
31
41
|
}
|
|
@@ -39,11 +49,18 @@ export async function pushReviewResults(
|
|
|
39
49
|
apiKey: string,
|
|
40
50
|
): Promise<ReviewPushResult> {
|
|
41
51
|
if (pending.length === 0) {
|
|
42
|
-
return { success: true, count: 0 };
|
|
52
|
+
return { success: true, count: 0, attempted: false };
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
const reviewsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/reviews/push`;
|
|
46
56
|
const batch = pending.splice(0, 100);
|
|
57
|
+
const attemptedAt = new Date().toISOString();
|
|
58
|
+
if (activeContext.context_dir) {
|
|
59
|
+
recordReviewDeliveryStatus(activeContext.context_dir, "pushed", {
|
|
60
|
+
reviewCount: batch.length,
|
|
61
|
+
attemptedAt,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
47
64
|
|
|
48
65
|
try {
|
|
49
66
|
const response = await fetch(reviewsUrl, {
|
|
@@ -64,16 +81,50 @@ export async function pushReviewResults(
|
|
|
64
81
|
|
|
65
82
|
if (!response.ok) {
|
|
66
83
|
pending.unshift(...batch);
|
|
67
|
-
|
|
84
|
+
if (activeContext.context_dir) {
|
|
85
|
+
recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
|
|
86
|
+
reviewCount: batch.length,
|
|
87
|
+
attemptedAt,
|
|
88
|
+
error: `HTTP ${response.status}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
count: 0,
|
|
94
|
+
error: `HTTP ${response.status}`,
|
|
95
|
+
attempted: true,
|
|
96
|
+
delivery_status: "failed",
|
|
97
|
+
};
|
|
68
98
|
}
|
|
69
99
|
|
|
70
|
-
|
|
100
|
+
if (activeContext.context_dir) {
|
|
101
|
+
recordReviewDeliveryStatus(activeContext.context_dir, "accepted", {
|
|
102
|
+
reviewCount: batch.length,
|
|
103
|
+
attemptedAt,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
count: batch.length,
|
|
109
|
+
attempted: true,
|
|
110
|
+
delivery_status: "accepted",
|
|
111
|
+
};
|
|
71
112
|
} catch (err) {
|
|
72
113
|
pending.unshift(...batch);
|
|
114
|
+
const error = err instanceof Error ? err.message : "unknown error";
|
|
115
|
+
if (activeContext.context_dir) {
|
|
116
|
+
recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
|
|
117
|
+
reviewCount: batch.length,
|
|
118
|
+
attemptedAt,
|
|
119
|
+
error,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
73
122
|
return {
|
|
74
123
|
success: false,
|
|
75
124
|
count: 0,
|
|
76
|
-
error
|
|
125
|
+
error,
|
|
126
|
+
attempted: true,
|
|
127
|
+
delivery_status: "failed",
|
|
77
128
|
};
|
|
78
129
|
}
|
|
79
130
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { setWorkflowReviewTrust } from "../workflow/state.js";
|
|
9
|
+
import { resolveWorkflowDefinition, type WorkflowSource } from "../../core/workflow/resolution.js";
|
|
10
|
+
|
|
11
|
+
export type ReviewDeliveryStatus =
|
|
12
|
+
| "never_attempted"
|
|
13
|
+
| "queued"
|
|
14
|
+
| "pushed"
|
|
15
|
+
| "accepted"
|
|
16
|
+
| "failed";
|
|
17
|
+
|
|
18
|
+
export type ReviewTrustWarning = {
|
|
19
|
+
code:
|
|
20
|
+
| "workflow-source-unverified"
|
|
21
|
+
| "workflow-unresolved"
|
|
22
|
+
| "review-delivery-pending"
|
|
23
|
+
| "review-delivery-failed"
|
|
24
|
+
| "policy-not-enforceable";
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ReviewSkippedPolicy = {
|
|
29
|
+
policy_id: string;
|
|
30
|
+
kind: string | null;
|
|
31
|
+
type: string | null;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ReviewTrustState = {
|
|
36
|
+
version: 1;
|
|
37
|
+
repo: string | null;
|
|
38
|
+
instance_id: string | null;
|
|
39
|
+
session_id: string | null;
|
|
40
|
+
workflow: {
|
|
41
|
+
id: string | null;
|
|
42
|
+
source: WorkflowSource | "unknown";
|
|
43
|
+
task_id: string | null;
|
|
44
|
+
};
|
|
45
|
+
last_review_at: string | null;
|
|
46
|
+
review_summary: {
|
|
47
|
+
total: number;
|
|
48
|
+
passed: number;
|
|
49
|
+
failed: number;
|
|
50
|
+
warnings: number;
|
|
51
|
+
skipped: number;
|
|
52
|
+
} | null;
|
|
53
|
+
skipped_policies: ReviewSkippedPolicy[];
|
|
54
|
+
delivery: {
|
|
55
|
+
status: ReviewDeliveryStatus;
|
|
56
|
+
review_count: number;
|
|
57
|
+
queued_at: string | null;
|
|
58
|
+
last_attempted_at: string | null;
|
|
59
|
+
last_pushed_at: string | null;
|
|
60
|
+
last_accepted_at: string | null;
|
|
61
|
+
last_error: string | null;
|
|
62
|
+
};
|
|
63
|
+
trust_warnings: ReviewTrustWarning[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type TrustContext = {
|
|
67
|
+
contextDir: string;
|
|
68
|
+
projectRoot: string;
|
|
69
|
+
repo?: string;
|
|
70
|
+
instance_id?: string;
|
|
71
|
+
session_id?: string;
|
|
72
|
+
task_id?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type ReviewSummaryInput = {
|
|
76
|
+
total: number;
|
|
77
|
+
passed: number;
|
|
78
|
+
failed: number;
|
|
79
|
+
warnings: number;
|
|
80
|
+
skipped: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type WorkflowInference = {
|
|
84
|
+
id: string | null;
|
|
85
|
+
source: WorkflowSource | "unknown";
|
|
86
|
+
task_id: string | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function trustStatePath(contextDir: string): string {
|
|
90
|
+
return join(contextDir, "review-trust.json");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function nowIso(): string {
|
|
94
|
+
return new Date().toISOString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function initialTrustState(): ReviewTrustState {
|
|
98
|
+
return {
|
|
99
|
+
version: 1,
|
|
100
|
+
repo: null,
|
|
101
|
+
instance_id: null,
|
|
102
|
+
session_id: null,
|
|
103
|
+
workflow: {
|
|
104
|
+
id: null,
|
|
105
|
+
source: "unknown",
|
|
106
|
+
task_id: null,
|
|
107
|
+
},
|
|
108
|
+
last_review_at: null,
|
|
109
|
+
review_summary: null,
|
|
110
|
+
skipped_policies: [],
|
|
111
|
+
delivery: {
|
|
112
|
+
status: "never_attempted",
|
|
113
|
+
review_count: 0,
|
|
114
|
+
queued_at: null,
|
|
115
|
+
last_attempted_at: null,
|
|
116
|
+
last_pushed_at: null,
|
|
117
|
+
last_accepted_at: null,
|
|
118
|
+
last_error: null,
|
|
119
|
+
},
|
|
120
|
+
trust_warnings: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureContextDir(contextDir: string): void {
|
|
125
|
+
mkdirSync(contextDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inferWorkflow(projectRoot: string, taskId?: string): WorkflowInference {
|
|
129
|
+
const normalizedTaskId = taskId?.trim();
|
|
130
|
+
if (!normalizedTaskId) {
|
|
131
|
+
return { id: null, source: "unknown", task_id: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const statePath = join(projectRoot, ".agents", normalizedTaskId, "state.json");
|
|
135
|
+
if (!existsSync(statePath)) {
|
|
136
|
+
return { id: null, source: "unknown", task_id: normalizedTaskId };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const raw = JSON.parse(readFileSync(statePath, "utf8")) as Record<string, unknown>;
|
|
141
|
+
const workflowId =
|
|
142
|
+
typeof raw.workflow_id === "string" ? raw.workflow_id : null;
|
|
143
|
+
if (!workflowId) {
|
|
144
|
+
return { id: null, source: "unknown", task_id: normalizedTaskId };
|
|
145
|
+
}
|
|
146
|
+
const resolved = resolveWorkflowDefinition(workflowId);
|
|
147
|
+
return {
|
|
148
|
+
id: workflowId,
|
|
149
|
+
source: resolved.source,
|
|
150
|
+
task_id: normalizedTaskId,
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return {
|
|
154
|
+
id: null,
|
|
155
|
+
source: "unknown",
|
|
156
|
+
task_id: normalizedTaskId,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function computeWarnings(state: ReviewTrustState): ReviewTrustWarning[] {
|
|
162
|
+
const warnings: ReviewTrustWarning[] = [];
|
|
163
|
+
|
|
164
|
+
if (!state.workflow.id) {
|
|
165
|
+
warnings.push({
|
|
166
|
+
code: "workflow-unresolved",
|
|
167
|
+
message:
|
|
168
|
+
"No active local workflow could be resolved for this review session, so workflow/review correlation is unproven.",
|
|
169
|
+
});
|
|
170
|
+
} else if (state.workflow.source !== "synced") {
|
|
171
|
+
warnings.push({
|
|
172
|
+
code: "workflow-source-unverified",
|
|
173
|
+
message:
|
|
174
|
+
`Workflow "${state.workflow.id}" is not proven to come from the synced org registry for this session.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (state.delivery.status === "queued" || state.delivery.status === "pushed") {
|
|
179
|
+
warnings.push({
|
|
180
|
+
code: "review-delivery-pending",
|
|
181
|
+
message:
|
|
182
|
+
"Local review results exist for this session, but Cortex has not yet confirmed acceptance by the control plane.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (state.delivery.status === "failed") {
|
|
187
|
+
warnings.push({
|
|
188
|
+
code: "review-delivery-failed",
|
|
189
|
+
message:
|
|
190
|
+
`Review delivery failed${state.delivery.last_error ? `: ${state.delivery.last_error}` : ""}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (state.skipped_policies.length > 0) {
|
|
195
|
+
warnings.push({
|
|
196
|
+
code: "policy-not-enforceable",
|
|
197
|
+
message:
|
|
198
|
+
`${state.skipped_policies.length} configured policy` +
|
|
199
|
+
`${state.skipped_policies.length === 1 ? " was" : "ies were"} not enforceable locally.`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return warnings;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function loadReviewTrustState(contextDir: string): ReviewTrustState {
|
|
207
|
+
ensureContextDir(contextDir);
|
|
208
|
+
const filePath = trustStatePath(contextDir);
|
|
209
|
+
if (!existsSync(filePath)) {
|
|
210
|
+
const initial = initialTrustState();
|
|
211
|
+
writeReviewTrustState(contextDir, initial);
|
|
212
|
+
return initial;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8")) as ReviewTrustState;
|
|
217
|
+
parsed.trust_warnings = computeWarnings(parsed);
|
|
218
|
+
return parsed;
|
|
219
|
+
} catch {
|
|
220
|
+
const initial = initialTrustState();
|
|
221
|
+
writeReviewTrustState(contextDir, initial);
|
|
222
|
+
return initial;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function writeReviewTrustState(
|
|
227
|
+
contextDir: string,
|
|
228
|
+
state: ReviewTrustState,
|
|
229
|
+
): ReviewTrustState {
|
|
230
|
+
ensureContextDir(contextDir);
|
|
231
|
+
state.trust_warnings = computeWarnings(state);
|
|
232
|
+
writeFileSync(trustStatePath(contextDir), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
233
|
+
setWorkflowReviewTrust(contextDir, state);
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function recordQueuedReviewTrustState(
|
|
238
|
+
context: TrustContext,
|
|
239
|
+
input: {
|
|
240
|
+
reviewedAt: string;
|
|
241
|
+
summary: ReviewSummaryInput;
|
|
242
|
+
skippedPolicies: ReviewSkippedPolicy[];
|
|
243
|
+
},
|
|
244
|
+
): ReviewTrustState {
|
|
245
|
+
const state = loadReviewTrustState(context.contextDir);
|
|
246
|
+
const workflow = inferWorkflow(context.projectRoot, context.task_id);
|
|
247
|
+
state.repo = context.repo ?? state.repo;
|
|
248
|
+
state.instance_id = context.instance_id ?? state.instance_id;
|
|
249
|
+
state.session_id = context.session_id ?? state.session_id;
|
|
250
|
+
state.workflow = workflow;
|
|
251
|
+
state.last_review_at = input.reviewedAt;
|
|
252
|
+
state.review_summary = {
|
|
253
|
+
total: input.summary.total,
|
|
254
|
+
passed: input.summary.passed,
|
|
255
|
+
failed: input.summary.failed,
|
|
256
|
+
warnings: input.summary.warnings,
|
|
257
|
+
skipped: input.summary.skipped,
|
|
258
|
+
};
|
|
259
|
+
state.skipped_policies = input.skippedPolicies;
|
|
260
|
+
const hasQueuedReviews = input.summary.total > 0;
|
|
261
|
+
state.delivery = {
|
|
262
|
+
status: hasQueuedReviews ? "queued" : "never_attempted",
|
|
263
|
+
review_count: input.summary.total,
|
|
264
|
+
queued_at: hasQueuedReviews ? input.reviewedAt : null,
|
|
265
|
+
last_attempted_at: null,
|
|
266
|
+
last_pushed_at: null,
|
|
267
|
+
last_accepted_at: null,
|
|
268
|
+
last_error: null,
|
|
269
|
+
};
|
|
270
|
+
return writeReviewTrustState(context.contextDir, state);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function recordReviewDeliveryStatus(
|
|
274
|
+
contextDir: string,
|
|
275
|
+
status: ReviewDeliveryStatus,
|
|
276
|
+
input: {
|
|
277
|
+
reviewCount?: number;
|
|
278
|
+
error?: string;
|
|
279
|
+
attemptedAt?: string;
|
|
280
|
+
} = {},
|
|
281
|
+
): ReviewTrustState {
|
|
282
|
+
const state = loadReviewTrustState(contextDir);
|
|
283
|
+
const at = input.attemptedAt ?? nowIso();
|
|
284
|
+
state.delivery.review_count = input.reviewCount ?? state.delivery.review_count;
|
|
285
|
+
state.delivery.last_attempted_at = at;
|
|
286
|
+
|
|
287
|
+
if (status === "pushed") {
|
|
288
|
+
state.delivery.status = "pushed";
|
|
289
|
+
state.delivery.last_pushed_at = at;
|
|
290
|
+
state.delivery.last_error = null;
|
|
291
|
+
} else if (status === "accepted") {
|
|
292
|
+
state.delivery.status = "accepted";
|
|
293
|
+
state.delivery.last_pushed_at = at;
|
|
294
|
+
state.delivery.last_accepted_at = at;
|
|
295
|
+
state.delivery.last_error = null;
|
|
296
|
+
} else if (status === "failed") {
|
|
297
|
+
state.delivery.status = "failed";
|
|
298
|
+
state.delivery.last_pushed_at = at;
|
|
299
|
+
state.delivery.last_error = input.error ?? "unknown error";
|
|
300
|
+
} else {
|
|
301
|
+
state.delivery.status = status;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return writeReviewTrustState(contextDir, state);
|
|
305
|
+
}
|
|
@@ -13,7 +13,9 @@ import type { InjectionMatch } from "../../core/policy/injection.js";
|
|
|
13
13
|
import { getLastPush } from "../telemetry/sync.js";
|
|
14
14
|
import { syncFromCloud, syncFromLocal, getLastSync } from "../policy/sync.js";
|
|
15
15
|
import { queueViolation } from "../violations/push.js";
|
|
16
|
-
import { queueReviewResult } from "../reviews/push.js";
|
|
16
|
+
import { getReviewPushContext, queueReviewResult } from "../reviews/push.js";
|
|
17
|
+
import { partitionReviewPolicies } from "../reviews/policy-selection.js";
|
|
18
|
+
import { recordQueuedReviewTrustState } from "../reviews/trust-state.js";
|
|
17
19
|
import { pushWorkflowSnapshot } from "../workflow/push.js";
|
|
18
20
|
import { OUTBOUND_DATA_BOUNDARY } from "../privacy/boundary.js";
|
|
19
21
|
import {
|
|
@@ -31,11 +33,7 @@ import {
|
|
|
31
33
|
} from "../workflow/state.js";
|
|
32
34
|
import { queryAuditLog } from "../../core/audit/query.js";
|
|
33
35
|
import { checkAccess, getAccessDeniedMessage, type Role } from "../../core/rbac/check.js";
|
|
34
|
-
import {
|
|
35
|
-
getGenericEvaluator,
|
|
36
|
-
getValidator,
|
|
37
|
-
runValidators,
|
|
38
|
-
} from "../../core/validators/engine.js";
|
|
36
|
+
import { runValidators } from "../../core/validators/engine.js";
|
|
39
37
|
import "../../core/validators/builtins.js";
|
|
40
38
|
import { recordToolActivity } from "../index.js";
|
|
41
39
|
|
|
@@ -904,41 +902,10 @@ export function registerEnterpriseTools(
|
|
|
904
902
|
// rules. Predefined rules leave type/config null and route to the
|
|
905
903
|
// name-based validator registry.
|
|
906
904
|
const policies = policyStore.getMergedPolicies();
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (p.id === "require-code-review") {
|
|
912
|
-
return {
|
|
913
|
-
policy_id: p.id,
|
|
914
|
-
kind: p.kind ?? null,
|
|
915
|
-
type: p.type ?? null,
|
|
916
|
-
reason: "Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.",
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
return {
|
|
920
|
-
policy_id: p.id,
|
|
921
|
-
kind: p.kind ?? null,
|
|
922
|
-
type: p.type ?? null,
|
|
923
|
-
reason: p.type
|
|
924
|
-
? `No evaluator registered for type "${p.type}"`
|
|
925
|
-
: "No executable validator registered for this policy",
|
|
926
|
-
};
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
const enforced = policies
|
|
930
|
-
.filter((p) => p.enforce)
|
|
931
|
-
.filter((p) => {
|
|
932
|
-
if (p.id === "require-code-review") return false;
|
|
933
|
-
if (p.type) return Boolean(getGenericEvaluator(p.type));
|
|
934
|
-
return Boolean(getValidator(p.id));
|
|
935
|
-
})
|
|
936
|
-
.map((p) => ({
|
|
937
|
-
id: p.id,
|
|
938
|
-
type: p.type ?? null,
|
|
939
|
-
config: p.config ?? null,
|
|
940
|
-
severity: p.severity ?? "block",
|
|
941
|
-
}));
|
|
905
|
+
const {
|
|
906
|
+
enforced,
|
|
907
|
+
skipped: skippedPolicies,
|
|
908
|
+
} = partitionReviewPolicies(policies);
|
|
942
909
|
|
|
943
910
|
const now = new Date().toISOString();
|
|
944
911
|
|
|
@@ -964,9 +931,9 @@ export function registerEnterpriseTools(
|
|
|
964
931
|
occurred_at: now,
|
|
965
932
|
});
|
|
966
933
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
934
|
+
queueReviewResult({
|
|
935
|
+
policy_id: r.policy_id,
|
|
936
|
+
pass: r.pass,
|
|
970
937
|
severity: r.severity,
|
|
971
938
|
message: r.message,
|
|
972
939
|
detail: r.detail,
|
|
@@ -1001,20 +968,43 @@ export function registerEnterpriseTools(
|
|
|
1001
968
|
reviewed_files: reviewedFiles,
|
|
1002
969
|
});
|
|
1003
970
|
const lastReview = workflowState.last_review;
|
|
971
|
+
const reviewedAt = lastReview?.reviewed_at ?? now;
|
|
1004
972
|
if (lastReview) {
|
|
1005
973
|
writeFileSync(
|
|
1006
974
|
join(contextDir, "review-status.json"),
|
|
1007
975
|
`${JSON.stringify({
|
|
1008
976
|
reviewed: lastReview.status === "passed",
|
|
1009
977
|
reviewer: "context.review",
|
|
1010
|
-
timestamp:
|
|
978
|
+
timestamp: reviewedAt,
|
|
1011
979
|
scope: parsed.scope,
|
|
1012
980
|
reviewed_files: reviewedFiles,
|
|
1013
981
|
}, null, 2)}\n`,
|
|
1014
982
|
"utf8",
|
|
1015
983
|
);
|
|
1016
984
|
}
|
|
1017
|
-
|
|
985
|
+
const trustState = recordQueuedReviewTrustState(
|
|
986
|
+
{
|
|
987
|
+
contextDir,
|
|
988
|
+
projectRoot,
|
|
989
|
+
repo: getReviewPushContext().repo,
|
|
990
|
+
instance_id: getReviewPushContext().instance_id,
|
|
991
|
+
session_id: getReviewPushContext().session_id,
|
|
992
|
+
task_id: process.env.CORTEX_ACTIVE_TASK_ID?.trim() || undefined,
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
reviewedAt,
|
|
996
|
+
summary: {
|
|
997
|
+
total: output.summary.total,
|
|
998
|
+
passed: output.summary.passed,
|
|
999
|
+
failed: output.summary.failed,
|
|
1000
|
+
warnings: output.summary.warnings,
|
|
1001
|
+
skipped: skippedPolicies.length,
|
|
1002
|
+
},
|
|
1003
|
+
skippedPolicies: skippedPolicies,
|
|
1004
|
+
},
|
|
1005
|
+
);
|
|
1006
|
+
const workflowWithTrust = loadWorkflowState(contextDir);
|
|
1007
|
+
await pushWorkflowStateIfConfigured(config, workflowWithTrust);
|
|
1018
1008
|
|
|
1019
1009
|
return buildToolResult({
|
|
1020
1010
|
scope: parsed.scope,
|
|
@@ -1024,7 +1014,10 @@ export function registerEnterpriseTools(
|
|
|
1024
1014
|
...output.summary,
|
|
1025
1015
|
skipped: skippedPolicies.length,
|
|
1026
1016
|
},
|
|
1027
|
-
workflow:
|
|
1017
|
+
workflow: workflowWithTrust,
|
|
1018
|
+
workflow_source: trustState.workflow.source,
|
|
1019
|
+
delivery: trustState.delivery,
|
|
1020
|
+
trust_warnings: trustState.trust_warnings,
|
|
1028
1021
|
});
|
|
1029
1022
|
},
|
|
1030
1023
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { isEnforcedMode } from "../../hooks/shared.js";
|
|
2
3
|
import {
|
|
3
4
|
WorkflowAdvanceInput,
|
|
4
5
|
WorkflowEnvelopeInput,
|
|
@@ -38,6 +39,7 @@ export function registerHarnessTools(server: McpServer): void {
|
|
|
38
39
|
async (input) => buildResult(
|
|
39
40
|
runWorkflowStart(WorkflowStartInput.parse(input ?? {}), {
|
|
40
41
|
cwd: resolveProjectRoot(),
|
|
42
|
+
bundledFallbackPolicy: isEnforcedMode(resolveProjectRoot()) ? "block" : "allow",
|
|
41
43
|
}) as ToolPayload,
|
|
42
44
|
),
|
|
43
45
|
);
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ReviewResult,
|
|
11
11
|
ReviewSummary,
|
|
12
12
|
} from "../../core/validators/engine.js";
|
|
13
|
+
import type { ReviewTrustState } from "../reviews/trust-state.js";
|
|
13
14
|
|
|
14
15
|
export type WorkflowPhase =
|
|
15
16
|
| "planning"
|
|
@@ -111,6 +112,7 @@ export type WorkflowState = {
|
|
|
111
112
|
notes: WorkflowNote[];
|
|
112
113
|
todos: WorkflowTodo[];
|
|
113
114
|
history: WorkflowHistoryEntry[];
|
|
115
|
+
review_trust: ReviewTrustState | null;
|
|
114
116
|
};
|
|
115
117
|
|
|
116
118
|
export type WorkflowMutationResult = {
|
|
@@ -145,6 +147,10 @@ function workflowStatePath(contextDir: string): string {
|
|
|
145
147
|
return join(workflowDir(contextDir), "state.json");
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
export function hasWorkflowState(contextDir: string): boolean {
|
|
151
|
+
return existsSync(workflowStatePath(contextDir));
|
|
152
|
+
}
|
|
153
|
+
|
|
148
154
|
function workflowReviewsDir(contextDir: string): string {
|
|
149
155
|
return join(workflowDir(contextDir), "reviews");
|
|
150
156
|
}
|
|
@@ -195,6 +201,7 @@ function initialState(): WorkflowState {
|
|
|
195
201
|
notes: [],
|
|
196
202
|
todos: [],
|
|
197
203
|
history: [],
|
|
204
|
+
review_trust: null,
|
|
198
205
|
};
|
|
199
206
|
}
|
|
200
207
|
|
|
@@ -457,6 +464,20 @@ export function recordWorkflowUpdate(
|
|
|
457
464
|
);
|
|
458
465
|
}
|
|
459
466
|
|
|
467
|
+
export function setWorkflowReviewTrust(
|
|
468
|
+
contextDir: string,
|
|
469
|
+
reviewTrust: ReviewTrustState,
|
|
470
|
+
): WorkflowState {
|
|
471
|
+
if (!hasWorkflowState(contextDir)) {
|
|
472
|
+
const state = initialState();
|
|
473
|
+
state.review_trust = reviewTrust;
|
|
474
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
475
|
+
}
|
|
476
|
+
const state = loadWorkflowState(contextDir);
|
|
477
|
+
state.review_trust = reviewTrust;
|
|
478
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
479
|
+
}
|
|
480
|
+
|
|
460
481
|
export function addWorkflowNote(
|
|
461
482
|
contextDir: string,
|
|
462
483
|
input: { title: string; details: string }
|