@kylecheng3146/agent-ops 0.2.2 → 0.2.4
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 +28 -14
- package/dist/packages/cli/src/args.js +34 -11
- package/dist/packages/cli/src/bin.js +21 -9
- package/dist/packages/cli/src/cli.js +15 -13
- package/dist/packages/cli/src/commands/review.js +123 -32
- package/dist/runtime/src/adapters/claude/output.js +2 -1
- package/dist/runtime/src/install/doctor.js +17 -7
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/review/attestation.js +180 -5
- package/dist/runtime/src/review/execute.js +183 -113
- package/dist/runtime/src/review/host-sandbox.js +3 -2
- package/dist/runtime/src/review/probe.js +3 -0
- package/dist/runtime/src/review/render.js +22 -6
- package/dist/runtime/src/review/roles.js +29 -20
- package/dist/runtime/src/review/runner.js +46 -31
- package/dist/runtime/src/task/completion.js +1 -1
- package/docs/en/guides/configuration.md +30 -27
- package/docs/en/spec/review.md +56 -28
- package/docs/zh-TW/guides/configuration.md +27 -24
- package/docs/zh-TW/spec/review.md +51 -22
- package/package.json +1 -1
|
@@ -1,9 +1,156 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
|
+
import { sha256 } from "../fs/hash.js";
|
|
3
4
|
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
5
|
+
import { redactSecrets } from "../security/redact.js";
|
|
6
|
+
import { safeTaskText } from "../task/render.js";
|
|
7
|
+
import { redactReviewReport } from "./runner.js";
|
|
4
8
|
const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/u;
|
|
5
9
|
const TASK_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;
|
|
6
10
|
export const REVIEW_ATTESTATION_DIRECTORY = ".agent-ops/reviews";
|
|
11
|
+
export const REVIEW_REPORT_ARTIFACT_SUFFIX = ".reports.json";
|
|
12
|
+
const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
|
|
13
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
14
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
15
|
+
const REPORT_ARTIFACT_MAX_BYTES = 512 * 1024;
|
|
16
|
+
function isReviewTarget(value) {
|
|
17
|
+
return typeof value === "string" && REVIEW_TARGETS.has(value);
|
|
18
|
+
}
|
|
19
|
+
function safeText(value) {
|
|
20
|
+
return safeTaskText(redactSecrets(value));
|
|
21
|
+
}
|
|
22
|
+
function safeAttempt(attempt) {
|
|
23
|
+
return {
|
|
24
|
+
target: attempt.target,
|
|
25
|
+
status: attempt.status,
|
|
26
|
+
...(attempt.sessionId === undefined ? {} : { sessionId: safeText(attempt.sessionId) }),
|
|
27
|
+
...(attempt.reason === undefined ? {} : { reason: safeText(attempt.reason) }),
|
|
28
|
+
...(attempt.diagnostic === undefined ? {} : { diagnostic: safeText(attempt.diagnostic) })
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function safePreflight(attempt) {
|
|
32
|
+
return {
|
|
33
|
+
target: attempt.target,
|
|
34
|
+
status: attempt.status,
|
|
35
|
+
...(attempt.reason === undefined ? {} : { reason: attempt.reason }),
|
|
36
|
+
...(attempt.diagnostic === undefined ? {} : { diagnostic: safeText(attempt.diagnostic) })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function artifactPath(root, fingerprint) {
|
|
40
|
+
return join(root, ...REVIEW_ATTESTATION_DIRECTORY.split("/"), `${fingerprint}${REVIEW_REPORT_ARTIFACT_SUFFIX}`);
|
|
41
|
+
}
|
|
42
|
+
export function reviewReportDigest(report) {
|
|
43
|
+
return sha256(JSON.stringify(redactReviewReport(report)));
|
|
44
|
+
}
|
|
45
|
+
function reportArtifact(result, sourceFingerprint, taskId) {
|
|
46
|
+
return {
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
sourceFingerprint,
|
|
49
|
+
...(taskId === undefined ? {} : { taskId }),
|
|
50
|
+
status: result.status,
|
|
51
|
+
harness: result.harness,
|
|
52
|
+
plannedTargets: [...(result.plannedTargets ?? [result.harness])],
|
|
53
|
+
...(result.hostTarget === undefined ? {} : { hostTarget: result.hostTarget }),
|
|
54
|
+
...(result.hostRestriction === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { hostRestriction: result.hostRestriction }),
|
|
57
|
+
...(result.independence === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { independence: result.independence }),
|
|
60
|
+
...(result.sessionIsolation === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { sessionIsolation: result.sessionIsolation }),
|
|
63
|
+
attempts: (result.attempts ?? []).map(safeAttempt),
|
|
64
|
+
preflight: (result.preflight ?? []).map(safePreflight),
|
|
65
|
+
...(result.reason === undefined ? {} : { reason: safeText(result.reason) }),
|
|
66
|
+
...(result.report === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { report: redactReviewReport(result.report) }),
|
|
69
|
+
...(result.adversarial === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
adversarial: {
|
|
73
|
+
target: result.adversarial.target,
|
|
74
|
+
refuted: result.adversarial.refuted,
|
|
75
|
+
report: redactReviewReport(result.adversarial.report)
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
createdAt: new Date().toISOString()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function matchingReportArtifact(value, attestation) {
|
|
82
|
+
if (typeof value !== "object" || value === null) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const record = value;
|
|
86
|
+
const attempts = record.attempts;
|
|
87
|
+
const plannedTargets = record.plannedTargets;
|
|
88
|
+
const adversarial = record.adversarial;
|
|
89
|
+
if (record.schemaVersion !== 1 ||
|
|
90
|
+
record.sourceFingerprint !== attestation.sourceFingerprint ||
|
|
91
|
+
record.status !== "PASS" ||
|
|
92
|
+
record.harness !== attestation.harness ||
|
|
93
|
+
record.harness !== attestation.reviewTargets[0] ||
|
|
94
|
+
record.taskId !== attestation.taskId ||
|
|
95
|
+
record.hostTarget !== attestation.hostTarget ||
|
|
96
|
+
!Array.isArray(plannedTargets) ||
|
|
97
|
+
plannedTargets.length !== 2 ||
|
|
98
|
+
plannedTargets[0] !== attestation.reviewTargets[0] ||
|
|
99
|
+
plannedTargets[1] !== attestation.reviewTargets[1] ||
|
|
100
|
+
!Array.isArray(attempts) ||
|
|
101
|
+
attempts.length !== 2 ||
|
|
102
|
+
!attempts.every((attempt, index) => typeof attempt === "object" &&
|
|
103
|
+
attempt !== null &&
|
|
104
|
+
attempt.target === attestation.reviewTargets[index] &&
|
|
105
|
+
attempt.status === "PASS" &&
|
|
106
|
+
typeof attempt.sessionId === "string" &&
|
|
107
|
+
SESSION_ID_PATTERN.test(attempt.sessionId)) ||
|
|
108
|
+
!Array.isArray(record.preflight) ||
|
|
109
|
+
typeof record.report !== "object" ||
|
|
110
|
+
record.report === null ||
|
|
111
|
+
typeof adversarial !== "object" ||
|
|
112
|
+
adversarial === null) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const adversarialRecord = adversarial;
|
|
116
|
+
return (adversarialRecord.target === attestation.reviewTargets[1] &&
|
|
117
|
+
adversarialRecord.refuted === false &&
|
|
118
|
+
typeof adversarialRecord.report === "object" &&
|
|
119
|
+
adversarialRecord.report !== null &&
|
|
120
|
+
reviewReportDigest(record.report) ===
|
|
121
|
+
attestation.primaryReportDigest &&
|
|
122
|
+
reviewReportDigest(adversarialRecord.report) ===
|
|
123
|
+
attestation.adversarialReportDigest &&
|
|
124
|
+
attempts.every((attempt, index) => attempt.sessionId === attestation.reviewSessionIds[index]));
|
|
125
|
+
}
|
|
126
|
+
async function hasMatchingReportArtifact(root, attestation) {
|
|
127
|
+
const source = await readPrivateFile(artifactPath(root, attestation.sourceFingerprint), root);
|
|
128
|
+
if (source === null) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return matchingReportArtifact(JSON.parse(source), attestation);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function saveReviewReportArtifact(root, result, sourceFingerprint, taskId) {
|
|
139
|
+
if (!FINGERPRINT_PATTERN.test(sourceFingerprint)) {
|
|
140
|
+
throw new AgentOpsError("REVIEW_REPORT_ARTIFACT_INVALID", "Review report artifact has an invalid source fingerprint.");
|
|
141
|
+
}
|
|
142
|
+
if (taskId !== undefined && !TASK_ID_PATTERN.test(taskId)) {
|
|
143
|
+
throw new AgentOpsError("REVIEW_REPORT_ARTIFACT_INVALID", "Review report artifact has an invalid task id.");
|
|
144
|
+
}
|
|
145
|
+
const value = reportArtifact(result, sourceFingerprint, taskId);
|
|
146
|
+
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
147
|
+
if (Buffer.byteLength(serialized, "utf8") > REPORT_ARTIFACT_MAX_BYTES) {
|
|
148
|
+
throw new AgentOpsError("REVIEW_REPORT_ARTIFACT_TOO_LARGE", "Review report artifact exceeds the capture limit.");
|
|
149
|
+
}
|
|
150
|
+
const relativePath = `${REVIEW_ATTESTATION_DIRECTORY}/${sourceFingerprint}${REVIEW_REPORT_ARTIFACT_SUFFIX}`;
|
|
151
|
+
await writePrivateFile(artifactPath(root, sourceFingerprint), serialized, root);
|
|
152
|
+
return relativePath;
|
|
153
|
+
}
|
|
7
154
|
function attestationPath(root, fingerprint) {
|
|
8
155
|
return join(root, ...REVIEW_ATTESTATION_DIRECTORY.split("/"), `${fingerprint}.json`);
|
|
9
156
|
}
|
|
@@ -12,25 +159,47 @@ function parseAttestation(value) {
|
|
|
12
159
|
return null;
|
|
13
160
|
}
|
|
14
161
|
const record = value;
|
|
15
|
-
if (record.schemaVersion !==
|
|
162
|
+
if (record.schemaVersion !== 2 ||
|
|
16
163
|
record.status !== "PASS" ||
|
|
17
164
|
(record.taskId !== undefined &&
|
|
18
165
|
(typeof record.taskId !== "string" ||
|
|
19
166
|
!TASK_ID_PATTERN.test(record.taskId))) ||
|
|
20
|
-
|
|
21
|
-
!TASK_ID_PATTERN.test(record.harness) ||
|
|
167
|
+
!isReviewTarget(record.harness) ||
|
|
22
168
|
typeof record.sourceFingerprint !== "string" ||
|
|
23
169
|
!FINGERPRINT_PATTERN.test(record.sourceFingerprint) ||
|
|
170
|
+
!Array.isArray(record.reviewTargets) ||
|
|
171
|
+
record.reviewTargets.length !== 2 ||
|
|
172
|
+
!record.reviewTargets.every(isReviewTarget) ||
|
|
173
|
+
record.reviewTargets[0] !== record.harness ||
|
|
174
|
+
!Array.isArray(record.reviewSessionIds) ||
|
|
175
|
+
record.reviewSessionIds.length !== 2 ||
|
|
176
|
+
!record.reviewSessionIds.every((sessionId) => typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId)) ||
|
|
177
|
+
record.sessionIsolation !== "fresh" ||
|
|
178
|
+
typeof record.primaryReportDigest !== "string" ||
|
|
179
|
+
!DIGEST_PATTERN.test(record.primaryReportDigest) ||
|
|
180
|
+
typeof record.adversarialReportDigest !== "string" ||
|
|
181
|
+
!DIGEST_PATTERN.test(record.adversarialReportDigest) ||
|
|
182
|
+
typeof record.reportArtifact !== "string" ||
|
|
183
|
+
record.reportArtifact !==
|
|
184
|
+
`${REVIEW_ATTESTATION_DIRECTORY}/${record.sourceFingerprint}${REVIEW_REPORT_ARTIFACT_SUFFIX}` ||
|
|
185
|
+
(record.hostTarget !== undefined && !isReviewTarget(record.hostTarget)) ||
|
|
24
186
|
typeof record.createdAt !== "string" ||
|
|
25
187
|
!Number.isFinite(Date.parse(record.createdAt))) {
|
|
26
188
|
return null;
|
|
27
189
|
}
|
|
28
190
|
return {
|
|
29
|
-
schemaVersion:
|
|
191
|
+
schemaVersion: 2,
|
|
30
192
|
...(record.taskId === undefined ? {} : { taskId: record.taskId }),
|
|
31
193
|
harness: record.harness,
|
|
32
194
|
status: "PASS",
|
|
33
195
|
sourceFingerprint: record.sourceFingerprint,
|
|
196
|
+
reviewTargets: [record.reviewTargets[0], record.reviewTargets[1]],
|
|
197
|
+
reviewSessionIds: [record.reviewSessionIds[0], record.reviewSessionIds[1]],
|
|
198
|
+
sessionIsolation: "fresh",
|
|
199
|
+
primaryReportDigest: record.primaryReportDigest,
|
|
200
|
+
adversarialReportDigest: record.adversarialReportDigest,
|
|
201
|
+
reportArtifact: record.reportArtifact,
|
|
202
|
+
...(record.hostTarget === undefined ? {} : { hostTarget: record.hostTarget }),
|
|
34
203
|
createdAt: record.createdAt
|
|
35
204
|
};
|
|
36
205
|
}
|
|
@@ -39,6 +208,9 @@ export async function saveReviewAttestation(root, attestation) {
|
|
|
39
208
|
if (validated === null) {
|
|
40
209
|
throw new AgentOpsError("REVIEW_ATTESTATION_INVALID", "Review attestation is invalid.");
|
|
41
210
|
}
|
|
211
|
+
if (!(await hasMatchingReportArtifact(root, validated))) {
|
|
212
|
+
throw new AgentOpsError("REVIEW_ATTESTATION_INVALID", "Review attestation has no matching report artifact.");
|
|
213
|
+
}
|
|
42
214
|
const relativePath = `${REVIEW_ATTESTATION_DIRECTORY}/${validated.sourceFingerprint}.json`;
|
|
43
215
|
await writePrivateFile(attestationPath(root, validated.sourceFingerprint), `${JSON.stringify(validated, null, 2)}\n`, root);
|
|
44
216
|
return relativePath;
|
|
@@ -58,7 +230,10 @@ export async function findReviewAttestation(root, sourceFingerprint) {
|
|
|
58
230
|
}
|
|
59
231
|
try {
|
|
60
232
|
const attestation = parseAttestation(JSON.parse(source));
|
|
61
|
-
return attestation?.sourceFingerprint === sourceFingerprint
|
|
233
|
+
return attestation?.sourceFingerprint === sourceFingerprint &&
|
|
234
|
+
await hasMatchingReportArtifact(root, attestation)
|
|
235
|
+
? attestation
|
|
236
|
+
: null;
|
|
62
237
|
}
|
|
63
238
|
catch {
|
|
64
239
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { chmod, copyFile, lstat, mkdir, mkdtemp, realpath, rm, stat } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -6,9 +7,10 @@ import { redactSecrets } from "../security/redact.js";
|
|
|
6
7
|
import { extractReviewObject } from "./extract.js";
|
|
7
8
|
import { buildTargetInvocation } from "./invocation.js";
|
|
8
9
|
import { reviewReportResults, reviewReportStatus, validateReviewReport } from "./report.js";
|
|
9
|
-
import { detectHostTarget,
|
|
10
|
-
import {
|
|
10
|
+
import { detectHostTarget, planReviewTargets } from "./roles.js";
|
|
11
|
+
import { detectHostRestriction } from "./host-sandbox.js";
|
|
11
12
|
import { buildAdversarialPrompt, buildReviewPrompt } from "./runner.js";
|
|
13
|
+
import { MAX_ADVERSARIAL_PROMPT_BYTES } from "./runner.js";
|
|
12
14
|
/**
|
|
13
15
|
* Full repository reviews need far more headroom than the lightweight auth
|
|
14
16
|
* probe. Five minutes was not enough: reviewing a real working tree, codex and
|
|
@@ -102,6 +104,20 @@ const ADVANCING = new Set([
|
|
|
102
104
|
"spawn-failed"
|
|
103
105
|
]);
|
|
104
106
|
const DIAGNOSTIC_MAX_CHARS = 200;
|
|
107
|
+
function preflightUnavailableReason(result) {
|
|
108
|
+
switch (result) {
|
|
109
|
+
case "missing-executable":
|
|
110
|
+
return "missing-cli";
|
|
111
|
+
case "timeout":
|
|
112
|
+
return "timeout";
|
|
113
|
+
case "unauthenticated":
|
|
114
|
+
return "login-required";
|
|
115
|
+
case "ineligible":
|
|
116
|
+
return "capability-unavailable";
|
|
117
|
+
case "capability-unavailable":
|
|
118
|
+
return "capability-unavailable";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* The target's first line of complaint, redacted and clipped. It travels on the
|
|
107
123
|
* attempt record rather than only in a progress line, because progress is
|
|
@@ -237,8 +253,10 @@ async function snapshotRepository(request, destination, options) {
|
|
|
237
253
|
*/
|
|
238
254
|
async function attemptTarget(request, options) {
|
|
239
255
|
const { target } = request;
|
|
240
|
-
const
|
|
256
|
+
const sessionId = randomUUID();
|
|
257
|
+
const skip = (reason, diagnostic, verb = "stopping review") => ({
|
|
241
258
|
kind: "skip",
|
|
259
|
+
sessionId,
|
|
242
260
|
reason,
|
|
243
261
|
diagnostic,
|
|
244
262
|
message: `${target}: ${reason} → ${verb} (${diagnostic})`
|
|
@@ -424,52 +442,66 @@ async function attemptTarget(request, options) {
|
|
|
424
442
|
: firstComplaint(spawned.stdout, spawned.stderr) ??
|
|
425
443
|
"the answer carried no review report");
|
|
426
444
|
}
|
|
427
|
-
return { kind: "verdict", report: parsed.value };
|
|
445
|
+
return { kind: "verdict", report: parsed.value, sessionId };
|
|
428
446
|
}
|
|
429
447
|
finally {
|
|
430
448
|
await rm(attemptDirectory, { recursive: true, force: true });
|
|
431
449
|
}
|
|
432
450
|
}
|
|
433
|
-
/**
|
|
434
|
-
* Builds the `execute` callback `runIndependentReview` expects: walk the
|
|
435
|
-
* configured targets in order and return the first real verdict. A PASS is then
|
|
436
|
-
* handed to a different target to refute, so a single agreeable reviewer cannot
|
|
437
|
-
* wave a change through on its own.
|
|
438
|
-
*/
|
|
451
|
+
/** Builds the `execute` callback with one necessary and one adversarial review. */
|
|
439
452
|
export function createReviewExecutor(options) {
|
|
440
453
|
const report = options.onProgress ?? (() => { });
|
|
441
454
|
const host = detectHostTarget(options.env ?? process.env);
|
|
442
|
-
const
|
|
455
|
+
const plan = planReviewTargets(options.targets, host);
|
|
456
|
+
const plannedTargets = plan.targets;
|
|
443
457
|
return async (request) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
458
|
+
if (plan.reason !== undefined) {
|
|
459
|
+
report("host: three review targets require explicit AGENT_OPS_HOST");
|
|
460
|
+
return {
|
|
461
|
+
status: "NOT_RUN",
|
|
462
|
+
reason: plan.reason,
|
|
463
|
+
attempts: []
|
|
464
|
+
};
|
|
465
|
+
}
|
|
447
466
|
const restriction = await detectHostRestriction({
|
|
448
467
|
env: options.env ?? process.env,
|
|
449
468
|
...(options.probeBind === undefined ? {} : { probeBind: options.probeBind })
|
|
450
469
|
});
|
|
451
|
-
if (restriction
|
|
452
|
-
report(
|
|
453
|
-
|
|
470
|
+
if (restriction !== "none") {
|
|
471
|
+
report(`host: ${restriction}; external review host runner required`);
|
|
472
|
+
return {
|
|
473
|
+
status: "NOT_RUN",
|
|
474
|
+
reason: "host-required",
|
|
475
|
+
hostRestriction: restriction,
|
|
476
|
+
attempts: []
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (plannedTargets.length === 0) {
|
|
454
480
|
return {
|
|
455
481
|
status: "NOT_RUN",
|
|
456
|
-
reason: "
|
|
457
|
-
...(host === undefined ? {} : { harness: host }),
|
|
482
|
+
reason: "missing-cli",
|
|
458
483
|
attempts: []
|
|
459
484
|
};
|
|
460
485
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
486
|
+
const preflight = [];
|
|
487
|
+
if (options.preflightTarget !== undefined) {
|
|
488
|
+
for (const target of new Set(plannedTargets)) {
|
|
489
|
+
const result = await options.preflightTarget(target);
|
|
490
|
+
if (result === "ok") {
|
|
491
|
+
preflight.push({ target, status: "PASS" });
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const reason = preflightUnavailableReason(result);
|
|
495
|
+
const diagnostic = `target preflight returned ${result}`;
|
|
496
|
+
preflight.push({ target, status: "NOT_RUN", reason, diagnostic });
|
|
497
|
+
report(`${target}: preflight ${reason} (${diagnostic})`);
|
|
498
|
+
return {
|
|
499
|
+
status: "NOT_RUN",
|
|
500
|
+
reason,
|
|
501
|
+
preflight,
|
|
502
|
+
attempts: []
|
|
503
|
+
};
|
|
504
|
+
}
|
|
473
505
|
}
|
|
474
506
|
const expectedCriterionIds = request.invocation.packet.criteria.map((criterion) => criterion.id);
|
|
475
507
|
const repositoryRoot = await realpath(options.cwd);
|
|
@@ -481,98 +513,136 @@ export function createReviewExecutor(options) {
|
|
|
481
513
|
: { changedFiles: request.invocation.scope.changedFiles })
|
|
482
514
|
};
|
|
483
515
|
const attempts = [];
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
516
|
+
const primaryTarget = plannedTargets[0];
|
|
517
|
+
const adversarialTarget = plannedTargets[1];
|
|
518
|
+
const primaryOutcome = await attemptTarget({
|
|
519
|
+
...shared,
|
|
520
|
+
target: primaryTarget,
|
|
521
|
+
label: `${primaryTarget}-primary`,
|
|
522
|
+
prompt: buildReviewPrompt({ ...request.invocation, harness: primaryTarget })
|
|
523
|
+
}, options);
|
|
524
|
+
if (primaryOutcome.kind === "skip") {
|
|
525
|
+
attempts.push({
|
|
526
|
+
target: primaryTarget,
|
|
527
|
+
status: "NOT_RUN",
|
|
528
|
+
sessionId: primaryOutcome.sessionId,
|
|
529
|
+
reason: primaryOutcome.attemptReason ?? primaryOutcome.reason,
|
|
530
|
+
diagnostic: primaryOutcome.diagnostic
|
|
531
|
+
});
|
|
532
|
+
report(primaryOutcome.message);
|
|
533
|
+
return {
|
|
534
|
+
status: "NOT_RUN",
|
|
535
|
+
reason: primaryOutcome.reason,
|
|
536
|
+
preflight,
|
|
537
|
+
attempts
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const primaryReport = primaryOutcome.report;
|
|
541
|
+
const primaryStatus = reviewReportStatus(primaryReport);
|
|
542
|
+
attempts.push({
|
|
543
|
+
target: primaryTarget,
|
|
544
|
+
status: primaryStatus,
|
|
545
|
+
sessionId: primaryOutcome.sessionId
|
|
546
|
+
});
|
|
547
|
+
const independence = primaryTarget === adversarialTarget
|
|
548
|
+
? "same-target"
|
|
549
|
+
: "different-target";
|
|
550
|
+
const verdict = {
|
|
551
|
+
results: reviewReportResults(primaryReport),
|
|
552
|
+
report: primaryReport,
|
|
553
|
+
harness: primaryTarget,
|
|
554
|
+
attempts,
|
|
555
|
+
preflight,
|
|
556
|
+
independence,
|
|
557
|
+
sessionIsolation: "fresh"
|
|
523
558
|
};
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
559
|
+
if (primaryStatus === "FAIL") {
|
|
560
|
+
return { status: "FAIL", ...verdict };
|
|
561
|
+
}
|
|
562
|
+
if (request.invocation.sourceFingerprint !== undefined &&
|
|
563
|
+
options.verifySourceFingerprint !== undefined) {
|
|
564
|
+
let unchanged = false;
|
|
565
|
+
try {
|
|
566
|
+
unchanged = await options.verifySourceFingerprint(request.invocation.sourceFingerprint);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
unchanged = false;
|
|
527
570
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
label: `${target}-${index}`,
|
|
532
|
-
prompt: buildReviewPrompt({ ...request.invocation, harness: target })
|
|
533
|
-
}, options);
|
|
534
|
-
if (outcome.kind === "skip") {
|
|
535
|
-
lastReason = outcome.reason;
|
|
536
|
-
attempts.push({
|
|
537
|
-
target,
|
|
571
|
+
if (!unchanged) {
|
|
572
|
+
report("source changed after the necessary reviewer PASS; stopping review");
|
|
573
|
+
return {
|
|
538
574
|
status: "NOT_RUN",
|
|
539
|
-
reason:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
575
|
+
reason: "source-changed-during-review",
|
|
576
|
+
report: primaryReport,
|
|
577
|
+
preflight,
|
|
578
|
+
independence,
|
|
579
|
+
sessionIsolation: "fresh",
|
|
580
|
+
attempts
|
|
581
|
+
};
|
|
544
582
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
583
|
+
}
|
|
584
|
+
const adversarialPrompt = buildAdversarialPrompt({ ...request.invocation, harness: adversarialTarget }, primaryReport);
|
|
585
|
+
if (Buffer.byteLength(adversarialPrompt, "utf8") > MAX_ADVERSARIAL_PROMPT_BYTES) {
|
|
586
|
+
const diagnostic = `the redacted primary report exceeds ${MAX_ADVERSARIAL_PROMPT_BYTES} bytes`;
|
|
587
|
+
attempts.push({
|
|
588
|
+
target: adversarialTarget,
|
|
589
|
+
status: "NOT_RUN",
|
|
590
|
+
reason: "output-too-large",
|
|
591
|
+
diagnostic
|
|
592
|
+
});
|
|
593
|
+
report(`${adversarialTarget}: output-too-large → stopping review (${diagnostic})`);
|
|
594
|
+
return {
|
|
595
|
+
status: "NOT_RUN",
|
|
596
|
+
reason: "output-too-large",
|
|
597
|
+
report: primaryReport,
|
|
598
|
+
preflight,
|
|
558
599
|
independence,
|
|
559
|
-
sessionIsolation: "fresh"
|
|
600
|
+
sessionIsolation: "fresh",
|
|
601
|
+
attempts
|
|
560
602
|
};
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
603
|
+
}
|
|
604
|
+
const adversarialOutcome = await attemptTarget({
|
|
605
|
+
...shared,
|
|
606
|
+
target: adversarialTarget,
|
|
607
|
+
label: `${adversarialTarget}-adversarial`,
|
|
608
|
+
prompt: adversarialPrompt
|
|
609
|
+
}, options);
|
|
610
|
+
if (adversarialOutcome.kind === "skip") {
|
|
611
|
+
attempts.push({
|
|
612
|
+
target: adversarialTarget,
|
|
613
|
+
status: "NOT_RUN",
|
|
614
|
+
sessionId: adversarialOutcome.sessionId,
|
|
615
|
+
reason: adversarialOutcome.attemptReason ?? adversarialOutcome.reason,
|
|
616
|
+
diagnostic: adversarialOutcome.diagnostic
|
|
617
|
+
});
|
|
618
|
+
report(`${adversarialTarget}: adversarial re-check unavailable (${adversarialOutcome.reason})`);
|
|
565
619
|
return {
|
|
566
|
-
status:
|
|
567
|
-
|
|
568
|
-
|
|
620
|
+
status: "NOT_RUN",
|
|
621
|
+
reason: adversarialOutcome.reason,
|
|
622
|
+
report: primaryReport,
|
|
623
|
+
preflight,
|
|
624
|
+
independence,
|
|
625
|
+
sessionIsolation: "fresh",
|
|
626
|
+
attempts
|
|
569
627
|
};
|
|
570
628
|
}
|
|
629
|
+
const adversarialReport = adversarialOutcome.report;
|
|
630
|
+
const refuted = reviewReportStatus(adversarialReport) === "FAIL";
|
|
631
|
+
attempts.push({
|
|
632
|
+
target: adversarialTarget,
|
|
633
|
+
status: refuted ? "FAIL" : "PASS",
|
|
634
|
+
sessionId: adversarialOutcome.sessionId
|
|
635
|
+
});
|
|
636
|
+
report(`${adversarialTarget}: adversarial re-check ${refuted ? "refuted the PASS" : "upheld the PASS"}`);
|
|
571
637
|
return {
|
|
572
|
-
status: "
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
638
|
+
status: refuted ? "FAIL" : "PASS",
|
|
639
|
+
...verdict,
|
|
640
|
+
attempts,
|
|
641
|
+
adversarial: {
|
|
642
|
+
target: adversarialTarget,
|
|
643
|
+
refuted,
|
|
644
|
+
report: adversarialReport
|
|
645
|
+
}
|
|
576
646
|
};
|
|
577
647
|
};
|
|
578
648
|
}
|
|
@@ -39,8 +39,9 @@ export async function probeLoopbackBind() {
|
|
|
39
39
|
*/
|
|
40
40
|
export async function detectHostRestriction(options = {}) {
|
|
41
41
|
const env = options.env ?? process.env;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// Keep the host declaration distinct from the narrower loopback probe. The
|
|
43
|
+
// executor treats this signal as advisory because a target may use a
|
|
44
|
+
// transport that differs from this process.
|
|
44
45
|
if (env.CODEX_SANDBOX_NETWORK_DISABLED === "1") {
|
|
45
46
|
return "network-blocked";
|
|
46
47
|
}
|
|
@@ -59,6 +59,9 @@ export async function probeReviewTarget(target, options) {
|
|
|
59
59
|
if (spawned.timedOut) {
|
|
60
60
|
return "timeout";
|
|
61
61
|
}
|
|
62
|
+
if (/(?:operation not permitted|permission denied|bind(?:ing)?[^\n]*(?:failed|denied))/iu.test(`${spawned.stderr}\n${spawned.stdout}`)) {
|
|
63
|
+
return "capability-unavailable";
|
|
64
|
+
}
|
|
62
65
|
if (!deep) {
|
|
63
66
|
return spawned.status === "PASS" ? "ok" : "unauthenticated";
|
|
64
67
|
}
|