@claudexor/review 1.0.1 → 2.1.3
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/dist/convergence.d.ts.map +1 -1
- package/dist/convergence.js +4 -2
- package/dist/convergence.js.map +1 -1
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js.map +1 -1
- package/dist/gates.d.ts +11 -2
- package/dist/gates.d.ts.map +1 -1
- package/dist/gates.js +126 -5
- package/dist/gates.js.map +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +5 -1
- package/dist/readiness.js.map +1 -1
- package/dist/reviewEngine.d.ts +11 -55
- package/dist/reviewEngine.d.ts.map +1 -1
- package/dist/reviewEngine.js +362 -383
- package/dist/reviewEngine.js.map +1 -1
- package/dist/reviewPrompt.d.ts +3 -0
- package/dist/reviewPrompt.d.ts.map +1 -0
- package/dist/reviewPrompt.js +40 -0
- package/dist/reviewPrompt.js.map +1 -0
- package/dist/reviewRuntimeTypes.d.ts +78 -0
- package/dist/reviewRuntimeTypes.d.ts.map +1 -0
- package/dist/reviewRuntimeTypes.js +26 -0
- package/dist/reviewRuntimeTypes.js.map +1 -0
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +5 -1
- package/dist/route.js.map +1 -1
- package/package.json +6 -5
package/dist/reviewEngine.js
CHANGED
|
@@ -1,54 +1,99 @@
|
|
|
1
1
|
import { parseUnifiedDiff, runCapture } from "@claudexor/core";
|
|
2
2
|
import { preflightEvidence, writeDiffEvidence } from "@claudexor/context";
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
|
+
import { HarnessRunSpec, ReviewFinding as ReviewFindingSchema } from "@claudexor/schema";
|
|
4
|
+
import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
5
5
|
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
8
|
-
import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sha256, writeJson, writeText, } from "@claudexor/util";
|
|
8
|
+
import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sensitiveResourcePolicy, sha256, writeJson, writeText, } from "@claudexor/util";
|
|
9
9
|
import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, } from "./findings.js";
|
|
10
|
+
import { buildReviewPrompt } from "./reviewPrompt.js";
|
|
10
11
|
import { buildRouteProof, classifyDiversity } from "./route.js";
|
|
12
|
+
import { reviewerAuthMode, reviewerAuthSwitchFromEvent, summarizeReviewerSpend, } from "./reviewRuntimeTypes.js";
|
|
11
13
|
const DEFAULT_REVIEWER_TIMEOUT_MS = 10 * 60_000;
|
|
12
14
|
const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
|
|
13
15
|
maxRetries: 2,
|
|
14
16
|
initialDelayMs: 1_000,
|
|
15
17
|
maxDelayMs: 10_000,
|
|
16
18
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
const BLOCKED_REVIEWER_RUNTIME_ROOTS = new Set("auth cache daemon home homes logs runs secrets state tmp workspaces".split(" "));
|
|
20
|
+
const TEXT_EVIDENCE_SUFFIXES = [".md", ".txt", ".json", ".yaml", ".yml", ".patch"];
|
|
21
|
+
const REVIEW_WAVE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
22
|
+
function readExistingDiffEvidence(dir, diff) {
|
|
23
|
+
const diffText = diff.endsWith("\n") ? diff : `${diff}\n`;
|
|
24
|
+
const summaryPath = join(dir, "DIFF_SUMMARY.md");
|
|
25
|
+
const summary = readTextSafe(summaryPath);
|
|
26
|
+
if (summary === null)
|
|
27
|
+
throw new Error("sealed review packet is missing DIFF_SUMMARY.md");
|
|
28
|
+
return {
|
|
29
|
+
diffPath: join(dir, "DIFF.patch"),
|
|
30
|
+
summaryPath,
|
|
31
|
+
diffSha256: sha256(diffText),
|
|
32
|
+
summary,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function reviewerRouteProof(reviewer, modelId, source, peerFamilies) {
|
|
36
|
+
return buildRouteProof({
|
|
37
|
+
harness_id: reviewer.adapter.id,
|
|
38
|
+
provider_family: reviewer.providerFamily,
|
|
39
|
+
model_hint: reviewer.requestedModel ?? null,
|
|
40
|
+
}, {
|
|
41
|
+
provider: reviewer.providerFamily,
|
|
42
|
+
model_id: modelId,
|
|
43
|
+
evidence_source: modelId ? source : "unavailable",
|
|
44
|
+
}, peerFamilies);
|
|
45
|
+
}
|
|
46
|
+
function reviewerInfo(reviewer, routeProofStatus, observedModel = null) {
|
|
47
|
+
return {
|
|
48
|
+
harness_id: reviewer.adapter.id,
|
|
49
|
+
requested_model: reviewer.requestedModel ?? null,
|
|
50
|
+
requested_effort: reviewer.requestedEffort ?? null,
|
|
51
|
+
observed_model: observedModel,
|
|
52
|
+
route_proof_status: routeProofStatus,
|
|
53
|
+
};
|
|
33
54
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Cross-family review of one anonymized candidate. Each reviewer runs its review
|
|
36
|
-
* intent and emits JSON findings; we attach route proofs and verify the
|
|
37
|
-
* reviewers span >= 2 distinct provider families.
|
|
38
|
-
*/
|
|
39
55
|
export async function reviewCandidate(input) {
|
|
40
56
|
const findingsByReviewer = input.reviewers.map(() => []);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
57
|
+
const reviewerFamilies = input.reviewers.map((reviewer) => reviewer.providerFamily);
|
|
58
|
+
const routeProofs = input.reviewers.map((reviewer, index) => reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies.filter((_, otherIndex) => otherIndex !== index)));
|
|
59
|
+
const reviewerRequests = input.reviewers.map((reviewer) => ({
|
|
60
|
+
harness_id: reviewer.adapter.id,
|
|
61
|
+
provider_family: reviewer.providerFamily,
|
|
62
|
+
requested_model: reviewer.requestedModel ?? null,
|
|
63
|
+
requested_effort: reviewer.requestedEffort ?? null,
|
|
64
|
+
}));
|
|
44
65
|
const healthyReviewerIndexes = new Set();
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
const reviewSpendByReviewer = input.reviewers.map(() => 0);
|
|
67
|
+
const reviewSpendEstimatedByReviewer = input.reviewers.map(() => false);
|
|
68
|
+
const reviewCashByReviewer = input.reviewers.map(() => 0);
|
|
69
|
+
const reviewValuationByReviewer = input.reviewers.map(() => 0);
|
|
70
|
+
const reviewUnknownByReviewer = input.reviewers.map(() => 0);
|
|
47
71
|
const reviewerTimeoutMs = input.reviewerTimeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
|
|
72
|
+
const reviewWaveId = input.env?.["CLAUDEXOR_REVIEW_WAVE_ID"] ?? process.env["CLAUDEXOR_REVIEW_WAVE_ID"] ?? null;
|
|
73
|
+
if (input.evidenceReadOnly && input.frozenIdentity && !REVIEW_WAVE_ID.test(reviewWaveId ?? "")) {
|
|
74
|
+
throw new Error("sealed release review requires CLAUDEXOR_REVIEW_WAVE_ID UUID");
|
|
75
|
+
}
|
|
76
|
+
const frozenMetadata = input.frozenIdentity
|
|
77
|
+
? {
|
|
78
|
+
candidate_sha: input.frozenIdentity.candidateSha,
|
|
79
|
+
candidate_tree: input.frozenIdentity.candidateTree,
|
|
80
|
+
packet_manifest_sha256: input.frozenIdentity.packetManifestSha256,
|
|
81
|
+
...(reviewWaveId ? { review_wave_id: reviewWaveId } : {}),
|
|
82
|
+
}
|
|
83
|
+
: {};
|
|
48
84
|
if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
|
|
49
85
|
throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
|
|
50
86
|
}
|
|
51
|
-
|
|
87
|
+
if (input.evidenceReadOnly) {
|
|
88
|
+
const packetDiff = readTextSafe(join(input.evidenceDir, "DIFF.patch"));
|
|
89
|
+
const normalizedDiff = input.diff.endsWith("\n") ? input.diff : `${input.diff}\n`;
|
|
90
|
+
if (packetDiff === null || packetDiff !== normalizedDiff) {
|
|
91
|
+
throw new Error("sealed review packet DIFF.patch does not match the verified review diff");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
writeDiffEvidence(input.evidenceDir, input.diff);
|
|
96
|
+
}
|
|
52
97
|
const preflight = preflightEvidence(input.evidenceDir);
|
|
53
98
|
if (!preflight.ok) {
|
|
54
99
|
const parts = [
|
|
@@ -60,69 +105,72 @@ export async function reviewCandidate(input) {
|
|
|
60
105
|
const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
|
|
61
106
|
ensureDir(artifactsBaseDir);
|
|
62
107
|
const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
|
|
63
|
-
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir);
|
|
64
|
-
const persistentPatch =
|
|
65
|
-
|
|
108
|
+
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir, input.evidenceReadOnly === true);
|
|
109
|
+
const persistentPatch = input.evidenceReadOnly
|
|
110
|
+
? readExistingDiffEvidence(persistentEvidenceDir, input.diff)
|
|
111
|
+
: writeDiffEvidence(persistentEvidenceDir, input.diff);
|
|
112
|
+
writeJson(input.evidenceReadOnly
|
|
113
|
+
? join(artifactsBaseDir, "evidence-metadata.json")
|
|
114
|
+
: join(persistentEvidenceDir, "metadata.json"), {
|
|
66
115
|
source_evidence_dir: input.evidenceDir,
|
|
67
116
|
candidate_root: input.cwd,
|
|
68
117
|
persistent_evidence_dir: persistentEvidenceDir,
|
|
69
118
|
diff_path: persistentPatch.diffPath,
|
|
70
119
|
summary_path: persistentPatch.summaryPath,
|
|
71
120
|
diff_sha256: persistentPatch.diffSha256,
|
|
121
|
+
review_subject: input.reviewSubject ?? "code",
|
|
122
|
+
...frozenMetadata,
|
|
72
123
|
});
|
|
73
|
-
const artifacts =
|
|
74
|
-
const reviewerFamilies = input.reviewers.map((r) => r.providerFamily);
|
|
124
|
+
const artifacts = input.reviewers.map(() => undefined);
|
|
75
125
|
const preservePaths = extractDiffTouchedPaths(input.diff);
|
|
76
126
|
const reviewerWorkspaceBaseDir = selectReviewerWorkspaceBaseDir(input.cwd, artifactsBaseDir, input.evidenceDir);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
const runReviewer = async (reviewer, index) => {
|
|
128
|
+
if (input.signal?.aborted)
|
|
129
|
+
return;
|
|
130
|
+
const artifact = createReviewerArtifactContext(artifactsBaseDir, index, reviewer);
|
|
131
|
+
artifacts[index] = artifact;
|
|
132
|
+
let reviewerWorkspace = null;
|
|
133
|
+
let spec = null;
|
|
134
|
+
try {
|
|
135
|
+
reviewerWorkspace = await prepareReviewerWorkspace({
|
|
136
|
+
sourceRoot: input.cwd,
|
|
137
|
+
sourceEvidenceDir: persistentEvidenceDir,
|
|
138
|
+
workspaceBaseDir: reviewerWorkspaceBaseDir,
|
|
139
|
+
reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
|
|
140
|
+
excludeRoots: [artifactsBaseDir],
|
|
141
|
+
preservePaths,
|
|
142
|
+
preserveEvidenceBytes: input.evidenceReadOnly === true,
|
|
86
143
|
});
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
reviewerWorkspace
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
cwd: reviewerWorkspace.root,
|
|
118
|
-
access: "readonly",
|
|
119
|
-
model_hint: reviewer.requestedModel ?? null,
|
|
120
|
-
effort_hint: reviewer.requestedEffort ?? null,
|
|
121
|
-
auth_preference: reviewer.authPreference ?? "auto",
|
|
122
|
-
env_inheritance: input.envInheritance ?? "mirror_native",
|
|
123
|
-
...(input.env ? { env: input.env } : {}),
|
|
124
|
-
});
|
|
125
|
-
writeText(artifact.promptPath, redactSecrets(`Persistent local replay evidence:
|
|
144
|
+
const reviewerPatch = input.evidenceReadOnly
|
|
145
|
+
? readExistingDiffEvidence(reviewerWorkspace.evidenceDir, input.diff)
|
|
146
|
+
: writeDiffEvidence(reviewerWorkspace.evidenceDir, input.diff);
|
|
147
|
+
updateReviewerMetadata(artifact, {
|
|
148
|
+
candidate_evidence_dir: reviewerWorkspace.evidenceDir,
|
|
149
|
+
candidate_root: reviewerWorkspace.root,
|
|
150
|
+
source_candidate_evidence_dir: input.evidenceDir,
|
|
151
|
+
source_candidate_root: input.cwd,
|
|
152
|
+
reviewer_workspace_root: reviewerWorkspace.root,
|
|
153
|
+
persistent_evidence_dir: persistentEvidenceDir,
|
|
154
|
+
persistent_diff_path: persistentPatch.diffPath,
|
|
155
|
+
persistent_summary_path: persistentPatch.summaryPath,
|
|
156
|
+
diff_sha256: persistentPatch.diffSha256,
|
|
157
|
+
review_subject: input.reviewSubject ?? "code",
|
|
158
|
+
...frozenMetadata,
|
|
159
|
+
});
|
|
160
|
+
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch, input.evidenceReadOnly === true, input.reviewSubject ?? "code");
|
|
161
|
+
spec = HarnessRunSpec.parse({
|
|
162
|
+
session_id: newId("rev"),
|
|
163
|
+
intent: "review",
|
|
164
|
+
prompt: runtimePrompt,
|
|
165
|
+
cwd: reviewerWorkspace.root,
|
|
166
|
+
access: "readonly",
|
|
167
|
+
model_hint: reviewer.requestedModel ?? null,
|
|
168
|
+
effort_hint: reviewer.requestedEffort ?? null,
|
|
169
|
+
auth_preference: reviewer.authPreference ?? "auto",
|
|
170
|
+
env_inheritance: input.envInheritance ?? "mirror_native",
|
|
171
|
+
...(input.env ? { env: input.env } : {}),
|
|
172
|
+
});
|
|
173
|
+
writeText(artifact.promptPath, redactSecrets(`Persistent local replay evidence:
|
|
126
174
|
- evidence_dir: ${persistentEvidenceDir}
|
|
127
175
|
- candidate_root: ${reviewerWorkspace.root}
|
|
128
176
|
- source_candidate_root: ${input.cwd}
|
|
@@ -134,172 +182,130 @@ Runtime prompt used during review follows. Its candidate-tree paths may be trans
|
|
|
134
182
|
|
|
135
183
|
${runtimePrompt}
|
|
136
184
|
`));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
|
|
155
|
-
const proof = buildRouteProof({
|
|
156
|
-
harness_id: reviewer.adapter.id,
|
|
157
|
-
provider_family: reviewer.providerFamily,
|
|
158
|
-
model_hint: reviewer.requestedModel ?? null,
|
|
159
|
-
}, {
|
|
160
|
-
provider: reviewer.providerFamily,
|
|
161
|
-
model_id: null,
|
|
162
|
-
evidence_source: "unavailable",
|
|
163
|
-
}, reviewerFamilies);
|
|
164
|
-
routeProofs.push(proof);
|
|
165
|
-
findingsByReviewer[index]?.push(insufficientEvidenceFinding({
|
|
166
|
-
harness_id: reviewer.adapter.id,
|
|
167
|
-
requested_model: reviewer.requestedModel ?? null,
|
|
168
|
-
requested_effort: reviewer.requestedEffort ?? null,
|
|
169
|
-
observed_model: null,
|
|
170
|
-
route_proof_status: proof.status,
|
|
171
|
-
}, `Reviewer setup failed: ${message}`));
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (!reviewerWorkspace || !spec)
|
|
175
|
-
continue;
|
|
176
|
-
let text = "";
|
|
177
|
-
// Stream-observed model: ONLY a model the native CLI actually emitted in its
|
|
178
|
-
// stream (stream_event/transcript/model_catalog). This is the honest
|
|
179
|
-
// `observed_model` for findings — an accepted argv echo is NOT an observation.
|
|
180
|
-
let streamObservedModel;
|
|
181
|
-
// Route-proof model: stream-observed when present, else the accepted argv arg
|
|
182
|
-
// (metadata tier). Drives RouteProof.observed.model_id + status.
|
|
183
|
-
let routeModel;
|
|
184
|
-
let routeSource = "unavailable";
|
|
185
|
-
let reviewerError = null;
|
|
186
|
-
try {
|
|
187
|
-
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY, artifact, input.onReviewerEvent, input.signal);
|
|
188
|
-
text = out.text;
|
|
189
|
-
streamObservedModel = out.observedModel;
|
|
190
|
-
routeModel = out.observedModel;
|
|
191
|
-
routeSource = out.observedSource;
|
|
192
|
-
reviewSpendUsd += out.costUsd;
|
|
193
|
-
if (out.costEstimated)
|
|
194
|
-
reviewSpendEstimated = true;
|
|
195
|
-
// accepted_model_arg semantics: when WE passed an explicit model argument
|
|
196
|
-
// and the native CLI completed without rejecting it, the accepted argv is
|
|
197
|
-
// metadata-level route evidence (weaker than stream-observed, stronger
|
|
198
|
-
// than nothing). Some CLIs (codex exec --json) never echo the model. This
|
|
199
|
-
// populates ONLY the route proof — never streamObservedModel, so the
|
|
200
|
-
// finding's observed_model stays null (an argv echo is not an observation).
|
|
201
|
-
if (!routeModel && reviewer.requestedModel) {
|
|
202
|
-
routeModel = reviewer.requestedModel;
|
|
203
|
-
routeSource = "metadata";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
reviewerError = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
208
|
-
// Budget truth: a reviewer that streamed paid tokens then timed out/failed
|
|
209
|
-
// still spent money. Fold the partial cost into the ledger (the success
|
|
210
|
-
// path adds out.costUsd above; these paths are mutually exclusive).
|
|
211
|
-
const partial = err;
|
|
212
|
-
if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
|
|
213
|
-
text = partial.partialText;
|
|
214
|
-
}
|
|
215
|
-
if (partial && typeof partial.partialCostUsd === "number" && partial.partialCostUsd > 0) {
|
|
216
|
-
reviewSpendUsd += partial.partialCostUsd;
|
|
217
|
-
if (partial.partialCostEstimated)
|
|
218
|
-
reviewSpendEstimated = true;
|
|
219
|
-
}
|
|
220
|
-
if (partial?.partialObservedModel) {
|
|
221
|
-
streamObservedModel = partial.partialObservedModel;
|
|
222
|
-
routeModel = partial.partialObservedModel;
|
|
223
|
-
routeSource = partial.partialObservedSource ?? "stream_event";
|
|
224
|
-
}
|
|
225
|
-
writeParseError(artifact, { error: reviewerError });
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const failedAt = nowIso();
|
|
188
|
+
const message = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
189
|
+
updateReviewerMetadata(artifact, {
|
|
190
|
+
status: "failed",
|
|
191
|
+
failure_time: failedAt,
|
|
192
|
+
error: `reviewer setup failed: ${message}`,
|
|
193
|
+
});
|
|
194
|
+
writeParseError(artifact, { error: `reviewer setup failed: ${message}` });
|
|
195
|
+
emitReviewerProgress(artifact, reviewer, input.onReviewerEvent, {
|
|
196
|
+
type: "reviewer.failed",
|
|
197
|
+
at: failedAt,
|
|
198
|
+
duration_ms: 0,
|
|
199
|
+
message: `Reviewer setup failed: ${message}`,
|
|
200
|
+
});
|
|
201
|
+
if (reviewerWorkspace)
|
|
228
202
|
await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
|
|
203
|
+
const proof = reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies);
|
|
204
|
+
routeProofs[index] = proof;
|
|
205
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(reviewerInfo(reviewer, proof.status), `Reviewer setup failed: ${message}`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!reviewerWorkspace || !spec)
|
|
209
|
+
return;
|
|
210
|
+
let text = "";
|
|
211
|
+
let streamObservedModel;
|
|
212
|
+
let routeModel;
|
|
213
|
+
let routeSource = "unavailable";
|
|
214
|
+
let reviewerError = null;
|
|
215
|
+
try {
|
|
216
|
+
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY, artifact, input.onReviewerEvent, input.signal);
|
|
217
|
+
text = out.text;
|
|
218
|
+
streamObservedModel = out.observedModel;
|
|
219
|
+
routeModel = out.observedModel;
|
|
220
|
+
routeSource = out.observedSource;
|
|
221
|
+
reviewSpendByReviewer[index] = out.costUsd;
|
|
222
|
+
reviewSpendEstimatedByReviewer[index] = out.costEstimated;
|
|
223
|
+
reviewCashByReviewer[index] = out.cashUsd;
|
|
224
|
+
reviewValuationByReviewer[index] = out.valuationUsd;
|
|
225
|
+
reviewUnknownByReviewer[index] = out.unknownUsd;
|
|
226
|
+
if (!routeModel && reviewer.requestedModel) {
|
|
227
|
+
routeModel = reviewer.requestedModel;
|
|
228
|
+
routeSource = "metadata";
|
|
229
229
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
model_id: routeModel ?? null,
|
|
237
|
-
evidence_source: routeModel ? routeSource : "unavailable",
|
|
238
|
-
},
|
|
239
|
-
// The other reviewers' families this route is meant to be diverse against
|
|
240
|
-
// (mirrors the implementer route proof; reviewer diversity is otherwise
|
|
241
|
-
// enforced via classifyDiversity's same_model_fallback status below).
|
|
242
|
-
reviewerFamilies.filter((_, i) => i !== index));
|
|
243
|
-
routeProofs.push(proof);
|
|
244
|
-
const info = {
|
|
245
|
-
harness_id: reviewer.adapter.id,
|
|
246
|
-
requested_model: reviewer.requestedModel ?? null,
|
|
247
|
-
requested_effort: reviewer.requestedEffort ?? null,
|
|
248
|
-
// Honest observation only: a finding's observed_model is the STREAM-observed
|
|
249
|
-
// model or null. An accepted argv arg lives in the route proof's model_id,
|
|
250
|
-
// not here — it must not masquerade as an observed model.
|
|
251
|
-
observed_model: streamObservedModel ?? null,
|
|
252
|
-
route_proof_status: proof.status,
|
|
253
|
-
};
|
|
254
|
-
const jsonBlocks = extractJsonBlocks(text);
|
|
255
|
-
writeJson(artifact.parsedPath, redactValue(jsonBlocks));
|
|
256
|
-
if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
|
|
257
|
-
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (text.trim() === "" || jsonBlocks.length === 0) {
|
|
261
|
-
writeParseError(artifact, { error: "no_parseable_json", text_sha256: sha256(text) });
|
|
262
|
-
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
const parsed = parseFindingsDetailed(text, info);
|
|
266
|
-
const parseError = {};
|
|
267
|
-
let parsedFindingsRecorded = false;
|
|
268
|
-
const recordParsedFindings = () => {
|
|
269
|
-
if (parsedFindingsRecorded)
|
|
270
|
-
return;
|
|
271
|
-
findingsByReviewer[index]?.push(...parsed.findings);
|
|
272
|
-
parsedFindingsRecorded = true;
|
|
273
|
-
};
|
|
274
|
-
if (parsed.malformed > 0) {
|
|
275
|
-
Object.assign(parseError, {
|
|
276
|
-
error: "malformed_findings",
|
|
277
|
-
malformed: parsed.malformed,
|
|
278
|
-
text_sha256: sha256(text),
|
|
279
|
-
});
|
|
280
|
-
recordParsedFindings();
|
|
281
|
-
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer produced ${parsed.malformed} malformed finding item(s).`));
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
reviewerError = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
233
|
+
const partial = err;
|
|
234
|
+
if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
|
|
235
|
+
text = partial.partialText;
|
|
282
236
|
}
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
recordParsedFindings();
|
|
290
|
-
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, parsed.findings.length === 0
|
|
291
|
-
? `Reviewer failed after parseable JSON with no findings: ${reviewerError}`
|
|
292
|
-
: `Reviewer failed after parseable JSON output: ${reviewerError}`));
|
|
237
|
+
if (partial && typeof partial.partialCostUsd === "number" && partial.partialCostUsd > 0) {
|
|
238
|
+
reviewSpendByReviewer[index] = partial.partialCostUsd;
|
|
239
|
+
reviewSpendEstimatedByReviewer[index] = partial.partialCostEstimated === true;
|
|
240
|
+
reviewCashByReviewer[index] = partial.partialCashUsd ?? 0;
|
|
241
|
+
reviewValuationByReviewer[index] = partial.partialValuationUsd ?? 0;
|
|
242
|
+
reviewUnknownByReviewer[index] = partial.partialUnknownUsd ?? 0;
|
|
293
243
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
244
|
+
if (partial?.partialObservedModel) {
|
|
245
|
+
streamObservedModel = partial.partialObservedModel;
|
|
246
|
+
routeModel = partial.partialObservedModel;
|
|
247
|
+
routeSource = partial.partialObservedSource ?? "stream_event";
|
|
297
248
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
249
|
+
writeParseError(artifact, { error: reviewerError });
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
await cleanupReviewerWorkspace(reviewerWorkspace, artifact);
|
|
253
|
+
}
|
|
254
|
+
const proof = reviewerRouteProof(reviewer, routeModel ?? null, routeSource, reviewerFamilies.filter((_, i) => i !== index));
|
|
255
|
+
routeProofs[index] = proof;
|
|
256
|
+
const info = reviewerInfo(reviewer, proof.status, streamObservedModel ?? null);
|
|
257
|
+
const jsonBlocks = extractJsonBlocks(text);
|
|
258
|
+
writeJson(artifact.parsedPath, redactValue(jsonBlocks));
|
|
259
|
+
if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
|
|
260
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (text.trim() === "" || jsonBlocks.length === 0) {
|
|
264
|
+
writeParseError(artifact, { error: "no_parseable_json", text_sha256: sha256(text) });
|
|
265
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const parsed = parseFindingsDetailed(text, info);
|
|
269
|
+
const parseError = {};
|
|
270
|
+
let parsedFindingsRecorded = false;
|
|
271
|
+
const recordParsedFindings = () => {
|
|
272
|
+
if (parsedFindingsRecorded)
|
|
273
|
+
return;
|
|
301
274
|
findingsByReviewer[index]?.push(...parsed.findings);
|
|
275
|
+
parsedFindingsRecorded = true;
|
|
276
|
+
};
|
|
277
|
+
if (parsed.malformed > 0) {
|
|
278
|
+
Object.assign(parseError, {
|
|
279
|
+
error: "malformed_findings",
|
|
280
|
+
malformed: parsed.malformed,
|
|
281
|
+
text_sha256: sha256(text),
|
|
282
|
+
});
|
|
283
|
+
recordParsedFindings();
|
|
284
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer produced ${parsed.malformed} malformed finding item(s).`));
|
|
285
|
+
}
|
|
286
|
+
if (reviewerError) {
|
|
287
|
+
Object.assign(parseError, {
|
|
288
|
+
error: reviewerError,
|
|
289
|
+
recovered_json_blocks: jsonBlocks.length,
|
|
290
|
+
text_sha256: sha256(text),
|
|
291
|
+
});
|
|
292
|
+
recordParsedFindings();
|
|
293
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, parsed.findings.length === 0
|
|
294
|
+
? `Reviewer failed after parseable JSON with no findings: ${reviewerError}`
|
|
295
|
+
: `Reviewer failed after parseable JSON output: ${reviewerError}`));
|
|
302
296
|
}
|
|
297
|
+
if (Object.keys(parseError).length > 0) {
|
|
298
|
+
writeParseError(artifact, parseError);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
healthyReviewerIndexes.add(index);
|
|
302
|
+
findingsByReviewer[index]?.push(...parsed.findings);
|
|
303
|
+
};
|
|
304
|
+
try {
|
|
305
|
+
const reviewerRuns = await Promise.allSettled(input.reviewers.map((reviewer, index) => runReviewer(reviewer, index)));
|
|
306
|
+
const failedRun = reviewerRuns.find((run) => run.status === "rejected");
|
|
307
|
+
if (failedRun)
|
|
308
|
+
throw failedRun.reason;
|
|
303
309
|
const classifiedProofs = classifyDiversity(routeProofs);
|
|
304
310
|
for (const [index, proof] of classifiedProofs.entries()) {
|
|
305
311
|
const artifact = artifacts[index];
|
|
@@ -321,12 +327,12 @@ ${runtimePrompt}
|
|
|
321
327
|
});
|
|
322
328
|
});
|
|
323
329
|
});
|
|
324
|
-
const healthyProviders = [
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
const healthyProviders = [
|
|
331
|
+
...new Set(input.reviewers
|
|
332
|
+
.filter((_, index) => healthyReviewerIndexes.has(index))
|
|
333
|
+
.map((reviewer) => reviewer.providerFamily)
|
|
334
|
+
.filter((family) => family !== "unknown")),
|
|
335
|
+
];
|
|
330
336
|
const observedFamilies = [
|
|
331
337
|
...new Set(classifiedProofs
|
|
332
338
|
.filter((p, index) => p.status === "verified" && healthyReviewerIndexes.has(index))
|
|
@@ -341,8 +347,7 @@ ${runtimePrompt}
|
|
|
341
347
|
healthyProviders,
|
|
342
348
|
crossFamilyVerified: observedFamilies.length >= 2,
|
|
343
349
|
distinctProviders: observedFamilies,
|
|
344
|
-
|
|
345
|
-
reviewSpendEstimated,
|
|
350
|
+
...summarizeReviewerSpend(reviewSpendByReviewer, reviewCashByReviewer, reviewValuationByReviewer, reviewUnknownByReviewer, reviewSpendEstimatedByReviewer),
|
|
346
351
|
};
|
|
347
352
|
}
|
|
348
353
|
finally {
|
|
@@ -376,11 +381,13 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
376
381
|
let firstEventTime = null;
|
|
377
382
|
let observedModel;
|
|
378
383
|
let observedSource = "unavailable";
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// the thrown error so the caller can fold it in.
|
|
384
|
+
let observedAuthMode = null;
|
|
385
|
+
let currentAuthMode = null;
|
|
382
386
|
let costUsd = 0;
|
|
383
387
|
let costEstimated = false;
|
|
388
|
+
let cashUsd = 0;
|
|
389
|
+
let valuationUsd = 0;
|
|
390
|
+
let unknownUsd = 0;
|
|
384
391
|
let partialText = "";
|
|
385
392
|
const isCancelled = () => cancelledBySignal || signal?.aborted === true || controller.signal.aborted;
|
|
386
393
|
const consumeOnce = async (nativeTry) => {
|
|
@@ -403,6 +410,10 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
403
410
|
}
|
|
404
411
|
if (ev.type === "message" && ev.payload?.["auth_switched"] === true) {
|
|
405
412
|
const authSwitch = reviewerAuthSwitchFromEvent(ev);
|
|
413
|
+
if (authSwitch.to_auth_mode === "subscription")
|
|
414
|
+
currentAuthMode = "local_session";
|
|
415
|
+
if (authSwitch.to_auth_mode === "api_key")
|
|
416
|
+
currentAuthMode = "api_key";
|
|
406
417
|
updateReviewerMetadata(artifact, { auth_switch: authSwitch });
|
|
407
418
|
emitReviewerProgress(artifact, reviewer, onReviewerEvent, {
|
|
408
419
|
type: "reviewer.auth_switched",
|
|
@@ -410,6 +421,14 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
410
421
|
...authSwitch,
|
|
411
422
|
});
|
|
412
423
|
}
|
|
424
|
+
const disclosedAuthMode = reviewerAuthMode(ev.credential_route);
|
|
425
|
+
if (disclosedAuthMode)
|
|
426
|
+
currentAuthMode = disclosedAuthMode;
|
|
427
|
+
if (!observedAuthMode) {
|
|
428
|
+
observedAuthMode = disclosedAuthMode;
|
|
429
|
+
if (observedAuthMode)
|
|
430
|
+
updateReviewerMetadata(artifact, { auth_mode: observedAuthMode });
|
|
431
|
+
}
|
|
413
432
|
if (!firstEventTime) {
|
|
414
433
|
firstEventTime = eventTime;
|
|
415
434
|
updateReviewerMetadata(artifact, { first_event_time: firstEventTime });
|
|
@@ -422,7 +441,19 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
422
441
|
costUsd += ev.usage.cost_usd;
|
|
423
442
|
if (ev.usage.estimated)
|
|
424
443
|
costEstimated = true;
|
|
425
|
-
|
|
444
|
+
if (currentAuthMode === "local_session")
|
|
445
|
+
valuationUsd += ev.usage.cost_usd;
|
|
446
|
+
else if (currentAuthMode === "api_key")
|
|
447
|
+
cashUsd += ev.usage.cost_usd;
|
|
448
|
+
else
|
|
449
|
+
unknownUsd += ev.usage.cost_usd;
|
|
450
|
+
updateReviewerMetadata(artifact, {
|
|
451
|
+
cost_usd: costUsd,
|
|
452
|
+
cost_estimated: costEstimated,
|
|
453
|
+
cash_usd: cashUsd,
|
|
454
|
+
valuation_usd: valuationUsd,
|
|
455
|
+
unknown_usd: unknownUsd,
|
|
456
|
+
});
|
|
426
457
|
}
|
|
427
458
|
if (ev.type === "message" && ev.text && ev.payload?.["auth_switched"] !== true) {
|
|
428
459
|
const safeText = redactSecrets(ev.text);
|
|
@@ -506,13 +537,15 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
506
537
|
text,
|
|
507
538
|
observedModel: attemptObservedModel,
|
|
508
539
|
observedSource: attemptObservedSource,
|
|
509
|
-
artifactDir: artifact.dir,
|
|
510
540
|
costUsd,
|
|
511
541
|
costEstimated,
|
|
542
|
+
cashUsd,
|
|
543
|
+
valuationUsd,
|
|
544
|
+
unknownUsd,
|
|
512
545
|
};
|
|
513
546
|
};
|
|
514
547
|
const consume = consumeOnce(0);
|
|
515
|
-
|
|
548
|
+
let removeExternalAbortListener = () => { };
|
|
516
549
|
const cancelled = new Promise((_, reject) => {
|
|
517
550
|
if (!signal)
|
|
518
551
|
return;
|
|
@@ -525,6 +558,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
525
558
|
reject(Object.assign(new Error("Reviewer cancelled"), {
|
|
526
559
|
partialCostUsd: costUsd,
|
|
527
560
|
partialCostEstimated: costEstimated,
|
|
561
|
+
partialCashUsd: cashUsd,
|
|
562
|
+
partialValuationUsd: valuationUsd,
|
|
563
|
+
partialUnknownUsd: unknownUsd,
|
|
528
564
|
partialObservedModel: observedModel,
|
|
529
565
|
partialObservedSource: observedSource,
|
|
530
566
|
partialText,
|
|
@@ -534,7 +570,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
534
570
|
queueMicrotask(onAbort);
|
|
535
571
|
else
|
|
536
572
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
537
|
-
|
|
573
|
+
removeExternalAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
538
574
|
});
|
|
539
575
|
const timed = new Promise((_, reject) => {
|
|
540
576
|
timeout = setTimeout(() => {
|
|
@@ -565,6 +601,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
565
601
|
reject(Object.assign(new Error(`Reviewer timed out after ${timeoutMs}ms`), {
|
|
566
602
|
partialCostUsd: costUsd,
|
|
567
603
|
partialCostEstimated: costEstimated,
|
|
604
|
+
partialCashUsd: cashUsd,
|
|
605
|
+
partialValuationUsd: valuationUsd,
|
|
606
|
+
partialUnknownUsd: unknownUsd,
|
|
568
607
|
partialObservedModel: observedModel,
|
|
569
608
|
partialObservedSource: observedSource,
|
|
570
609
|
partialText,
|
|
@@ -599,6 +638,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
599
638
|
Object.assign(err, {
|
|
600
639
|
partialCostUsd: costUsd,
|
|
601
640
|
partialCostEstimated: costEstimated,
|
|
641
|
+
partialCashUsd: cashUsd,
|
|
642
|
+
partialValuationUsd: valuationUsd,
|
|
643
|
+
partialUnknownUsd: unknownUsd,
|
|
602
644
|
partialObservedModel: observedModel,
|
|
603
645
|
partialObservedSource: observedSource,
|
|
604
646
|
partialText,
|
|
@@ -610,22 +652,12 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
610
652
|
settled = true;
|
|
611
653
|
if (timeout)
|
|
612
654
|
clearTimeout(timeout);
|
|
613
|
-
|
|
614
|
-
removeExternalAbortListener();
|
|
615
|
-
}
|
|
655
|
+
removeExternalAbortListener();
|
|
616
656
|
consume.catch(() => {
|
|
617
657
|
/* timeout path: consume may reject after the race already returned */
|
|
618
658
|
});
|
|
619
659
|
}
|
|
620
660
|
}
|
|
621
|
-
function reviewerAuthSwitchFromEvent(ev) {
|
|
622
|
-
const reason = FallbackReason.safeParse(ev.payload?.["reason"]);
|
|
623
|
-
return {
|
|
624
|
-
from_auth_mode: typeof ev.payload?.["from_auth_mode"] === "string" ? ev.payload["from_auth_mode"] : "unknown",
|
|
625
|
-
to_auth_mode: typeof ev.payload?.["to_auth_mode"] === "string" ? ev.payload["to_auth_mode"] : "unknown",
|
|
626
|
-
reason: reason.success ? reason.data : "auth_unavailable",
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
661
|
function selectReviewerWorkspaceBaseDir(sourceRoot, artifactsBaseDir, sourceEvidenceDir) {
|
|
630
662
|
const durableBase = join(artifactsBaseDir, "workspaces");
|
|
631
663
|
if (!isSameOrInside(sourceRoot, durableBase) && !isSameOrInside(sourceEvidenceDir, durableBase)) {
|
|
@@ -665,11 +697,13 @@ async function prepareReviewerWorkspace(input) {
|
|
|
665
697
|
const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
|
|
666
698
|
const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
|
|
667
699
|
await rm(evidenceDir, { recursive: true, force: true });
|
|
668
|
-
await cp(sourceEvidenceDir, evidenceDir,
|
|
669
|
-
recursive: true,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
700
|
+
await cp(sourceEvidenceDir, evidenceDir, input.preserveEvidenceBytes
|
|
701
|
+
? { recursive: true, dereference: false }
|
|
702
|
+
: {
|
|
703
|
+
recursive: true,
|
|
704
|
+
dereference: false,
|
|
705
|
+
filter: (sourcePath) => shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, evidenceExcludeRoots),
|
|
706
|
+
});
|
|
673
707
|
}
|
|
674
708
|
await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
|
|
675
709
|
await initializeReviewerWorkspaceGit(root);
|
|
@@ -680,14 +714,20 @@ async function prepareReviewerWorkspace(input) {
|
|
|
680
714
|
throw err;
|
|
681
715
|
}
|
|
682
716
|
}
|
|
683
|
-
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir) {
|
|
717
|
+
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir, preserveBytes = false) {
|
|
684
718
|
const source = resolve(sourceEvidenceDir);
|
|
685
719
|
const target = resolve(persistentEvidenceDir);
|
|
686
720
|
await rm(target, { recursive: true, force: true });
|
|
687
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
688
721
|
if (!existsSync(source)) {
|
|
722
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
689
723
|
return;
|
|
690
724
|
}
|
|
725
|
+
if (preserveBytes) {
|
|
726
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
727
|
+
await cp(source, target, { recursive: true, dereference: false });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
691
731
|
const resolvedSource = realpathSync(source);
|
|
692
732
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
693
733
|
const sourcePath = join(source, entry.name);
|
|
@@ -725,13 +765,7 @@ async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidence
|
|
|
725
765
|
await cp(sourcePath, targetPath, { recursive: false, dereference: false });
|
|
726
766
|
}
|
|
727
767
|
function shouldTextSanitizeEvidenceFile(path) {
|
|
728
|
-
|
|
729
|
-
return (lower.endsWith(".md") ||
|
|
730
|
-
lower.endsWith(".txt") ||
|
|
731
|
-
lower.endsWith(".json") ||
|
|
732
|
-
lower.endsWith(".yaml") ||
|
|
733
|
-
lower.endsWith(".yml") ||
|
|
734
|
-
lower.endsWith(".patch"));
|
|
768
|
+
return TEXT_EVIDENCE_SUFFIXES.some((extension) => path.toLowerCase().endsWith(extension));
|
|
735
769
|
}
|
|
736
770
|
function shouldFailClosedEvidenceFile(path) {
|
|
737
771
|
return path.toLowerCase().endsWith(".patch");
|
|
@@ -742,9 +776,7 @@ function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceD
|
|
|
742
776
|
return false;
|
|
743
777
|
if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
|
|
744
778
|
return false;
|
|
745
|
-
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [
|
|
746
|
-
targetEvidenceDir,
|
|
747
|
-
]);
|
|
779
|
+
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [targetEvidenceDir], new Set(), false);
|
|
748
780
|
}
|
|
749
781
|
async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
750
782
|
try {
|
|
@@ -777,7 +809,7 @@ async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifa
|
|
|
777
809
|
}
|
|
778
810
|
}
|
|
779
811
|
}
|
|
780
|
-
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set()) {
|
|
812
|
+
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set(), enforceContentPolicy = true) {
|
|
781
813
|
const resolvedSourcePath = resolve(sourcePath);
|
|
782
814
|
if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
|
|
783
815
|
return false;
|
|
@@ -788,55 +820,35 @@ function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excl
|
|
|
788
820
|
if (!rel)
|
|
789
821
|
return true;
|
|
790
822
|
const parts = rel.split(/[\\/]+/);
|
|
791
|
-
if (
|
|
823
|
+
if (sensitiveResourcePolicy.classifyPath(rel).sensitive) {
|
|
792
824
|
return false;
|
|
793
825
|
}
|
|
794
826
|
if (parts[0] === ".claudexor") {
|
|
795
|
-
return isCopyableReviewerClaudexorPath(rel, parts, preservePaths)
|
|
827
|
+
return (isCopyableReviewerClaudexorPath(rel, parts, preservePaths) &&
|
|
828
|
+
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
796
829
|
}
|
|
797
|
-
if (parts.some((part) => [
|
|
798
|
-
".git",
|
|
799
|
-
".adversarial-review",
|
|
800
|
-
".turbo",
|
|
801
|
-
"node_modules",
|
|
802
|
-
].includes(part))) {
|
|
830
|
+
if (parts.some((part) => [".git", ".adversarial-review", ".turbo", "node_modules"].includes(part))) {
|
|
803
831
|
return false;
|
|
804
832
|
}
|
|
805
833
|
if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
|
|
806
834
|
!isPreservedReviewerPath(rel, preservePaths)) {
|
|
807
835
|
return false;
|
|
808
836
|
}
|
|
809
|
-
return !rel.endsWith(".tsbuildinfo")
|
|
837
|
+
return (!rel.endsWith(".tsbuildinfo") &&
|
|
838
|
+
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
810
839
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if ([
|
|
816
|
-
".npmrc",
|
|
817
|
-
".netrc",
|
|
818
|
-
".pypirc",
|
|
819
|
-
".git-credentials",
|
|
820
|
-
".ssh",
|
|
821
|
-
".aws",
|
|
822
|
-
".azure",
|
|
823
|
-
".gcloud",
|
|
824
|
-
".cursor",
|
|
825
|
-
".codex",
|
|
826
|
-
".claude",
|
|
827
|
-
".anthropic",
|
|
828
|
-
".openai",
|
|
829
|
-
].includes(lower)) {
|
|
830
|
-
return true;
|
|
840
|
+
function reviewerFileContentAllowed(path) {
|
|
841
|
+
let targetStat;
|
|
842
|
+
try {
|
|
843
|
+
targetStat = statSync(path);
|
|
831
844
|
}
|
|
832
|
-
|
|
833
|
-
return
|
|
834
|
-
|
|
845
|
+
catch {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
if (!targetStat.isFile())
|
|
835
849
|
return true;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
function isSafeEnvTemplateName(lower) {
|
|
839
|
-
return [".env.example", ".env.sample", ".env.template"].includes(lower);
|
|
850
|
+
const content = readTextSafe(path);
|
|
851
|
+
return content !== null && !sensitiveResourcePolicy.containsSensitiveContent(content);
|
|
840
852
|
}
|
|
841
853
|
function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
842
854
|
if (parts.length === 1) {
|
|
@@ -846,22 +858,8 @@ function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
|
846
858
|
return true;
|
|
847
859
|
}
|
|
848
860
|
const runtimeRoot = parts[1]?.toLowerCase();
|
|
849
|
-
if (runtimeRoot &&
|
|
850
|
-
[
|
|
851
|
-
"auth",
|
|
852
|
-
"cache",
|
|
853
|
-
"daemon",
|
|
854
|
-
"home",
|
|
855
|
-
"homes",
|
|
856
|
-
"logs",
|
|
857
|
-
"runs",
|
|
858
|
-
"secrets",
|
|
859
|
-
"state",
|
|
860
|
-
"tmp",
|
|
861
|
-
"workspaces",
|
|
862
|
-
].includes(runtimeRoot)) {
|
|
861
|
+
if (runtimeRoot && BLOCKED_REVIEWER_RUNTIME_ROOTS.has(runtimeRoot))
|
|
863
862
|
return false;
|
|
864
|
-
}
|
|
865
863
|
return isPreservedReviewerPath(rel, preservePaths);
|
|
866
864
|
}
|
|
867
865
|
function isPreservedReviewerPath(rel, preservePaths) {
|
|
@@ -877,15 +875,10 @@ function isPreservedReviewerPath(rel, preservePaths) {
|
|
|
877
875
|
}
|
|
878
876
|
return false;
|
|
879
877
|
}
|
|
880
|
-
/** Test-only alias for the preserve-set extractor. */
|
|
881
878
|
export function __testExtractDiffTouchedPaths(diff) {
|
|
882
879
|
return extractDiffTouchedPaths(diff);
|
|
883
880
|
}
|
|
884
881
|
function extractDiffTouchedPaths(diff) {
|
|
885
|
-
// One structural parser owns diff headers (INV-050): parseUnifiedDiff is
|
|
886
|
-
// quote-aware and decodes git's C-quoted paths (incl. octal escapes for
|
|
887
|
-
// non-ASCII), which the old private tokenizer mis-decoded — a mis-decoded
|
|
888
|
-
// touched path silently dropped the file from the reviewer preserve set.
|
|
889
882
|
const paths = new Set();
|
|
890
883
|
for (const file of parseUnifiedDiff(diff).files) {
|
|
891
884
|
if (file.oldPath)
|
|
@@ -921,57 +914,40 @@ function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, e
|
|
|
921
914
|
return true;
|
|
922
915
|
let linkTarget = "";
|
|
923
916
|
let resolvedTarget = "";
|
|
917
|
+
let targetKind = "other";
|
|
924
918
|
try {
|
|
925
919
|
linkTarget = readlinkSync(sourcePath);
|
|
926
920
|
resolvedTarget = realpathSync(sourcePath);
|
|
921
|
+
const targetStat = statSync(sourcePath);
|
|
922
|
+
targetKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "other";
|
|
927
923
|
}
|
|
928
924
|
catch {
|
|
929
925
|
return false;
|
|
930
926
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (relocatedFirstPart === ".." || isAbsolute(relocatedTargetRel))
|
|
943
|
-
return false;
|
|
944
|
-
const relocatedTargetPath = resolve(sourceRoot, relocatedTargetRel);
|
|
945
|
-
if (!isSameOrInside(sourceRoot, relocatedTargetPath))
|
|
946
|
-
return false;
|
|
947
|
-
return !excludeRoots.some((root) => isSameOrInside(root, relocatedTargetPath));
|
|
927
|
+
return sensitiveResourcePolicy.assessSymlink({
|
|
928
|
+
sourceRoot,
|
|
929
|
+
canonicalSourceRoot: resolvedSourceRoot,
|
|
930
|
+
sourcePath,
|
|
931
|
+
linkTarget,
|
|
932
|
+
resolvedTargetPath: resolvedTarget,
|
|
933
|
+
targetKind,
|
|
934
|
+
allowedTargetKinds: ["file", "directory"],
|
|
935
|
+
excludedRoots: excludeRoots,
|
|
936
|
+
relocationRoot: sourceRoot,
|
|
937
|
+
}).allowed;
|
|
948
938
|
}
|
|
949
939
|
async function initializeReviewerWorkspaceGit(root) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
"-
|
|
954
|
-
"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
"core.hooksPath=/dev/null",
|
|
960
|
-
"config",
|
|
961
|
-
"user.email",
|
|
962
|
-
"claudexor-review@example.invalid",
|
|
963
|
-
]);
|
|
964
|
-
await runGitOrThrow("config user.name", root, [
|
|
965
|
-
"-c",
|
|
966
|
-
"core.hooksPath=/dev/null",
|
|
967
|
-
"config",
|
|
968
|
-
"user.name",
|
|
969
|
-
"Claudexor Review",
|
|
970
|
-
]);
|
|
971
|
-
await runGitOrThrow("add", root, ["-c", "core.hooksPath=/dev/null", "add", "-A", "--force"]);
|
|
940
|
+
const noHooks = ["-c", "core.hooksPath=/dev/null"];
|
|
941
|
+
await runGitOrThrow("init", root, ["-c", "init.templateDir=", ...noHooks, "init"]);
|
|
942
|
+
for (const [key, value] of [
|
|
943
|
+
["user.email", "claudexor-review@example.invalid"],
|
|
944
|
+
["user.name", "Claudexor Review"],
|
|
945
|
+
]) {
|
|
946
|
+
await runGitOrThrow(`config ${key}`, root, [...noHooks, "config", key, value]);
|
|
947
|
+
}
|
|
948
|
+
await runGitOrThrow("add", root, [...noHooks, "add", "-A", "--force"]);
|
|
972
949
|
await runGitOrThrow("commit", root, [
|
|
973
|
-
|
|
974
|
-
"core.hooksPath=/dev/null",
|
|
950
|
+
...noHooks,
|
|
975
951
|
"commit",
|
|
976
952
|
"--allow-empty",
|
|
977
953
|
"--no-verify",
|
|
@@ -1034,6 +1010,9 @@ function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
|
|
|
1034
1010
|
requested_model: reviewer.requestedModel ?? null,
|
|
1035
1011
|
requested_effort: reviewer.requestedEffort ?? null,
|
|
1036
1012
|
artifact_dir: artifact.dir,
|
|
1013
|
+
...(typeof artifact.metadata["review_wave_id"] === "string"
|
|
1014
|
+
? { review_wave_id: artifact.metadata["review_wave_id"] }
|
|
1015
|
+
: {}),
|
|
1037
1016
|
...patch,
|
|
1038
1017
|
};
|
|
1039
1018
|
const redacted = redactValue(event);
|