@kylecheng3146/agent-ops 0.2.2 → 0.2.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.
@@ -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 !== 1 ||
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
- typeof record.harness !== "string" ||
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: 1,
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 ? attestation : null;
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, orderChain } from "./roles.js";
10
- import { BIND_DEPENDENT_TARGETS, detectHostRestriction } from "./host-sandbox.js";
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 skip = (reason, diagnostic, verb = "trying next target") => ({
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 ordered = orderChain(options.targets, host);
455
+ const plan = planReviewTargets(options.targets, host);
456
+ const plannedTargets = plan.targets;
443
457
  return async (request) => {
444
- // Asked before anything is spent. A reviewer inherits this process's
445
- // sandbox, so a restriction found here is a restriction every target in
446
- // the chain would hit — one at a time, minutes apart.
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 === "network-blocked") {
452
- report("host: the sandbox around this process blocks network access, so no " +
453
- "reviewer can answer → not running the chain");
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: "host-sandboxed",
457
- ...(host === undefined ? {} : { harness: host }),
482
+ reason: "missing-cli",
458
483
  attempts: []
459
484
  };
460
485
  }
461
- // Only agy needs a loopback listener, so a bind-blocked host is not a dead
462
- // end — it is a reason to spend the other targets first rather than
463
- // discovering the same failure at the head of the chain every time.
464
- const chain = restriction === "bind-blocked"
465
- ? [
466
- ...ordered.filter((target) => !BIND_DEPENDENT_TARGETS.includes(target)),
467
- ...ordered.filter((target) => BIND_DEPENDENT_TARGETS.includes(target))
468
- ]
469
- : ordered;
470
- if (restriction === "bind-blocked" && chain.join() !== ordered.join()) {
471
- report("host: this process cannot open a loopback listener, so targets that " +
472
- `need one run last (chain: ${chain.join(" → ")})`);
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
- let lastReason = "missing-cli";
485
- /**
486
- * Ask a target other than the one that passed — and other than the host —
487
- * to refute the verdict. Returns undefined when no such target produced a
488
- * report, which leaves the primary PASS standing unchallenged. Targets the
489
- * primary pass already walked past are excluded: a CLI that could not
490
- * answer the review prompt will not answer this one either.
491
- */
492
- const refute = async (primary, primaryTarget) => {
493
- const walked = new Set(attempts.map((attempt) => attempt.target));
494
- const candidates = chain.filter((target) => target !== primaryTarget && target !== host && !walked.has(target));
495
- for (const [index, target] of candidates.entries()) {
496
- const outcome = await attemptTarget({
497
- ...shared,
498
- target,
499
- label: `adversarial-${target}-${index}`,
500
- prompt: buildAdversarialPrompt({ ...request.invocation, harness: target }, primary)
501
- }, options);
502
- if (outcome.kind === "skip") {
503
- // Recorded, not just reported: stderr progress is transient, and
504
- // without this a PASS with no `adversarial` field cannot be told
505
- // apart from a PASS that had no second target to challenge it.
506
- attempts.push({
507
- target,
508
- status: "NOT_RUN",
509
- reason: outcome.attemptReason ?? outcome.reason,
510
- diagnostic: outcome.diagnostic
511
- });
512
- report(`${target}: adversarial re-check unavailable (${outcome.reason})`);
513
- continue;
514
- }
515
- const refuted = reviewReportStatus(outcome.report) === "FAIL";
516
- report(`${target}: adversarial re-check ${refuted ? "refuted the PASS" : "upheld the PASS"}`);
517
- return { target, refuted, report: outcome.report };
518
- }
519
- if (candidates.length > 0) {
520
- report("no independent target completed an adversarial re-check");
521
- }
522
- return undefined;
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
- for (const [index, target] of chain.entries()) {
525
- if (target === host) {
526
- report(`${target}: no other usable CLI remained; running isolated self-review`);
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
- const outcome = await attemptTarget({
529
- ...shared,
530
- target,
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: outcome.attemptReason ?? outcome.reason,
540
- diagnostic: outcome.diagnostic
541
- });
542
- report(outcome.message);
543
- continue;
575
+ reason: "source-changed-during-review",
576
+ report: primaryReport,
577
+ preflight,
578
+ independence,
579
+ sessionIsolation: "fresh",
580
+ attempts
581
+ };
544
582
  }
545
- const reportValue = outcome.report;
546
- const status = reviewReportStatus(reportValue);
547
- attempts.push({ target, status });
548
- const independence = host === undefined
549
- ? "unknown"
550
- : host === target
551
- ? "same-target"
552
- : "different-target";
553
- const verdict = {
554
- results: reviewReportResults(reportValue),
555
- report: reportValue,
556
- harness: target,
557
- attempts,
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
- if (status === "FAIL") {
562
- return { status, ...verdict };
563
- }
564
- const adversarial = await refute(reportValue, target);
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: adversarial?.refuted === true ? "FAIL" : "PASS",
567
- ...verdict,
568
- ...(adversarial === undefined ? {} : { adversarial })
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: "NOT_RUN",
573
- reason: lastReason,
574
- ...(attempts.length === 0 ? {} : { harness: attempts.at(-1)?.target }),
575
- attempts
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
- // Declared and total: no reviewer can reach its own API, so probing the
43
- // narrower loopback capability would only add latency to a settled answer.
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
  }