codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14
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 +27 -388
- package/dist/reviewer-runtime-build.json +96 -32
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/plan-risk.d.ts +2 -0
- package/dist/src/alpha6/plan-risk.js +10 -3
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +5 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +37 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +6 -5
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +2 -2
- package/dist/src/state/corrective-yield-executor.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -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-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +48 -63
- package/dist/src/workflow.js +698 -1470
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -363
- package/docs/updating-existing-project.md +19 -717
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +39 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2438 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +843 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +572 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +660 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10172 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- 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/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
package/src/reviewer.ts
ADDED
|
@@ -0,0 +1,1879 @@
|
|
|
1
|
+
import { SUPPORTED_NODE_VERSION, isSupportedNodeVersion } from './runtime.js';
|
|
2
|
+
import { verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
PROTOCOL_VERSION,
|
|
12
|
+
STATE_SCHEMA_VERSION,
|
|
13
|
+
type ContextEnvelope,
|
|
14
|
+
type ReviewInput,
|
|
15
|
+
type StepEvidence,
|
|
16
|
+
type TaskState,
|
|
17
|
+
} from './contracts.js';
|
|
18
|
+
import { canonicalJsonStringify } from './alpha6/store-sidecars.js';
|
|
19
|
+
import { changedFiles, headCommit, runGit } from './repository.js';
|
|
20
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
21
|
+
|
|
22
|
+
export type ReviewMode = 'ordinary' | 'security';
|
|
23
|
+
|
|
24
|
+
export interface ReviewerRuntimePreflight {
|
|
25
|
+
protocol: 'codex-workflow-reviewer-runtime-preflight-v1';
|
|
26
|
+
prepared: boolean;
|
|
27
|
+
node: { executable: string; version: string; requiredVersion: string; supported: boolean };
|
|
28
|
+
workflowPackage: {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
runtimeModulePath: string;
|
|
32
|
+
runtimeModuleSha256: string;
|
|
33
|
+
installedPackageRoot: string;
|
|
34
|
+
installedPackageManifestPath: string;
|
|
35
|
+
installedPackageManifestSha256: string | null;
|
|
36
|
+
declaredVersion: string | null;
|
|
37
|
+
lockedVersion: string | null;
|
|
38
|
+
installedVersion: string | null;
|
|
39
|
+
exactRepositoryLocal: boolean;
|
|
40
|
+
};
|
|
41
|
+
buildReadiness: {
|
|
42
|
+
state: 'ready' | 'missing' | 'stale';
|
|
43
|
+
manifestPath: string;
|
|
44
|
+
manifestSha256: string | null;
|
|
45
|
+
manifestFingerprint: string | null;
|
|
46
|
+
verifiedFileCount: number;
|
|
47
|
+
expectedFileCount: number;
|
|
48
|
+
cliEntrypointPath: string;
|
|
49
|
+
cliEntrypointSha256: string | null;
|
|
50
|
+
runningCliMatchesInstalledBuild: boolean;
|
|
51
|
+
reviewerModulePath: string;
|
|
52
|
+
reviewerModuleSha256: string | null;
|
|
53
|
+
runningModuleMatchesInstalledBuild: boolean;
|
|
54
|
+
gatewayModulePath: string;
|
|
55
|
+
gatewayModuleSha256: string | null;
|
|
56
|
+
runningGatewayMatchesInstalledBuild: boolean;
|
|
57
|
+
workflowModulePath: string;
|
|
58
|
+
workflowModuleSha256: string | null;
|
|
59
|
+
runningWorkflowMatchesInstalledBuild: boolean;
|
|
60
|
+
evidenceSha256: string;
|
|
61
|
+
};
|
|
62
|
+
runtimeDependencyReadiness: {
|
|
63
|
+
state: 'ready' | 'missing' | 'stale' | 'not-run';
|
|
64
|
+
expectedPackageCount: number;
|
|
65
|
+
verifiedPackageCount: number;
|
|
66
|
+
expectedFileCount: number;
|
|
67
|
+
verifiedFileCount: number;
|
|
68
|
+
packages: Array<{
|
|
69
|
+
name: string;
|
|
70
|
+
version: string;
|
|
71
|
+
packageRoot: string;
|
|
72
|
+
packageManifestPath: string;
|
|
73
|
+
packageManifestSha256: string | null;
|
|
74
|
+
expectedFileCount: number;
|
|
75
|
+
verifiedFileCount: number;
|
|
76
|
+
state: 'ready' | 'missing' | 'stale';
|
|
77
|
+
}>;
|
|
78
|
+
evidenceSha256: string;
|
|
79
|
+
};
|
|
80
|
+
runtimeCheck: {
|
|
81
|
+
kind: 'repository-local-gateway-load';
|
|
82
|
+
state: 'passed' | 'failed' | 'not-run';
|
|
83
|
+
loader: 'node:createRequire';
|
|
84
|
+
executable: string;
|
|
85
|
+
gatewayEntrypointPath: string;
|
|
86
|
+
gatewayEntrypointSha256: string | null;
|
|
87
|
+
reviewerEntrypointPath: string;
|
|
88
|
+
reviewerEntrypointSha256: string | null;
|
|
89
|
+
workflowEntrypointPath: string;
|
|
90
|
+
workflowEntrypointSha256: string | null;
|
|
91
|
+
dependencyClosureEvidenceSha256: string;
|
|
92
|
+
observedPackageName: string | null;
|
|
93
|
+
observedPackageVersion: string | null;
|
|
94
|
+
observedProtocolVersion: number | null;
|
|
95
|
+
observedStateSchemaVersion: number | null;
|
|
96
|
+
resultSha256: string | null;
|
|
97
|
+
failureKind: 'module-load-failed' | 'contract-mismatch' | null;
|
|
98
|
+
evidenceSha256: string;
|
|
99
|
+
sealedCheckoutMutationAllowed: false;
|
|
100
|
+
};
|
|
101
|
+
repositoryDependencyReadiness: {
|
|
102
|
+
packageManifestPath: string | null;
|
|
103
|
+
packageManifestSha256: string | null;
|
|
104
|
+
packageLockPath: string | null;
|
|
105
|
+
packageLockSha256: string | null;
|
|
106
|
+
nodeModulesPath: string;
|
|
107
|
+
nodeModulesPresent: boolean;
|
|
108
|
+
state:
|
|
109
|
+
| 'ready'
|
|
110
|
+
| 'not-installed'
|
|
111
|
+
| 'not-applicable'
|
|
112
|
+
| 'version-mismatch'
|
|
113
|
+
| 'build-missing'
|
|
114
|
+
| 'build-stale'
|
|
115
|
+
| 'runtime-dependency-missing'
|
|
116
|
+
| 'runtime-dependency-stale'
|
|
117
|
+
| 'runtime-check-failed';
|
|
118
|
+
installAttempted: false;
|
|
119
|
+
installCommand: null;
|
|
120
|
+
};
|
|
121
|
+
ephemeralCache: {
|
|
122
|
+
path: string;
|
|
123
|
+
writable: boolean;
|
|
124
|
+
outsideSealedCheckout: boolean;
|
|
125
|
+
lifecycle: 'os-temporary';
|
|
126
|
+
};
|
|
127
|
+
sealedCheckoutMutationAllowed: false;
|
|
128
|
+
blockers: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ReviewerProcessResult {
|
|
132
|
+
status: number | null;
|
|
133
|
+
stdout: string;
|
|
134
|
+
stderr: string;
|
|
135
|
+
timedOut?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ReviewerCommandRunner = (
|
|
139
|
+
command: string,
|
|
140
|
+
args: string[],
|
|
141
|
+
options: { cwd: string; input: string; timeoutMs: number },
|
|
142
|
+
) => ReviewerProcessResult;
|
|
143
|
+
|
|
144
|
+
export interface StrictReviewOutput extends ReviewInput {
|
|
145
|
+
isolationProbe: 'denied' | 'written' | 'not-attempted';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface StrictReviewerLaunchResult {
|
|
149
|
+
review: ReviewInput;
|
|
150
|
+
verifiedIsolation: boolean;
|
|
151
|
+
command: string[];
|
|
152
|
+
processStatus: number | null;
|
|
153
|
+
timedOut: boolean;
|
|
154
|
+
stderr: string;
|
|
155
|
+
before: RepositorySeal;
|
|
156
|
+
after: RepositorySeal;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface StrictStepReviewerLaunchResult extends StrictReviewerLaunchResult {
|
|
160
|
+
reviewedCommit: string;
|
|
161
|
+
reviewerOutputHash: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface ParsedStrictReviewResult {
|
|
165
|
+
output: StrictReviewOutput;
|
|
166
|
+
outputHash: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface RepositorySeal {
|
|
170
|
+
headCommit: string;
|
|
171
|
+
changedFiles: string[];
|
|
172
|
+
contentHash: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ExternalStrictStepReviewPacket {
|
|
176
|
+
protocol: 'codex-workflow-external-strict-step-review-v1';
|
|
177
|
+
taskId: string;
|
|
178
|
+
stepId: string;
|
|
179
|
+
reviewedCommit: string;
|
|
180
|
+
packet: Record<string, unknown>;
|
|
181
|
+
packetHash: string;
|
|
182
|
+
repositorySeal: RepositorySeal;
|
|
183
|
+
repositorySealHash: string;
|
|
184
|
+
reviewMode: ReviewMode;
|
|
185
|
+
reviewerRuntime: ReviewerRuntimePreflight;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ExternalStrictStepReviewInput {
|
|
189
|
+
protocol: 'codex-workflow-external-strict-step-review-v1';
|
|
190
|
+
packetHash: string;
|
|
191
|
+
repositorySealHash: string;
|
|
192
|
+
reviewerThreadId: string;
|
|
193
|
+
reviewMode: ReviewMode;
|
|
194
|
+
review: ReviewInput;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface ExternalStrictTaskReviewPacket {
|
|
198
|
+
protocol: 'codex-workflow-external-strict-task-review-v1';
|
|
199
|
+
taskId: string;
|
|
200
|
+
reviewedCommit: string;
|
|
201
|
+
packet: Record<string, unknown>;
|
|
202
|
+
packetHash: string;
|
|
203
|
+
repositorySeal: RepositorySeal;
|
|
204
|
+
repositorySealHash: string;
|
|
205
|
+
reviewMode: ReviewMode;
|
|
206
|
+
reviewerRuntime: ReviewerRuntimePreflight;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ExternalStrictTaskReviewInput {
|
|
210
|
+
protocol: 'codex-workflow-external-strict-task-review-v1';
|
|
211
|
+
packetHash: string;
|
|
212
|
+
repositorySealHash: string;
|
|
213
|
+
reviewerThreadId: string;
|
|
214
|
+
reviewMode: ReviewMode;
|
|
215
|
+
review: ReviewInput;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ReviewScopeAugmentation {
|
|
219
|
+
stepId: string;
|
|
220
|
+
paths: string[];
|
|
221
|
+
authority: 'machine-derived-check-support';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const FULL_GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
225
|
+
export const STRICT_REVIEWER_TIMEOUT_MS = 180_000;
|
|
226
|
+
|
|
227
|
+
export const STRICT_REVIEW_OUTPUT_SCHEMA = {
|
|
228
|
+
type: 'object',
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
required: ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'],
|
|
231
|
+
properties: {
|
|
232
|
+
status: { enum: ['passed', 'failed', 'unverified'] },
|
|
233
|
+
reviewer: { type: 'string', minLength: 1 },
|
|
234
|
+
summary: { type: 'string', minLength: 1 },
|
|
235
|
+
isolationProbe: { enum: ['denied', 'written', 'not-attempted'] },
|
|
236
|
+
findings: {
|
|
237
|
+
type: 'array',
|
|
238
|
+
items: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
required: [
|
|
242
|
+
'id',
|
|
243
|
+
'severity',
|
|
244
|
+
'requirement',
|
|
245
|
+
'summary',
|
|
246
|
+
'evidence',
|
|
247
|
+
'requiredAction',
|
|
248
|
+
'causeId',
|
|
249
|
+
'route',
|
|
250
|
+
'planConflict',
|
|
251
|
+
],
|
|
252
|
+
properties: {
|
|
253
|
+
id: { type: 'string', minLength: 1 },
|
|
254
|
+
severity: { enum: ['critical', 'major', 'minor'] },
|
|
255
|
+
requirement: { type: ['string', 'null'] },
|
|
256
|
+
summary: { type: 'string', minLength: 1 },
|
|
257
|
+
evidence: { type: 'string', minLength: 1 },
|
|
258
|
+
requiredAction: { type: 'string', minLength: 1 },
|
|
259
|
+
causeId: {
|
|
260
|
+
anyOf: [
|
|
261
|
+
{ type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$' },
|
|
262
|
+
{ type: 'null' },
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
route: { enum: ['fix', 'replan'] },
|
|
266
|
+
planConflict: {
|
|
267
|
+
anyOf: [
|
|
268
|
+
{ type: 'null' },
|
|
269
|
+
{
|
|
270
|
+
type: 'object',
|
|
271
|
+
additionalProperties: false,
|
|
272
|
+
required: ['planClause', 'whyFixCannotFit', 'minimalPlanChange'],
|
|
273
|
+
properties: {
|
|
274
|
+
planClause: { type: 'string', minLength: 1 },
|
|
275
|
+
whyFixCannotFit: { type: 'string', minLength: 1 },
|
|
276
|
+
minimalPlanChange: { type: 'string', minLength: 1 },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
} as const;
|
|
286
|
+
|
|
287
|
+
/** @deprecated Use STRICT_REVIEW_OUTPUT_SCHEMA. Retained for direct-module compatibility. */
|
|
288
|
+
export const REVIEW_SCHEMA = STRICT_REVIEW_OUTPUT_SCHEMA;
|
|
289
|
+
|
|
290
|
+
export function launchStrictReviewer(
|
|
291
|
+
repositoryRoot: string,
|
|
292
|
+
task: TaskState,
|
|
293
|
+
taskRoot: string,
|
|
294
|
+
envelope: ContextEnvelope,
|
|
295
|
+
runner: ReviewerCommandRunner = spawnReviewer,
|
|
296
|
+
scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
|
|
297
|
+
): StrictReviewerLaunchResult {
|
|
298
|
+
const launch = runStrictReviewerLaunch(
|
|
299
|
+
repositoryRoot,
|
|
300
|
+
{
|
|
301
|
+
protocol: 'codex-workflow-strict-review-v1',
|
|
302
|
+
task,
|
|
303
|
+
taskRoot,
|
|
304
|
+
envelope,
|
|
305
|
+
commandSummary: 'Strict reviewer did not return valid output proving the denied write probe.',
|
|
306
|
+
packet: {
|
|
307
|
+
protocol: 'codex-workflow-strict-review-v1',
|
|
308
|
+
instruction: [
|
|
309
|
+
'Act only as the independent reviewer described by the context envelope.',
|
|
310
|
+
'Before reviewing, attempt to create .codex-reviewer-write-probe in the repository.',
|
|
311
|
+
'The attempt must be denied by the sandbox. Report isolationProbe="denied" only when it was denied.',
|
|
312
|
+
'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.',
|
|
313
|
+
'Give each finding a stable causeId for the underlying defect; never encode the review attempt number.',
|
|
314
|
+
'Do not modify any repository file. Return only the requested JSON result.',
|
|
315
|
+
],
|
|
316
|
+
envelope,
|
|
317
|
+
task: {
|
|
318
|
+
id: task.id,
|
|
319
|
+
baseCommit: task.baseCommit,
|
|
320
|
+
headCommit: headCommit(repositoryRoot),
|
|
321
|
+
brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
|
|
322
|
+
plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
|
|
323
|
+
evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
|
|
324
|
+
diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
|
|
325
|
+
scopeAugmentations,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
runner,
|
|
330
|
+
);
|
|
331
|
+
return {
|
|
332
|
+
review: launch.review,
|
|
333
|
+
verifiedIsolation: launch.verifiedIsolation,
|
|
334
|
+
command: launch.command,
|
|
335
|
+
processStatus: launch.processStatus,
|
|
336
|
+
timedOut: launch.timedOut,
|
|
337
|
+
stderr: launch.stderr,
|
|
338
|
+
before: launch.before,
|
|
339
|
+
after: launch.after,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function launchStrictStepReviewer(
|
|
344
|
+
repositoryRoot: string,
|
|
345
|
+
task: TaskState,
|
|
346
|
+
taskRoot: string,
|
|
347
|
+
envelope: ContextEnvelope,
|
|
348
|
+
stepId: string,
|
|
349
|
+
currentEvidence: StepEvidence,
|
|
350
|
+
runner: ReviewerCommandRunner = spawnReviewer,
|
|
351
|
+
scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
|
|
352
|
+
): StrictStepReviewerLaunchResult {
|
|
353
|
+
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
354
|
+
if (!step) {
|
|
355
|
+
return failedStepLaunch(
|
|
356
|
+
repositoryRoot,
|
|
357
|
+
[],
|
|
358
|
+
null,
|
|
359
|
+
`Step ${stepId} was not found on task ${task.id}.`,
|
|
360
|
+
currentEvidence.commitSha,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (!step.evidence) {
|
|
364
|
+
return failedStepLaunch(
|
|
365
|
+
repositoryRoot,
|
|
366
|
+
[],
|
|
367
|
+
null,
|
|
368
|
+
`Step ${stepId} has no canonical evidence to review on task ${task.id}.`,
|
|
369
|
+
currentEvidence.commitSha,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
|
|
373
|
+
return failedStepLaunch(
|
|
374
|
+
repositoryRoot,
|
|
375
|
+
[],
|
|
376
|
+
null,
|
|
377
|
+
`Strict step reviewer requires the exact canonical Step evidence for ${stepId} on task ${task.id}.`,
|
|
378
|
+
currentEvidence.commitSha,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const reviewedCommit = currentEvidence.commitSha;
|
|
382
|
+
if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
|
|
383
|
+
return failedStepLaunch(
|
|
384
|
+
repositoryRoot,
|
|
385
|
+
[],
|
|
386
|
+
null,
|
|
387
|
+
`Strict step reviewer requires a full lowercase Git SHA for ${stepId} on task ${task.id}.`,
|
|
388
|
+
reviewedCommit,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const currentHead = headCommit(repositoryRoot);
|
|
392
|
+
if (reviewedCommit !== currentHead) {
|
|
393
|
+
return failedStepLaunch(
|
|
394
|
+
repositoryRoot,
|
|
395
|
+
[],
|
|
396
|
+
null,
|
|
397
|
+
`Strict step reviewer requires the canonical Step evidence commit to match repository HEAD for ${stepId} on task ${task.id}.`,
|
|
398
|
+
reviewedCommit,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const packet = strictStepReviewPayload(
|
|
402
|
+
repositoryRoot,
|
|
403
|
+
task,
|
|
404
|
+
taskRoot,
|
|
405
|
+
envelope,
|
|
406
|
+
step,
|
|
407
|
+
reviewedCommit,
|
|
408
|
+
currentEvidence,
|
|
409
|
+
scopeAugmentations,
|
|
410
|
+
);
|
|
411
|
+
const launch = runStrictReviewerLaunch(
|
|
412
|
+
repositoryRoot,
|
|
413
|
+
{
|
|
414
|
+
protocol: 'codex-workflow-strict-step-review-v1',
|
|
415
|
+
task,
|
|
416
|
+
taskRoot,
|
|
417
|
+
envelope,
|
|
418
|
+
commandSummary: 'Strict step reviewer did not return valid output proving the denied write probe.',
|
|
419
|
+
packet,
|
|
420
|
+
},
|
|
421
|
+
runner,
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
review: launch.review,
|
|
425
|
+
verifiedIsolation: launch.verifiedIsolation,
|
|
426
|
+
command: launch.command,
|
|
427
|
+
processStatus: launch.processStatus,
|
|
428
|
+
timedOut: launch.timedOut,
|
|
429
|
+
stderr: launch.stderr,
|
|
430
|
+
before: launch.before,
|
|
431
|
+
after: launch.after,
|
|
432
|
+
reviewedCommit,
|
|
433
|
+
reviewerOutputHash: launch.reviewerOutputHash,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function prepareExternalStrictStepReviewPacket(
|
|
438
|
+
repositoryRoot: string,
|
|
439
|
+
task: TaskState,
|
|
440
|
+
taskRoot: string,
|
|
441
|
+
envelope: ContextEnvelope,
|
|
442
|
+
stepId: string,
|
|
443
|
+
currentEvidence: StepEvidence,
|
|
444
|
+
scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
|
|
445
|
+
reviewMode: ReviewMode = 'ordinary',
|
|
446
|
+
): ExternalStrictStepReviewPacket {
|
|
447
|
+
assertReviewMode(reviewMode);
|
|
448
|
+
const step = task.steps.find((candidate) => candidate.id === stepId);
|
|
449
|
+
if (!step || !step.evidence || canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
|
|
450
|
+
throw new Error(`Strict Step Review packet requires exact canonical evidence for ${stepId}.`);
|
|
451
|
+
}
|
|
452
|
+
const reviewedCommit = currentEvidence.commitSha;
|
|
453
|
+
const pendingReviewUpdate = verifiedPendingReviewTransport(repositoryRoot, task, stepId, taskRoot);
|
|
454
|
+
if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit) || (reviewedCommit !== headCommit(repositoryRoot) && !pendingReviewUpdate)) {
|
|
455
|
+
throw new Error(`Strict Step Review packet requires ${stepId} evidence to match repository HEAD.`);
|
|
456
|
+
}
|
|
457
|
+
const reviewerRuntime = prepareReviewerRuntimePreflight(
|
|
458
|
+
repositoryRoot,
|
|
459
|
+
`${task.id}:${stepId}:${reviewedCommit}:${reviewMode}`,
|
|
460
|
+
);
|
|
461
|
+
assertReviewerRuntimePrepared(reviewerRuntime);
|
|
462
|
+
const basePacket = strictStepReviewPayload(
|
|
463
|
+
repositoryRoot,
|
|
464
|
+
task,
|
|
465
|
+
taskRoot,
|
|
466
|
+
envelope,
|
|
467
|
+
step,
|
|
468
|
+
reviewedCommit,
|
|
469
|
+
currentEvidence,
|
|
470
|
+
scopeAugmentations,
|
|
471
|
+
);
|
|
472
|
+
const packet = {
|
|
473
|
+
...basePacket,
|
|
474
|
+
...(pendingReviewUpdate ? { pendingReviewUpdate } : {}),
|
|
475
|
+
instruction: [...(basePacket.instruction as string[]), reviewInstruction(reviewMode)],
|
|
476
|
+
reviewMode,
|
|
477
|
+
reviewPolicy: reviewPolicy(reviewMode),
|
|
478
|
+
reviewerRuntime,
|
|
479
|
+
};
|
|
480
|
+
const repositorySeal = sealRepository(repositoryRoot);
|
|
481
|
+
return {
|
|
482
|
+
protocol: 'codex-workflow-external-strict-step-review-v1',
|
|
483
|
+
taskId: task.id,
|
|
484
|
+
stepId,
|
|
485
|
+
reviewedCommit,
|
|
486
|
+
packet,
|
|
487
|
+
packetHash: canonicalHash(packet),
|
|
488
|
+
repositorySeal,
|
|
489
|
+
repositorySealHash: canonicalHash(repositorySeal),
|
|
490
|
+
reviewMode,
|
|
491
|
+
reviewerRuntime,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function prepareExternalStrictTaskReviewPacket(
|
|
496
|
+
repositoryRoot: string,
|
|
497
|
+
task: TaskState,
|
|
498
|
+
taskRoot: string,
|
|
499
|
+
envelope: ContextEnvelope,
|
|
500
|
+
scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
|
|
501
|
+
reviewMode: ReviewMode = 'ordinary',
|
|
502
|
+
): ExternalStrictTaskReviewPacket {
|
|
503
|
+
assertReviewMode(reviewMode);
|
|
504
|
+
const reviewedCommit = headCommit(repositoryRoot);
|
|
505
|
+
if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
|
|
506
|
+
throw new Error(`Strict Task Review packet requires a full lowercase Git SHA for ${task.id}.`);
|
|
507
|
+
}
|
|
508
|
+
const reviewerRuntime = prepareReviewerRuntimePreflight(
|
|
509
|
+
repositoryRoot,
|
|
510
|
+
`${task.id}:task:${reviewedCommit}:${reviewMode}`,
|
|
511
|
+
);
|
|
512
|
+
assertReviewerRuntimePrepared(reviewerRuntime);
|
|
513
|
+
const packet = {
|
|
514
|
+
protocol: 'codex-workflow-strict-review-v1',
|
|
515
|
+
instruction: [
|
|
516
|
+
'Act only as the independent reviewer described by the context envelope.',
|
|
517
|
+
'Review the supplied Task result and bind every finding to reviewedCommit.',
|
|
518
|
+
'For each finding use route="fix" by default. Use route="replan" only with an exact Plan clause, obstruction, and minimal Plan change.',
|
|
519
|
+
'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
|
|
520
|
+
reviewInstruction(reviewMode),
|
|
521
|
+
],
|
|
522
|
+
reviewMode,
|
|
523
|
+
reviewPolicy: reviewPolicy(reviewMode),
|
|
524
|
+
reviewerRuntime,
|
|
525
|
+
envelope,
|
|
526
|
+
task: { ...strictTaskReviewSubject(repositoryRoot, task, taskRoot), scopeAugmentations },
|
|
527
|
+
};
|
|
528
|
+
const repositorySeal = sealRepository(repositoryRoot);
|
|
529
|
+
return {
|
|
530
|
+
protocol: 'codex-workflow-external-strict-task-review-v1',
|
|
531
|
+
taskId: task.id,
|
|
532
|
+
reviewedCommit,
|
|
533
|
+
packet,
|
|
534
|
+
packetHash: canonicalHash(packet),
|
|
535
|
+
repositorySeal,
|
|
536
|
+
repositorySealHash: canonicalHash(repositorySeal),
|
|
537
|
+
reviewMode,
|
|
538
|
+
reviewerRuntime,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function validateExternalStrictStepReviewInput(
|
|
543
|
+
value: unknown,
|
|
544
|
+
expected: ExternalStrictStepReviewPacket,
|
|
545
|
+
): ReviewInput {
|
|
546
|
+
return validateExternalReviewEnvelope(value, expected);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function validateExternalStrictTaskReviewInput(
|
|
550
|
+
value: unknown,
|
|
551
|
+
expected: ExternalStrictTaskReviewPacket,
|
|
552
|
+
): ReviewInput {
|
|
553
|
+
return validateExternalReviewEnvelope(value, expected);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function strictReviewInputHash(review: ReviewInput): string {
|
|
557
|
+
return hashReviewInput(review);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function strictStepReviewPayload(
|
|
561
|
+
repositoryRoot: string,
|
|
562
|
+
task: TaskState,
|
|
563
|
+
taskRoot: string,
|
|
564
|
+
envelope: ContextEnvelope,
|
|
565
|
+
step: TaskState['steps'][number],
|
|
566
|
+
reviewedCommit: string,
|
|
567
|
+
currentEvidence: StepEvidence,
|
|
568
|
+
scopeAugmentations: readonly ReviewScopeAugmentation[],
|
|
569
|
+
): Record<string, unknown> {
|
|
570
|
+
return {
|
|
571
|
+
protocol: 'codex-workflow-strict-step-review-v1',
|
|
572
|
+
instruction: [
|
|
573
|
+
'Act only as the independent reviewer described by the context envelope.',
|
|
574
|
+
'Review only the supplied Task Step and bind findings to reviewedCommit.',
|
|
575
|
+
'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.',
|
|
576
|
+
'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.',
|
|
577
|
+
'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
|
|
578
|
+
],
|
|
579
|
+
envelope,
|
|
580
|
+
task: {
|
|
581
|
+
id: task.id,
|
|
582
|
+
baseCommit: task.baseCommit,
|
|
583
|
+
headCommit: headCommit(repositoryRoot),
|
|
584
|
+
brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
|
|
585
|
+
plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
|
|
586
|
+
diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
|
|
587
|
+
},
|
|
588
|
+
step: {
|
|
589
|
+
id: step.id,
|
|
590
|
+
title: step.title,
|
|
591
|
+
objective: step.objective,
|
|
592
|
+
requirements: step.requirements,
|
|
593
|
+
dependencies: step.dependencies,
|
|
594
|
+
expectedOutputs: step.expectedOutputs,
|
|
595
|
+
allowedWrites: step.allowedWrites,
|
|
596
|
+
forbiddenScope: step.forbiddenScope,
|
|
597
|
+
checks: step.checks,
|
|
598
|
+
status: step.status,
|
|
599
|
+
reviewedCommit,
|
|
600
|
+
evidence: currentEvidence,
|
|
601
|
+
scopeAugmentations,
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function strictTaskReviewSubject(
|
|
607
|
+
repositoryRoot: string,
|
|
608
|
+
task: TaskState,
|
|
609
|
+
taskRoot: string,
|
|
610
|
+
): Record<string, unknown> {
|
|
611
|
+
return {
|
|
612
|
+
id: task.id,
|
|
613
|
+
baseCommit: task.baseCommit,
|
|
614
|
+
headCommit: headCommit(repositoryRoot),
|
|
615
|
+
brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
|
|
616
|
+
plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
|
|
617
|
+
evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
|
|
618
|
+
diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function validateExternalReviewEnvelope(
|
|
623
|
+
value: unknown,
|
|
624
|
+
expected: ExternalStrictStepReviewPacket | ExternalStrictTaskReviewPacket,
|
|
625
|
+
): ReviewInput {
|
|
626
|
+
if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'reviewMode', 'review'])) {
|
|
627
|
+
throw new Error('invalid external review envelope');
|
|
628
|
+
}
|
|
629
|
+
if (value.protocol !== expected.protocol) throw new Error('invalid external review protocol');
|
|
630
|
+
if (value.packetHash !== expected.packetHash) throw new Error('external review packet hash mismatch');
|
|
631
|
+
if (value.repositorySealHash !== expected.repositorySealHash) throw new Error('external review repository seal mismatch');
|
|
632
|
+
if (value.reviewMode !== expected.reviewMode) throw new Error('external review mode mismatch');
|
|
633
|
+
if (typeof value.reviewerThreadId !== 'string' || !value.reviewerThreadId.trim()) {
|
|
634
|
+
throw new Error('external reviewer thread id is required');
|
|
635
|
+
}
|
|
636
|
+
if (!isExactRecord(value.review, ['status', 'reviewer', 'summary', 'findings'])) {
|
|
637
|
+
throw new Error('invalid external strict review');
|
|
638
|
+
}
|
|
639
|
+
const review = value.review;
|
|
640
|
+
if (!['passed', 'failed'].includes(String(review.status))
|
|
641
|
+
|| typeof review.reviewer !== 'string' || !review.reviewer.trim()
|
|
642
|
+
|| typeof review.summary !== 'string' || !review.summary.trim()
|
|
643
|
+
|| !Array.isArray(review.findings)) {
|
|
644
|
+
throw new Error('invalid external strict review');
|
|
645
|
+
}
|
|
646
|
+
const findings = review.findings.map((candidate) => {
|
|
647
|
+
if (!isReviewFindingRecord(candidate)
|
|
648
|
+
|| typeof candidate.id !== 'string' || !candidate.id.trim()
|
|
649
|
+
|| !['critical', 'major', 'minor'].includes(String(candidate.severity))
|
|
650
|
+
|| (candidate.requirement !== null && typeof candidate.requirement !== 'string')
|
|
651
|
+
|| typeof candidate.summary !== 'string' || !candidate.summary.trim()
|
|
652
|
+
|| typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
|
|
653
|
+
|| typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
|
|
654
|
+
throw new Error('invalid external strict review finding');
|
|
655
|
+
}
|
|
656
|
+
const route = (candidate.route === undefined ? 'fix' : candidate.route) as 'fix' | 'replan';
|
|
657
|
+
if (route !== 'fix' && route !== 'replan') throw new Error('invalid external strict review finding route');
|
|
658
|
+
const causeId = candidate.causeId;
|
|
659
|
+
if (causeId !== undefined
|
|
660
|
+
&& (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
|
|
661
|
+
throw new Error('invalid external strict review finding causeId');
|
|
662
|
+
}
|
|
663
|
+
const planConflict = validatePlanConflict(candidate.planConflict, route);
|
|
664
|
+
return {
|
|
665
|
+
id: candidate.id,
|
|
666
|
+
severity: candidate.severity as 'critical' | 'major' | 'minor',
|
|
667
|
+
requirement: candidate.requirement as string | null,
|
|
668
|
+
summary: candidate.summary,
|
|
669
|
+
evidence: candidate.evidence,
|
|
670
|
+
requiredAction: candidate.requiredAction,
|
|
671
|
+
...(causeId === undefined ? {} : { causeId }),
|
|
672
|
+
route,
|
|
673
|
+
planConflict,
|
|
674
|
+
};
|
|
675
|
+
});
|
|
676
|
+
if ((review.status === 'passed' && findings.length > 0) || (review.status === 'failed' && findings.length === 0)) {
|
|
677
|
+
throw new Error('invalid external strict review finding cardinality');
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
status: review.status as 'passed' | 'failed',
|
|
681
|
+
reviewer: review.reviewer,
|
|
682
|
+
summary: review.summary,
|
|
683
|
+
findings,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function prepareReviewerRuntimePreflight(
|
|
688
|
+
repositoryRoot: string,
|
|
689
|
+
binding: string,
|
|
690
|
+
): ReviewerRuntimePreflight {
|
|
691
|
+
const root = path.resolve(repositoryRoot);
|
|
692
|
+
const manifest = path.join(root, 'package.json');
|
|
693
|
+
const lockfile = path.join(root, 'package-lock.json');
|
|
694
|
+
const nodeModulesPath = path.join(root, 'node_modules');
|
|
695
|
+
const runtimeModulePath = fileURLToPath(import.meta.url);
|
|
696
|
+
const installedPackageRoot = path.join(nodeModulesPath, PACKAGE_NAME);
|
|
697
|
+
const installedPackageManifestPath = path.join(installedPackageRoot, 'package.json');
|
|
698
|
+
const runtimeBuildManifestPath = path.join(installedPackageRoot, 'dist', 'reviewer-runtime-build.json');
|
|
699
|
+
const installedReviewerModulePath = path.join(installedPackageRoot, 'dist', 'src', 'reviewer.js');
|
|
700
|
+
const installedCliPath = path.join(installedPackageRoot, 'dist', 'src', 'cli.js');
|
|
701
|
+
const installedGatewayModulePath = path.join(installedPackageRoot, 'dist', 'src', 'gateway-handshake.js');
|
|
702
|
+
const installedWorkflowModulePath = path.join(installedPackageRoot, 'dist', 'src', 'workflow.js');
|
|
703
|
+
const cachePath = path.join(
|
|
704
|
+
os.tmpdir(),
|
|
705
|
+
'codex-workflow-review-runtime',
|
|
706
|
+
canonicalHash({ repositoryRoot: root, binding }),
|
|
707
|
+
);
|
|
708
|
+
const nodeSupported = isSupportedNodeVersion(process.versions.node);
|
|
709
|
+
let cacheWritable = false;
|
|
710
|
+
if (nodeSupported) {
|
|
711
|
+
mkdirSync(cachePath, { recursive: true, mode: 0o700 });
|
|
712
|
+
chmodSync(cachePath, 0o700);
|
|
713
|
+
const cacheProbe = path.join(cachePath, '.write-probe');
|
|
714
|
+
try {
|
|
715
|
+
writeFileSync(cacheProbe, 'ok\n', { mode: 0o600 });
|
|
716
|
+
rmSync(cacheProbe, { force: true });
|
|
717
|
+
cacheWritable = true;
|
|
718
|
+
} catch {
|
|
719
|
+
rmSync(cacheProbe, { force: true });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const outsideSealedCheckout = isOutside(root, cachePath);
|
|
723
|
+
const packageManifestPath = existsSync(manifest) ? manifest : null;
|
|
724
|
+
const packageLockPath = existsSync(lockfile) ? lockfile : null;
|
|
725
|
+
const nodeModulesPresent = existsSync(nodeModulesPath) && statSync(nodeModulesPath).isDirectory();
|
|
726
|
+
const repositoryManifest = readJsonRecord(packageManifestPath);
|
|
727
|
+
const repositoryLock = readJsonRecord(packageLockPath);
|
|
728
|
+
const installedPackageDirectory = isPlainDirectory(installedPackageRoot);
|
|
729
|
+
const installedManifest = readJsonRecord(
|
|
730
|
+
installedPackageDirectory && isPlainFileInside(installedPackageRoot, installedPackageManifestPath)
|
|
731
|
+
? installedPackageManifestPath
|
|
732
|
+
: null,
|
|
733
|
+
);
|
|
734
|
+
const declaredVersion = nestedString(repositoryManifest, ['devDependencies', PACKAGE_NAME]);
|
|
735
|
+
const lockedVersion = nestedString(repositoryLock, ['packages', `node_modules/${PACKAGE_NAME}`, 'version']);
|
|
736
|
+
const lockDeclaredVersion = nestedString(repositoryLock, ['packages', '', 'devDependencies', PACKAGE_NAME]);
|
|
737
|
+
const installedVersion = stringField(installedManifest, 'version');
|
|
738
|
+
const installedName = stringField(installedManifest, 'name');
|
|
739
|
+
const exactRepositoryLocal = declaredVersion === PACKAGE_VERSION
|
|
740
|
+
&& lockedVersion === PACKAGE_VERSION
|
|
741
|
+
&& lockDeclaredVersion === PACKAGE_VERSION
|
|
742
|
+
&& installedName === PACKAGE_NAME
|
|
743
|
+
&& installedVersion === PACKAGE_VERSION;
|
|
744
|
+
const runtimeModuleSha256 = sha256File(runtimeModulePath);
|
|
745
|
+
const runtimeCliModulePath = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
746
|
+
const runtimeGatewayModulePath = fileURLToPath(new URL('./gateway-handshake.js', import.meta.url));
|
|
747
|
+
const runtimeWorkflowModulePath = fileURLToPath(new URL('./workflow.js', import.meta.url));
|
|
748
|
+
const buildReadiness = inspectReviewerBuild(
|
|
749
|
+
installedPackageRoot,
|
|
750
|
+
runtimeBuildManifestPath,
|
|
751
|
+
installedPackageManifestPath,
|
|
752
|
+
installedCliPath,
|
|
753
|
+
sha256File(runtimeCliModulePath),
|
|
754
|
+
installedReviewerModulePath,
|
|
755
|
+
runtimeModuleSha256,
|
|
756
|
+
installedGatewayModulePath,
|
|
757
|
+
sha256File(runtimeGatewayModulePath),
|
|
758
|
+
installedWorkflowModulePath,
|
|
759
|
+
sha256File(runtimeWorkflowModulePath),
|
|
760
|
+
);
|
|
761
|
+
const runtimeBuildManifest = buildReadiness.state === 'ready'
|
|
762
|
+
? parseRuntimeBuildManifest(readJsonRecord(runtimeBuildManifestPath))
|
|
763
|
+
: null;
|
|
764
|
+
const runtimeDependencyReadiness = inspectRuntimeDependencyClosure(
|
|
765
|
+
nodeModulesPath,
|
|
766
|
+
installedPackageRoot,
|
|
767
|
+
runtimeBuildManifest,
|
|
768
|
+
exactRepositoryLocal && buildReadiness.state === 'ready',
|
|
769
|
+
);
|
|
770
|
+
const runtimeCheck = inspectReviewerRuntimeCheck(
|
|
771
|
+
installedPackageManifestPath,
|
|
772
|
+
installedGatewayModulePath,
|
|
773
|
+
installedReviewerModulePath,
|
|
774
|
+
installedWorkflowModulePath,
|
|
775
|
+
runtimeDependencyReadiness.evidenceSha256,
|
|
776
|
+
nodeSupported && exactRepositoryLocal
|
|
777
|
+
&& buildReadiness.state === 'ready'
|
|
778
|
+
&& runtimeDependencyReadiness.state === 'ready',
|
|
779
|
+
);
|
|
780
|
+
const dependencyState = packageManifestPath === null
|
|
781
|
+
? 'not-applicable'
|
|
782
|
+
: !nodeModulesPresent || installedManifest === null
|
|
783
|
+
? 'not-installed'
|
|
784
|
+
: !exactRepositoryLocal
|
|
785
|
+
? 'version-mismatch'
|
|
786
|
+
: buildReadiness.state === 'missing'
|
|
787
|
+
? 'build-missing'
|
|
788
|
+
: buildReadiness.state === 'stale'
|
|
789
|
+
? 'build-stale'
|
|
790
|
+
: runtimeDependencyReadiness.state === 'missing'
|
|
791
|
+
? 'runtime-dependency-missing'
|
|
792
|
+
: runtimeDependencyReadiness.state === 'stale'
|
|
793
|
+
? 'runtime-dependency-stale'
|
|
794
|
+
: runtimeCheck.state !== 'passed'
|
|
795
|
+
? 'runtime-check-failed'
|
|
796
|
+
: 'ready';
|
|
797
|
+
const blockers = [
|
|
798
|
+
...(nodeSupported ? [] : [`Node ${process.version} is unsupported; required exact version ${SUPPORTED_NODE_VERSION}.`]),
|
|
799
|
+
...(packageManifestPath ? [] : ['Repository package.json is required to prove the exact Workflow dependency.']),
|
|
800
|
+
...(declaredVersion === PACKAGE_VERSION
|
|
801
|
+
? []
|
|
802
|
+
: [`Repository devDependencies must declare exact ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${declaredVersion ?? 'missing'}.`]),
|
|
803
|
+
...(packageLockPath ? [] : ['Repository package-lock.json is required to prove the locked Workflow dependency.']),
|
|
804
|
+
...(lockedVersion === PACKAGE_VERSION && lockDeclaredVersion === PACKAGE_VERSION
|
|
805
|
+
? []
|
|
806
|
+
: [`Repository lockfile must bind both root and installed ${PACKAGE_NAME} entries to ${PACKAGE_VERSION}; found root ${lockDeclaredVersion ?? 'missing'}, package ${lockedVersion ?? 'missing'}.`]),
|
|
807
|
+
...(installedManifest ? [] : [`Install exact ${PACKAGE_NAME}@${PACKAGE_VERSION} in repository-local node_modules before dispatching review.`]),
|
|
808
|
+
...(installedManifest === null || (installedName === PACKAGE_NAME && installedVersion === PACKAGE_VERSION)
|
|
809
|
+
? []
|
|
810
|
+
: [`Repository-local installed Workflow package must be ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${installedName ?? 'missing'}@${installedVersion ?? 'missing'}.`]),
|
|
811
|
+
...(installedManifest === null || installedName !== PACKAGE_NAME || installedVersion !== PACKAGE_VERSION
|
|
812
|
+
? []
|
|
813
|
+
: buildReadiness.state === 'ready'
|
|
814
|
+
? []
|
|
815
|
+
: buildReadiness.state === 'missing'
|
|
816
|
+
? [`Build evidence is missing or incomplete at ${runtimeBuildManifestPath}; reinstall the exact published package or build it before sealing review.`]
|
|
817
|
+
: [`Build evidence at ${runtimeBuildManifestPath} does not match installed runtime bytes; reinstall the exact package before sealing review.`]),
|
|
818
|
+
...(exactRepositoryLocal && buildReadiness.state === 'ready' && runtimeDependencyReadiness.state !== 'ready'
|
|
819
|
+
? runtimeDependencyReadiness.state === 'missing'
|
|
820
|
+
? ['The attested repository-local runtime dependency closure is incomplete; reinstall the exact package dependencies before sealing review.']
|
|
821
|
+
: ['The attested repository-local runtime dependency closure has version, path, symlink, or content drift; reinstall the exact package dependencies before sealing review.']
|
|
822
|
+
: []),
|
|
823
|
+
...(nodeSupported && exactRepositoryLocal
|
|
824
|
+
&& buildReadiness.state === 'ready'
|
|
825
|
+
&& runtimeDependencyReadiness.state === 'ready'
|
|
826
|
+
&& runtimeCheck.state !== 'passed'
|
|
827
|
+
? runtimeCheck.failureKind === 'contract-mismatch'
|
|
828
|
+
? [`Repository-local gateway handshake did not report exact package/protocol/schema authority from ${installedGatewayModulePath}; reinstall the exact package before sealing review.`]
|
|
829
|
+
: [`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.`]
|
|
830
|
+
: []),
|
|
831
|
+
...(nodeSupported && !cacheWritable ? ['Ephemeral reviewer cache is not writable.'] : []),
|
|
832
|
+
...(outsideSealedCheckout ? [] : ['Ephemeral reviewer cache is inside the sealed checkout.']),
|
|
833
|
+
];
|
|
834
|
+
return {
|
|
835
|
+
protocol: 'codex-workflow-reviewer-runtime-preflight-v1',
|
|
836
|
+
prepared: blockers.length === 0,
|
|
837
|
+
node: {
|
|
838
|
+
executable: process.execPath,
|
|
839
|
+
version: process.version,
|
|
840
|
+
requiredVersion: SUPPORTED_NODE_VERSION,
|
|
841
|
+
supported: nodeSupported,
|
|
842
|
+
},
|
|
843
|
+
workflowPackage: {
|
|
844
|
+
name: PACKAGE_NAME,
|
|
845
|
+
version: PACKAGE_VERSION,
|
|
846
|
+
runtimeModulePath,
|
|
847
|
+
runtimeModuleSha256,
|
|
848
|
+
installedPackageRoot,
|
|
849
|
+
installedPackageManifestPath,
|
|
850
|
+
installedPackageManifestSha256: installedManifest ? sha256File(installedPackageManifestPath) : null,
|
|
851
|
+
declaredVersion,
|
|
852
|
+
lockedVersion,
|
|
853
|
+
installedVersion,
|
|
854
|
+
exactRepositoryLocal,
|
|
855
|
+
},
|
|
856
|
+
buildReadiness,
|
|
857
|
+
runtimeDependencyReadiness,
|
|
858
|
+
runtimeCheck,
|
|
859
|
+
repositoryDependencyReadiness: {
|
|
860
|
+
packageManifestPath,
|
|
861
|
+
packageManifestSha256: packageManifestPath ? sha256File(packageManifestPath) : null,
|
|
862
|
+
packageLockPath,
|
|
863
|
+
packageLockSha256: packageLockPath ? sha256File(packageLockPath) : null,
|
|
864
|
+
nodeModulesPath,
|
|
865
|
+
nodeModulesPresent,
|
|
866
|
+
state: dependencyState,
|
|
867
|
+
installAttempted: false,
|
|
868
|
+
installCommand: null,
|
|
869
|
+
},
|
|
870
|
+
ephemeralCache: {
|
|
871
|
+
path: cachePath,
|
|
872
|
+
writable: cacheWritable,
|
|
873
|
+
outsideSealedCheckout,
|
|
874
|
+
lifecycle: 'os-temporary',
|
|
875
|
+
},
|
|
876
|
+
sealedCheckoutMutationAllowed: false,
|
|
877
|
+
blockers,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function inspectReviewerRuntimeCheck(
|
|
882
|
+
installedPackageManifestPath: string,
|
|
883
|
+
gatewayEntrypointPath: string,
|
|
884
|
+
reviewerEntrypointPath: string,
|
|
885
|
+
workflowEntrypointPath: string,
|
|
886
|
+
dependencyClosureEvidenceSha256: string,
|
|
887
|
+
eligible: boolean,
|
|
888
|
+
): ReviewerRuntimePreflight['runtimeCheck'] {
|
|
889
|
+
const gatewayEntrypointSha256 = eligible ? sha256File(gatewayEntrypointPath) : null;
|
|
890
|
+
const reviewerEntrypointSha256 = eligible ? sha256File(reviewerEntrypointPath) : null;
|
|
891
|
+
const workflowEntrypointSha256 = eligible ? sha256File(workflowEntrypointPath) : null;
|
|
892
|
+
if (!eligible) {
|
|
893
|
+
return runtimeCheckEvidence({
|
|
894
|
+
state: 'not-run',
|
|
895
|
+
gatewayEntrypointPath,
|
|
896
|
+
gatewayEntrypointSha256,
|
|
897
|
+
reviewerEntrypointPath,
|
|
898
|
+
reviewerEntrypointSha256,
|
|
899
|
+
workflowEntrypointPath,
|
|
900
|
+
workflowEntrypointSha256,
|
|
901
|
+
dependencyClosureEvidenceSha256,
|
|
902
|
+
observedPackageName: null,
|
|
903
|
+
observedPackageVersion: null,
|
|
904
|
+
observedProtocolVersion: null,
|
|
905
|
+
observedStateSchemaVersion: null,
|
|
906
|
+
resultSha256: null,
|
|
907
|
+
failureKind: null,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
let observedPackageName: string | null = null;
|
|
911
|
+
let observedPackageVersion: string | null = null;
|
|
912
|
+
let observedProtocolVersion: number | null = null;
|
|
913
|
+
let observedStateSchemaVersion: number | null = null;
|
|
914
|
+
let resultSha256: string | null = null;
|
|
915
|
+
try {
|
|
916
|
+
const requireInstalled = createRequire(installedPackageManifestPath);
|
|
917
|
+
const gatewayModule = runtimeModuleRecord(requireInstalled(gatewayEntrypointPath));
|
|
918
|
+
const reviewerModule = runtimeModuleRecord(requireInstalled(reviewerEntrypointPath));
|
|
919
|
+
const workflowModule = runtimeModuleRecord(requireInstalled(workflowEntrypointPath));
|
|
920
|
+
const handshake = gatewayModule && typeof gatewayModule.gatewayHandshake === 'function'
|
|
921
|
+
? runtimeModuleRecord(gatewayModule.gatewayHandshake())
|
|
922
|
+
: null;
|
|
923
|
+
if (handshake) {
|
|
924
|
+
observedPackageName = typeof handshake.packageName === 'string' ? handshake.packageName : null;
|
|
925
|
+
observedPackageVersion = typeof handshake.packageVersion === 'string' ? handshake.packageVersion : null;
|
|
926
|
+
observedProtocolVersion = typeof handshake.protocolVersion === 'number' ? handshake.protocolVersion : null;
|
|
927
|
+
observedStateSchemaVersion = typeof handshake.stateSchemaVersion === 'number' ? handshake.stateSchemaVersion : null;
|
|
928
|
+
resultSha256 = canonicalHash(handshake);
|
|
929
|
+
}
|
|
930
|
+
const passed = handshake !== null
|
|
931
|
+
&& reviewerModule !== null
|
|
932
|
+
&& typeof reviewerModule.prepareReviewerRuntimePreflight === 'function'
|
|
933
|
+
&& workflowModule !== null
|
|
934
|
+
&& typeof workflowModule.WorkflowService === 'function'
|
|
935
|
+
&& observedPackageName === PACKAGE_NAME
|
|
936
|
+
&& observedPackageVersion === PACKAGE_VERSION
|
|
937
|
+
&& observedProtocolVersion === PROTOCOL_VERSION
|
|
938
|
+
&& observedStateSchemaVersion === STATE_SCHEMA_VERSION;
|
|
939
|
+
return runtimeCheckEvidence({
|
|
940
|
+
state: passed ? 'passed' : 'failed',
|
|
941
|
+
gatewayEntrypointPath,
|
|
942
|
+
gatewayEntrypointSha256,
|
|
943
|
+
reviewerEntrypointPath,
|
|
944
|
+
reviewerEntrypointSha256,
|
|
945
|
+
workflowEntrypointPath,
|
|
946
|
+
workflowEntrypointSha256,
|
|
947
|
+
dependencyClosureEvidenceSha256,
|
|
948
|
+
observedPackageName,
|
|
949
|
+
observedPackageVersion,
|
|
950
|
+
observedProtocolVersion,
|
|
951
|
+
observedStateSchemaVersion,
|
|
952
|
+
resultSha256,
|
|
953
|
+
failureKind: passed ? null : 'contract-mismatch',
|
|
954
|
+
});
|
|
955
|
+
} catch {
|
|
956
|
+
return runtimeCheckEvidence({
|
|
957
|
+
state: 'failed',
|
|
958
|
+
gatewayEntrypointPath,
|
|
959
|
+
gatewayEntrypointSha256,
|
|
960
|
+
reviewerEntrypointPath,
|
|
961
|
+
reviewerEntrypointSha256,
|
|
962
|
+
workflowEntrypointPath,
|
|
963
|
+
workflowEntrypointSha256,
|
|
964
|
+
dependencyClosureEvidenceSha256,
|
|
965
|
+
observedPackageName,
|
|
966
|
+
observedPackageVersion,
|
|
967
|
+
observedProtocolVersion,
|
|
968
|
+
observedStateSchemaVersion,
|
|
969
|
+
resultSha256,
|
|
970
|
+
failureKind: 'module-load-failed',
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function assertReviewerRuntimePrepared(preflight: ReviewerRuntimePreflight): void {
|
|
976
|
+
if (!preflight.prepared) {
|
|
977
|
+
throw new Error(`External sealed review packet requires a prepared reviewer runtime: ${preflight.blockers.join(' ')}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function runtimeCheckEvidence(
|
|
982
|
+
value: Omit<ReviewerRuntimePreflight['runtimeCheck'],
|
|
983
|
+
'kind' | 'loader' | 'executable' | 'evidenceSha256' | 'sealedCheckoutMutationAllowed'>,
|
|
984
|
+
): ReviewerRuntimePreflight['runtimeCheck'] {
|
|
985
|
+
const evidence = {
|
|
986
|
+
kind: 'repository-local-gateway-load' as const,
|
|
987
|
+
loader: 'node:createRequire' as const,
|
|
988
|
+
executable: process.execPath,
|
|
989
|
+
...value,
|
|
990
|
+
sealedCheckoutMutationAllowed: false as const,
|
|
991
|
+
};
|
|
992
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function runtimeModuleRecord(value: unknown): Record<string, unknown> | null {
|
|
996
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
997
|
+
? value as Record<string, unknown>
|
|
998
|
+
: null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
interface RuntimeBuildManifest {
|
|
1002
|
+
protocol: 'codex-workflow-runtime-build-manifest-v1';
|
|
1003
|
+
packageName: string;
|
|
1004
|
+
packageVersion: string;
|
|
1005
|
+
packageManifestSha256: string;
|
|
1006
|
+
requiredEntrypoints: string[];
|
|
1007
|
+
files: Array<{ path: string; sha256: string }>;
|
|
1008
|
+
runtimeDependencies: Array<{
|
|
1009
|
+
name: string;
|
|
1010
|
+
version: string;
|
|
1011
|
+
packageManifestSha256: string;
|
|
1012
|
+
files: Array<{ path: string; sha256: string }>;
|
|
1013
|
+
}>;
|
|
1014
|
+
fingerprint: string;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function inspectRuntimeDependencyClosure(
|
|
1018
|
+
nodeModulesPath: string,
|
|
1019
|
+
installedPackageRoot: string,
|
|
1020
|
+
manifest: RuntimeBuildManifest | null,
|
|
1021
|
+
eligible: boolean,
|
|
1022
|
+
): ReviewerRuntimePreflight['runtimeDependencyReadiness'] {
|
|
1023
|
+
const expected = manifest?.runtimeDependencies ?? [];
|
|
1024
|
+
const expectedFileCount = expected.reduce((total, dependency) => total + dependency.files.length, 0);
|
|
1025
|
+
if (!eligible || !manifest) {
|
|
1026
|
+
const evidence = {
|
|
1027
|
+
state: 'not-run' as const,
|
|
1028
|
+
expectedPackageCount: expected.length,
|
|
1029
|
+
verifiedPackageCount: 0,
|
|
1030
|
+
expectedFileCount,
|
|
1031
|
+
verifiedFileCount: 0,
|
|
1032
|
+
packages: [],
|
|
1033
|
+
};
|
|
1034
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
1035
|
+
}
|
|
1036
|
+
const nodeModulesReady = (() => {
|
|
1037
|
+
try {
|
|
1038
|
+
return lstatSync(nodeModulesPath).isDirectory()
|
|
1039
|
+
&& isUnredirectedPathInside(path.dirname(nodeModulesPath), nodeModulesPath);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
})();
|
|
1044
|
+
const repositoryRoot = path.dirname(nodeModulesPath);
|
|
1045
|
+
const expectedByName = new Map(expected.map((dependency) => [dependency.name, dependency]));
|
|
1046
|
+
const installedWorkflowManifest = readJsonRecord(path.join(installedPackageRoot, 'package.json'));
|
|
1047
|
+
const rootDependencyNames = runtimeDependencyNames(installedWorkflowManifest);
|
|
1048
|
+
let graphDrift = rootDependencyNames === null;
|
|
1049
|
+
const queue: Array<{ name: string; parentPackageRoot: string }> = (rootDependencyNames ?? [])
|
|
1050
|
+
.map((name) => ({ name, parentPackageRoot: installedPackageRoot }));
|
|
1051
|
+
const queuedEdges = new Set(queue.map(({ name, parentPackageRoot }) => `${parentPackageRoot}\0${name}`));
|
|
1052
|
+
const inspectedInstances = new Map<string, ReviewerRuntimePreflight['runtimeDependencyReadiness']['packages'][number]>();
|
|
1053
|
+
while (queue.length > 0) {
|
|
1054
|
+
const edge = queue.shift()!;
|
|
1055
|
+
const expectedDependency = expectedByName.get(edge.name);
|
|
1056
|
+
if (!expectedDependency) {
|
|
1057
|
+
graphDrift = true;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
const resolution = resolveRuntimeDependencyPackageRoot(repositoryRoot, edge.parentPackageRoot, edge.name);
|
|
1061
|
+
const packageRoot = resolution ?? runtimeDependencySearchPaths(
|
|
1062
|
+
repositoryRoot,
|
|
1063
|
+
edge.parentPackageRoot,
|
|
1064
|
+
edge.name,
|
|
1065
|
+
)[0] ?? path.join(nodeModulesPath, ...edge.name.split('/'));
|
|
1066
|
+
const instanceKey = `${edge.name}\0${packageRoot}`;
|
|
1067
|
+
let inspected = inspectedInstances.get(instanceKey);
|
|
1068
|
+
if (!inspected) {
|
|
1069
|
+
inspected = inspectRuntimeDependencyPackage(
|
|
1070
|
+
repositoryRoot,
|
|
1071
|
+
packageRoot,
|
|
1072
|
+
expectedDependency,
|
|
1073
|
+
nodeModulesReady,
|
|
1074
|
+
);
|
|
1075
|
+
inspectedInstances.set(instanceKey, inspected);
|
|
1076
|
+
}
|
|
1077
|
+
if (inspected.state !== 'ready') continue;
|
|
1078
|
+
const installedDependencyManifest = readJsonRecord(inspected.packageManifestPath);
|
|
1079
|
+
const childNames = runtimeDependencyNames(installedDependencyManifest);
|
|
1080
|
+
if (childNames === null) {
|
|
1081
|
+
graphDrift = true;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
for (const childName of childNames) {
|
|
1085
|
+
if (!expectedByName.has(childName)) {
|
|
1086
|
+
graphDrift = true;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const edgeKey = `${packageRoot}\0${childName}`;
|
|
1090
|
+
if (queuedEdges.has(edgeKey)) continue;
|
|
1091
|
+
queuedEdges.add(edgeKey);
|
|
1092
|
+
queue.push({ name: childName, parentPackageRoot: packageRoot });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const packages = [...inspectedInstances.values()].sort((left, right) => (
|
|
1096
|
+
left.name.localeCompare(right.name) || left.packageRoot.localeCompare(right.packageRoot)
|
|
1097
|
+
));
|
|
1098
|
+
const packageInstancesByName = new Map<string, typeof packages>();
|
|
1099
|
+
for (const dependency of packages) {
|
|
1100
|
+
const instances = packageInstancesByName.get(dependency.name) ?? [];
|
|
1101
|
+
instances.push(dependency);
|
|
1102
|
+
packageInstancesByName.set(dependency.name, instances);
|
|
1103
|
+
}
|
|
1104
|
+
const verifiedNames = expected.filter((dependency) => {
|
|
1105
|
+
const instances = packageInstancesByName.get(dependency.name) ?? [];
|
|
1106
|
+
return instances.length > 0 && instances.every((instance) => instance.state === 'ready');
|
|
1107
|
+
});
|
|
1108
|
+
const expectedNamesReached = verifiedNames.length === expected.length;
|
|
1109
|
+
const state: 'ready' | 'missing' | 'stale' = graphDrift
|
|
1110
|
+
|| packages.some((dependency) => dependency.state === 'stale')
|
|
1111
|
+
|| (packages.length === 0 && expected.length > 0 && nodeModulesReady)
|
|
1112
|
+
? 'stale'
|
|
1113
|
+
: packages.some((dependency) => dependency.state === 'missing')
|
|
1114
|
+
|| !nodeModulesReady
|
|
1115
|
+
? 'missing'
|
|
1116
|
+
: expectedNamesReached
|
|
1117
|
+
? 'ready'
|
|
1118
|
+
: 'stale';
|
|
1119
|
+
const evidence = {
|
|
1120
|
+
state,
|
|
1121
|
+
expectedPackageCount: expected.length,
|
|
1122
|
+
verifiedPackageCount: verifiedNames.length,
|
|
1123
|
+
expectedFileCount,
|
|
1124
|
+
verifiedFileCount: verifiedNames.reduce((total, dependency) => total + dependency.files.length, 0),
|
|
1125
|
+
packages,
|
|
1126
|
+
};
|
|
1127
|
+
return { ...evidence, evidenceSha256: canonicalHash(evidence) };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function inspectRuntimeDependencyPackage(
|
|
1131
|
+
repositoryRoot: string,
|
|
1132
|
+
packageRoot: string,
|
|
1133
|
+
expected: RuntimeBuildManifest['runtimeDependencies'][number],
|
|
1134
|
+
nodeModulesReady: boolean,
|
|
1135
|
+
): ReviewerRuntimePreflight['runtimeDependencyReadiness']['packages'][number] {
|
|
1136
|
+
const packageManifestPath = path.join(packageRoot, 'package.json');
|
|
1137
|
+
const base = {
|
|
1138
|
+
name: expected.name,
|
|
1139
|
+
version: expected.version,
|
|
1140
|
+
packageRoot,
|
|
1141
|
+
packageManifestPath,
|
|
1142
|
+
expectedFileCount: expected.files.length,
|
|
1143
|
+
};
|
|
1144
|
+
if (!nodeModulesReady || !existsSync(packageRoot)) {
|
|
1145
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
if (!isPackageName(expected.name) || !lstatSync(packageRoot).isDirectory()) {
|
|
1149
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
1150
|
+
}
|
|
1151
|
+
const realRepositoryRoot = realpathSync(repositoryRoot);
|
|
1152
|
+
const realPackageRoot = realpathSync(packageRoot);
|
|
1153
|
+
if (!isUnredirectedPathInside(repositoryRoot, packageRoot)
|
|
1154
|
+
|| isOutside(realRepositoryRoot, realPackageRoot)) {
|
|
1155
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
1156
|
+
}
|
|
1157
|
+
const manifestFile = inspectRuntimeFile(packageRoot, packageManifestPath);
|
|
1158
|
+
if (manifestFile.state === 'missing') {
|
|
1159
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
|
|
1160
|
+
}
|
|
1161
|
+
const installedManifest = readJsonRecord(manifestFile.state === 'ready' ? packageManifestPath : null);
|
|
1162
|
+
if (manifestFile.state !== 'ready'
|
|
1163
|
+
|| manifestFile.sha256 !== expected.packageManifestSha256
|
|
1164
|
+
|| stringField(installedManifest, 'name') !== expected.name
|
|
1165
|
+
|| stringField(installedManifest, 'version') !== expected.version) {
|
|
1166
|
+
return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
|
|
1167
|
+
}
|
|
1168
|
+
const collected = collectRuntimeDependencyFiles(packageRoot);
|
|
1169
|
+
if (collected.unsafe) {
|
|
1170
|
+
return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
|
|
1171
|
+
}
|
|
1172
|
+
const observed = new Map(collected.files.map((file) => [file.path, file.sha256]));
|
|
1173
|
+
let missing = false;
|
|
1174
|
+
let drift = collected.files.length !== expected.files.length;
|
|
1175
|
+
let verifiedFileCount = 0;
|
|
1176
|
+
for (const file of expected.files) {
|
|
1177
|
+
if (safeRuntimeDependencyPath(file.path) === null) {
|
|
1178
|
+
drift = true;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const hash = observed.get(file.path);
|
|
1182
|
+
if (hash === undefined) missing = true;
|
|
1183
|
+
else if (hash !== file.sha256) drift = true;
|
|
1184
|
+
else verifiedFileCount += 1;
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
...base,
|
|
1188
|
+
packageManifestSha256: manifestFile.sha256,
|
|
1189
|
+
verifiedFileCount,
|
|
1190
|
+
state: missing ? 'missing' : drift ? 'stale' : 'ready',
|
|
1191
|
+
};
|
|
1192
|
+
} catch {
|
|
1193
|
+
return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function resolveRuntimeDependencyPackageRoot(
|
|
1198
|
+
repositoryRoot: string,
|
|
1199
|
+
parentPackageRoot: string,
|
|
1200
|
+
dependencyName: string,
|
|
1201
|
+
): string | null {
|
|
1202
|
+
for (const candidate of runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName)) {
|
|
1203
|
+
try {
|
|
1204
|
+
lstatSync(candidate);
|
|
1205
|
+
return candidate;
|
|
1206
|
+
} catch {
|
|
1207
|
+
// Continue through Node's parent-aware node_modules search order.
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function runtimeDependencySearchPaths(
|
|
1214
|
+
repositoryRoot: string,
|
|
1215
|
+
parentPackageRoot: string,
|
|
1216
|
+
dependencyName: string,
|
|
1217
|
+
): string[] {
|
|
1218
|
+
if (!isPackageName(dependencyName)) return [];
|
|
1219
|
+
const root = path.resolve(repositoryRoot);
|
|
1220
|
+
let cursor = path.resolve(parentPackageRoot);
|
|
1221
|
+
if (isOutside(root, cursor)) return [];
|
|
1222
|
+
const candidates: string[] = [];
|
|
1223
|
+
while (true) {
|
|
1224
|
+
if (path.basename(cursor) !== 'node_modules') {
|
|
1225
|
+
candidates.push(path.join(cursor, 'node_modules', ...dependencyName.split('/')));
|
|
1226
|
+
}
|
|
1227
|
+
if (cursor === root) break;
|
|
1228
|
+
const parent = path.dirname(cursor);
|
|
1229
|
+
if (parent === cursor || isOutside(root, parent)) break;
|
|
1230
|
+
cursor = parent;
|
|
1231
|
+
}
|
|
1232
|
+
return candidates;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function runtimeDependencyNames(manifest: Record<string, unknown> | null): string[] | null {
|
|
1236
|
+
const dependencies = manifest?.dependencies;
|
|
1237
|
+
if (dependencies === undefined) return [];
|
|
1238
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) return null;
|
|
1239
|
+
const names = Object.keys(dependencies);
|
|
1240
|
+
return names.every(isPackageName) ? names.sort((left, right) => left.localeCompare(right)) : null;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function collectRuntimeDependencyFiles(
|
|
1244
|
+
packageRoot: string,
|
|
1245
|
+
directory: string = packageRoot,
|
|
1246
|
+
): { unsafe: boolean; files: Array<{ path: string; sha256: string }> } {
|
|
1247
|
+
try {
|
|
1248
|
+
const files: Array<{ path: string; sha256: string }> = [];
|
|
1249
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1250
|
+
const absolutePath = path.join(directory, entry.name);
|
|
1251
|
+
if (entry.isSymbolicLink()) return { unsafe: true, files: [] };
|
|
1252
|
+
if (entry.isDirectory()) {
|
|
1253
|
+
if (entry.name === 'node_modules') return { unsafe: true, files: [] };
|
|
1254
|
+
const nested = collectRuntimeDependencyFiles(packageRoot, absolutePath);
|
|
1255
|
+
if (nested.unsafe) return nested;
|
|
1256
|
+
files.push(...nested.files);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (!entry.isFile() || entry.name === 'package.json' || !/\.(?:cjs|js|json|mjs|node)$/.test(entry.name)) continue;
|
|
1260
|
+
const relativePath = path.relative(packageRoot, absolutePath).split(path.sep).join('/');
|
|
1261
|
+
if (safeRuntimeDependencyPath(relativePath) === null
|
|
1262
|
+
|| !lstatSync(absolutePath).isFile()
|
|
1263
|
+
|| isOutside(realpathSync(packageRoot), realpathSync(absolutePath))) {
|
|
1264
|
+
return { unsafe: true, files: [] };
|
|
1265
|
+
}
|
|
1266
|
+
files.push({ path: relativePath, sha256: sha256File(absolutePath) });
|
|
1267
|
+
}
|
|
1268
|
+
return { unsafe: false, files: files.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
1269
|
+
} catch {
|
|
1270
|
+
return { unsafe: true, files: [] };
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function inspectReviewerBuild(
|
|
1275
|
+
installedPackageRoot: string,
|
|
1276
|
+
manifestPath: string,
|
|
1277
|
+
installedPackageManifestPath: string,
|
|
1278
|
+
cliPath: string,
|
|
1279
|
+
runningCliSha256: string,
|
|
1280
|
+
reviewerPath: string,
|
|
1281
|
+
runningRuntimeSha256: string,
|
|
1282
|
+
gatewayPath: string,
|
|
1283
|
+
runningGatewaySha256: string,
|
|
1284
|
+
workflowPath: string,
|
|
1285
|
+
runningWorkflowSha256: string,
|
|
1286
|
+
): ReviewerRuntimePreflight['buildReadiness'] {
|
|
1287
|
+
const manifestFile = inspectRuntimeFile(installedPackageRoot, manifestPath);
|
|
1288
|
+
const rawManifest = readJsonRecord(manifestFile.state === 'ready' ? manifestPath : null);
|
|
1289
|
+
const parsed = parseRuntimeBuildManifest(rawManifest);
|
|
1290
|
+
const installedPackageManifestSha256 = inspectRuntimeFile(
|
|
1291
|
+
installedPackageRoot,
|
|
1292
|
+
installedPackageManifestPath,
|
|
1293
|
+
).sha256;
|
|
1294
|
+
let state: 'ready' | 'missing' | 'stale' = manifestFile.state === 'unsafe'
|
|
1295
|
+
? 'stale'
|
|
1296
|
+
: parsed
|
|
1297
|
+
? 'ready'
|
|
1298
|
+
: 'missing';
|
|
1299
|
+
let verifiedFileCount = 0;
|
|
1300
|
+
if (parsed) {
|
|
1301
|
+
const expectedFingerprint = runtimeBuildFingerprint(parsed);
|
|
1302
|
+
const filePaths = new Set(parsed.files.map((file) => file.path));
|
|
1303
|
+
if (
|
|
1304
|
+
parsed.packageName !== PACKAGE_NAME
|
|
1305
|
+
|| parsed.packageVersion !== PACKAGE_VERSION
|
|
1306
|
+
|| parsed.packageManifestSha256 !== installedPackageManifestSha256
|
|
1307
|
+
|| parsed.fingerprint !== expectedFingerprint
|
|
1308
|
+
|| canonicalJsonStringify(parsed.requiredEntrypoints) !== canonicalJsonStringify([
|
|
1309
|
+
'dist/src/cli.js',
|
|
1310
|
+
'dist/src/gateway-handshake.js',
|
|
1311
|
+
'dist/src/reviewer.js',
|
|
1312
|
+
'dist/src/workflow.js',
|
|
1313
|
+
])
|
|
1314
|
+
|| !parsed.requiredEntrypoints.every((entry) => filePaths.has(entry))
|
|
1315
|
+
) {
|
|
1316
|
+
state = 'stale';
|
|
1317
|
+
}
|
|
1318
|
+
for (const file of parsed.files) {
|
|
1319
|
+
const safeRelative = safeRuntimeBuildPath(file.path);
|
|
1320
|
+
if (safeRelative === null) {
|
|
1321
|
+
state = 'stale';
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const absolutePath = path.join(installedPackageRoot, safeRelative);
|
|
1325
|
+
const observed = inspectRuntimeFile(installedPackageRoot, absolutePath);
|
|
1326
|
+
if (observed.state === 'missing') {
|
|
1327
|
+
state = 'missing';
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (observed.state === 'unsafe' || observed.sha256 !== file.sha256) {
|
|
1331
|
+
state = 'stale';
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
verifiedFileCount += 1;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const cliFile = inspectRuntimeFile(installedPackageRoot, cliPath);
|
|
1338
|
+
const reviewerFile = inspectRuntimeFile(installedPackageRoot, reviewerPath);
|
|
1339
|
+
const gatewayFile = inspectRuntimeFile(installedPackageRoot, gatewayPath);
|
|
1340
|
+
const workflowFile = inspectRuntimeFile(installedPackageRoot, workflowPath);
|
|
1341
|
+
const cliSha256 = cliFile.sha256;
|
|
1342
|
+
const reviewerSha256 = reviewerFile.sha256;
|
|
1343
|
+
const gatewaySha256 = gatewayFile.sha256;
|
|
1344
|
+
const workflowSha256 = workflowFile.sha256;
|
|
1345
|
+
if (parsed && [cliFile, reviewerFile, gatewayFile, workflowFile].some((file) => file.state === 'unsafe')) {
|
|
1346
|
+
state = 'stale';
|
|
1347
|
+
} else if (parsed && [cliSha256, reviewerSha256, gatewaySha256, workflowSha256].some((hash) => hash === null)) {
|
|
1348
|
+
state = 'missing';
|
|
1349
|
+
}
|
|
1350
|
+
const runningCliMatchesInstalledBuild = cliSha256 !== null && cliSha256 === runningCliSha256;
|
|
1351
|
+
const runningModuleMatchesInstalledBuild = reviewerSha256 !== null && reviewerSha256 === runningRuntimeSha256;
|
|
1352
|
+
const runningGatewayMatchesInstalledBuild = gatewaySha256 !== null && gatewaySha256 === runningGatewaySha256;
|
|
1353
|
+
const runningWorkflowMatchesInstalledBuild = workflowSha256 !== null && workflowSha256 === runningWorkflowSha256;
|
|
1354
|
+
if (parsed && (!runningCliMatchesInstalledBuild
|
|
1355
|
+
|| !runningModuleMatchesInstalledBuild
|
|
1356
|
+
|| !runningGatewayMatchesInstalledBuild
|
|
1357
|
+
|| !runningWorkflowMatchesInstalledBuild)) state = 'stale';
|
|
1358
|
+
const manifestSha256 = manifestFile.sha256;
|
|
1359
|
+
const manifestFingerprint = parsed?.fingerprint ?? null;
|
|
1360
|
+
const expectedFileCount = parsed?.files.length ?? 0;
|
|
1361
|
+
return {
|
|
1362
|
+
state,
|
|
1363
|
+
manifestPath,
|
|
1364
|
+
manifestSha256,
|
|
1365
|
+
manifestFingerprint,
|
|
1366
|
+
verifiedFileCount,
|
|
1367
|
+
expectedFileCount,
|
|
1368
|
+
cliEntrypointPath: cliPath,
|
|
1369
|
+
cliEntrypointSha256: cliSha256,
|
|
1370
|
+
runningCliMatchesInstalledBuild,
|
|
1371
|
+
reviewerModulePath: reviewerPath,
|
|
1372
|
+
reviewerModuleSha256: reviewerSha256,
|
|
1373
|
+
runningModuleMatchesInstalledBuild,
|
|
1374
|
+
gatewayModulePath: gatewayPath,
|
|
1375
|
+
gatewayModuleSha256: gatewaySha256,
|
|
1376
|
+
runningGatewayMatchesInstalledBuild,
|
|
1377
|
+
workflowModulePath: workflowPath,
|
|
1378
|
+
workflowModuleSha256: workflowSha256,
|
|
1379
|
+
runningWorkflowMatchesInstalledBuild,
|
|
1380
|
+
evidenceSha256: canonicalHash({
|
|
1381
|
+
installedPackageRoot,
|
|
1382
|
+
manifestSha256,
|
|
1383
|
+
manifestFingerprint,
|
|
1384
|
+
installedPackageManifestSha256,
|
|
1385
|
+
verifiedFileCount,
|
|
1386
|
+
expectedFileCount,
|
|
1387
|
+
cliSha256,
|
|
1388
|
+
runningCliSha256,
|
|
1389
|
+
reviewerSha256,
|
|
1390
|
+
runningRuntimeSha256,
|
|
1391
|
+
gatewaySha256,
|
|
1392
|
+
runningGatewaySha256,
|
|
1393
|
+
workflowSha256,
|
|
1394
|
+
runningWorkflowSha256,
|
|
1395
|
+
state,
|
|
1396
|
+
}),
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function parseRuntimeBuildManifest(value: Record<string, unknown> | null): RuntimeBuildManifest | null {
|
|
1401
|
+
if (!value
|
|
1402
|
+
|| value.protocol !== 'codex-workflow-runtime-build-manifest-v1'
|
|
1403
|
+
|| typeof value.packageName !== 'string'
|
|
1404
|
+
|| typeof value.packageVersion !== 'string'
|
|
1405
|
+
|| typeof value.packageManifestSha256 !== 'string'
|
|
1406
|
+
|| !/^[a-f0-9]{64}$/.test(value.packageManifestSha256)
|
|
1407
|
+
|| !Array.isArray(value.requiredEntrypoints)
|
|
1408
|
+
|| !value.requiredEntrypoints.every((entry) => typeof entry === 'string')
|
|
1409
|
+
|| !Array.isArray(value.files)
|
|
1410
|
+
|| !Array.isArray(value.runtimeDependencies)
|
|
1411
|
+
|| typeof value.fingerprint !== 'string') return null;
|
|
1412
|
+
const files = parseAttestedFiles(value.files);
|
|
1413
|
+
if (files.length !== value.files.length || new Set(files.map((entry) => entry.path)).size !== files.length) return null;
|
|
1414
|
+
const runtimeDependencies = value.runtimeDependencies.flatMap((entry) => {
|
|
1415
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
|
|
1416
|
+
const candidate = entry as Record<string, unknown>;
|
|
1417
|
+
if (typeof candidate.name !== 'string'
|
|
1418
|
+
|| !isPackageName(candidate.name)
|
|
1419
|
+
|| typeof candidate.version !== 'string'
|
|
1420
|
+
|| typeof candidate.packageManifestSha256 !== 'string'
|
|
1421
|
+
|| !/^[a-f0-9]{64}$/.test(candidate.packageManifestSha256)
|
|
1422
|
+
|| !Array.isArray(candidate.files)) return [];
|
|
1423
|
+
const dependencyFiles = parseAttestedFiles(candidate.files);
|
|
1424
|
+
return dependencyFiles.length === candidate.files.length
|
|
1425
|
+
&& new Set(dependencyFiles.map((file) => file.path)).size === dependencyFiles.length
|
|
1426
|
+
? [{
|
|
1427
|
+
name: candidate.name,
|
|
1428
|
+
version: candidate.version,
|
|
1429
|
+
packageManifestSha256: candidate.packageManifestSha256,
|
|
1430
|
+
files: dependencyFiles,
|
|
1431
|
+
}]
|
|
1432
|
+
: [];
|
|
1433
|
+
});
|
|
1434
|
+
if (runtimeDependencies.length !== value.runtimeDependencies.length
|
|
1435
|
+
|| new Set(runtimeDependencies.map((entry) => entry.name)).size !== runtimeDependencies.length) return null;
|
|
1436
|
+
return {
|
|
1437
|
+
protocol: value.protocol,
|
|
1438
|
+
packageName: value.packageName,
|
|
1439
|
+
packageVersion: value.packageVersion,
|
|
1440
|
+
packageManifestSha256: value.packageManifestSha256,
|
|
1441
|
+
requiredEntrypoints: [...value.requiredEntrypoints] as string[],
|
|
1442
|
+
files,
|
|
1443
|
+
runtimeDependencies,
|
|
1444
|
+
fingerprint: value.fingerprint,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function parseAttestedFiles(value: unknown[]): Array<{ path: string; sha256: string }> {
|
|
1449
|
+
return value.flatMap((entry) => {
|
|
1450
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
|
|
1451
|
+
const candidate = entry as Record<string, unknown>;
|
|
1452
|
+
return typeof candidate.path === 'string' && /^[a-f0-9]{64}$/.test(String(candidate.sha256))
|
|
1453
|
+
? [{ path: candidate.path, sha256: String(candidate.sha256) }]
|
|
1454
|
+
: [];
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function runtimeBuildFingerprint(value: Omit<RuntimeBuildManifest, 'fingerprint'> | RuntimeBuildManifest): string {
|
|
1459
|
+
const lines = [
|
|
1460
|
+
value.protocol,
|
|
1461
|
+
value.packageName,
|
|
1462
|
+
value.packageVersion,
|
|
1463
|
+
value.packageManifestSha256,
|
|
1464
|
+
...value.requiredEntrypoints,
|
|
1465
|
+
];
|
|
1466
|
+
for (const file of value.files) lines.push(file.path, file.sha256);
|
|
1467
|
+
for (const dependency of value.runtimeDependencies) {
|
|
1468
|
+
lines.push(dependency.name, dependency.version, dependency.packageManifestSha256);
|
|
1469
|
+
for (const file of dependency.files) lines.push(file.path, file.sha256);
|
|
1470
|
+
}
|
|
1471
|
+
return createHash('sha256').update(lines.join('\0')).digest('hex');
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function safeRuntimeBuildPath(value: string): string | null {
|
|
1475
|
+
const normalized = value.split('\\').join('/');
|
|
1476
|
+
if (!normalized.startsWith('dist/src/') || !normalized.endsWith('.js') || path.posix.normalize(normalized) !== normalized) return null;
|
|
1477
|
+
if (path.posix.isAbsolute(normalized) || normalized.split('/').includes('..')) return null;
|
|
1478
|
+
return normalized;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function safeRuntimeDependencyPath(value: string): string | null {
|
|
1482
|
+
const normalized = value.split('\\').join('/');
|
|
1483
|
+
if (!/\.(?:cjs|js|json|mjs|node)$/.test(normalized)
|
|
1484
|
+
|| path.posix.normalize(normalized) !== normalized
|
|
1485
|
+
|| path.posix.isAbsolute(normalized)
|
|
1486
|
+
|| normalized.split('/').includes('..')
|
|
1487
|
+
|| normalized.split('/').includes('node_modules')) return null;
|
|
1488
|
+
return normalized;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function isPackageName(value: string): boolean {
|
|
1492
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function readJsonRecord(filePath: string | null): Record<string, unknown> | null {
|
|
1496
|
+
if (!filePath) return null;
|
|
1497
|
+
try {
|
|
1498
|
+
const value: unknown = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1499
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function nestedString(value: Record<string, unknown> | null, keys: string[]): string | null {
|
|
1506
|
+
let cursor: unknown = value;
|
|
1507
|
+
for (const key of keys) {
|
|
1508
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) return null;
|
|
1509
|
+
cursor = (cursor as Record<string, unknown>)[key];
|
|
1510
|
+
}
|
|
1511
|
+
return typeof cursor === 'string' ? cursor : null;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function stringField(value: Record<string, unknown> | null, key: string): string | null {
|
|
1515
|
+
const candidate = value?.[key];
|
|
1516
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function isPlainDirectory(directoryPath: string): boolean {
|
|
1520
|
+
try {
|
|
1521
|
+
return existsSync(directoryPath) && lstatSync(directoryPath).isDirectory();
|
|
1522
|
+
} catch {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function isUnredirectedPathInside(root: string, candidate: string): boolean {
|
|
1528
|
+
try {
|
|
1529
|
+
const logicalRoot = path.resolve(root);
|
|
1530
|
+
const logicalCandidate = path.resolve(candidate);
|
|
1531
|
+
if (isOutside(logicalRoot, logicalCandidate)) return false;
|
|
1532
|
+
const expectedRealPath = path.join(realpathSync(logicalRoot), path.relative(logicalRoot, logicalCandidate));
|
|
1533
|
+
return realpathSync(logicalCandidate) === expectedRealPath;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function isPlainFileInside(root: string, filePath: string): boolean {
|
|
1540
|
+
return inspectRuntimeFile(root, filePath).state === 'ready';
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function inspectRuntimeFile(
|
|
1544
|
+
root: string,
|
|
1545
|
+
filePath: string,
|
|
1546
|
+
): { state: 'ready' | 'missing' | 'unsafe'; sha256: string | null } {
|
|
1547
|
+
try {
|
|
1548
|
+
if (!existsSync(filePath)) return { state: 'missing', sha256: null };
|
|
1549
|
+
if (!lstatSync(filePath).isFile()) return { state: 'unsafe', sha256: null };
|
|
1550
|
+
const realRoot = realpathSync(root);
|
|
1551
|
+
const realFile = realpathSync(filePath);
|
|
1552
|
+
if (isOutside(realRoot, realFile)) return { state: 'unsafe', sha256: null };
|
|
1553
|
+
return { state: 'ready', sha256: sha256File(realFile) };
|
|
1554
|
+
} catch {
|
|
1555
|
+
return { state: 'unsafe', sha256: null };
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function assertReviewMode(value: string): asserts value is ReviewMode {
|
|
1560
|
+
if (value !== 'ordinary' && value !== 'security') {
|
|
1561
|
+
throw new Error('review mode must be ordinary or security');
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function reviewInstruction(mode: ReviewMode): string {
|
|
1566
|
+
return mode === 'security'
|
|
1567
|
+
? 'Perform the requested security-focused review within the supplied sealed scope.'
|
|
1568
|
+
: '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.';
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function reviewPolicy(mode: ReviewMode): Record<string, unknown> {
|
|
1572
|
+
return { mode, implicitSecurityScan: false, securityFocusRequested: mode === 'security' };
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function isOutside(root: string, candidate: string): boolean {
|
|
1576
|
+
const relative = path.relative(root, candidate);
|
|
1577
|
+
return relative.startsWith('..') && !path.isAbsolute(relative);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function sha256File(filePath: string): string {
|
|
1581
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function isReviewFindingRecord(value: unknown): value is Record<string, unknown> {
|
|
1585
|
+
return isExactRecord(value, [
|
|
1586
|
+
'id',
|
|
1587
|
+
'severity',
|
|
1588
|
+
'requirement',
|
|
1589
|
+
'summary',
|
|
1590
|
+
'evidence',
|
|
1591
|
+
'requiredAction',
|
|
1592
|
+
'causeId',
|
|
1593
|
+
'route',
|
|
1594
|
+
'planConflict',
|
|
1595
|
+
]);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function validatePlanConflict(
|
|
1599
|
+
value: unknown,
|
|
1600
|
+
route: 'fix' | 'replan',
|
|
1601
|
+
): NonNullable<ReviewInput['findings'][number]['planConflict']> | null {
|
|
1602
|
+
if (route === 'fix') {
|
|
1603
|
+
if (value !== undefined && value !== null) throw new Error('fix findings cannot carry planConflict');
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
if (!isExactRecord(value, ['planClause', 'whyFixCannotFit', 'minimalPlanChange'])) {
|
|
1607
|
+
throw new Error('replan findings require an exact planConflict');
|
|
1608
|
+
}
|
|
1609
|
+
for (const key of ['planClause', 'whyFixCannotFit', 'minimalPlanChange'] as const) {
|
|
1610
|
+
if (typeof value[key] !== 'string' || !value[key].trim()) {
|
|
1611
|
+
throw new Error('replan findings require non-empty planConflict fields');
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
planClause: value.planClause as string,
|
|
1616
|
+
whyFixCannotFit: value.whyFixCannotFit as string,
|
|
1617
|
+
minimalPlanChange: value.minimalPlanChange as string,
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function isExactRecord(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
1622
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
1623
|
+
const actual = Object.keys(value).sort();
|
|
1624
|
+
return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index]);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function canonicalHash(value: unknown): string {
|
|
1628
|
+
return createHash('sha256').update(canonicalJsonStringify(value)).digest('hex');
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function spawnReviewer(
|
|
1632
|
+
command: string,
|
|
1633
|
+
args: string[],
|
|
1634
|
+
options: { cwd: string; input: string; timeoutMs: number },
|
|
1635
|
+
): ReviewerProcessResult {
|
|
1636
|
+
const result = spawnSync(command, args, {
|
|
1637
|
+
cwd: options.cwd,
|
|
1638
|
+
input: options.input,
|
|
1639
|
+
encoding: 'utf8',
|
|
1640
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1641
|
+
timeout: options.timeoutMs,
|
|
1642
|
+
killSignal: 'SIGKILL',
|
|
1643
|
+
});
|
|
1644
|
+
const timedOut = result.error !== undefined && 'code' in result.error && result.error.code === 'ETIMEDOUT';
|
|
1645
|
+
return {
|
|
1646
|
+
status: result.status,
|
|
1647
|
+
stdout: result.stdout ?? '',
|
|
1648
|
+
stderr: [result.stderr ?? '', result.error?.message ?? ''].filter(Boolean).join('\n'),
|
|
1649
|
+
timedOut,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function runStrictReviewerLaunch(
|
|
1654
|
+
repositoryRoot: string,
|
|
1655
|
+
options: {
|
|
1656
|
+
protocol: string;
|
|
1657
|
+
task: TaskState;
|
|
1658
|
+
taskRoot: string;
|
|
1659
|
+
envelope: ContextEnvelope;
|
|
1660
|
+
commandSummary: string;
|
|
1661
|
+
packet: Record<string, unknown>;
|
|
1662
|
+
},
|
|
1663
|
+
runner: ReviewerCommandRunner,
|
|
1664
|
+
): StrictReviewerLaunchResult & { reviewerOutputHash: string } {
|
|
1665
|
+
const temporary = mkdtempSync(path.join(os.tmpdir(), 'codex-workflow-review-'));
|
|
1666
|
+
const schemaFile = path.join(temporary, 'review-result.schema.json');
|
|
1667
|
+
const outputFile = path.join(temporary, 'last-message.json');
|
|
1668
|
+
writeFileSync(schemaFile, `${JSON.stringify(STRICT_REVIEW_OUTPUT_SCHEMA, null, 2)}\n`, { mode: 0o600 });
|
|
1669
|
+
const probe = path.join(repositoryRoot, '.codex-reviewer-write-probe');
|
|
1670
|
+
if (existsSync(probe)) {
|
|
1671
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
1672
|
+
const failed = failedLaunch(repositoryRoot, [], null, 'Reviewer probe path already exists; isolation cannot be tested.');
|
|
1673
|
+
return { ...failed, reviewerOutputHash: hashReviewInput(failed.review) };
|
|
1674
|
+
}
|
|
1675
|
+
const before = sealRepository(repositoryRoot);
|
|
1676
|
+
const command = [
|
|
1677
|
+
'codex', '--ask-for-approval', 'never', 'exec',
|
|
1678
|
+
'--ephemeral', '--ignore-user-config', '--ignore-rules', '--sandbox', 'read-only',
|
|
1679
|
+
'--output-schema', schemaFile, '--output-last-message', outputFile, '--json', '-C', repositoryRoot, '-',
|
|
1680
|
+
];
|
|
1681
|
+
let processResult: ReviewerProcessResult;
|
|
1682
|
+
try {
|
|
1683
|
+
processResult = runner(command[0]!, command.slice(1), {
|
|
1684
|
+
cwd: repositoryRoot,
|
|
1685
|
+
input: `${JSON.stringify(options.packet)}\n`,
|
|
1686
|
+
timeoutMs: STRICT_REVIEWER_TIMEOUT_MS,
|
|
1687
|
+
});
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
processResult = { status: null, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
|
|
1690
|
+
}
|
|
1691
|
+
const probeWritten = existsSync(probe);
|
|
1692
|
+
const after = sealRepository(repositoryRoot);
|
|
1693
|
+
if (probeWritten) rmSync(probe, { force: true });
|
|
1694
|
+
let output: StrictReviewOutput | null = null;
|
|
1695
|
+
if (processResult.status === 0 && existsSync(outputFile)) {
|
|
1696
|
+
try {
|
|
1697
|
+
const parsed = parseStrictReviewWithHash(readFileSync(outputFile, 'utf8'));
|
|
1698
|
+
output = parsed.output;
|
|
1699
|
+
} catch {
|
|
1700
|
+
output = null;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
1704
|
+
const unchanged = sealsEqual(before, after) && !probeWritten;
|
|
1705
|
+
const verifiedIsolation = unchanged && output?.isolationProbe === 'denied' && processResult.status === 0;
|
|
1706
|
+
const review = verifiedIsolation && output
|
|
1707
|
+
? stripProbe(output)
|
|
1708
|
+
: unverifiedReview(
|
|
1709
|
+
processResult.timedOut
|
|
1710
|
+
? `Strict reviewer exceeded the ${STRICT_REVIEWER_TIMEOUT_MS}ms execution deadline.`
|
|
1711
|
+
: processResult.status !== 0
|
|
1712
|
+
? `Strict reviewer process failed with status ${String(processResult.status)}.`
|
|
1713
|
+
: probeWritten || !unchanged
|
|
1714
|
+
? 'Strict reviewer modified repository state; review discarded.'
|
|
1715
|
+
: options.commandSummary,
|
|
1716
|
+
);
|
|
1717
|
+
const reviewerOutputHash = hashReviewInput(review);
|
|
1718
|
+
return {
|
|
1719
|
+
review,
|
|
1720
|
+
verifiedIsolation,
|
|
1721
|
+
command,
|
|
1722
|
+
processStatus: processResult.status,
|
|
1723
|
+
timedOut: processResult.timedOut === true,
|
|
1724
|
+
stderr: processResult.stderr,
|
|
1725
|
+
before,
|
|
1726
|
+
after,
|
|
1727
|
+
reviewerOutputHash,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function sealRepository(repositoryRoot: string): RepositorySeal {
|
|
1732
|
+
return {
|
|
1733
|
+
headCommit: headCommit(repositoryRoot),
|
|
1734
|
+
changedFiles: changedFiles(repositoryRoot),
|
|
1735
|
+
contentHash: hashReviewableTree(repositoryRoot),
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function hashReviewableTree(repositoryRoot: string): string {
|
|
1740
|
+
const listed = spawnSync(
|
|
1741
|
+
'git',
|
|
1742
|
+
['-C', repositoryRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
|
|
1743
|
+
{ encoding: 'utf8' },
|
|
1744
|
+
);
|
|
1745
|
+
if (listed.status !== 0) {
|
|
1746
|
+
throw new Error('Unable to enumerate reviewable repository files.');
|
|
1747
|
+
}
|
|
1748
|
+
const hash = createHash('sha256');
|
|
1749
|
+
const paths = listed.stdout.split('\0').filter(Boolean).sort();
|
|
1750
|
+
for (const relative of paths) {
|
|
1751
|
+
const absolute = path.join(repositoryRoot, relative);
|
|
1752
|
+
hash.update(`${relative}\0`);
|
|
1753
|
+
if (!existsSync(absolute)) {
|
|
1754
|
+
hash.update('missing');
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
const stat = lstatSync(absolute);
|
|
1758
|
+
hash.update(`${stat.mode & 0o777}\0`);
|
|
1759
|
+
if (stat.isSymbolicLink()) hash.update(`link:${readlinkSync(absolute)}`);
|
|
1760
|
+
else if (stat.isFile()) hash.update(readFileSync(absolute));
|
|
1761
|
+
}
|
|
1762
|
+
return hash.digest('hex');
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function sealsEqual(left: RepositorySeal, right: RepositorySeal): boolean {
|
|
1766
|
+
return left.headCommit === right.headCommit
|
|
1767
|
+
&& left.contentHash === right.contentHash
|
|
1768
|
+
&& JSON.stringify(left.changedFiles) === JSON.stringify(right.changedFiles);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function parseStrictReview(value: string): StrictReviewOutput {
|
|
1772
|
+
const parsed: unknown = JSON.parse(value);
|
|
1773
|
+
if (!isExactRecord(parsed, ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'])) {
|
|
1774
|
+
throw new Error('invalid review');
|
|
1775
|
+
}
|
|
1776
|
+
if (!['passed', 'failed', 'unverified'].includes(String(parsed.status))) throw new Error('invalid status');
|
|
1777
|
+
if (typeof parsed.reviewer !== 'string' || !parsed.reviewer.trim()
|
|
1778
|
+
|| typeof parsed.summary !== 'string' || !parsed.summary.trim()
|
|
1779
|
+
|| !Array.isArray(parsed.findings)) throw new Error('invalid review');
|
|
1780
|
+
if (!['denied', 'written', 'not-attempted'].includes(String(parsed.isolationProbe))) throw new Error('invalid probe');
|
|
1781
|
+
const findings = parsed.findings.map((candidate) => {
|
|
1782
|
+
if (!isReviewFindingRecord(candidate)
|
|
1783
|
+
|| typeof candidate.id !== 'string' || !candidate.id.trim()
|
|
1784
|
+
|| !['critical', 'major', 'minor'].includes(String(candidate.severity))
|
|
1785
|
+
|| (candidate.requirement !== null && typeof candidate.requirement !== 'string')
|
|
1786
|
+
|| typeof candidate.summary !== 'string' || !candidate.summary.trim()
|
|
1787
|
+
|| typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
|
|
1788
|
+
|| typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
|
|
1789
|
+
throw new Error('invalid review finding');
|
|
1790
|
+
}
|
|
1791
|
+
if (candidate.route !== 'fix' && candidate.route !== 'replan') throw new Error('invalid review finding route');
|
|
1792
|
+
const route: 'fix' | 'replan' = candidate.route;
|
|
1793
|
+
const causeId = candidate.causeId;
|
|
1794
|
+
if (causeId !== null
|
|
1795
|
+
&& (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
|
|
1796
|
+
throw new Error('invalid review finding causeId');
|
|
1797
|
+
}
|
|
1798
|
+
const planConflict = validatePlanConflict(candidate.planConflict, route);
|
|
1799
|
+
return {
|
|
1800
|
+
id: candidate.id,
|
|
1801
|
+
severity: candidate.severity as 'critical' | 'major' | 'minor',
|
|
1802
|
+
requirement: candidate.requirement as string | null,
|
|
1803
|
+
summary: candidate.summary,
|
|
1804
|
+
evidence: candidate.evidence,
|
|
1805
|
+
requiredAction: candidate.requiredAction,
|
|
1806
|
+
...(causeId === null ? {} : { causeId }),
|
|
1807
|
+
route,
|
|
1808
|
+
planConflict,
|
|
1809
|
+
};
|
|
1810
|
+
});
|
|
1811
|
+
if ((parsed.status === 'passed' && findings.length > 0)
|
|
1812
|
+
|| (parsed.status === 'failed' && findings.length === 0)) throw new Error('invalid finding cardinality');
|
|
1813
|
+
return {
|
|
1814
|
+
status: parsed.status as StrictReviewOutput['status'],
|
|
1815
|
+
reviewer: parsed.reviewer,
|
|
1816
|
+
summary: parsed.summary,
|
|
1817
|
+
findings,
|
|
1818
|
+
isolationProbe: parsed.isolationProbe as StrictReviewOutput['isolationProbe'],
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function parseStrictReviewWithHash(value: string): ParsedStrictReviewResult {
|
|
1823
|
+
const output = parseStrictReview(value);
|
|
1824
|
+
return {
|
|
1825
|
+
output,
|
|
1826
|
+
outputHash: hashReviewInput(stripProbe(output)),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function hashReviewInput(review: ReviewInput): string {
|
|
1831
|
+
return createHash('sha256').update(canonicalJsonStringify(review)).digest('hex');
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function stripProbe(output: StrictReviewOutput): ReviewInput {
|
|
1835
|
+
return {
|
|
1836
|
+
status: output.status,
|
|
1837
|
+
reviewer: output.reviewer,
|
|
1838
|
+
summary: output.summary,
|
|
1839
|
+
findings: output.findings,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function unverifiedReview(summary: string): ReviewInput {
|
|
1844
|
+
return { status: 'unverified', reviewer: 'strict-codex-reviewer', summary, findings: [] };
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function failedLaunch(
|
|
1848
|
+
repositoryRoot: string,
|
|
1849
|
+
command: string[],
|
|
1850
|
+
processStatus: number | null,
|
|
1851
|
+
summary: string,
|
|
1852
|
+
): StrictReviewerLaunchResult {
|
|
1853
|
+
const seal = sealRepository(repositoryRoot);
|
|
1854
|
+
return {
|
|
1855
|
+
review: unverifiedReview(summary),
|
|
1856
|
+
verifiedIsolation: false,
|
|
1857
|
+
command,
|
|
1858
|
+
processStatus,
|
|
1859
|
+
timedOut: false,
|
|
1860
|
+
stderr: summary,
|
|
1861
|
+
before: seal,
|
|
1862
|
+
after: seal,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function failedStepLaunch(
|
|
1867
|
+
repositoryRoot: string,
|
|
1868
|
+
command: string[],
|
|
1869
|
+
processStatus: number | null,
|
|
1870
|
+
summary: string,
|
|
1871
|
+
reviewedCommit: string,
|
|
1872
|
+
): StrictStepReviewerLaunchResult {
|
|
1873
|
+
const base = failedLaunch(repositoryRoot, command, processStatus, summary);
|
|
1874
|
+
return {
|
|
1875
|
+
...base,
|
|
1876
|
+
reviewedCommit,
|
|
1877
|
+
reviewerOutputHash: hashReviewInput(base.review),
|
|
1878
|
+
};
|
|
1879
|
+
}
|