@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
package/README.md
CHANGED
|
@@ -301,12 +301,12 @@ the project configuration defines those workflows.
|
|
|
301
301
|
|
|
302
302
|
### External review
|
|
303
303
|
|
|
304
|
-
`agent-ops review` can hand
|
|
305
|
-
judged by the agent that produced it.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
is
|
|
304
|
+
`agent-ops review --task <id> --yes` can hand a complete task-bound review to
|
|
305
|
+
agent CLIs, so the work is not judged only by the agent that produced it.
|
|
306
|
+
Enable review targets during `agent-ops init` (the default is off). Every task
|
|
307
|
+
criterion and its original description are reviewed; partial criteria,
|
|
308
|
+
generic reviews, and missing `--yes` are rejected. Current verification
|
|
309
|
+
evidence is required before any reviewer starts.
|
|
310
310
|
|
|
311
311
|
Each attempt uses a fresh temporary session and disposable repository clone, a
|
|
312
312
|
small allowlisted environment, and a target-native read-only mode. Claude
|
|
@@ -315,14 +315,28 @@ persistence, while Agy runs in sandboxed plan mode. Codex and Agy preserve their
|
|
|
315
315
|
existing login environment, so their context isolation is intentionally weaker
|
|
316
316
|
than Claude's. `opencode` is not a review target.
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
and
|
|
325
|
-
|
|
318
|
+
Exactly two fresh sessions are planned. With three configured targets, the
|
|
319
|
+
explicit `AGENT_OPS_HOST` is excluded and `agy` is primary when available; with
|
|
320
|
+
two targets both run in configured order; with one target the same CLI is run
|
|
321
|
+
twice. The first session is the necessary review. Only its PASS starts the
|
|
322
|
+
second adversarial session, which receives the first full redacted report as
|
|
323
|
+
untrusted data. The first FAIL or NOT_RUN stops; a second FAIL is final FAIL,
|
|
324
|
+
and a missing second verdict is NOT_RUN. A same-target pair is still
|
|
325
|
+
independent because the sessions and clones are fresh.
|
|
326
|
+
|
|
327
|
+
Host network or loopback restrictions return `REVIEW_NOT_RUN` before any
|
|
328
|
+
reviewer is started. The managed instructions require a trusted outer host
|
|
329
|
+
runner to start the exact same command with both capabilities on its first
|
|
330
|
+
invocation; an in-sandbox retry is not an elevation. The result remains
|
|
331
|
+
`NOT_RUN` if that host is unavailable. There is no repository-level permission
|
|
332
|
+
bypass.
|
|
333
|
+
Reviewer attempts and preflight diagnostics remain visible in JSON. `--yes` is
|
|
334
|
+
required because a complete run spends provider quota twice.
|
|
335
|
+
|
|
336
|
+
After a complete PASS, the full redacted primary and adversarial reports are
|
|
337
|
+
stored privately under `.agent-ops/reviews/`; the source-fingerprint attestation
|
|
338
|
+
stores only the task, reviewer/session identities, report digests, and artifact
|
|
339
|
+
reference. Any artifact or attestation write failure is NOT_RUN, never PASS.
|
|
326
340
|
|
|
327
341
|
Authentication is diagnosed, never guessed:
|
|
328
342
|
|
|
@@ -74,6 +74,7 @@ export function parseArgs(argv) {
|
|
|
74
74
|
const criteria = [];
|
|
75
75
|
const evidence = [];
|
|
76
76
|
let checkAuth = false;
|
|
77
|
+
const checkAuthTargets = [];
|
|
77
78
|
let completionGate;
|
|
78
79
|
let dryRun = false;
|
|
79
80
|
let json = false;
|
|
@@ -205,6 +206,18 @@ export function parseArgs(argv) {
|
|
|
205
206
|
}
|
|
206
207
|
checkAuth = true;
|
|
207
208
|
break;
|
|
209
|
+
case "--check-auth-target": {
|
|
210
|
+
const value = readOptionValue(argv, index, token);
|
|
211
|
+
if (!REVIEW_TARGETS.has(value)) {
|
|
212
|
+
invalidValue(token, value);
|
|
213
|
+
}
|
|
214
|
+
if (checkAuthTargets.includes(value)) {
|
|
215
|
+
duplicate(`${token} ${value}`);
|
|
216
|
+
}
|
|
217
|
+
checkAuthTargets.push(value);
|
|
218
|
+
index += 1;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
208
221
|
case "--completion-gate":
|
|
209
222
|
if (completionGate !== undefined) {
|
|
210
223
|
duplicate(token);
|
|
@@ -322,6 +335,7 @@ export function parseArgs(argv) {
|
|
|
322
335
|
sessionId !== undefined ||
|
|
323
336
|
base !== undefined ||
|
|
324
337
|
checkAuth ||
|
|
338
|
+
checkAuthTargets.length > 0 ||
|
|
325
339
|
dryRun ||
|
|
326
340
|
yes) {
|
|
327
341
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
@@ -367,9 +381,15 @@ export function parseArgs(argv) {
|
|
|
367
381
|
if (checkAuth && command !== "doctor") {
|
|
368
382
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth may be used only with doctor.");
|
|
369
383
|
}
|
|
384
|
+
if (checkAuthTargets.length > 0 && command !== "doctor") {
|
|
385
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth-target may be used only with doctor.");
|
|
386
|
+
}
|
|
387
|
+
if (checkAuthTargets.length > 0 && !checkAuth) {
|
|
388
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth-target requires --check-auth.");
|
|
389
|
+
}
|
|
370
390
|
if (reviewTargets.length > 0 && command !== "init") {
|
|
371
391
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", command === "review"
|
|
372
|
-
? "--review-target configures init;
|
|
392
|
+
? "--review-target configures init; review uses the configured reviewer pair."
|
|
373
393
|
: "--review-target may be used only with init.");
|
|
374
394
|
}
|
|
375
395
|
if (completionGate !== undefined && command !== "init") {
|
|
@@ -425,18 +445,20 @@ export function parseArgs(argv) {
|
|
|
425
445
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Verify accepts only scope, task or session, base, and json options.");
|
|
426
446
|
}
|
|
427
447
|
if (command === "review" &&
|
|
428
|
-
(title !== undefined || sessionId !== undefined)) {
|
|
429
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review accepts
|
|
448
|
+
(title !== undefined || sessionId !== undefined || dryRun)) {
|
|
449
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review accepts task, base, json, and yes options.");
|
|
430
450
|
}
|
|
431
|
-
if (command === "review" &&
|
|
432
|
-
|
|
433
|
-
(criteria.length > 0 || evidence.length > 0)) {
|
|
434
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review criteria and evidence require --task.");
|
|
451
|
+
if (command === "review" && taskId === undefined) {
|
|
452
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review requires --task for the complete task-bound review.");
|
|
435
453
|
}
|
|
436
|
-
if (command === "review" &&
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
454
|
+
if (command === "review" && !yes) {
|
|
455
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review requires --yes to authorize both reviewer sessions.");
|
|
456
|
+
}
|
|
457
|
+
if (command === "review" && harness !== undefined) {
|
|
458
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Complete review uses the configured reviewer pair; omit --harness.");
|
|
459
|
+
}
|
|
460
|
+
if (command === "review" && (criteria.length > 0 || evidence.length > 0)) {
|
|
461
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Complete review covers every task criterion; omit --criterion and --evidence.");
|
|
440
462
|
}
|
|
441
463
|
if (command === "allow-stop" &&
|
|
442
464
|
(sessionId === undefined ||
|
|
@@ -469,6 +491,7 @@ export function parseArgs(argv) {
|
|
|
469
491
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
470
492
|
...(base === undefined ? {} : { base }),
|
|
471
493
|
...(checkAuth ? { checkAuth } : {}),
|
|
494
|
+
...(checkAuthTargets.length === 0 ? {} : { checkAuthTargets }),
|
|
472
495
|
dryRun,
|
|
473
496
|
json,
|
|
474
497
|
yes
|
|
@@ -15,6 +15,7 @@ import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
|
15
15
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
16
16
|
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
17
17
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
18
|
+
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
18
19
|
import { VerificationService } from "../../../runtime/src/verify/service.js";
|
|
19
20
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
20
21
|
import { runCli } from "./cli.js";
|
|
@@ -31,7 +32,8 @@ import { runTaskCommand } from "./commands/task.js";
|
|
|
31
32
|
import { runReviewCommand } from "./commands/review.js";
|
|
32
33
|
import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
|
|
33
34
|
import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
|
|
34
|
-
import {
|
|
35
|
+
import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
|
|
36
|
+
import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
|
|
35
37
|
import { runTrustCommand } from "./commands/trust.js";
|
|
36
38
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
37
39
|
import { runAllowStopCommand } from "./commands/allow-stop.js";
|
|
@@ -308,7 +310,10 @@ else {
|
|
|
308
310
|
},
|
|
309
311
|
...(args.checkAuth === true
|
|
310
312
|
? { checkReviewTargetAuth: true }
|
|
311
|
-
: {})
|
|
313
|
+
: {}),
|
|
314
|
+
...(args.checkAuthTargets === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { checkAuthTargets: args.checkAuthTargets })
|
|
312
317
|
});
|
|
313
318
|
}
|
|
314
319
|
if (args.command === "uninstall") {
|
|
@@ -386,12 +391,10 @@ else {
|
|
|
386
391
|
}
|
|
387
392
|
if (args.command === "review") {
|
|
388
393
|
const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
394
|
+
const reviewGit = gitRunner(root);
|
|
389
395
|
const reviewConfig = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
390
396
|
const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
|
|
391
|
-
const
|
|
392
|
-
const plannedReviewTargets = orderChain(selectedReviewTarget === undefined
|
|
393
|
-
? reviewRole?.targets ?? []
|
|
394
|
-
: [selectedReviewTarget], detectHostTarget(process.env));
|
|
397
|
+
const configuredReviewTargets = reviewRole?.targets ?? [];
|
|
395
398
|
const controller = new AbortController();
|
|
396
399
|
let interruptedBy;
|
|
397
400
|
const interrupt = (signal) => {
|
|
@@ -414,15 +417,15 @@ else {
|
|
|
414
417
|
...(reviewConfig.reviewRoles === undefined
|
|
415
418
|
? {}
|
|
416
419
|
: { roles: reviewConfig.reviewRoles }),
|
|
417
|
-
targets:
|
|
420
|
+
targets: configuredReviewTargets,
|
|
418
421
|
root,
|
|
419
|
-
gitRunner:
|
|
422
|
+
gitRunner: reviewGit,
|
|
420
423
|
policyConfigHash: calculateConfigHash(reviewConfig),
|
|
421
424
|
currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
|
|
422
425
|
config: reviewConfig,
|
|
423
426
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
424
427
|
execute: createReviewExecutor({
|
|
425
|
-
targets:
|
|
428
|
+
targets: configuredReviewTargets,
|
|
426
429
|
cwd: root,
|
|
427
430
|
...(reviewRole?.model === undefined
|
|
428
431
|
? {}
|
|
@@ -433,6 +436,15 @@ else {
|
|
|
433
436
|
...(reviewRole?.timeoutMs === undefined
|
|
434
437
|
? {}
|
|
435
438
|
: { timeoutMs: reviewRole.timeoutMs }),
|
|
439
|
+
preflightTarget: async (target) => await probeReviewTarget(target, { cwd: root, deep: true }),
|
|
440
|
+
verifySourceFingerprint: async (expected) => {
|
|
441
|
+
const currentScope = await resolveReviewScope({
|
|
442
|
+
root,
|
|
443
|
+
runner: reviewGit,
|
|
444
|
+
...(args.base === undefined ? {} : { base: args.base })
|
|
445
|
+
});
|
|
446
|
+
return await calculateSourceFingerprint(root, currentScope, reviewGit) === expected;
|
|
447
|
+
},
|
|
436
448
|
signal: controller.signal,
|
|
437
449
|
onProgress: (line) => {
|
|
438
450
|
process.stderr.write(`${line}\n`);
|
|
@@ -32,14 +32,14 @@ Commands:
|
|
|
32
32
|
|
|
33
33
|
Options:
|
|
34
34
|
--scope <project|user>
|
|
35
|
-
--harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall; review
|
|
35
|
+
--harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall; review uses configured targets
|
|
36
36
|
--hook-target <harness=surface-id> Repeatable advanced init/update option
|
|
37
37
|
--profile <core|advisory|guardrails|loop> Repeatable
|
|
38
|
-
--review-target <codex|agy|claude> Repeatable init option;
|
|
39
|
-
targets in fallback-chain order
|
|
38
|
+
--review-target <codex|agy|claude> Repeatable init option; review pair order
|
|
40
39
|
--completion-gate Init only: enable the project-loop completion gate
|
|
41
|
-
--check-auth Doctor only: probe
|
|
40
|
+
--check-auth Doctor only: probe selected review targets'
|
|
42
41
|
authentication with one real call
|
|
42
|
+
--check-auth-target <target> Repeatable doctor filter with --check-auth
|
|
43
43
|
--task <id>
|
|
44
44
|
--parent <task-id> Task create: record a subtask of this
|
|
45
45
|
task; task status: list its subtasks
|
|
@@ -159,18 +159,16 @@ Options:
|
|
|
159
159
|
--base <git-ref> Verify a clean committed range
|
|
160
160
|
--json
|
|
161
161
|
`,
|
|
162
|
-
review: `Usage: agent-ops review [options]
|
|
162
|
+
review: `Usage: agent-ops review --task <id> --yes [options]
|
|
163
163
|
|
|
164
|
-
Run
|
|
164
|
+
Run the complete task-bound review: one necessary reviewer followed by one
|
|
165
|
+
fresh adversarial reviewer from the configured pair.
|
|
165
166
|
|
|
166
167
|
Options:
|
|
167
|
-
--task <id>
|
|
168
|
-
--
|
|
169
|
-
--criterion <id> Repeatable: review only these task criteria
|
|
170
|
-
--harness <target> One configured review target
|
|
171
|
-
--base <git-ref>
|
|
168
|
+
--task <id> Required task whose original criteria are reviewed
|
|
169
|
+
--base <git-ref> Review a clean committed range
|
|
172
170
|
--json
|
|
173
|
-
--yes
|
|
171
|
+
--yes Required authorization for both reviewer sessions
|
|
174
172
|
`,
|
|
175
173
|
"allow-stop": `Usage: agent-ops allow-stop --session <id> [options]
|
|
176
174
|
|
|
@@ -234,7 +232,11 @@ export async function runCli(argv, io, services) {
|
|
|
234
232
|
return writeAndReturn(io, errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`), args.json, 1);
|
|
235
233
|
}
|
|
236
234
|
const result = await execute(args);
|
|
237
|
-
return writeAndReturn(io, result, args.json, result.status === "ok"
|
|
235
|
+
return writeAndReturn(io, result, args.json, result.status === "ok"
|
|
236
|
+
? 0
|
|
237
|
+
: result.code === "REVIEW_NOT_RUN"
|
|
238
|
+
? 2
|
|
239
|
+
: 1);
|
|
238
240
|
}
|
|
239
241
|
catch (error) {
|
|
240
242
|
if (error instanceof CliArgumentError) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildReviewPacket } from "../../../../runtime/src/review/packet.js";
|
|
2
2
|
import { runIndependentReview } from "../../../../runtime/src/review/runner.js";
|
|
3
3
|
import { renderReviewResult } from "../../../../runtime/src/review/render.js";
|
|
4
|
-
import { invalidateReviewAttestation, saveReviewAttestation } from "../../../../runtime/src/review/attestation.js";
|
|
5
|
-
import { resolveReviewRole } from "../../../../runtime/src/review/roles.js";
|
|
4
|
+
import { invalidateReviewAttestation, reviewReportDigest, saveReviewAttestation, saveReviewReportArtifact } from "../../../../runtime/src/review/attestation.js";
|
|
5
|
+
import { detectHostTarget, planReviewTargets, resolveReviewRole } from "../../../../runtime/src/review/roles.js";
|
|
6
6
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
7
7
|
import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, reviewScopeSignature } from "../../../../runtime/src/review/scope.js";
|
|
8
8
|
import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
|
|
@@ -222,6 +222,12 @@ function sourceChangedResult(result) {
|
|
|
222
222
|
...(result.plannedTargets === undefined
|
|
223
223
|
? {}
|
|
224
224
|
: { plannedTargets: result.plannedTargets }),
|
|
225
|
+
...(result.sourceFingerprint === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { sourceFingerprint: result.sourceFingerprint }),
|
|
228
|
+
...(result.hostTarget === undefined
|
|
229
|
+
? {}
|
|
230
|
+
: { hostTarget: result.hostTarget }),
|
|
225
231
|
...(result.scope === undefined ? {} : { scope: result.scope }),
|
|
226
232
|
...(result.independence === undefined
|
|
227
233
|
? {}
|
|
@@ -230,11 +236,90 @@ function sourceChangedResult(result) {
|
|
|
230
236
|
? {}
|
|
231
237
|
: { sessionIsolation: result.sessionIsolation }),
|
|
232
238
|
...(result.attempts === undefined ? {} : { attempts: result.attempts }),
|
|
239
|
+
...(result.report === undefined ? {} : { report: result.report }),
|
|
240
|
+
...(result.adversarial === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: { adversarial: result.adversarial }),
|
|
233
243
|
...(result.verification === undefined
|
|
234
244
|
? {}
|
|
235
245
|
: { verification: result.verification })
|
|
236
246
|
};
|
|
237
247
|
}
|
|
248
|
+
function hasCompleteReviewEvidence(result) {
|
|
249
|
+
const attempts = result.attempts;
|
|
250
|
+
const plannedTargets = result.plannedTargets;
|
|
251
|
+
return result.status === "PASS" &&
|
|
252
|
+
result.report !== undefined &&
|
|
253
|
+
result.adversarial !== undefined &&
|
|
254
|
+
result.adversarial.refuted === false &&
|
|
255
|
+
result.sessionIsolation === "fresh" &&
|
|
256
|
+
plannedTargets !== undefined &&
|
|
257
|
+
plannedTargets.length === 2 &&
|
|
258
|
+
result.harness === plannedTargets[0] &&
|
|
259
|
+
result.adversarial.target === plannedTargets[1] &&
|
|
260
|
+
attempts !== undefined &&
|
|
261
|
+
attempts.length === 2 &&
|
|
262
|
+
attempts.every((attempt, index) => attempt.target === plannedTargets[index] &&
|
|
263
|
+
attempt.status === "PASS" &&
|
|
264
|
+
attempt.sessionId !== undefined);
|
|
265
|
+
}
|
|
266
|
+
async function persistReviewEvidence(options, result, sourceFingerprint, context) {
|
|
267
|
+
if (options.root === undefined || sourceFingerprint === undefined) {
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
const complete = hasCompleteReviewEvidence(result);
|
|
271
|
+
const artifactResult = result.status === "PASS" && !complete
|
|
272
|
+
? {
|
|
273
|
+
...result,
|
|
274
|
+
status: "NOT_RUN",
|
|
275
|
+
reason: "adversarial-review-missing"
|
|
276
|
+
}
|
|
277
|
+
: result;
|
|
278
|
+
let reportArtifact;
|
|
279
|
+
try {
|
|
280
|
+
reportArtifact = await saveReviewReportArtifact(options.root, artifactResult, sourceFingerprint, context?.taskId);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return {
|
|
284
|
+
...result,
|
|
285
|
+
status: "NOT_RUN",
|
|
286
|
+
reason: "evidence-write-failed"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!complete) {
|
|
290
|
+
return artifactResult;
|
|
291
|
+
}
|
|
292
|
+
if (result.status !== "PASS" || context?.allCriteriaReviewed === false) {
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const attempts = result.attempts;
|
|
297
|
+
const plannedTargets = result.plannedTargets;
|
|
298
|
+
await saveReviewAttestation(options.root, {
|
|
299
|
+
schemaVersion: 2,
|
|
300
|
+
...(context === undefined ? {} : { taskId: context.taskId }),
|
|
301
|
+
harness: result.harness,
|
|
302
|
+
status: "PASS",
|
|
303
|
+
sourceFingerprint,
|
|
304
|
+
reviewTargets: [plannedTargets[0], plannedTargets[1]],
|
|
305
|
+
reviewSessionIds: [attempts[0].sessionId, attempts[1].sessionId],
|
|
306
|
+
sessionIsolation: "fresh",
|
|
307
|
+
primaryReportDigest: reviewReportDigest(result.report),
|
|
308
|
+
adversarialReportDigest: reviewReportDigest(result.adversarial.report),
|
|
309
|
+
reportArtifact,
|
|
310
|
+
...(result.hostTarget === undefined ? {} : { hostTarget: result.hostTarget }),
|
|
311
|
+
createdAt: new Date().toISOString()
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return {
|
|
316
|
+
...result,
|
|
317
|
+
status: "NOT_RUN",
|
|
318
|
+
reason: "evidence-write-failed"
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
238
323
|
export async function runReviewCommand(options) {
|
|
239
324
|
const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
|
|
240
325
|
const selectedHarness = harness(options.args.harness);
|
|
@@ -243,17 +328,30 @@ export async function runReviewCommand(options) {
|
|
|
243
328
|
!configuredTargets.includes(selectedHarness)) {
|
|
244
329
|
throw new AgentOpsError("REVIEW_TARGET_NOT_CONFIGURED", `${selectedHarness} is not enabled for independent-review. Configure it with init --review-target ${selectedHarness}.`);
|
|
245
330
|
}
|
|
246
|
-
const
|
|
331
|
+
const configuredSelection = selectedHarness === undefined
|
|
247
332
|
? options.targets ?? configuredTargets
|
|
248
333
|
: [selectedHarness];
|
|
334
|
+
const targetPlan = planReviewTargets(configuredSelection, detectHostTarget(options.env ?? process.env));
|
|
335
|
+
const hostTarget = detectHostTarget(options.env ?? process.env);
|
|
336
|
+
const plannedTargets = targetPlan.reason === undefined
|
|
337
|
+
? targetPlan.targets
|
|
338
|
+
: configuredSelection;
|
|
249
339
|
const target = plannedTargets[0] ?? selectedHarness ?? "codex";
|
|
250
340
|
const resultBase = {
|
|
251
341
|
harness: target,
|
|
252
342
|
plannedTargets,
|
|
253
343
|
model: role?.model ?? options.model ?? "configured",
|
|
254
344
|
effort: role?.effort ?? options.effort ?? "configured",
|
|
255
|
-
prompt: ""
|
|
345
|
+
prompt: "",
|
|
346
|
+
...(hostTarget === undefined ? {} : { hostTarget })
|
|
256
347
|
};
|
|
348
|
+
if (targetPlan.reason !== undefined) {
|
|
349
|
+
return notRunEnvelope({
|
|
350
|
+
...resultBase,
|
|
351
|
+
status: "NOT_RUN",
|
|
352
|
+
reason: targetPlan.reason
|
|
353
|
+
});
|
|
354
|
+
}
|
|
257
355
|
const context = await taskContext(options);
|
|
258
356
|
const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
|
|
259
357
|
const separator = value.indexOf("=");
|
|
@@ -358,6 +456,8 @@ export async function runReviewCommand(options) {
|
|
|
358
456
|
invocation: {
|
|
359
457
|
harness: target,
|
|
360
458
|
plannedTargets,
|
|
459
|
+
...(sourceFingerprint === undefined ? {} : { sourceFingerprint }),
|
|
460
|
+
...(hostTarget === undefined ? {} : { hostTarget }),
|
|
361
461
|
model: role?.model ?? options.model ?? "configured",
|
|
362
462
|
effort: role?.effort ?? options.effort ?? "configured",
|
|
363
463
|
packet,
|
|
@@ -408,55 +508,46 @@ export async function runReviewCommand(options) {
|
|
|
408
508
|
if (reviewScopeSignature(scope) !== reviewScopeSignature(postflight) ||
|
|
409
509
|
(options.policyConfigHash !== undefined && currentHash !== options.policyConfigHash) ||
|
|
410
510
|
postflightFingerprint !== sourceFingerprint) {
|
|
411
|
-
|
|
511
|
+
const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
|
|
512
|
+
return notRunEnvelope(changed);
|
|
412
513
|
}
|
|
413
514
|
}
|
|
414
515
|
catch {
|
|
415
|
-
|
|
516
|
+
const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
|
|
517
|
+
return notRunEnvelope(changed);
|
|
416
518
|
}
|
|
417
519
|
}
|
|
520
|
+
const persisted = await persistReviewEvidence(options, result, sourceFingerprint, context);
|
|
521
|
+
if (persisted.status === "NOT_RUN") {
|
|
522
|
+
return notRunEnvelope(persisted);
|
|
523
|
+
}
|
|
524
|
+
const finalResult = persisted;
|
|
418
525
|
// Evidence is only appended while the task is active: a completed record
|
|
419
526
|
// must stay exactly as it was verified.
|
|
420
527
|
if (context !== undefined &&
|
|
421
528
|
options.tasks !== undefined &&
|
|
422
|
-
|
|
529
|
+
finalResult.status === "PASS" &&
|
|
423
530
|
context.active &&
|
|
424
|
-
|
|
425
|
-
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(
|
|
531
|
+
finalResult.results !== undefined) {
|
|
532
|
+
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(finalResult.results.map((item) => [
|
|
426
533
|
item.criterionId,
|
|
427
|
-
item.evidence.map((reference) => `review:${
|
|
534
|
+
item.evidence.map((reference) => `review:${finalResult.harness}:${reference}`)
|
|
428
535
|
])));
|
|
429
536
|
}
|
|
430
|
-
|
|
431
|
-
// source fingerprint, so it stops satisfying the gate the moment the tree
|
|
432
|
-
// changes again.
|
|
433
|
-
if (options.root !== undefined &&
|
|
434
|
-
result.status === "PASS" &&
|
|
435
|
-
sourceFingerprint !== undefined &&
|
|
436
|
-
(context === undefined || context.allCriteriaReviewed)) {
|
|
437
|
-
await saveReviewAttestation(options.root, {
|
|
438
|
-
schemaVersion: 1,
|
|
439
|
-
...(context === undefined ? {} : { taskId: context.taskId }),
|
|
440
|
-
harness: result.harness,
|
|
441
|
-
status: "PASS",
|
|
442
|
-
sourceFingerprint,
|
|
443
|
-
createdAt: new Date().toISOString()
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
const message = result.status === "PASS"
|
|
537
|
+
const message = finalResult.status === "PASS"
|
|
447
538
|
? "Independent review passed."
|
|
448
|
-
:
|
|
539
|
+
: finalResult.status === "FAIL"
|
|
449
540
|
? "Independent review failed."
|
|
450
541
|
: "Independent review was not run.";
|
|
451
542
|
const data = {
|
|
452
543
|
message,
|
|
453
|
-
result,
|
|
454
|
-
text: renderReviewResult(
|
|
544
|
+
result: finalResult,
|
|
545
|
+
text: renderReviewResult(finalResult)
|
|
455
546
|
};
|
|
456
|
-
if (
|
|
547
|
+
if (finalResult.status === "PASS") {
|
|
457
548
|
return okEnvelope("REVIEW_RESULT", data);
|
|
458
549
|
}
|
|
459
|
-
const code =
|
|
550
|
+
const code = finalResult.status === "FAIL" ? "REVIEW_FAILED" : "REVIEW_NOT_RUN";
|
|
460
551
|
return {
|
|
461
552
|
code,
|
|
462
553
|
status: "error",
|
|
@@ -40,7 +40,8 @@ export function claudeHookOutput(event, result) {
|
|
|
40
40
|
decision: "block",
|
|
41
41
|
reason: `agent-ops: ${result.code} reported FAIL for ${failed.join(", ")}. ` +
|
|
42
42
|
"Resolve every failing item — an unsatisfied independent-review " +
|
|
43
|
-
"gate is cleared by running `agent-ops review
|
|
43
|
+
"gate is cleared by running `agent-ops review --task <task-id> --yes` " +
|
|
44
|
+
"to a PASS — then " +
|
|
44
45
|
"stop again.",
|
|
45
46
|
evidence: result.evidence
|
|
46
47
|
});
|
|
@@ -4,7 +4,7 @@ import { sha256 } from "../fs/hash.js";
|
|
|
4
4
|
import { parseInstallManifest, PROJECT_MANIFEST_PATH } from "../fs/manifest.js";
|
|
5
5
|
import { resolveContainedPath } from "../fs/paths.js";
|
|
6
6
|
import { validateConfig } from "../schema/validate.js";
|
|
7
|
-
import {
|
|
7
|
+
import { detectHostRestriction } from "../review/host-sandbox.js";
|
|
8
8
|
import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
|
|
9
9
|
import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
|
|
10
10
|
import { harnessDescriptor, managedRules } from "./harness.js";
|
|
@@ -409,14 +409,20 @@ async function checkHostSandbox(detect) {
|
|
|
409
409
|
"target can answer. Any authentication verdict below is unreliable.", undefined, "Run agent-ops outside the sandbox, or grant it escalated execution.");
|
|
410
410
|
}
|
|
411
411
|
if (restriction === "bind-blocked") {
|
|
412
|
-
return check("host-sandbox", "DEGRADED",
|
|
413
|
-
|
|
414
|
-
"unable to answer.", undefined, "Run agent-ops outside the sandbox, or grant it escalated execution.");
|
|
412
|
+
return check("host-sandbox", "DEGRADED", "This process cannot open a loopback listener, so review fails closed " +
|
|
413
|
+
"with host-required before any reviewer starts.", undefined, "Run agent-ops outside the sandbox, or grant it escalated execution.");
|
|
415
414
|
}
|
|
416
415
|
return check("host-sandbox", "PASS", "No host sandbox restriction affects review targets.");
|
|
417
416
|
}
|
|
418
|
-
async function checkReviewTargets(config, probe, checkAuth) {
|
|
419
|
-
const
|
|
417
|
+
async function checkReviewTargets(config, probe, checkAuth, selectedTargets = []) {
|
|
418
|
+
const configuredTargets = config?.reviewRoles?.find((role) => role.role === "independent-review")?.targets ?? [];
|
|
419
|
+
const unconfigured = selectedTargets.filter((target) => !configuredTargets.includes(target));
|
|
420
|
+
if (unconfigured.length > 0) {
|
|
421
|
+
return check("review-targets", "FAIL", `Review target selection is not configured: ${unconfigured.join(", ")}.`, undefined, "Configure the target with agent-ops init --review-target, then retry.");
|
|
422
|
+
}
|
|
423
|
+
const targets = selectedTargets.length > 0
|
|
424
|
+
? selectedTargets
|
|
425
|
+
: configuredTargets;
|
|
420
426
|
if (targets.length === 0) {
|
|
421
427
|
return check("review-targets", "PASS", "External review disabled. Re-run agent-ops init to enable.");
|
|
422
428
|
}
|
|
@@ -445,6 +451,10 @@ async function checkReviewTargets(config, probe, checkAuth) {
|
|
|
445
451
|
failures.push(check("review-targets", "FAIL", `${target} did not answer in time.`, undefined, "Re-run: agent-ops doctor --check-auth"));
|
|
446
452
|
continue;
|
|
447
453
|
}
|
|
454
|
+
if (result === "capability-unavailable") {
|
|
455
|
+
failures.push(check("review-targets", "FAIL", `${target} could not complete its read-only capability probe.`, undefined, "Run agent-ops review on a host with network and loopback permission, then retry."));
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
448
458
|
if (checkAuth && result !== "ok") {
|
|
449
459
|
failures.push(check("review-targets", "FAIL", `${target} is installed but not authenticated, or it rejected the call.`, undefined, `Run: ${target} login`));
|
|
450
460
|
}
|
|
@@ -483,7 +493,7 @@ export async function doctorInstallation(options) {
|
|
|
483
493
|
: []),
|
|
484
494
|
await checkProbe("repository-trust", options.probes?.repositoryTrust),
|
|
485
495
|
await checkProbe("smoke-availability", options.probes?.smokeAvailability),
|
|
486
|
-
await checkReviewTargets(config.config, options.probes?.reviewTarget, options.checkReviewTargetAuth === true),
|
|
496
|
+
await checkReviewTargets(config.config, options.probes?.reviewTarget, options.checkReviewTargetAuth === true, options.checkAuthTargets),
|
|
487
497
|
await checkHostSandbox(options.probes?.hostRestriction),
|
|
488
498
|
checkVerificationCommands(config.config)
|
|
489
499
|
];
|
|
@@ -296,7 +296,7 @@ export function managedRules(descriptor, context) {
|
|
|
296
296
|
""
|
|
297
297
|
];
|
|
298
298
|
if (context.capabilities.includes("rules")) {
|
|
299
|
-
lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion, via", " `agent-ops review --yes` (or the CLI's equivalent invocation). Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", "
|
|
299
|
+
lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain complete independent review before claiming completion, via", " `agent-ops review --task <task-id> --yes` (or the CLI's equivalent invocation).", " Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", " Start this command through the trusted outer host runner as its first", " and only invocation. On Codex, the outer tool request must use", " `sandbox_permissions: \"require_escalated\"` so network and loopback", " are available; in that same request, unset", " `CODEX_SANDBOX_NETWORK_DISABLED` and set `AGENT_OPS_HOST` when three", " targets are configured. The Codex shell form is", " `env -u CODEX_SANDBOX_NETWORK_DISABLED AGENT_OPS_HOST=<current-host> agent-ops review --task <task-id> --yes`.", " Do not run it once in the restricted sandbox and retry; changing the", " variable inside that sandbox is not elevation.", " Review runs exactly two fresh sessions: with three configured targets,", " set `AGENT_OPS_HOST` to the current CLI id so it is excluded; with two", " targets both run in configured order; with one target it runs twice.", " The first session is necessary; a PASS then requires an adversarial", " re-check carrying the first report. A first FAIL or NOT_RUN stops.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
|
|
300
300
|
}
|
|
301
301
|
if (context.capabilities.includes("task")) {
|
|
302
302
|
lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria, verification, and independent review;", "completing one never completes its parent.", "");
|