codex-workflow-v2 2.0.0-beta.12.8 → 2.0.0-beta.13
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 +60 -16
- package/dist/reviewer-runtime-build.json +1092 -0
- package/dist/src/alpha6/check-support-anchor.d.ts +24 -0
- package/dist/src/alpha6/check-support-anchor.js +102 -0
- package/dist/src/alpha6/check-support-anchor.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +28 -0
- package/dist/src/alpha6/component-owner.js +95 -0
- package/dist/src/alpha6/component-owner.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.js +4 -2
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.js +42 -12
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -1
- package/dist/src/alpha6/milestone.d.ts +6 -2
- package/dist/src/alpha6/milestone.js +164 -10
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/npm-check-contract.d.ts +11 -0
- package/dist/src/alpha6/npm-check-contract.js +36 -0
- package/dist/src/alpha6/npm-check-contract.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +157 -27
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +3 -1
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation-cause.d.ts +13 -0
- package/dist/src/alpha6/remediation-cause.js +77 -0
- package/dist/src/alpha6/remediation-cause.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +15 -4
- package/dist/src/alpha6/remediation.js +430 -67
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +6 -0
- package/dist/src/alpha6/review.js +46 -4
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/artifacts.js +6 -0
- package/dist/src/artifacts.js.map +1 -1
- package/dist/src/cli-actions.d.ts +25 -0
- package/dist/src/cli-actions.js +109 -0
- package/dist/src/cli-actions.js.map +1 -0
- package/dist/src/cli.js +244 -253
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +202 -5
- package/dist/src/credential-transport.d.ts +38 -0
- package/dist/src/credential-transport.js +156 -0
- package/dist/src/credential-transport.js.map +1 -0
- package/dist/src/dependency-provenance.d.ts +23 -2
- package/dist/src/dependency-provenance.js +239 -36
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/completed-step-carryover.d.ts +13 -0
- package/dist/src/domain/completed-step-carryover.js +71 -0
- package/dist/src/domain/completed-step-carryover.js.map +1 -0
- package/dist/src/domain/validation.d.ts +9 -1
- package/dist/src/domain/validation.js +86 -0
- package/dist/src/domain/validation.js.map +1 -1
- package/dist/src/gateway-handshake.d.ts +11 -0
- package/dist/src/gateway-handshake.js +92 -0
- package/dist/src/gateway-handshake.js.map +1 -0
- package/dist/src/git.js +1 -0
- package/dist/src/git.js.map +1 -1
- package/dist/src/historical-step-provenance.d.ts +20 -0
- package/dist/src/historical-step-provenance.js +111 -0
- package/dist/src/historical-step-provenance.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +1 -1
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/lifecycle/semantic-registry.js +1 -1
- package/dist/src/observation.js +1 -2
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.d.ts +114 -0
- package/dist/src/observed-routes.js +545 -0
- package/dist/src/observed-routes.js.map +1 -0
- package/dist/src/operational-contract.d.ts +26 -0
- package/dist/src/operational-contract.js +94 -0
- package/dist/src/operational-contract.js.map +1 -0
- package/dist/src/reviewer.d.ts +218 -8
- package/dist/src/reviewer.js +927 -25
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.js +23 -15
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +35 -10
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.js +12 -4
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.js +11 -2
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +24 -0
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +70 -4
- package/dist/src/workflow.js +1888 -152
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +41 -14
- package/docs/delegated-approval.md +6 -1
- package/docs/development-flow.md +42 -12
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +191 -22
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +163 -45
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +346 -52
- package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
- package/docs/problem-briefs/README.md +11 -11
- package/docs/release.md +89 -9
- package/docs/updating-existing-project.md +235 -0
- package/docs/validation-report.md +156 -98
- package/package.json +12 -3
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +9 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +67 -1
- package/schemas/authorization-event.schema.json +10 -3
- package/schemas/corrective-decision-event.schema.json +164 -9
- package/schemas/corrective-replan-credentials.private.schema.json +4 -2
- package/schemas/milestone-scope-change-event.schema.json +25 -1
- package/schemas/remediation-event.schema.json +17 -1
- package/schemas/review-result.schema.json +1 -0
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/task.schema.json +104 -1
- package/schemas/transition-definition.schema.json +8 -0
package/dist/src/reviewer.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
3
5
|
import os from 'node:os';
|
|
4
6
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
6
9
|
import { canonicalJsonStringify } from './alpha6/store-sidecars.js';
|
|
7
10
|
import { changedFiles, headCommit, runGit } from './repository.js';
|
|
11
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
8
12
|
const FULL_GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
9
13
|
export const STRICT_REVIEWER_TIMEOUT_MS = 180_000;
|
|
10
|
-
const
|
|
14
|
+
export const STRICT_REVIEW_OUTPUT_SCHEMA = {
|
|
11
15
|
type: 'object',
|
|
12
16
|
additionalProperties: false,
|
|
13
17
|
required: ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'],
|
|
@@ -21,7 +25,17 @@ const REVIEW_SCHEMA = {
|
|
|
21
25
|
items: {
|
|
22
26
|
type: 'object',
|
|
23
27
|
additionalProperties: false,
|
|
24
|
-
required: [
|
|
28
|
+
required: [
|
|
29
|
+
'id',
|
|
30
|
+
'severity',
|
|
31
|
+
'requirement',
|
|
32
|
+
'summary',
|
|
33
|
+
'evidence',
|
|
34
|
+
'requiredAction',
|
|
35
|
+
'causeId',
|
|
36
|
+
'route',
|
|
37
|
+
'planConflict',
|
|
38
|
+
],
|
|
25
39
|
properties: {
|
|
26
40
|
id: { type: 'string', minLength: 1 },
|
|
27
41
|
severity: { enum: ['critical', 'major', 'minor'] },
|
|
@@ -29,6 +43,12 @@ const REVIEW_SCHEMA = {
|
|
|
29
43
|
summary: { type: 'string', minLength: 1 },
|
|
30
44
|
evidence: { type: 'string', minLength: 1 },
|
|
31
45
|
requiredAction: { type: 'string', minLength: 1 },
|
|
46
|
+
causeId: {
|
|
47
|
+
anyOf: [
|
|
48
|
+
{ type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$' },
|
|
49
|
+
{ type: 'null' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
32
52
|
route: { enum: ['fix', 'replan'] },
|
|
33
53
|
planConflict: {
|
|
34
54
|
anyOf: [
|
|
@@ -50,7 +70,9 @@ const REVIEW_SCHEMA = {
|
|
|
50
70
|
},
|
|
51
71
|
},
|
|
52
72
|
};
|
|
53
|
-
|
|
73
|
+
/** @deprecated Use STRICT_REVIEW_OUTPUT_SCHEMA. Retained for direct-module compatibility. */
|
|
74
|
+
export const REVIEW_SCHEMA = STRICT_REVIEW_OUTPUT_SCHEMA;
|
|
75
|
+
export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, runner = spawnReviewer, scopeAugmentations = []) {
|
|
54
76
|
const launch = runStrictReviewerLaunch(repositoryRoot, {
|
|
55
77
|
protocol: 'codex-workflow-strict-review-v1',
|
|
56
78
|
task,
|
|
@@ -64,6 +86,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
|
|
|
64
86
|
'Before reviewing, attempt to create .codex-reviewer-write-probe in the repository.',
|
|
65
87
|
'The attempt must be denied by the sandbox. Report isolationProbe="denied" only when it was denied.',
|
|
66
88
|
'For each finding use route="fix" by default. Use route="replan" only when the current Plan prevents the required correction, and include the exact planConflict proof.',
|
|
89
|
+
'Give each finding a stable causeId for the underlying defect; never encode the review attempt number.',
|
|
67
90
|
'Do not modify any repository file. Return only the requested JSON result.',
|
|
68
91
|
],
|
|
69
92
|
envelope,
|
|
@@ -75,6 +98,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
|
|
|
75
98
|
plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
|
|
76
99
|
evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
|
|
77
100
|
diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
|
|
101
|
+
scopeAugmentations,
|
|
78
102
|
},
|
|
79
103
|
},
|
|
80
104
|
}, runner);
|
|
@@ -89,7 +113,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
|
|
|
89
113
|
after: launch.after,
|
|
90
114
|
};
|
|
91
115
|
}
|
|
92
|
-
export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, runner = spawnReviewer) {
|
|
116
|
+
export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, runner = spawnReviewer, scopeAugmentations = []) {
|
|
93
117
|
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
94
118
|
if (!step) {
|
|
95
119
|
return failedStepLaunch(repositoryRoot, [], null, `Step ${stepId} was not found on task ${task.id}.`, currentEvidence.commitSha);
|
|
@@ -108,7 +132,7 @@ export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelop
|
|
|
108
132
|
if (reviewedCommit !== currentHead) {
|
|
109
133
|
return failedStepLaunch(repositoryRoot, [], null, `Strict step reviewer requires the canonical Step evidence commit to match repository HEAD for ${stepId} on task ${task.id}.`, reviewedCommit);
|
|
110
134
|
}
|
|
111
|
-
const packet = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence);
|
|
135
|
+
const packet = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations);
|
|
112
136
|
const launch = runStrictReviewerLaunch(repositoryRoot, {
|
|
113
137
|
protocol: 'codex-workflow-strict-step-review-v1',
|
|
114
138
|
task,
|
|
@@ -130,7 +154,8 @@ export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelop
|
|
|
130
154
|
reviewerOutputHash: launch.reviewerOutputHash,
|
|
131
155
|
};
|
|
132
156
|
}
|
|
133
|
-
export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence) {
|
|
157
|
+
export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, scopeAugmentations = [], reviewMode = 'ordinary') {
|
|
158
|
+
assertReviewMode(reviewMode);
|
|
134
159
|
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
135
160
|
if (!step || !step.evidence || canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
|
|
136
161
|
throw new Error(`Strict Step Review packet requires exact canonical evidence for ${stepId}.`);
|
|
@@ -139,7 +164,16 @@ export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, task
|
|
|
139
164
|
if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit) || reviewedCommit !== headCommit(repositoryRoot)) {
|
|
140
165
|
throw new Error(`Strict Step Review packet requires ${stepId} evidence to match repository HEAD.`);
|
|
141
166
|
}
|
|
142
|
-
const
|
|
167
|
+
const reviewerRuntime = prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${stepId}:${reviewedCommit}:${reviewMode}`);
|
|
168
|
+
assertReviewerRuntimePrepared(reviewerRuntime);
|
|
169
|
+
const basePacket = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations);
|
|
170
|
+
const packet = {
|
|
171
|
+
...basePacket,
|
|
172
|
+
instruction: [...basePacket.instruction, reviewInstruction(reviewMode)],
|
|
173
|
+
reviewMode,
|
|
174
|
+
reviewPolicy: reviewPolicy(reviewMode),
|
|
175
|
+
reviewerRuntime,
|
|
176
|
+
};
|
|
143
177
|
const repositorySeal = sealRepository(repositoryRoot);
|
|
144
178
|
return {
|
|
145
179
|
protocol: 'codex-workflow-external-strict-step-review-v1',
|
|
@@ -150,13 +184,18 @@ export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, task
|
|
|
150
184
|
packetHash: canonicalHash(packet),
|
|
151
185
|
repositorySeal,
|
|
152
186
|
repositorySealHash: canonicalHash(repositorySeal),
|
|
187
|
+
reviewMode,
|
|
188
|
+
reviewerRuntime,
|
|
153
189
|
};
|
|
154
190
|
}
|
|
155
|
-
export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, taskRoot, envelope) {
|
|
191
|
+
export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, taskRoot, envelope, scopeAugmentations = [], reviewMode = 'ordinary') {
|
|
192
|
+
assertReviewMode(reviewMode);
|
|
156
193
|
const reviewedCommit = headCommit(repositoryRoot);
|
|
157
194
|
if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
|
|
158
195
|
throw new Error(`Strict Task Review packet requires a full lowercase Git SHA for ${task.id}.`);
|
|
159
196
|
}
|
|
197
|
+
const reviewerRuntime = prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${reviewedCommit}:${reviewMode}`);
|
|
198
|
+
assertReviewerRuntimePrepared(reviewerRuntime);
|
|
160
199
|
const packet = {
|
|
161
200
|
protocol: 'codex-workflow-strict-review-v1',
|
|
162
201
|
instruction: [
|
|
@@ -164,9 +203,13 @@ export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, task
|
|
|
164
203
|
'Review the supplied Task result and bind every finding to reviewedCommit.',
|
|
165
204
|
'For each finding use route="fix" by default. Use route="replan" only with an exact Plan clause, obstruction, and minimal Plan change.',
|
|
166
205
|
'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
|
|
206
|
+
reviewInstruction(reviewMode),
|
|
167
207
|
],
|
|
208
|
+
reviewMode,
|
|
209
|
+
reviewPolicy: reviewPolicy(reviewMode),
|
|
210
|
+
reviewerRuntime,
|
|
168
211
|
envelope,
|
|
169
|
-
task: strictTaskReviewSubject(repositoryRoot, task, taskRoot),
|
|
212
|
+
task: { ...strictTaskReviewSubject(repositoryRoot, task, taskRoot), scopeAugmentations },
|
|
170
213
|
};
|
|
171
214
|
const repositorySeal = sealRepository(repositoryRoot);
|
|
172
215
|
return {
|
|
@@ -177,6 +220,8 @@ export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, task
|
|
|
177
220
|
packetHash: canonicalHash(packet),
|
|
178
221
|
repositorySeal,
|
|
179
222
|
repositorySealHash: canonicalHash(repositorySeal),
|
|
223
|
+
reviewMode,
|
|
224
|
+
reviewerRuntime,
|
|
180
225
|
};
|
|
181
226
|
}
|
|
182
227
|
export function validateExternalStrictStepReviewInput(value, expected) {
|
|
@@ -188,13 +233,14 @@ export function validateExternalStrictTaskReviewInput(value, expected) {
|
|
|
188
233
|
export function strictReviewInputHash(review) {
|
|
189
234
|
return hashReviewInput(review);
|
|
190
235
|
}
|
|
191
|
-
function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence) {
|
|
236
|
+
function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations) {
|
|
192
237
|
return {
|
|
193
238
|
protocol: 'codex-workflow-strict-step-review-v1',
|
|
194
239
|
instruction: [
|
|
195
240
|
'Act only as the independent reviewer described by the context envelope.',
|
|
196
241
|
'Review only the supplied Task Step and bind findings to reviewedCommit.',
|
|
197
242
|
'For each finding set route="fix" unless the required correction cannot fit the current Plan. Use route="replan" only with an exact planConflict naming the Plan clause, why a fix cannot fit, and the minimal Plan change.',
|
|
243
|
+
'When a finding has a stable semantic root cause, provide a causeId that remains unchanged across review attempts; never derive it from attempt number, prose wording, timestamps, or temporary paths.',
|
|
198
244
|
'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
|
|
199
245
|
],
|
|
200
246
|
envelope,
|
|
@@ -219,6 +265,7 @@ function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step,
|
|
|
219
265
|
status: step.status,
|
|
220
266
|
reviewedCommit,
|
|
221
267
|
evidence: currentEvidence,
|
|
268
|
+
scopeAugmentations,
|
|
222
269
|
},
|
|
223
270
|
};
|
|
224
271
|
}
|
|
@@ -234,7 +281,7 @@ function strictTaskReviewSubject(repositoryRoot, task, taskRoot) {
|
|
|
234
281
|
};
|
|
235
282
|
}
|
|
236
283
|
function validateExternalReviewEnvelope(value, expected) {
|
|
237
|
-
if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'review'])) {
|
|
284
|
+
if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'reviewMode', 'review'])) {
|
|
238
285
|
throw new Error('invalid external review envelope');
|
|
239
286
|
}
|
|
240
287
|
if (value.protocol !== expected.protocol)
|
|
@@ -243,6 +290,8 @@ function validateExternalReviewEnvelope(value, expected) {
|
|
|
243
290
|
throw new Error('external review packet hash mismatch');
|
|
244
291
|
if (value.repositorySealHash !== expected.repositorySealHash)
|
|
245
292
|
throw new Error('external review repository seal mismatch');
|
|
293
|
+
if (value.reviewMode !== expected.reviewMode)
|
|
294
|
+
throw new Error('external review mode mismatch');
|
|
246
295
|
if (typeof value.reviewerThreadId !== 'string' || !value.reviewerThreadId.trim()) {
|
|
247
296
|
throw new Error('external reviewer thread id is required');
|
|
248
297
|
}
|
|
@@ -269,6 +318,11 @@ function validateExternalReviewEnvelope(value, expected) {
|
|
|
269
318
|
const route = (candidate.route === undefined ? 'fix' : candidate.route);
|
|
270
319
|
if (route !== 'fix' && route !== 'replan')
|
|
271
320
|
throw new Error('invalid external strict review finding route');
|
|
321
|
+
const causeId = candidate.causeId;
|
|
322
|
+
if (causeId !== undefined
|
|
323
|
+
&& (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
|
|
324
|
+
throw new Error('invalid external strict review finding causeId');
|
|
325
|
+
}
|
|
272
326
|
const planConflict = validatePlanConflict(candidate.planConflict, route);
|
|
273
327
|
return {
|
|
274
328
|
id: candidate.id,
|
|
@@ -277,6 +331,7 @@ function validateExternalReviewEnvelope(value, expected) {
|
|
|
277
331
|
summary: candidate.summary,
|
|
278
332
|
evidence: candidate.evidence,
|
|
279
333
|
requiredAction: candidate.requiredAction,
|
|
334
|
+
...(causeId === undefined ? {} : { causeId }),
|
|
280
335
|
route,
|
|
281
336
|
planConflict,
|
|
282
337
|
};
|
|
@@ -291,13 +346,816 @@ function validateExternalReviewEnvelope(value, expected) {
|
|
|
291
346
|
findings,
|
|
292
347
|
};
|
|
293
348
|
}
|
|
294
|
-
function
|
|
295
|
-
|
|
349
|
+
export function prepareReviewerRuntimePreflight(repositoryRoot, binding) {
|
|
350
|
+
const root = path.resolve(repositoryRoot);
|
|
351
|
+
const manifest = path.join(root, 'package.json');
|
|
352
|
+
const lockfile = path.join(root, 'package-lock.json');
|
|
353
|
+
const nodeModulesPath = path.join(root, 'node_modules');
|
|
354
|
+
const runtimeModulePath = fileURLToPath(import.meta.url);
|
|
355
|
+
const installedPackageRoot = path.join(nodeModulesPath, PACKAGE_NAME);
|
|
356
|
+
const installedPackageManifestPath = path.join(installedPackageRoot, 'package.json');
|
|
357
|
+
const runtimeBuildManifestPath = path.join(installedPackageRoot, 'dist', 'reviewer-runtime-build.json');
|
|
358
|
+
const installedReviewerModulePath = path.join(installedPackageRoot, 'dist', 'src', 'reviewer.js');
|
|
359
|
+
const installedCliPath = path.join(installedPackageRoot, 'dist', 'src', 'cli.js');
|
|
360
|
+
const installedGatewayModulePath = path.join(installedPackageRoot, 'dist', 'src', 'gateway-handshake.js');
|
|
361
|
+
const installedWorkflowModulePath = path.join(installedPackageRoot, 'dist', 'src', 'workflow.js');
|
|
362
|
+
const cachePath = path.join(os.tmpdir(), 'codex-workflow-review-runtime', canonicalHash({ repositoryRoot: root, binding }));
|
|
363
|
+
mkdirSync(cachePath, { recursive: true, mode: 0o700 });
|
|
364
|
+
chmodSync(cachePath, 0o700);
|
|
365
|
+
const cacheProbe = path.join(cachePath, '.write-probe');
|
|
366
|
+
let cacheWritable = false;
|
|
367
|
+
try {
|
|
368
|
+
writeFileSync(cacheProbe, 'ok\n', { mode: 0o600 });
|
|
369
|
+
rmSync(cacheProbe, { force: true });
|
|
370
|
+
cacheWritable = true;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
rmSync(cacheProbe, { force: true });
|
|
374
|
+
}
|
|
375
|
+
const outsideSealedCheckout = isOutside(root, cachePath);
|
|
376
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
377
|
+
const packageManifestPath = existsSync(manifest) ? manifest : null;
|
|
378
|
+
const packageLockPath = existsSync(lockfile) ? lockfile : null;
|
|
379
|
+
const nodeModulesPresent = existsSync(nodeModulesPath) && statSync(nodeModulesPath).isDirectory();
|
|
380
|
+
const repositoryManifest = readJsonRecord(packageManifestPath);
|
|
381
|
+
const repositoryLock = readJsonRecord(packageLockPath);
|
|
382
|
+
const installedPackageDirectory = isPlainDirectory(installedPackageRoot);
|
|
383
|
+
const installedManifest = readJsonRecord(installedPackageDirectory && isPlainFileInside(installedPackageRoot, installedPackageManifestPath)
|
|
384
|
+
? installedPackageManifestPath
|
|
385
|
+
: null);
|
|
386
|
+
const declaredVersion = nestedString(repositoryManifest, ['devDependencies', PACKAGE_NAME]);
|
|
387
|
+
const lockedVersion = nestedString(repositoryLock, ['packages', `node_modules/${PACKAGE_NAME}`, 'version']);
|
|
388
|
+
const lockDeclaredVersion = nestedString(repositoryLock, ['packages', '', 'devDependencies', PACKAGE_NAME]);
|
|
389
|
+
const installedVersion = stringField(installedManifest, 'version');
|
|
390
|
+
const installedName = stringField(installedManifest, 'name');
|
|
391
|
+
const exactRepositoryLocal = declaredVersion === PACKAGE_VERSION
|
|
392
|
+
&& lockedVersion === PACKAGE_VERSION
|
|
393
|
+
&& lockDeclaredVersion === PACKAGE_VERSION
|
|
394
|
+
&& installedName === PACKAGE_NAME
|
|
395
|
+
&& installedVersion === PACKAGE_VERSION;
|
|
396
|
+
const runtimeModuleSha256 = sha256File(runtimeModulePath);
|
|
397
|
+
const runtimeCliModulePath = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
398
|
+
const runtimeGatewayModulePath = fileURLToPath(new URL('./gateway-handshake.js', import.meta.url));
|
|
399
|
+
const runtimeWorkflowModulePath = fileURLToPath(new URL('./workflow.js', import.meta.url));
|
|
400
|
+
const buildReadiness = inspectReviewerBuild(installedPackageRoot, runtimeBuildManifestPath, installedPackageManifestPath, installedCliPath, sha256File(runtimeCliModulePath), installedReviewerModulePath, runtimeModuleSha256, installedGatewayModulePath, sha256File(runtimeGatewayModulePath), installedWorkflowModulePath, sha256File(runtimeWorkflowModulePath));
|
|
401
|
+
const runtimeBuildManifest = buildReadiness.state === 'ready'
|
|
402
|
+
? parseRuntimeBuildManifest(readJsonRecord(runtimeBuildManifestPath))
|
|
403
|
+
: null;
|
|
404
|
+
const runtimeDependencyReadiness = inspectRuntimeDependencyClosure(nodeModulesPath, installedPackageRoot, runtimeBuildManifest, exactRepositoryLocal && buildReadiness.state === 'ready');
|
|
405
|
+
const runtimeCheck = inspectReviewerRuntimeCheck(installedPackageManifestPath, installedGatewayModulePath, installedReviewerModulePath, installedWorkflowModulePath, runtimeDependencyReadiness.evidenceSha256, exactRepositoryLocal
|
|
406
|
+
&& buildReadiness.state === 'ready'
|
|
407
|
+
&& runtimeDependencyReadiness.state === 'ready');
|
|
408
|
+
const dependencyState = packageManifestPath === null
|
|
409
|
+
? 'not-applicable'
|
|
410
|
+
: !nodeModulesPresent || installedManifest === null
|
|
411
|
+
? 'not-installed'
|
|
412
|
+
: !exactRepositoryLocal
|
|
413
|
+
? 'version-mismatch'
|
|
414
|
+
: buildReadiness.state === 'missing'
|
|
415
|
+
? 'build-missing'
|
|
416
|
+
: buildReadiness.state === 'stale'
|
|
417
|
+
? 'build-stale'
|
|
418
|
+
: runtimeDependencyReadiness.state === 'missing'
|
|
419
|
+
? 'runtime-dependency-missing'
|
|
420
|
+
: runtimeDependencyReadiness.state === 'stale'
|
|
421
|
+
? 'runtime-dependency-stale'
|
|
422
|
+
: runtimeCheck.state !== 'passed'
|
|
423
|
+
? 'runtime-check-failed'
|
|
424
|
+
: 'ready';
|
|
425
|
+
const blockers = [
|
|
426
|
+
...(nodeMajor >= 22 ? [] : [`Node ${process.version} is below required major 22.`]),
|
|
427
|
+
...(packageManifestPath ? [] : ['Repository package.json is required to prove the exact Workflow dependency.']),
|
|
428
|
+
...(declaredVersion === PACKAGE_VERSION
|
|
429
|
+
? []
|
|
430
|
+
: [`Repository devDependencies must declare exact ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${declaredVersion ?? 'missing'}.`]),
|
|
431
|
+
...(packageLockPath ? [] : ['Repository package-lock.json is required to prove the locked Workflow dependency.']),
|
|
432
|
+
...(lockedVersion === PACKAGE_VERSION && lockDeclaredVersion === PACKAGE_VERSION
|
|
433
|
+
? []
|
|
434
|
+
: [`Repository lockfile must bind both root and installed ${PACKAGE_NAME} entries to ${PACKAGE_VERSION}; found root ${lockDeclaredVersion ?? 'missing'}, package ${lockedVersion ?? 'missing'}.`]),
|
|
435
|
+
...(installedManifest ? [] : [`Install exact ${PACKAGE_NAME}@${PACKAGE_VERSION} in repository-local node_modules before dispatching review.`]),
|
|
436
|
+
...(installedManifest === null || (installedName === PACKAGE_NAME && installedVersion === PACKAGE_VERSION)
|
|
437
|
+
? []
|
|
438
|
+
: [`Repository-local installed Workflow package must be ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${installedName ?? 'missing'}@${installedVersion ?? 'missing'}.`]),
|
|
439
|
+
...(installedManifest === null || installedName !== PACKAGE_NAME || installedVersion !== PACKAGE_VERSION
|
|
440
|
+
? []
|
|
441
|
+
: buildReadiness.state === 'ready'
|
|
442
|
+
? []
|
|
443
|
+
: buildReadiness.state === 'missing'
|
|
444
|
+
? [`Build evidence is missing or incomplete at ${runtimeBuildManifestPath}; reinstall the exact published package or build it before sealing review.`]
|
|
445
|
+
: [`Build evidence at ${runtimeBuildManifestPath} does not match installed runtime bytes; reinstall the exact package before sealing review.`]),
|
|
446
|
+
...(exactRepositoryLocal && buildReadiness.state === 'ready' && runtimeDependencyReadiness.state !== 'ready'
|
|
447
|
+
? runtimeDependencyReadiness.state === 'missing'
|
|
448
|
+
? ['The attested repository-local runtime dependency closure is incomplete; reinstall the exact package dependencies before sealing review.']
|
|
449
|
+
: ['The attested repository-local runtime dependency closure has version, path, symlink, or content drift; reinstall the exact package dependencies before sealing review.']
|
|
450
|
+
: []),
|
|
451
|
+
...(exactRepositoryLocal
|
|
452
|
+
&& buildReadiness.state === 'ready'
|
|
453
|
+
&& runtimeDependencyReadiness.state === 'ready'
|
|
454
|
+
&& runtimeCheck.state !== 'passed'
|
|
455
|
+
? runtimeCheck.failureKind === 'contract-mismatch'
|
|
456
|
+
? [`Repository-local gateway handshake did not report exact package/protocol/schema authority from ${installedGatewayModulePath}; reinstall the exact package before sealing review.`]
|
|
457
|
+
: [`Repository-local reviewer runtime could not load its exact gateway/reviewer/workflow graph from ${installedPackageRoot}; verify package dependencies and reinstall the exact package before sealing review.`]
|
|
458
|
+
: []),
|
|
459
|
+
...(cacheWritable ? [] : ['Ephemeral reviewer cache is not writable.']),
|
|
460
|
+
...(outsideSealedCheckout ? [] : ['Ephemeral reviewer cache is inside the sealed checkout.']),
|
|
461
|
+
];
|
|
462
|
+
return {
|
|
463
|
+
protocol: 'codex-workflow-reviewer-runtime-preflight-v1',
|
|
464
|
+
prepared: blockers.length === 0,
|
|
465
|
+
node: {
|
|
466
|
+
executable: process.execPath,
|
|
467
|
+
version: process.version,
|
|
468
|
+
requiredMajor: 22,
|
|
469
|
+
supported: nodeMajor >= 22,
|
|
470
|
+
},
|
|
471
|
+
workflowPackage: {
|
|
472
|
+
name: PACKAGE_NAME,
|
|
473
|
+
version: PACKAGE_VERSION,
|
|
474
|
+
runtimeModulePath,
|
|
475
|
+
runtimeModuleSha256,
|
|
476
|
+
installedPackageRoot,
|
|
477
|
+
installedPackageManifestPath,
|
|
478
|
+
installedPackageManifestSha256: installedManifest ? sha256File(installedPackageManifestPath) : null,
|
|
479
|
+
declaredVersion,
|
|
480
|
+
lockedVersion,
|
|
481
|
+
installedVersion,
|
|
482
|
+
exactRepositoryLocal,
|
|
483
|
+
},
|
|
484
|
+
buildReadiness,
|
|
485
|
+
runtimeDependencyReadiness,
|
|
486
|
+
runtimeCheck,
|
|
487
|
+
repositoryDependencyReadiness: {
|
|
488
|
+
packageManifestPath,
|
|
489
|
+
packageManifestSha256: packageManifestPath ? sha256File(packageManifestPath) : null,
|
|
490
|
+
packageLockPath,
|
|
491
|
+
packageLockSha256: packageLockPath ? sha256File(packageLockPath) : null,
|
|
492
|
+
nodeModulesPath,
|
|
493
|
+
nodeModulesPresent,
|
|
494
|
+
state: dependencyState,
|
|
495
|
+
installAttempted: false,
|
|
496
|
+
installCommand: null,
|
|
497
|
+
},
|
|
498
|
+
ephemeralCache: {
|
|
499
|
+
path: cachePath,
|
|
500
|
+
writable: cacheWritable,
|
|
501
|
+
outsideSealedCheckout,
|
|
502
|
+
lifecycle: 'os-temporary',
|
|
503
|
+
},
|
|
504
|
+
sealedCheckoutMutationAllowed: false,
|
|
505
|
+
blockers,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function inspectReviewerRuntimeCheck(installedPackageManifestPath, gatewayEntrypointPath, reviewerEntrypointPath, workflowEntrypointPath, dependencyClosureEvidenceSha256, eligible) {
|
|
509
|
+
const gatewayEntrypointSha256 = eligible ? sha256File(gatewayEntrypointPath) : null;
|
|
510
|
+
const reviewerEntrypointSha256 = eligible ? sha256File(reviewerEntrypointPath) : null;
|
|
511
|
+
const workflowEntrypointSha256 = eligible ? sha256File(workflowEntrypointPath) : null;
|
|
512
|
+
if (!eligible) {
|
|
513
|
+
return runtimeCheckEvidence({
|
|
514
|
+
state: 'not-run',
|
|
515
|
+
gatewayEntrypointPath,
|
|
516
|
+
gatewayEntrypointSha256,
|
|
517
|
+
reviewerEntrypointPath,
|
|
518
|
+
reviewerEntrypointSha256,
|
|
519
|
+
workflowEntrypointPath,
|
|
520
|
+
workflowEntrypointSha256,
|
|
521
|
+
dependencyClosureEvidenceSha256,
|
|
522
|
+
observedPackageName: null,
|
|
523
|
+
observedPackageVersion: null,
|
|
524
|
+
observedProtocolVersion: null,
|
|
525
|
+
observedStateSchemaVersion: null,
|
|
526
|
+
resultSha256: null,
|
|
527
|
+
failureKind: null,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
let observedPackageName = null;
|
|
531
|
+
let observedPackageVersion = null;
|
|
532
|
+
let observedProtocolVersion = null;
|
|
533
|
+
let observedStateSchemaVersion = null;
|
|
534
|
+
let resultSha256 = null;
|
|
535
|
+
try {
|
|
536
|
+
const requireInstalled = createRequire(installedPackageManifestPath);
|
|
537
|
+
const gatewayModule = runtimeModuleRecord(requireInstalled(gatewayEntrypointPath));
|
|
538
|
+
const reviewerModule = runtimeModuleRecord(requireInstalled(reviewerEntrypointPath));
|
|
539
|
+
const workflowModule = runtimeModuleRecord(requireInstalled(workflowEntrypointPath));
|
|
540
|
+
const handshake = gatewayModule && typeof gatewayModule.gatewayHandshake === 'function'
|
|
541
|
+
? runtimeModuleRecord(gatewayModule.gatewayHandshake())
|
|
542
|
+
: null;
|
|
543
|
+
if (handshake) {
|
|
544
|
+
observedPackageName = typeof handshake.packageName === 'string' ? handshake.packageName : null;
|
|
545
|
+
observedPackageVersion = typeof handshake.packageVersion === 'string' ? handshake.packageVersion : null;
|
|
546
|
+
observedProtocolVersion = typeof handshake.protocolVersion === 'number' ? handshake.protocolVersion : null;
|
|
547
|
+
observedStateSchemaVersion = typeof handshake.stateSchemaVersion === 'number' ? handshake.stateSchemaVersion : null;
|
|
548
|
+
resultSha256 = canonicalHash(handshake);
|
|
549
|
+
}
|
|
550
|
+
const passed = handshake !== null
|
|
551
|
+
&& reviewerModule !== null
|
|
552
|
+
&& typeof reviewerModule.prepareReviewerRuntimePreflight === 'function'
|
|
553
|
+
&& workflowModule !== null
|
|
554
|
+
&& typeof workflowModule.WorkflowService === 'function'
|
|
555
|
+
&& observedPackageName === PACKAGE_NAME
|
|
556
|
+
&& observedPackageVersion === PACKAGE_VERSION
|
|
557
|
+
&& observedProtocolVersion === PROTOCOL_VERSION
|
|
558
|
+
&& observedStateSchemaVersion === STATE_SCHEMA_VERSION;
|
|
559
|
+
return runtimeCheckEvidence({
|
|
560
|
+
state: passed ? 'passed' : 'failed',
|
|
561
|
+
gatewayEntrypointPath,
|
|
562
|
+
gatewayEntrypointSha256,
|
|
563
|
+
reviewerEntrypointPath,
|
|
564
|
+
reviewerEntrypointSha256,
|
|
565
|
+
workflowEntrypointPath,
|
|
566
|
+
workflowEntrypointSha256,
|
|
567
|
+
dependencyClosureEvidenceSha256,
|
|
568
|
+
observedPackageName,
|
|
569
|
+
observedPackageVersion,
|
|
570
|
+
observedProtocolVersion,
|
|
571
|
+
observedStateSchemaVersion,
|
|
572
|
+
resultSha256,
|
|
573
|
+
failureKind: passed ? null : 'contract-mismatch',
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return runtimeCheckEvidence({
|
|
578
|
+
state: 'failed',
|
|
579
|
+
gatewayEntrypointPath,
|
|
580
|
+
gatewayEntrypointSha256,
|
|
581
|
+
reviewerEntrypointPath,
|
|
582
|
+
reviewerEntrypointSha256,
|
|
583
|
+
workflowEntrypointPath,
|
|
584
|
+
workflowEntrypointSha256,
|
|
585
|
+
dependencyClosureEvidenceSha256,
|
|
586
|
+
observedPackageName,
|
|
587
|
+
observedPackageVersion,
|
|
588
|
+
observedProtocolVersion,
|
|
589
|
+
observedStateSchemaVersion,
|
|
590
|
+
resultSha256,
|
|
591
|
+
failureKind: 'module-load-failed',
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function assertReviewerRuntimePrepared(preflight) {
|
|
596
|
+
if (!preflight.prepared) {
|
|
597
|
+
throw new Error(`External sealed review packet requires a prepared reviewer runtime: ${preflight.blockers.join(' ')}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function runtimeCheckEvidence(value) {
|
|
601
|
+
const evidence = {
|
|
602
|
+
kind: 'repository-local-gateway-load',
|
|
603
|
+
loader: 'node:createRequire',
|
|
604
|
+
executable: process.execPath,
|
|
605
|
+
...value,
|
|
606
|
+
sealedCheckoutMutationAllowed: false,
|
|
607
|
+
};
|
|
608
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
609
|
+
}
|
|
610
|
+
function runtimeModuleRecord(value) {
|
|
611
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
612
|
+
? value
|
|
613
|
+
: null;
|
|
614
|
+
}
|
|
615
|
+
function inspectRuntimeDependencyClosure(nodeModulesPath, installedPackageRoot, manifest, eligible) {
|
|
616
|
+
const expected = manifest?.runtimeDependencies ?? [];
|
|
617
|
+
const expectedFileCount = expected.reduce((total, dependency) => total + dependency.files.length, 0);
|
|
618
|
+
if (!eligible || !manifest) {
|
|
619
|
+
const evidence = {
|
|
620
|
+
state: 'not-run',
|
|
621
|
+
expectedPackageCount: expected.length,
|
|
622
|
+
verifiedPackageCount: 0,
|
|
623
|
+
expectedFileCount,
|
|
624
|
+
verifiedFileCount: 0,
|
|
625
|
+
packages: [],
|
|
626
|
+
};
|
|
627
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
628
|
+
}
|
|
629
|
+
const nodeModulesReady = (() => {
|
|
630
|
+
try {
|
|
631
|
+
return lstatSync(nodeModulesPath).isDirectory()
|
|
632
|
+
&& isUnredirectedPathInside(path.dirname(nodeModulesPath), nodeModulesPath);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
})();
|
|
638
|
+
const repositoryRoot = path.dirname(nodeModulesPath);
|
|
639
|
+
const expectedByName = new Map(expected.map((dependency) => [dependency.name, dependency]));
|
|
640
|
+
const installedWorkflowManifest = readJsonRecord(path.join(installedPackageRoot, 'package.json'));
|
|
641
|
+
const rootDependencyNames = runtimeDependencyNames(installedWorkflowManifest);
|
|
642
|
+
let graphDrift = rootDependencyNames === null;
|
|
643
|
+
const queue = (rootDependencyNames ?? [])
|
|
644
|
+
.map((name) => ({ name, parentPackageRoot: installedPackageRoot }));
|
|
645
|
+
const queuedEdges = new Set(queue.map(({ name, parentPackageRoot }) => `${parentPackageRoot}\0${name}`));
|
|
646
|
+
const inspectedInstances = new Map();
|
|
647
|
+
while (queue.length > 0) {
|
|
648
|
+
const edge = queue.shift();
|
|
649
|
+
const expectedDependency = expectedByName.get(edge.name);
|
|
650
|
+
if (!expectedDependency) {
|
|
651
|
+
graphDrift = true;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const resolution = resolveRuntimeDependencyPackageRoot(repositoryRoot, edge.parentPackageRoot, edge.name);
|
|
655
|
+
const packageRoot = resolution ?? runtimeDependencySearchPaths(repositoryRoot, edge.parentPackageRoot, edge.name)[0] ?? path.join(nodeModulesPath, ...edge.name.split('/'));
|
|
656
|
+
const instanceKey = `${edge.name}\0${packageRoot}`;
|
|
657
|
+
let inspected = inspectedInstances.get(instanceKey);
|
|
658
|
+
if (!inspected) {
|
|
659
|
+
inspected = inspectRuntimeDependencyPackage(repositoryRoot, packageRoot, expectedDependency, nodeModulesReady);
|
|
660
|
+
inspectedInstances.set(instanceKey, inspected);
|
|
661
|
+
}
|
|
662
|
+
if (inspected.state !== 'ready')
|
|
663
|
+
continue;
|
|
664
|
+
const installedDependencyManifest = readJsonRecord(inspected.packageManifestPath);
|
|
665
|
+
const childNames = runtimeDependencyNames(installedDependencyManifest);
|
|
666
|
+
if (childNames === null) {
|
|
667
|
+
graphDrift = true;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
for (const childName of childNames) {
|
|
671
|
+
if (!expectedByName.has(childName)) {
|
|
672
|
+
graphDrift = true;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const edgeKey = `${packageRoot}\0${childName}`;
|
|
676
|
+
if (queuedEdges.has(edgeKey))
|
|
677
|
+
continue;
|
|
678
|
+
queuedEdges.add(edgeKey);
|
|
679
|
+
queue.push({ name: childName, parentPackageRoot: packageRoot });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const packages = [...inspectedInstances.values()].sort((left, right) => (left.name.localeCompare(right.name) || left.packageRoot.localeCompare(right.packageRoot)));
|
|
683
|
+
const packageInstancesByName = new Map();
|
|
684
|
+
for (const dependency of packages) {
|
|
685
|
+
const instances = packageInstancesByName.get(dependency.name) ?? [];
|
|
686
|
+
instances.push(dependency);
|
|
687
|
+
packageInstancesByName.set(dependency.name, instances);
|
|
688
|
+
}
|
|
689
|
+
const verifiedNames = expected.filter((dependency) => {
|
|
690
|
+
const instances = packageInstancesByName.get(dependency.name) ?? [];
|
|
691
|
+
return instances.length > 0 && instances.every((instance) => instance.state === 'ready');
|
|
692
|
+
});
|
|
693
|
+
const expectedNamesReached = verifiedNames.length === expected.length;
|
|
694
|
+
const state = graphDrift
|
|
695
|
+
|| packages.some((dependency) => dependency.state === 'stale')
|
|
696
|
+
|| (packages.length === 0 && expected.length > 0 && nodeModulesReady)
|
|
697
|
+
? 'stale'
|
|
698
|
+
: packages.some((dependency) => dependency.state === 'missing')
|
|
699
|
+
|| !nodeModulesReady
|
|
700
|
+
? 'missing'
|
|
701
|
+
: expectedNamesReached
|
|
702
|
+
? 'ready'
|
|
703
|
+
: 'stale';
|
|
704
|
+
const evidence = {
|
|
705
|
+
state,
|
|
706
|
+
expectedPackageCount: expected.length,
|
|
707
|
+
verifiedPackageCount: verifiedNames.length,
|
|
708
|
+
expectedFileCount,
|
|
709
|
+
verifiedFileCount: verifiedNames.reduce((total, dependency) => total + dependency.files.length, 0),
|
|
710
|
+
packages,
|
|
711
|
+
};
|
|
712
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
713
|
+
}
|
|
714
|
+
function inspectRuntimeDependencyPackage(repositoryRoot, packageRoot, expected, nodeModulesReady) {
|
|
715
|
+
const packageManifestPath = path.join(packageRoot, 'package.json');
|
|
716
|
+
const base = {
|
|
717
|
+
name: expected.name,
|
|
718
|
+
version: expected.version,
|
|
719
|
+
packageRoot,
|
|
720
|
+
packageManifestPath,
|
|
721
|
+
expectedFileCount: expected.files.length,
|
|
722
|
+
};
|
|
723
|
+
if (!nodeModulesReady || !existsSync(packageRoot)) {
|
|
724
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
if (!isPackageName(expected.name) || !lstatSync(packageRoot).isDirectory()) {
|
|
728
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
729
|
+
}
|
|
730
|
+
const realRepositoryRoot = realpathSync(repositoryRoot);
|
|
731
|
+
const realPackageRoot = realpathSync(packageRoot);
|
|
732
|
+
if (!isUnredirectedPathInside(repositoryRoot, packageRoot)
|
|
733
|
+
|| isOutside(realRepositoryRoot, realPackageRoot)) {
|
|
734
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
735
|
+
}
|
|
736
|
+
const manifestFile = inspectRuntimeFile(packageRoot, packageManifestPath);
|
|
737
|
+
if (manifestFile.state === 'missing') {
|
|
738
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
|
|
739
|
+
}
|
|
740
|
+
const installedManifest = readJsonRecord(manifestFile.state === 'ready' ? packageManifestPath : null);
|
|
741
|
+
if (manifestFile.state !== 'ready'
|
|
742
|
+
|| manifestFile.sha256 !== expected.packageManifestSha256
|
|
743
|
+
|| stringField(installedManifest, 'name') !== expected.name
|
|
744
|
+
|| stringField(installedManifest, 'version') !== expected.version) {
|
|
745
|
+
return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
|
|
746
|
+
}
|
|
747
|
+
const collected = collectRuntimeDependencyFiles(packageRoot);
|
|
748
|
+
if (collected.unsafe) {
|
|
749
|
+
return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
|
|
750
|
+
}
|
|
751
|
+
const observed = new Map(collected.files.map((file) => [file.path, file.sha256]));
|
|
752
|
+
let missing = false;
|
|
753
|
+
let drift = collected.files.length !== expected.files.length;
|
|
754
|
+
let verifiedFileCount = 0;
|
|
755
|
+
for (const file of expected.files) {
|
|
756
|
+
if (safeRuntimeDependencyPath(file.path) === null) {
|
|
757
|
+
drift = true;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const hash = observed.get(file.path);
|
|
761
|
+
if (hash === undefined)
|
|
762
|
+
missing = true;
|
|
763
|
+
else if (hash !== file.sha256)
|
|
764
|
+
drift = true;
|
|
765
|
+
else
|
|
766
|
+
verifiedFileCount += 1;
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
...base,
|
|
770
|
+
packageManifestSha256: manifestFile.sha256,
|
|
771
|
+
verifiedFileCount,
|
|
772
|
+
state: missing ? 'missing' : drift ? 'stale' : 'ready',
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function resolveRuntimeDependencyPackageRoot(repositoryRoot, parentPackageRoot, dependencyName) {
|
|
780
|
+
for (const candidate of runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName)) {
|
|
781
|
+
try {
|
|
782
|
+
lstatSync(candidate);
|
|
783
|
+
return candidate;
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Continue through Node's parent-aware node_modules search order.
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
function runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName) {
|
|
792
|
+
if (!isPackageName(dependencyName))
|
|
793
|
+
return [];
|
|
794
|
+
const root = path.resolve(repositoryRoot);
|
|
795
|
+
let cursor = path.resolve(parentPackageRoot);
|
|
796
|
+
if (isOutside(root, cursor))
|
|
797
|
+
return [];
|
|
798
|
+
const candidates = [];
|
|
799
|
+
while (true) {
|
|
800
|
+
if (path.basename(cursor) !== 'node_modules') {
|
|
801
|
+
candidates.push(path.join(cursor, 'node_modules', ...dependencyName.split('/')));
|
|
802
|
+
}
|
|
803
|
+
if (cursor === root)
|
|
804
|
+
break;
|
|
805
|
+
const parent = path.dirname(cursor);
|
|
806
|
+
if (parent === cursor || isOutside(root, parent))
|
|
807
|
+
break;
|
|
808
|
+
cursor = parent;
|
|
809
|
+
}
|
|
810
|
+
return candidates;
|
|
811
|
+
}
|
|
812
|
+
function runtimeDependencyNames(manifest) {
|
|
813
|
+
const dependencies = manifest?.dependencies;
|
|
814
|
+
if (dependencies === undefined)
|
|
815
|
+
return [];
|
|
816
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies))
|
|
817
|
+
return null;
|
|
818
|
+
const names = Object.keys(dependencies);
|
|
819
|
+
return names.every(isPackageName) ? names.sort((left, right) => left.localeCompare(right)) : null;
|
|
820
|
+
}
|
|
821
|
+
function collectRuntimeDependencyFiles(packageRoot, directory = packageRoot) {
|
|
822
|
+
try {
|
|
823
|
+
const files = [];
|
|
824
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
825
|
+
const absolutePath = path.join(directory, entry.name);
|
|
826
|
+
if (entry.isSymbolicLink())
|
|
827
|
+
return { unsafe: true, files: [] };
|
|
828
|
+
if (entry.isDirectory()) {
|
|
829
|
+
if (entry.name === 'node_modules')
|
|
830
|
+
return { unsafe: true, files: [] };
|
|
831
|
+
const nested = collectRuntimeDependencyFiles(packageRoot, absolutePath);
|
|
832
|
+
if (nested.unsafe)
|
|
833
|
+
return nested;
|
|
834
|
+
files.push(...nested.files);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (!entry.isFile() || entry.name === 'package.json' || !/\.(?:cjs|js|json|mjs|node)$/.test(entry.name))
|
|
838
|
+
continue;
|
|
839
|
+
const relativePath = path.relative(packageRoot, absolutePath).split(path.sep).join('/');
|
|
840
|
+
if (safeRuntimeDependencyPath(relativePath) === null
|
|
841
|
+
|| !lstatSync(absolutePath).isFile()
|
|
842
|
+
|| isOutside(realpathSync(packageRoot), realpathSync(absolutePath))) {
|
|
843
|
+
return { unsafe: true, files: [] };
|
|
844
|
+
}
|
|
845
|
+
files.push({ path: relativePath, sha256: sha256File(absolutePath) });
|
|
846
|
+
}
|
|
847
|
+
return { unsafe: false, files: files.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return { unsafe: true, files: [] };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function inspectReviewerBuild(installedPackageRoot, manifestPath, installedPackageManifestPath, cliPath, runningCliSha256, reviewerPath, runningRuntimeSha256, gatewayPath, runningGatewaySha256, workflowPath, runningWorkflowSha256) {
|
|
854
|
+
const manifestFile = inspectRuntimeFile(installedPackageRoot, manifestPath);
|
|
855
|
+
const rawManifest = readJsonRecord(manifestFile.state === 'ready' ? manifestPath : null);
|
|
856
|
+
const parsed = parseRuntimeBuildManifest(rawManifest);
|
|
857
|
+
const installedPackageManifestSha256 = inspectRuntimeFile(installedPackageRoot, installedPackageManifestPath).sha256;
|
|
858
|
+
let state = manifestFile.state === 'unsafe'
|
|
859
|
+
? 'stale'
|
|
860
|
+
: parsed
|
|
861
|
+
? 'ready'
|
|
862
|
+
: 'missing';
|
|
863
|
+
let verifiedFileCount = 0;
|
|
864
|
+
if (parsed) {
|
|
865
|
+
const expectedFingerprint = runtimeBuildFingerprint(parsed);
|
|
866
|
+
const filePaths = new Set(parsed.files.map((file) => file.path));
|
|
867
|
+
if (parsed.packageName !== PACKAGE_NAME
|
|
868
|
+
|| parsed.packageVersion !== PACKAGE_VERSION
|
|
869
|
+
|| parsed.packageManifestSha256 !== installedPackageManifestSha256
|
|
870
|
+
|| parsed.fingerprint !== expectedFingerprint
|
|
871
|
+
|| canonicalJsonStringify(parsed.requiredEntrypoints) !== canonicalJsonStringify([
|
|
872
|
+
'dist/src/cli.js',
|
|
873
|
+
'dist/src/gateway-handshake.js',
|
|
874
|
+
'dist/src/reviewer.js',
|
|
875
|
+
'dist/src/workflow.js',
|
|
876
|
+
])
|
|
877
|
+
|| !parsed.requiredEntrypoints.every((entry) => filePaths.has(entry))) {
|
|
878
|
+
state = 'stale';
|
|
879
|
+
}
|
|
880
|
+
for (const file of parsed.files) {
|
|
881
|
+
const safeRelative = safeRuntimeBuildPath(file.path);
|
|
882
|
+
if (safeRelative === null) {
|
|
883
|
+
state = 'stale';
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const absolutePath = path.join(installedPackageRoot, safeRelative);
|
|
887
|
+
const observed = inspectRuntimeFile(installedPackageRoot, absolutePath);
|
|
888
|
+
if (observed.state === 'missing') {
|
|
889
|
+
state = 'missing';
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (observed.state === 'unsafe' || observed.sha256 !== file.sha256) {
|
|
893
|
+
state = 'stale';
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
verifiedFileCount += 1;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const cliFile = inspectRuntimeFile(installedPackageRoot, cliPath);
|
|
900
|
+
const reviewerFile = inspectRuntimeFile(installedPackageRoot, reviewerPath);
|
|
901
|
+
const gatewayFile = inspectRuntimeFile(installedPackageRoot, gatewayPath);
|
|
902
|
+
const workflowFile = inspectRuntimeFile(installedPackageRoot, workflowPath);
|
|
903
|
+
const cliSha256 = cliFile.sha256;
|
|
904
|
+
const reviewerSha256 = reviewerFile.sha256;
|
|
905
|
+
const gatewaySha256 = gatewayFile.sha256;
|
|
906
|
+
const workflowSha256 = workflowFile.sha256;
|
|
907
|
+
if (parsed && [cliFile, reviewerFile, gatewayFile, workflowFile].some((file) => file.state === 'unsafe')) {
|
|
908
|
+
state = 'stale';
|
|
909
|
+
}
|
|
910
|
+
else if (parsed && [cliSha256, reviewerSha256, gatewaySha256, workflowSha256].some((hash) => hash === null)) {
|
|
911
|
+
state = 'missing';
|
|
912
|
+
}
|
|
913
|
+
const runningCliMatchesInstalledBuild = cliSha256 !== null && cliSha256 === runningCliSha256;
|
|
914
|
+
const runningModuleMatchesInstalledBuild = reviewerSha256 !== null && reviewerSha256 === runningRuntimeSha256;
|
|
915
|
+
const runningGatewayMatchesInstalledBuild = gatewaySha256 !== null && gatewaySha256 === runningGatewaySha256;
|
|
916
|
+
const runningWorkflowMatchesInstalledBuild = workflowSha256 !== null && workflowSha256 === runningWorkflowSha256;
|
|
917
|
+
if (parsed && (!runningCliMatchesInstalledBuild
|
|
918
|
+
|| !runningModuleMatchesInstalledBuild
|
|
919
|
+
|| !runningGatewayMatchesInstalledBuild
|
|
920
|
+
|| !runningWorkflowMatchesInstalledBuild))
|
|
921
|
+
state = 'stale';
|
|
922
|
+
const manifestSha256 = manifestFile.sha256;
|
|
923
|
+
const manifestFingerprint = parsed?.fingerprint ?? null;
|
|
924
|
+
const expectedFileCount = parsed?.files.length ?? 0;
|
|
925
|
+
return {
|
|
926
|
+
state,
|
|
927
|
+
manifestPath,
|
|
928
|
+
manifestSha256,
|
|
929
|
+
manifestFingerprint,
|
|
930
|
+
verifiedFileCount,
|
|
931
|
+
expectedFileCount,
|
|
932
|
+
cliEntrypointPath: cliPath,
|
|
933
|
+
cliEntrypointSha256: cliSha256,
|
|
934
|
+
runningCliMatchesInstalledBuild,
|
|
935
|
+
reviewerModulePath: reviewerPath,
|
|
936
|
+
reviewerModuleSha256: reviewerSha256,
|
|
937
|
+
runningModuleMatchesInstalledBuild,
|
|
938
|
+
gatewayModulePath: gatewayPath,
|
|
939
|
+
gatewayModuleSha256: gatewaySha256,
|
|
940
|
+
runningGatewayMatchesInstalledBuild,
|
|
941
|
+
workflowModulePath: workflowPath,
|
|
942
|
+
workflowModuleSha256: workflowSha256,
|
|
943
|
+
runningWorkflowMatchesInstalledBuild,
|
|
944
|
+
evidenceSha256: canonicalHash({
|
|
945
|
+
installedPackageRoot,
|
|
946
|
+
manifestSha256,
|
|
947
|
+
manifestFingerprint,
|
|
948
|
+
installedPackageManifestSha256,
|
|
949
|
+
verifiedFileCount,
|
|
950
|
+
expectedFileCount,
|
|
951
|
+
cliSha256,
|
|
952
|
+
runningCliSha256,
|
|
953
|
+
reviewerSha256,
|
|
954
|
+
runningRuntimeSha256,
|
|
955
|
+
gatewaySha256,
|
|
956
|
+
runningGatewaySha256,
|
|
957
|
+
workflowSha256,
|
|
958
|
+
runningWorkflowSha256,
|
|
959
|
+
state,
|
|
960
|
+
}),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function parseRuntimeBuildManifest(value) {
|
|
964
|
+
if (!value
|
|
965
|
+
|| value.protocol !== 'codex-workflow-runtime-build-manifest-v1'
|
|
966
|
+
|| typeof value.packageName !== 'string'
|
|
967
|
+
|| typeof value.packageVersion !== 'string'
|
|
968
|
+
|| typeof value.packageManifestSha256 !== 'string'
|
|
969
|
+
|| !/^[a-f0-9]{64}$/.test(value.packageManifestSha256)
|
|
970
|
+
|| !Array.isArray(value.requiredEntrypoints)
|
|
971
|
+
|| !value.requiredEntrypoints.every((entry) => typeof entry === 'string')
|
|
972
|
+
|| !Array.isArray(value.files)
|
|
973
|
+
|| !Array.isArray(value.runtimeDependencies)
|
|
974
|
+
|| typeof value.fingerprint !== 'string')
|
|
975
|
+
return null;
|
|
976
|
+
const files = parseAttestedFiles(value.files);
|
|
977
|
+
if (files.length !== value.files.length || new Set(files.map((entry) => entry.path)).size !== files.length)
|
|
978
|
+
return null;
|
|
979
|
+
const runtimeDependencies = value.runtimeDependencies.flatMap((entry) => {
|
|
980
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
981
|
+
return [];
|
|
982
|
+
const candidate = entry;
|
|
983
|
+
if (typeof candidate.name !== 'string'
|
|
984
|
+
|| !isPackageName(candidate.name)
|
|
985
|
+
|| typeof candidate.version !== 'string'
|
|
986
|
+
|| typeof candidate.packageManifestSha256 !== 'string'
|
|
987
|
+
|| !/^[a-f0-9]{64}$/.test(candidate.packageManifestSha256)
|
|
988
|
+
|| !Array.isArray(candidate.files))
|
|
989
|
+
return [];
|
|
990
|
+
const dependencyFiles = parseAttestedFiles(candidate.files);
|
|
991
|
+
return dependencyFiles.length === candidate.files.length
|
|
992
|
+
&& new Set(dependencyFiles.map((file) => file.path)).size === dependencyFiles.length
|
|
993
|
+
? [{
|
|
994
|
+
name: candidate.name,
|
|
995
|
+
version: candidate.version,
|
|
996
|
+
packageManifestSha256: candidate.packageManifestSha256,
|
|
997
|
+
files: dependencyFiles,
|
|
998
|
+
}]
|
|
999
|
+
: [];
|
|
1000
|
+
});
|
|
1001
|
+
if (runtimeDependencies.length !== value.runtimeDependencies.length
|
|
1002
|
+
|| new Set(runtimeDependencies.map((entry) => entry.name)).size !== runtimeDependencies.length)
|
|
1003
|
+
return null;
|
|
1004
|
+
return {
|
|
1005
|
+
protocol: value.protocol,
|
|
1006
|
+
packageName: value.packageName,
|
|
1007
|
+
packageVersion: value.packageVersion,
|
|
1008
|
+
packageManifestSha256: value.packageManifestSha256,
|
|
1009
|
+
requiredEntrypoints: [...value.requiredEntrypoints],
|
|
1010
|
+
files,
|
|
1011
|
+
runtimeDependencies,
|
|
1012
|
+
fingerprint: value.fingerprint,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function parseAttestedFiles(value) {
|
|
1016
|
+
return value.flatMap((entry) => {
|
|
1017
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
1018
|
+
return [];
|
|
1019
|
+
const candidate = entry;
|
|
1020
|
+
return typeof candidate.path === 'string' && /^[a-f0-9]{64}$/.test(String(candidate.sha256))
|
|
1021
|
+
? [{ path: candidate.path, sha256: String(candidate.sha256) }]
|
|
1022
|
+
: [];
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function runtimeBuildFingerprint(value) {
|
|
1026
|
+
const lines = [
|
|
1027
|
+
value.protocol,
|
|
1028
|
+
value.packageName,
|
|
1029
|
+
value.packageVersion,
|
|
1030
|
+
value.packageManifestSha256,
|
|
1031
|
+
...value.requiredEntrypoints,
|
|
1032
|
+
];
|
|
1033
|
+
for (const file of value.files)
|
|
1034
|
+
lines.push(file.path, file.sha256);
|
|
1035
|
+
for (const dependency of value.runtimeDependencies) {
|
|
1036
|
+
lines.push(dependency.name, dependency.version, dependency.packageManifestSha256);
|
|
1037
|
+
for (const file of dependency.files)
|
|
1038
|
+
lines.push(file.path, file.sha256);
|
|
1039
|
+
}
|
|
1040
|
+
return createHash('sha256').update(lines.join('\0')).digest('hex');
|
|
1041
|
+
}
|
|
1042
|
+
function safeRuntimeBuildPath(value) {
|
|
1043
|
+
const normalized = value.split('\\').join('/');
|
|
1044
|
+
if (!normalized.startsWith('dist/src/') || !normalized.endsWith('.js') || path.posix.normalize(normalized) !== normalized)
|
|
1045
|
+
return null;
|
|
1046
|
+
if (path.posix.isAbsolute(normalized) || normalized.split('/').includes('..'))
|
|
1047
|
+
return null;
|
|
1048
|
+
return normalized;
|
|
1049
|
+
}
|
|
1050
|
+
function safeRuntimeDependencyPath(value) {
|
|
1051
|
+
const normalized = value.split('\\').join('/');
|
|
1052
|
+
if (!/\.(?:cjs|js|json|mjs|node)$/.test(normalized)
|
|
1053
|
+
|| path.posix.normalize(normalized) !== normalized
|
|
1054
|
+
|| path.posix.isAbsolute(normalized)
|
|
1055
|
+
|| normalized.split('/').includes('..')
|
|
1056
|
+
|| normalized.split('/').includes('node_modules'))
|
|
1057
|
+
return null;
|
|
1058
|
+
return normalized;
|
|
1059
|
+
}
|
|
1060
|
+
function isPackageName(value) {
|
|
1061
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
|
|
1062
|
+
}
|
|
1063
|
+
function readJsonRecord(filePath) {
|
|
1064
|
+
if (!filePath)
|
|
1065
|
+
return null;
|
|
1066
|
+
try {
|
|
1067
|
+
const value = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1068
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function nestedString(value, keys) {
|
|
1075
|
+
let cursor = value;
|
|
1076
|
+
for (const key of keys) {
|
|
1077
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
1078
|
+
return null;
|
|
1079
|
+
cursor = cursor[key];
|
|
1080
|
+
}
|
|
1081
|
+
return typeof cursor === 'string' ? cursor : null;
|
|
1082
|
+
}
|
|
1083
|
+
function stringField(value, key) {
|
|
1084
|
+
const candidate = value?.[key];
|
|
1085
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
1086
|
+
}
|
|
1087
|
+
function isPlainDirectory(directoryPath) {
|
|
1088
|
+
try {
|
|
1089
|
+
return existsSync(directoryPath) && lstatSync(directoryPath).isDirectory();
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
296
1092
|
return false;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function isUnredirectedPathInside(root, candidate) {
|
|
1096
|
+
try {
|
|
1097
|
+
const logicalRoot = path.resolve(root);
|
|
1098
|
+
const logicalCandidate = path.resolve(candidate);
|
|
1099
|
+
if (isOutside(logicalRoot, logicalCandidate))
|
|
1100
|
+
return false;
|
|
1101
|
+
const expectedRealPath = path.join(realpathSync(logicalRoot), path.relative(logicalRoot, logicalCandidate));
|
|
1102
|
+
return realpathSync(logicalCandidate) === expectedRealPath;
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function isPlainFileInside(root, filePath) {
|
|
1109
|
+
return inspectRuntimeFile(root, filePath).state === 'ready';
|
|
1110
|
+
}
|
|
1111
|
+
function inspectRuntimeFile(root, filePath) {
|
|
1112
|
+
try {
|
|
1113
|
+
if (!existsSync(filePath))
|
|
1114
|
+
return { state: 'missing', sha256: null };
|
|
1115
|
+
if (!lstatSync(filePath).isFile())
|
|
1116
|
+
return { state: 'unsafe', sha256: null };
|
|
1117
|
+
const realRoot = realpathSync(root);
|
|
1118
|
+
const realFile = realpathSync(filePath);
|
|
1119
|
+
if (isOutside(realRoot, realFile))
|
|
1120
|
+
return { state: 'unsafe', sha256: null };
|
|
1121
|
+
return { state: 'ready', sha256: sha256File(realFile) };
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
return { state: 'unsafe', sha256: null };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function assertReviewMode(value) {
|
|
1128
|
+
if (value !== 'ordinary' && value !== 'security') {
|
|
1129
|
+
throw new Error('review mode must be ordinary or security');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function reviewInstruction(mode) {
|
|
1133
|
+
return mode === 'security'
|
|
1134
|
+
? 'Perform the requested security-focused review within the supplied sealed scope.'
|
|
1135
|
+
: 'Perform an ordinary correctness and acceptance review. Do not initiate or substitute an implicit security scan; report security-relevant defects only when encountered in the supplied scope.';
|
|
1136
|
+
}
|
|
1137
|
+
function reviewPolicy(mode) {
|
|
1138
|
+
return { mode, implicitSecurityScan: false, securityFocusRequested: mode === 'security' };
|
|
1139
|
+
}
|
|
1140
|
+
function isOutside(root, candidate) {
|
|
1141
|
+
const relative = path.relative(root, candidate);
|
|
1142
|
+
return relative.startsWith('..') && !path.isAbsolute(relative);
|
|
1143
|
+
}
|
|
1144
|
+
function sha256File(filePath) {
|
|
1145
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
1146
|
+
}
|
|
1147
|
+
function isReviewFindingRecord(value) {
|
|
1148
|
+
return isExactRecord(value, [
|
|
1149
|
+
'id',
|
|
1150
|
+
'severity',
|
|
1151
|
+
'requirement',
|
|
1152
|
+
'summary',
|
|
1153
|
+
'evidence',
|
|
1154
|
+
'requiredAction',
|
|
1155
|
+
'causeId',
|
|
1156
|
+
'route',
|
|
1157
|
+
'planConflict',
|
|
1158
|
+
]);
|
|
301
1159
|
}
|
|
302
1160
|
function validatePlanConflict(value, route) {
|
|
303
1161
|
if (route === 'fix') {
|
|
@@ -349,7 +1207,7 @@ function runStrictReviewerLaunch(repositoryRoot, options, runner) {
|
|
|
349
1207
|
const temporary = mkdtempSync(path.join(os.tmpdir(), 'codex-workflow-review-'));
|
|
350
1208
|
const schemaFile = path.join(temporary, 'review-result.schema.json');
|
|
351
1209
|
const outputFile = path.join(temporary, 'last-message.json');
|
|
352
|
-
writeFileSync(schemaFile, `${JSON.stringify(
|
|
1210
|
+
writeFileSync(schemaFile, `${JSON.stringify(STRICT_REVIEW_OUTPUT_SCHEMA, null, 2)}\n`, { mode: 0o600 });
|
|
353
1211
|
const probe = path.join(repositoryRoot, '.codex-reviewer-write-probe');
|
|
354
1212
|
if (existsSync(probe)) {
|
|
355
1213
|
rmSync(temporary, { recursive: true, force: true });
|
|
@@ -449,13 +1307,58 @@ function sealsEqual(left, right) {
|
|
|
449
1307
|
}
|
|
450
1308
|
function parseStrictReview(value) {
|
|
451
1309
|
const parsed = JSON.parse(value);
|
|
452
|
-
if (!['
|
|
1310
|
+
if (!isExactRecord(parsed, ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'])) {
|
|
1311
|
+
throw new Error('invalid review');
|
|
1312
|
+
}
|
|
1313
|
+
if (!['passed', 'failed', 'unverified'].includes(String(parsed.status)))
|
|
453
1314
|
throw new Error('invalid status');
|
|
454
|
-
if (
|
|
1315
|
+
if (typeof parsed.reviewer !== 'string' || !parsed.reviewer.trim()
|
|
1316
|
+
|| typeof parsed.summary !== 'string' || !parsed.summary.trim()
|
|
1317
|
+
|| !Array.isArray(parsed.findings))
|
|
455
1318
|
throw new Error('invalid review');
|
|
456
|
-
if (!['denied', 'written', 'not-attempted'].includes(parsed.isolationProbe
|
|
1319
|
+
if (!['denied', 'written', 'not-attempted'].includes(String(parsed.isolationProbe)))
|
|
457
1320
|
throw new Error('invalid probe');
|
|
458
|
-
|
|
1321
|
+
const findings = parsed.findings.map((candidate) => {
|
|
1322
|
+
if (!isReviewFindingRecord(candidate)
|
|
1323
|
+
|| typeof candidate.id !== 'string' || !candidate.id.trim()
|
|
1324
|
+
|| !['critical', 'major', 'minor'].includes(String(candidate.severity))
|
|
1325
|
+
|| (candidate.requirement !== null && typeof candidate.requirement !== 'string')
|
|
1326
|
+
|| typeof candidate.summary !== 'string' || !candidate.summary.trim()
|
|
1327
|
+
|| typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
|
|
1328
|
+
|| typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
|
|
1329
|
+
throw new Error('invalid review finding');
|
|
1330
|
+
}
|
|
1331
|
+
if (candidate.route !== 'fix' && candidate.route !== 'replan')
|
|
1332
|
+
throw new Error('invalid review finding route');
|
|
1333
|
+
const route = candidate.route;
|
|
1334
|
+
const causeId = candidate.causeId;
|
|
1335
|
+
if (causeId !== null
|
|
1336
|
+
&& (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
|
|
1337
|
+
throw new Error('invalid review finding causeId');
|
|
1338
|
+
}
|
|
1339
|
+
const planConflict = validatePlanConflict(candidate.planConflict, route);
|
|
1340
|
+
return {
|
|
1341
|
+
id: candidate.id,
|
|
1342
|
+
severity: candidate.severity,
|
|
1343
|
+
requirement: candidate.requirement,
|
|
1344
|
+
summary: candidate.summary,
|
|
1345
|
+
evidence: candidate.evidence,
|
|
1346
|
+
requiredAction: candidate.requiredAction,
|
|
1347
|
+
...(causeId === null ? {} : { causeId }),
|
|
1348
|
+
route,
|
|
1349
|
+
planConflict,
|
|
1350
|
+
};
|
|
1351
|
+
});
|
|
1352
|
+
if ((parsed.status === 'passed' && findings.length > 0)
|
|
1353
|
+
|| (parsed.status === 'failed' && findings.length === 0))
|
|
1354
|
+
throw new Error('invalid finding cardinality');
|
|
1355
|
+
return {
|
|
1356
|
+
status: parsed.status,
|
|
1357
|
+
reviewer: parsed.reviewer,
|
|
1358
|
+
summary: parsed.summary,
|
|
1359
|
+
findings,
|
|
1360
|
+
isolationProbe: parsed.isolationProbe,
|
|
1361
|
+
};
|
|
459
1362
|
}
|
|
460
1363
|
function parseStrictReviewWithHash(value) {
|
|
461
1364
|
const output = parseStrictReview(value);
|
|
@@ -499,5 +1402,4 @@ function failedStepLaunch(repositoryRoot, command, processStatus, summary, revie
|
|
|
499
1402
|
reviewerOutputHash: hashReviewInput(base.review),
|
|
500
1403
|
};
|
|
501
1404
|
}
|
|
502
|
-
export { REVIEW_SCHEMA };
|
|
503
1405
|
//# sourceMappingURL=reviewer.js.map
|