@danielblomma/cortex-mcp 2.0.15 → 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/cli/telemetry-test.ts +2 -1
- package/scaffold/mcp/src/core/license.ts +3 -2
- package/scaffold/mcp/src/core/telemetry/collector.ts +5 -4
- package/scaffold/mcp/src/core/telemetry/state-dir.ts +40 -0
- 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/daemon/main.ts +4 -3
- 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/telemetry-collector.test.mjs +31 -1
- 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,113 @@
|
|
|
1
|
+
import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
|
|
2
|
+
import { loadSyncedWorkflows } from "./synced-registry.js";
|
|
3
|
+
import type { WorkflowDefinition } from "./schemas.js";
|
|
4
|
+
|
|
5
|
+
export type WorkflowSource = "bundled" | "synced" | "injected";
|
|
6
|
+
|
|
7
|
+
export type WorkflowResolutionWarning = {
|
|
8
|
+
code: "bundled-workflow-fallback";
|
|
9
|
+
workflow_id: string;
|
|
10
|
+
message: string;
|
|
11
|
+
bundled_available: boolean;
|
|
12
|
+
synced_available: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type WorkflowResolution = {
|
|
16
|
+
workflow: WorkflowDefinition;
|
|
17
|
+
source: WorkflowSource;
|
|
18
|
+
warnings: WorkflowResolutionWarning[];
|
|
19
|
+
bundled_ids: string[];
|
|
20
|
+
synced_ids: string[];
|
|
21
|
+
available_ids: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ResolveWorkflowOptions = {
|
|
25
|
+
registry?: Record<string, WorkflowDefinition>;
|
|
26
|
+
syncedDir?: string;
|
|
27
|
+
emitBundledFallbackWarning?: boolean;
|
|
28
|
+
bundledFallbackPolicy?: "allow" | "warn" | "block";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function sortIds(ids: Iterable<string>): string[] {
|
|
32
|
+
return [...ids].sort((left, right) => left.localeCompare(right));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatIds(ids: string[]): string {
|
|
36
|
+
return ids.length > 0 ? ids.join(", ") : "<none>";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveWorkflowDefinition(
|
|
40
|
+
workflowId: string,
|
|
41
|
+
options: ResolveWorkflowOptions = {},
|
|
42
|
+
): WorkflowResolution {
|
|
43
|
+
if (options.registry) {
|
|
44
|
+
const availableIds = sortIds(Object.keys(options.registry));
|
|
45
|
+
const workflow = options.registry[workflowId];
|
|
46
|
+
if (!workflow) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Unknown workflow_id: ${workflowId}. Available injected: ${formatIds(availableIds)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
workflow,
|
|
53
|
+
source: "injected",
|
|
54
|
+
warnings: [],
|
|
55
|
+
bundled_ids: [],
|
|
56
|
+
synced_ids: [],
|
|
57
|
+
available_ids: availableIds,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bundled = DEFAULT_WORKFLOWS;
|
|
62
|
+
const synced = loadSyncedWorkflows(options.syncedDir);
|
|
63
|
+
const bundledIds = sortIds(Object.keys(bundled));
|
|
64
|
+
const syncedIds = sortIds(Object.keys(synced));
|
|
65
|
+
const merged = { ...bundled, ...synced };
|
|
66
|
+
const availableIds = sortIds(Object.keys(merged));
|
|
67
|
+
const workflow = merged[workflowId];
|
|
68
|
+
|
|
69
|
+
if (!workflow) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Unknown workflow_id: ${workflowId}. Available bundled: ${formatIds(bundledIds)}. ` +
|
|
72
|
+
`Available synced: ${formatIds(syncedIds)}. Available all: ${formatIds(availableIds)}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const source: WorkflowSource = Object.prototype.hasOwnProperty.call(synced, workflowId)
|
|
77
|
+
? "synced"
|
|
78
|
+
: "bundled";
|
|
79
|
+
const warnings: WorkflowResolutionWarning[] = [];
|
|
80
|
+
const fallbackPolicy = options.bundledFallbackPolicy ?? "allow";
|
|
81
|
+
|
|
82
|
+
if (source === "bundled" && fallbackPolicy === "block") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Workflow "${workflowId}" is only available from the bundled registry, ` +
|
|
85
|
+
"but enforced govern mode requires a synced org workflow. " +
|
|
86
|
+
`Available synced: ${formatIds(syncedIds)}.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
source === "bundled" &&
|
|
92
|
+
(options.emitBundledFallbackWarning || fallbackPolicy === "warn")
|
|
93
|
+
) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
code: "bundled-workflow-fallback",
|
|
96
|
+
workflow_id: workflowId,
|
|
97
|
+
bundled_available: true,
|
|
98
|
+
synced_available: syncedIds.length > 0,
|
|
99
|
+
message:
|
|
100
|
+
`Workflow "${workflowId}" was resolved from the bundled registry because ` +
|
|
101
|
+
"no synced org workflow with that id exists in the local cache.",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
workflow,
|
|
107
|
+
source,
|
|
108
|
+
warnings,
|
|
109
|
+
bundled_ids: bundledIds,
|
|
110
|
+
synced_ids: syncedIds,
|
|
111
|
+
available_ids: availableIds,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
|
|
14
14
|
import { pushMetrics } from "../enterprise/telemetry/sync.js";
|
|
15
15
|
import { TelemetryCollector, type TelemetryMetrics } from "../core/telemetry/collector.js";
|
|
16
|
+
import { resolveTelemetryStateDir, telemetryStatePath } from "../core/telemetry/state-dir.js";
|
|
16
17
|
import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
|
|
17
18
|
import { PolicyStore } from "../core/policy/store.js";
|
|
18
19
|
import {
|
|
@@ -84,7 +85,7 @@ async function policyCheck(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function readMetrics(contextDir: string): TelemetryMetrics | null {
|
|
87
|
-
const path =
|
|
88
|
+
const path = telemetryStatePath(contextDir, "metrics.json");
|
|
88
89
|
if (!existsSync(path)) return null;
|
|
89
90
|
try {
|
|
90
91
|
return JSON.parse(readFileSync(path, "utf8")) as TelemetryMetrics;
|
|
@@ -103,7 +104,7 @@ type PendingPush = {
|
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
function pendingPushPath(contextDir: string): string {
|
|
106
|
-
return
|
|
107
|
+
return telemetryStatePath(contextDir, "pending-push.json");
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function readPendingPush(contextDir: string): PendingPush | null {
|
|
@@ -118,7 +119,7 @@ function readPendingPush(contextDir: string): PendingPush | null {
|
|
|
118
119
|
|
|
119
120
|
function writePendingPush(contextDir: string, pending: PendingPush): void {
|
|
120
121
|
const path = pendingPushPath(contextDir);
|
|
121
|
-
mkdirSync(
|
|
122
|
+
mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
|
|
122
123
|
writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -17,7 +17,8 @@ import { registerEnterpriseTools } from "./tools/enterprise.js";
|
|
|
17
17
|
import { registerHarnessTools } from "./tools/harness.js";
|
|
18
18
|
import { pushViolations, setViolationPushContext } from "./violations/push.js";
|
|
19
19
|
import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
|
|
20
|
-
import { setWorkflowPushContext } from "./workflow/push.js";
|
|
20
|
+
import { pushWorkflowSnapshot, setWorkflowPushContext } from "./workflow/push.js";
|
|
21
|
+
import { hasWorkflowState, loadWorkflowState } from "./workflow/state.js";
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const pkg = require("../package.json") as { version: string };
|
|
@@ -32,6 +33,7 @@ let activeAuditWriter: AuditWriter | null = null;
|
|
|
32
33
|
let activeInstanceId: string | null = null;
|
|
33
34
|
let activeSessionId: string | null = null;
|
|
34
35
|
let activeRepo: string | null = null;
|
|
36
|
+
let activeContextDir: string | null = null;
|
|
35
37
|
|
|
36
38
|
async function flushComplianceQueues(
|
|
37
39
|
config: EnterpriseConfig,
|
|
@@ -64,6 +66,15 @@ async function flushComplianceQueues(
|
|
|
64
66
|
if (!result.success) {
|
|
65
67
|
process.stderr.write(`[cortex-enterprise] ${reason} reviews push failed: ${result.error}\n`);
|
|
66
68
|
}
|
|
69
|
+
if (result.attempted && activeContextDir && hasWorkflowState(activeContextDir)) {
|
|
70
|
+
const workflowState = loadWorkflowState(activeContextDir);
|
|
71
|
+
const workflowResult = await pushWorkflowSnapshot(baseUrl, apiKey, workflowState);
|
|
72
|
+
if (!workflowResult.success) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[cortex-enterprise] ${reason} workflow snapshot refresh after review push failed: ${workflowResult.error}\n`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
} catch (err) {
|
|
68
79
|
process.stderr.write(`[cortex-enterprise] ${reason} reviews push error: ${err}\n`);
|
|
69
80
|
}
|
|
@@ -113,6 +124,7 @@ export function shutdown(): void {
|
|
|
113
124
|
activeInstanceId = null;
|
|
114
125
|
activeSessionId = null;
|
|
115
126
|
activeRepo = null;
|
|
127
|
+
activeContextDir = null;
|
|
116
128
|
setAuditPushContext({});
|
|
117
129
|
setViolationPushContext({});
|
|
118
130
|
setReviewPushContext({});
|
|
@@ -248,6 +260,7 @@ export async function onSessionEvent(event: SessionEvent): Promise<void> {
|
|
|
248
260
|
export async function register(server: McpServer): Promise<void> {
|
|
249
261
|
const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
250
262
|
const contextDir = path.join(projectRoot, ".context");
|
|
263
|
+
activeContextDir = contextDir;
|
|
251
264
|
|
|
252
265
|
const config = loadEnterpriseConfig(contextDir);
|
|
253
266
|
const activation = resolveEnterpriseActivation(config);
|
|
@@ -295,6 +308,8 @@ export async function register(server: McpServer): Promise<void> {
|
|
|
295
308
|
repo: activeRepo ?? undefined,
|
|
296
309
|
instance_id: activeInstanceId ?? undefined,
|
|
297
310
|
session_id: activeSessionId ?? undefined,
|
|
311
|
+
context_dir: contextDir,
|
|
312
|
+
project_root: projectRoot,
|
|
298
313
|
});
|
|
299
314
|
setWorkflowPushContext({
|
|
300
315
|
repo: activeRepo ?? undefined,
|
|
@@ -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
|
+
}
|