@kylecheng3146/agent-ops 0.1.14 → 0.1.16
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 +32 -25
- package/dist/packages/cli/src/args.js +5 -0
- package/dist/packages/cli/src/bin.js +28 -27
- package/dist/packages/cli/src/commands/init.js +36 -9
- package/dist/packages/cli/src/commands/review.js +52 -32
- package/dist/packages/cli/src/commands/trust.js +28 -0
- package/dist/packages/cli/src/commands/uninstall.js +36 -9
- package/dist/packages/cli/src/commands/update.js +36 -9
- package/dist/packages/cli/src/context.js +13 -8
- package/dist/packages/cli/src/public-plan.js +8 -6
- package/dist/runtime/src/adapters/claude/config.js +33 -17
- package/dist/runtime/src/adapters/claude/output.js +13 -0
- package/dist/runtime/src/hooks/stop-service.js +45 -6
- package/dist/runtime/src/install/codex-loop.js +33 -3
- package/dist/runtime/src/install/harness.js +2 -2
- package/dist/runtime/src/install/hooks.js +1 -1
- package/dist/runtime/src/install/ownership.js +25 -9
- package/dist/runtime/src/install/plan.js +11 -7
- package/dist/runtime/src/install/probes.js +2 -2
- package/dist/runtime/src/review/attestation.js +65 -0
- package/dist/runtime/src/review/execute.js +66 -39
- package/dist/runtime/src/review/invocation.js +14 -6
- package/dist/runtime/src/review/probe.js +1 -4
- package/dist/runtime/src/review/render.js +7 -0
- package/dist/runtime/src/review/runner.js +14 -3
- package/dist/runtime/src/security/permissions.js +18 -3
- package/docs/en/guides/configuration.md +31 -26
- package/docs/en/spec/README.md +2 -1
- package/docs/en/spec/harness-adapters.md +2 -1
- package/docs/zh-TW/guides/configuration.md +26 -19
- package/docs/zh-TW/spec/README.md +2 -2
- package/docs/zh-TW/spec/harness-adapters.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,8 +67,8 @@ agent-ops uninstall --dry-run --json
|
|
|
67
67
|
### Project-local loop
|
|
68
68
|
|
|
69
69
|
`loop` is an explicit, project-only profile for Codex, Claude Code, or both.
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
Claude Code supports native Windows through a generated PowerShell launcher;
|
|
71
|
+
Codex's loop launcher still requires POSIX-compatible `bash`. Preview it
|
|
72
72
|
first, then install only after reviewing the plan. `loop` also implies the
|
|
73
73
|
`core` baseline, so the project retains the managed rules and routing files:
|
|
74
74
|
|
|
@@ -77,12 +77,16 @@ agent-ops init --dry-run --scope project --harness codex,claude --profile loop -
|
|
|
77
77
|
agent-ops init --scope project --harness codex,claude --profile loop --yes
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
On native Windows, select `--harness claude` unless Codex is running in a
|
|
81
|
+
POSIX environment; the Codex loop still invokes `bash`.
|
|
82
|
+
|
|
83
|
+
It creates managed launchers per selected native host:
|
|
81
84
|
|
|
82
85
|
- Codex: `.codex/hooks/agent-ops-loop.sh`, `.codex/config.toml` when absent,
|
|
83
86
|
`.codex/loop-goal.md`, `.codex/loop-state.md`, and
|
|
84
87
|
`.codex/loop-telemetry.jsonl`.
|
|
85
|
-
- Claude Code: `.claude/hooks/agent-ops-loop.sh`,
|
|
88
|
+
- Claude Code: `.claude/hooks/agent-ops-loop.sh`,
|
|
89
|
+
`.claude/hooks/agent-ops-loop.ps1`, `.claude/loop-goal.md`,
|
|
86
90
|
`.claude/loop-state.md`, and `.claude/loop-telemetry.jsonl`.
|
|
87
91
|
|
|
88
92
|
The launchers delegate to one installed Node runtime; agent-ops does not copy
|
|
@@ -146,8 +150,10 @@ node dist/packages/cli/src/bin.js init \
|
|
|
146
150
|
--dry-run --scope project --harness all --profile core --json
|
|
147
151
|
```
|
|
148
152
|
|
|
149
|
-
After reviewing the plan, apply it explicitly with `--yes`.
|
|
150
|
-
|
|
153
|
+
After reviewing the plan, apply it explicitly with `--yes`. Confirmed
|
|
154
|
+
project-scope init/update automatically grants the displayed trust binding when
|
|
155
|
+
verification commands are configured; uninstall revokes it. Diagnostics and
|
|
156
|
+
manual trust management remain separate commands:
|
|
151
157
|
|
|
152
158
|
```bash
|
|
153
159
|
node dist/packages/cli/src/bin.js init --scope project --harness all --profile core --yes
|
|
@@ -165,11 +171,9 @@ directory. `update` also requires that the project already has a valid managed
|
|
|
165
171
|
`.agent-ops/manifest.json` created by `init`.
|
|
166
172
|
|
|
167
173
|
The commands after `init --yes` are post-apply operations. `doctor` reports
|
|
168
|
-
`UNKNOWN` for a probe that has nothing to verify yet: `repository-trust`
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
to unlock), and `smoke-availability` until the configuration declares a
|
|
172
|
-
verification command. Every non-`PASS` check carries a `remediation` string
|
|
174
|
+
`UNKNOWN` for a probe that has nothing to verify yet: `repository-trust` when
|
|
175
|
+
no verification command exists, and `smoke-availability` until the
|
|
176
|
+
configuration declares one. Every non-`PASS` check carries a `remediation` string
|
|
173
177
|
explaining what, if anything, to do about it; text output prints it as an
|
|
174
178
|
indented ` → ` line, and `--json` exposes it as a field. `doctor` never
|
|
175
179
|
writes: it only reports what `agent-ops update` or `agent-ops trust grant`
|
|
@@ -239,9 +243,9 @@ only with confirmed commands, for example the relevant config fragment is:
|
|
|
239
243
|
}
|
|
240
244
|
```
|
|
241
245
|
|
|
242
|
-
After changing Stop configuration, run `agent-ops update
|
|
243
|
-
registrations
|
|
244
|
-
|
|
246
|
+
After changing Stop configuration, run `agent-ops update`; its approved plan
|
|
247
|
+
updates native registrations and the exact trust binding together. Stop is
|
|
248
|
+
report-only: `PASS`, `FAIL`, and `UNKNOWN` continue
|
|
245
249
|
the harness, emit bounded command evidence, and never complete a task.
|
|
246
250
|
Config v1 migrates to config v2 with Stop disabled; migration invalidates the
|
|
247
251
|
old trust binding, and pre-v1 binaries cannot read the migrated config.
|
|
@@ -277,19 +281,22 @@ the project configuration defines those workflows.
|
|
|
277
281
|
|
|
278
282
|
`agent-ops review` can hand the review to another agent CLI, so the work is not
|
|
279
283
|
judged by the agent that produced it. Enable it during `agent-ops init` (the
|
|
280
|
-
default is off).
|
|
281
|
-
|
|
282
|
-
|
|
284
|
+
default is off). A bare review applies the built-in `change-quality` criterion
|
|
285
|
+
to a deterministic worktree (or `--base`) scope. `--task` reviews keep their
|
|
286
|
+
task criteria and require current verification evidence. The detailed report
|
|
287
|
+
is displayed; a minimal source-fingerprint attestation is persisted after PASS.
|
|
283
288
|
|
|
284
289
|
Each attempt uses a fresh temporary cwd, a small allowlisted environment, and a
|
|
285
|
-
target-native read-only
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
target-native read-only mode. Claude additionally uses its complete safe mode;
|
|
291
|
+
Codex ignores user config and persistence, while Agy disables slash-command
|
|
292
|
+
expansion. Codex and Agy preserve their existing login environment, so their
|
|
293
|
+
context isolation is intentionally weaker than Claude's. `opencode` is not a
|
|
294
|
+
review target.
|
|
295
|
+
|
|
296
|
+
The first valid `PASS` or `FAIL` is final. Attempts that produce no verdict —
|
|
297
|
+
including login failures and unparseable output — advance to the next target
|
|
298
|
+
and remain visible in the result's `attempts`. `--yes` is still required for
|
|
299
|
+
every run, since each run spends another provider's quota.
|
|
293
300
|
|
|
294
301
|
Authentication is diagnosed, never guessed:
|
|
295
302
|
|
|
@@ -376,6 +376,11 @@ export function parseArgs(argv) {
|
|
|
376
376
|
(title !== undefined || sessionId !== undefined)) {
|
|
377
377
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review accepts harness, criteria, evidence, scope, dry-run, json, and yes options.");
|
|
378
378
|
}
|
|
379
|
+
if (command === "review" &&
|
|
380
|
+
taskId === undefined &&
|
|
381
|
+
(criteria.length > 0 || evidence.length > 0)) {
|
|
382
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review criteria and evidence require --task.");
|
|
383
|
+
}
|
|
379
384
|
if (command === "review" && harness !== undefined && harness.length !== 1) {
|
|
380
385
|
invalidValue("--harness", harness.join(","));
|
|
381
386
|
}
|
|
@@ -10,15 +10,14 @@ import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
|
|
|
10
10
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
11
11
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
12
12
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
13
|
-
import { FileTrustStore
|
|
13
|
+
import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
14
14
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
15
15
|
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
16
|
-
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
17
16
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
18
17
|
import { VerificationService } from "../../../runtime/src/verify/service.js";
|
|
19
18
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
20
19
|
import { runCli } from "./cli.js";
|
|
21
|
-
import { loadEffectiveConfig, repositoryTrust } from "./context.js";
|
|
20
|
+
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
22
21
|
import { runHookProcess } from "./hook-process.js";
|
|
23
22
|
import { selectYesNo, writeBanner } from "./ui.js";
|
|
24
23
|
import { CLI_VERSION } from "./version.js";
|
|
@@ -93,18 +92,28 @@ function gitRunner(root) {
|
|
|
93
92
|
}
|
|
94
93
|
};
|
|
95
94
|
}
|
|
96
|
-
async function confirmInit(plan) {
|
|
95
|
+
async function confirmInit(plan, trust) {
|
|
97
96
|
writeBanner({
|
|
98
97
|
isTTY: process.stdout.isTTY === true,
|
|
99
98
|
columns: process.stdout.columns,
|
|
100
99
|
write: (value) => process.stdout.write(value)
|
|
101
100
|
});
|
|
102
|
-
return await confirmPlan(formatInstallPlan(plan));
|
|
101
|
+
return await confirmPlan(formatInstallPlan(plan, trust));
|
|
103
102
|
}
|
|
104
103
|
async function confirmPlan(text) {
|
|
105
104
|
process.stdout.write(text);
|
|
106
105
|
return await selectYesNo("Apply this installation plan?", { input: process.stdin, output: process.stdout }, false);
|
|
107
106
|
}
|
|
107
|
+
function trustStore() {
|
|
108
|
+
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
109
|
+
return new FileTrustStore(state.trustStore, state.anchorDirectory);
|
|
110
|
+
}
|
|
111
|
+
async function plannedTrustBinding(root, projectConfig) {
|
|
112
|
+
const config = (await loadEffectiveConfig(root, "project", projectConfig)).config;
|
|
113
|
+
return config.verification.commands.length === 0
|
|
114
|
+
? null
|
|
115
|
+
: await repositoryTrustBinding(root, config, CLI_VERSION);
|
|
116
|
+
}
|
|
108
117
|
const argv = process.argv.slice(2);
|
|
109
118
|
if (argv[0] === "hook") {
|
|
110
119
|
process.exitCode = await runHookProcess(argv.slice(1), {
|
|
@@ -140,6 +149,7 @@ else {
|
|
|
140
149
|
process.stdin.isTTY === true &&
|
|
141
150
|
process.stdout.isTTY === true;
|
|
142
151
|
if (args.command === "init") {
|
|
152
|
+
const store = trustStore();
|
|
143
153
|
return await runInitCommand({
|
|
144
154
|
args,
|
|
145
155
|
root,
|
|
@@ -150,7 +160,9 @@ else {
|
|
|
150
160
|
...(args.hookTargets === undefined
|
|
151
161
|
? {}
|
|
152
162
|
: { hookTargets: args.hookTargets }),
|
|
153
|
-
|
|
163
|
+
trustStore: store,
|
|
164
|
+
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
165
|
+
confirm: async (plan, trust) => await confirmInit(plan, trust)
|
|
154
166
|
});
|
|
155
167
|
}
|
|
156
168
|
if (args.command === "doctor") {
|
|
@@ -222,14 +234,18 @@ else {
|
|
|
222
234
|
});
|
|
223
235
|
}
|
|
224
236
|
if (args.command === "uninstall") {
|
|
237
|
+
const store = trustStore();
|
|
225
238
|
return await runUninstallCommand({
|
|
226
239
|
args,
|
|
227
240
|
root,
|
|
228
241
|
isTTY,
|
|
229
|
-
|
|
242
|
+
trustStore: store,
|
|
243
|
+
calculateTrustBinding: async () => await plannedTrustBinding(root),
|
|
244
|
+
confirm: async (plan, trust) => await confirmPlan(formatUninstallPlan(plan, trust))
|
|
230
245
|
});
|
|
231
246
|
}
|
|
232
247
|
if (args.command === "update") {
|
|
248
|
+
const store = trustStore();
|
|
233
249
|
return await runUpdateCommand({
|
|
234
250
|
args,
|
|
235
251
|
root,
|
|
@@ -241,7 +257,9 @@ else {
|
|
|
241
257
|
...(args.hookTargets === undefined
|
|
242
258
|
? {}
|
|
243
259
|
: { hookTargets: args.hookTargets }),
|
|
244
|
-
|
|
260
|
+
trustStore: store,
|
|
261
|
+
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
262
|
+
confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
|
|
245
263
|
...(args.targetVersion === undefined
|
|
246
264
|
? {}
|
|
247
265
|
: { targetVersion: args.targetVersion })
|
|
@@ -326,24 +344,7 @@ else {
|
|
|
326
344
|
}
|
|
327
345
|
if (args.command === "trust") {
|
|
328
346
|
const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
329
|
-
const
|
|
330
|
-
const remote = (() => {
|
|
331
|
-
try {
|
|
332
|
-
return execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
333
|
-
cwd: root,
|
|
334
|
-
encoding: "utf8"
|
|
335
|
-
}).trim();
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
return `local:${root}`;
|
|
339
|
-
}
|
|
340
|
-
})();
|
|
341
|
-
const binding = await calculateTrustBinding({
|
|
342
|
-
repositoryPath: root,
|
|
343
|
-
remoteUrl: remote,
|
|
344
|
-
configHash: calculateConfigHash(config),
|
|
345
|
-
runtimeHash: sha256(CLI_VERSION)
|
|
346
|
-
});
|
|
347
|
+
const binding = await repositoryTrustBinding(root, config, CLI_VERSION);
|
|
347
348
|
return await runTrustCommand({
|
|
348
349
|
action: args.action,
|
|
349
350
|
yes: args.yes,
|
|
@@ -351,7 +352,7 @@ else {
|
|
|
351
352
|
calculateBinding: async () => binding,
|
|
352
353
|
presentBinding: async () => undefined,
|
|
353
354
|
confirmGrant: async () => await confirmPlan(JSON.stringify(binding, null, 2)),
|
|
354
|
-
store:
|
|
355
|
+
store: trustStore()
|
|
355
356
|
});
|
|
356
357
|
}
|
|
357
358
|
return errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`);
|
|
@@ -4,7 +4,8 @@ import { applyInstallPlan } from "../../../../runtime/src/install/apply.js";
|
|
|
4
4
|
import { okEnvelope } from "../output.js";
|
|
5
5
|
import { formatOperationPlan } from "../plan-output.js";
|
|
6
6
|
import { toPublicInstallPlan } from "../public-plan.js";
|
|
7
|
-
|
|
7
|
+
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
8
|
+
export function formatInstallPlan(plan, trust) {
|
|
8
9
|
const hooks = plan.manifest.hooks ?? [];
|
|
9
10
|
const detectedVerification = plan.detectedVerification;
|
|
10
11
|
return formatOperationPlan({
|
|
@@ -13,6 +14,7 @@ export function formatInstallPlan(plan) {
|
|
|
13
14
|
`Scope: ${plan.scope}`,
|
|
14
15
|
`Harness: ${plan.harness}`,
|
|
15
16
|
`Profiles: ${plan.profiles.join(", ")}`,
|
|
17
|
+
...(trust === undefined ? [] : formatTrustChange(trust)),
|
|
16
18
|
...(hooks.length === 0
|
|
17
19
|
? ["Hooks: none selected"]
|
|
18
20
|
: [
|
|
@@ -40,14 +42,27 @@ function appliedMessage(plan) {
|
|
|
40
42
|
...hooks.map((hook) => `- ${hook.harness}: ${hook.path} (${hook.events.join(", ")})`)
|
|
41
43
|
].join("\n");
|
|
42
44
|
}
|
|
43
|
-
function initError(code, message, plan) {
|
|
45
|
+
function initError(code, message, plan, trust, applied = false) {
|
|
44
46
|
return {
|
|
45
47
|
code,
|
|
46
48
|
status: "error",
|
|
47
|
-
data: { applied
|
|
49
|
+
data: { applied, plan: toPublicInstallPlan(plan, trust), message },
|
|
48
50
|
errors: [{ code, message }]
|
|
49
51
|
};
|
|
50
52
|
}
|
|
53
|
+
async function trustChange(options, plan) {
|
|
54
|
+
if (plan.scope === "user") {
|
|
55
|
+
return { action: "skipped", reason: "user-scope" };
|
|
56
|
+
}
|
|
57
|
+
if (options.calculateTrustBinding === undefined ||
|
|
58
|
+
options.trustStore === undefined) {
|
|
59
|
+
return { action: "skipped", reason: "not-configured" };
|
|
60
|
+
}
|
|
61
|
+
const binding = await options.calculateTrustBinding(plan.config);
|
|
62
|
+
return binding === null
|
|
63
|
+
? { action: "skipped", reason: "no-verification-commands" }
|
|
64
|
+
: await planTrustGrant(binding, options.trustStore);
|
|
65
|
+
}
|
|
51
66
|
export async function runInitCommand(options) {
|
|
52
67
|
const { args } = options;
|
|
53
68
|
if (args.command !== "init" ||
|
|
@@ -75,26 +90,38 @@ export async function runInitCommand(options) {
|
|
|
75
90
|
? {}
|
|
76
91
|
: { reviewTargets: args.reviewTargets })
|
|
77
92
|
});
|
|
93
|
+
const trust = await trustChange(options, plan);
|
|
78
94
|
if (args.dryRun) {
|
|
79
95
|
return okEnvelope("INIT_PLAN_READY", {
|
|
80
96
|
applied: false,
|
|
81
|
-
plan: toPublicInstallPlan(plan),
|
|
97
|
+
plan: toPublicInstallPlan(plan, trust),
|
|
82
98
|
message: "Installation plan calculated; no files were written.",
|
|
83
|
-
text: formatInstallPlan(plan)
|
|
99
|
+
text: formatInstallPlan(plan, trust)
|
|
84
100
|
});
|
|
85
101
|
}
|
|
86
102
|
if (!args.yes && !options.isTTY) {
|
|
87
|
-
return initError("INIT_CONFIRMATION_REQUIRED", "Non-interactive init requires --yes after all choices are explicit.", plan);
|
|
103
|
+
return initError("INIT_CONFIRMATION_REQUIRED", "Non-interactive init requires --yes after all choices are explicit.", plan, trust);
|
|
88
104
|
}
|
|
89
105
|
if (!args.yes &&
|
|
90
106
|
options.isTTY &&
|
|
91
|
-
!(await options.confirm(plan))) {
|
|
92
|
-
return initError("INIT_CANCELLED", "Installation was cancelled; no files were written.", plan);
|
|
107
|
+
!(await options.confirm(plan, trust))) {
|
|
108
|
+
return initError("INIT_CANCELLED", "Installation was cancelled; no files were written.", plan, trust);
|
|
93
109
|
}
|
|
94
110
|
await applyInstallPlan(options.root, plan);
|
|
111
|
+
if (trust.action === "grant") {
|
|
112
|
+
try {
|
|
113
|
+
if (options.trustStore === undefined) {
|
|
114
|
+
throw new Error("Trust store is unavailable.");
|
|
115
|
+
}
|
|
116
|
+
await options.trustStore.grant(trust.binding);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return initError("INIT_TRUST_FAILED", "Installation was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, true);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
95
122
|
return okEnvelope("INIT_APPLIED", {
|
|
96
123
|
applied: true,
|
|
97
|
-
plan: toPublicInstallPlan(plan),
|
|
124
|
+
plan: toPublicInstallPlan(plan, trust),
|
|
98
125
|
message: appliedMessage(plan)
|
|
99
126
|
});
|
|
100
127
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { saveReviewAttestation } from "../../../../runtime/src/review/attestation.js";
|
|
4
5
|
import { resolveReviewRole } from "../../../../runtime/src/review/roles.js";
|
|
5
6
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
6
7
|
import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, reviewScopeSignature } from "../../../../runtime/src/review/scope.js";
|
|
@@ -20,6 +21,11 @@ function harness(value) {
|
|
|
20
21
|
? undefined
|
|
21
22
|
: selected;
|
|
22
23
|
}
|
|
24
|
+
const GENERIC_REQUEST = "Review the current Git change surface.";
|
|
25
|
+
const GENERIC_CRITERIA = [{
|
|
26
|
+
id: "change-quality",
|
|
27
|
+
description: "The change is correct, safe, backward compatible, focused, and verified in proportion to its risk; report every material defect as a finding."
|
|
28
|
+
}];
|
|
23
29
|
/**
|
|
24
30
|
* Criterion descriptions come from the task store, never from the id. A
|
|
25
31
|
* reviewer handed `criterion: tests` cannot review anything, so a review with
|
|
@@ -42,8 +48,13 @@ async function taskContext(options) {
|
|
|
42
48
|
try {
|
|
43
49
|
record = await tasks.status(query);
|
|
44
50
|
}
|
|
45
|
-
catch {
|
|
46
|
-
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (options.taskId === undefined &&
|
|
53
|
+
error instanceof AgentOpsError &&
|
|
54
|
+
error.code === "TASK_SESSION_UNATTACHED") {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
47
58
|
}
|
|
48
59
|
const requested = options.args.criteria ?? [];
|
|
49
60
|
const criteria = record.task.criteria
|
|
@@ -55,7 +66,7 @@ async function taskContext(options) {
|
|
|
55
66
|
}));
|
|
56
67
|
if (criteria.length === 0 ||
|
|
57
68
|
(requested.length > 0 && criteria.length !== requested.length)) {
|
|
58
|
-
|
|
69
|
+
throw new AgentOpsError("REVIEW_CRITERIA_NOT_FOUND", "Every requested review criterion must exist on the selected task.");
|
|
59
70
|
}
|
|
60
71
|
return {
|
|
61
72
|
taskId: record.task.id,
|
|
@@ -111,7 +122,7 @@ async function currentEvidence(options, context, criterionId, commandId, configH
|
|
|
111
122
|
}
|
|
112
123
|
return { current, hasReference, unreadable, stale };
|
|
113
124
|
}
|
|
114
|
-
async function preflightReview(options, context,
|
|
125
|
+
async function preflightReview(options, context, sourceFingerprint) {
|
|
115
126
|
if (options.config === undefined ||
|
|
116
127
|
options.evidenceStore === undefined ||
|
|
117
128
|
options.root === undefined ||
|
|
@@ -120,7 +131,6 @@ async function preflightReview(options, context, scope) {
|
|
|
120
131
|
return { ok: false, reason: "stale-verification" };
|
|
121
132
|
}
|
|
122
133
|
const configHash = calculateConfigHash(options.config);
|
|
123
|
-
const sourceFingerprint = await calculateSourceFingerprint(options.root, scope, options.gitRunner);
|
|
124
134
|
const commands = [];
|
|
125
135
|
for (const criterion of context.criteria) {
|
|
126
136
|
for (const commandId of criterion.verifierIds ?? []) {
|
|
@@ -207,6 +217,7 @@ function sourceChangedResult(result) {
|
|
|
207
217
|
...(result.independence === undefined
|
|
208
218
|
? {}
|
|
209
219
|
: { independence: result.independence }),
|
|
220
|
+
...(result.attempts === undefined ? {} : { attempts: result.attempts }),
|
|
210
221
|
...(result.verification === undefined
|
|
211
222
|
? {}
|
|
212
223
|
: { verification: result.verification })
|
|
@@ -224,17 +235,8 @@ export async function runReviewCommand(options) {
|
|
|
224
235
|
requirement: separator < 0 ? value : value.slice(separator + 1)
|
|
225
236
|
};
|
|
226
237
|
});
|
|
227
|
-
if (context === undefined) {
|
|
228
|
-
return notRunEnvelope({
|
|
229
|
-
status: "NOT_RUN",
|
|
230
|
-
reason: "no-task-context",
|
|
231
|
-
harness: target,
|
|
232
|
-
model: role?.model ?? options.model ?? "configured",
|
|
233
|
-
effort: role?.effort ?? options.effort ?? "configured",
|
|
234
|
-
prompt: ""
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
238
|
let scope;
|
|
239
|
+
let sourceFingerprint;
|
|
238
240
|
let verification;
|
|
239
241
|
if (options.root !== undefined && options.gitRunner !== undefined) {
|
|
240
242
|
try {
|
|
@@ -269,7 +271,8 @@ export async function runReviewCommand(options) {
|
|
|
269
271
|
scope
|
|
270
272
|
});
|
|
271
273
|
}
|
|
272
|
-
|
|
274
|
+
sourceFingerprint = await calculateSourceFingerprint(options.root, scope, options.gitRunner);
|
|
275
|
+
if (context !== undefined && options.policyConfigHash !== undefined) {
|
|
273
276
|
if (context.policyConfigHash === null) {
|
|
274
277
|
return notRunEnvelope({
|
|
275
278
|
status: "NOT_RUN", reason: "reviewer-policy-baseline-missing",
|
|
@@ -285,21 +288,25 @@ export async function runReviewCommand(options) {
|
|
|
285
288
|
});
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
if (context !== undefined) {
|
|
292
|
+
const preflight = await preflightReview(options, context, sourceFingerprint);
|
|
293
|
+
if (!preflight.ok) {
|
|
294
|
+
return notRunEnvelope({
|
|
295
|
+
status: "NOT_RUN", reason: preflight.reason,
|
|
296
|
+
harness: target, model: role?.model ?? options.model ?? "configured",
|
|
297
|
+
effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
verification = preflight.summary;
|
|
295
301
|
}
|
|
296
|
-
verification = preflight.summary;
|
|
297
302
|
}
|
|
298
|
-
const criteria = [
|
|
303
|
+
const criteria = [
|
|
304
|
+
...(context?.criteria ?? GENERIC_CRITERIA)
|
|
305
|
+
];
|
|
299
306
|
let packet;
|
|
300
307
|
try {
|
|
301
308
|
packet = buildReviewPacket({
|
|
302
|
-
request: context
|
|
309
|
+
request: context?.title ?? GENERIC_REQUEST,
|
|
303
310
|
criteria,
|
|
304
311
|
artifactRefs: scope?.changedFiles ?? [],
|
|
305
312
|
evidenceRequirements
|
|
@@ -371,13 +378,10 @@ export async function runReviewCommand(options) {
|
|
|
371
378
|
const currentHash = options.currentPolicyConfigHash === undefined
|
|
372
379
|
? options.policyConfigHash
|
|
373
380
|
: await options.currentPolicyConfigHash();
|
|
374
|
-
const postflightFingerprint =
|
|
375
|
-
? undefined
|
|
376
|
-
: await calculateSourceFingerprint(options.root, postflight, options.gitRunner);
|
|
381
|
+
const postflightFingerprint = await calculateSourceFingerprint(options.root, postflight, options.gitRunner);
|
|
377
382
|
if (reviewScopeSignature(scope) !== reviewScopeSignature(postflight) ||
|
|
378
383
|
(options.policyConfigHash !== undefined && currentHash !== options.policyConfigHash) ||
|
|
379
|
-
|
|
380
|
-
postflightFingerprint !== verification.sourceFingerprint)) {
|
|
384
|
+
postflightFingerprint !== sourceFingerprint) {
|
|
381
385
|
return notRunEnvelope(sourceChangedResult(result));
|
|
382
386
|
}
|
|
383
387
|
}
|
|
@@ -387,7 +391,8 @@ export async function runReviewCommand(options) {
|
|
|
387
391
|
}
|
|
388
392
|
// Evidence is only appended while the task is active: a completed record
|
|
389
393
|
// must stay exactly as it was verified.
|
|
390
|
-
if (
|
|
394
|
+
if (context !== undefined &&
|
|
395
|
+
options.tasks !== undefined &&
|
|
391
396
|
result.status === "PASS" &&
|
|
392
397
|
context.active &&
|
|
393
398
|
result.results !== undefined) {
|
|
@@ -396,6 +401,21 @@ export async function runReviewCommand(options) {
|
|
|
396
401
|
item.evidence.map((reference) => `review:${result.harness}:${reference}`)
|
|
397
402
|
])));
|
|
398
403
|
}
|
|
404
|
+
// The attestation is what a Stop gate reads. It is keyed by the verified
|
|
405
|
+
// source fingerprint, so it stops satisfying the gate the moment the tree
|
|
406
|
+
// changes again.
|
|
407
|
+
if (options.root !== undefined &&
|
|
408
|
+
result.status === "PASS" &&
|
|
409
|
+
sourceFingerprint !== undefined) {
|
|
410
|
+
await saveReviewAttestation(options.root, {
|
|
411
|
+
schemaVersion: 1,
|
|
412
|
+
...(context === undefined ? {} : { taskId: context.taskId }),
|
|
413
|
+
harness: result.harness,
|
|
414
|
+
status: "PASS",
|
|
415
|
+
sourceFingerprint,
|
|
416
|
+
createdAt: new Date().toISOString()
|
|
417
|
+
});
|
|
418
|
+
}
|
|
399
419
|
const message = result.status === "PASS"
|
|
400
420
|
? "Independent review passed."
|
|
401
421
|
: result.status === "FAIL"
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { okEnvelope } from "../output.js";
|
|
2
|
+
export async function planTrustGrant(binding, store) {
|
|
3
|
+
const trust = await store.status(binding);
|
|
4
|
+
return {
|
|
5
|
+
action: trust.status === "TRUSTED" ? "unchanged" : "grant",
|
|
6
|
+
binding,
|
|
7
|
+
status: trust.status
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export async function planTrustRevoke(binding, store) {
|
|
11
|
+
const trust = await store.status(binding);
|
|
12
|
+
return {
|
|
13
|
+
action: trust.status === "TRUSTED" ? "revoke" : "unchanged",
|
|
14
|
+
binding,
|
|
15
|
+
status: trust.status
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function formatTrustChange(trust) {
|
|
19
|
+
if (trust.action === "skipped") {
|
|
20
|
+
return [`Trust: skipped (${trust.reason})`];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
`Trust: ${trust.action} (current: ${trust.status})`,
|
|
24
|
+
` - canonical path: ${trust.binding.canonicalPath}`,
|
|
25
|
+
` - remote identity: ${trust.binding.remoteIdentity}`,
|
|
26
|
+
` - config hash: ${trust.binding.configHash}`,
|
|
27
|
+
` - runtime hash: ${trust.binding.runtimeHash}`
|
|
28
|
+
];
|
|
29
|
+
}
|
|
2
30
|
function trustError(code, message, binding) {
|
|
3
31
|
return {
|
|
4
32
|
code,
|
|
@@ -2,11 +2,13 @@ import { applyUninstallPlan, createUninstallPlan } from "../../../../runtime/src
|
|
|
2
2
|
import { okEnvelope } from "../output.js";
|
|
3
3
|
import { formatOperationPlan } from "../plan-output.js";
|
|
4
4
|
import { toPublicUninstallPlan } from "../public-plan.js";
|
|
5
|
-
|
|
5
|
+
import { formatTrustChange, planTrustRevoke } from "./trust.js";
|
|
6
|
+
function formatUninstallPlan(plan, trust) {
|
|
6
7
|
return formatOperationPlan({
|
|
7
8
|
title: "Uninstall plan",
|
|
8
9
|
metadata: [
|
|
9
10
|
`Installed: ${plan.installed ? "yes" : "no"}`,
|
|
11
|
+
...(trust === undefined ? [] : formatTrustChange(trust)),
|
|
10
12
|
...(plan.manifest === null
|
|
11
13
|
? []
|
|
12
14
|
: [
|
|
@@ -17,14 +19,27 @@ function formatUninstallPlan(plan) {
|
|
|
17
19
|
operations: toPublicUninstallPlan(plan).operations
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
|
-
function uninstallError(code, message, plan) {
|
|
22
|
+
function uninstallError(code, message, plan, trust, applied = false) {
|
|
21
23
|
return {
|
|
22
24
|
code,
|
|
23
25
|
status: "error",
|
|
24
|
-
data: { applied
|
|
26
|
+
data: { applied, plan: toPublicUninstallPlan(plan, trust), message },
|
|
25
27
|
errors: [{ code, message }]
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
async function trustChange(options, plan) {
|
|
31
|
+
if (plan.manifest?.scope === "user") {
|
|
32
|
+
return { action: "skipped", reason: "user-scope" };
|
|
33
|
+
}
|
|
34
|
+
if (options.calculateTrustBinding === undefined ||
|
|
35
|
+
options.trustStore === undefined) {
|
|
36
|
+
return { action: "skipped", reason: "not-configured" };
|
|
37
|
+
}
|
|
38
|
+
const binding = await options.calculateTrustBinding();
|
|
39
|
+
return binding === null
|
|
40
|
+
? { action: "skipped", reason: "no-verification-commands" }
|
|
41
|
+
: await planTrustRevoke(binding, options.trustStore);
|
|
42
|
+
}
|
|
28
43
|
export async function runUninstallCommand(options) {
|
|
29
44
|
const plan = await createUninstallPlan(options.root);
|
|
30
45
|
if (!plan.installed) {
|
|
@@ -34,25 +49,37 @@ export async function runUninstallCommand(options) {
|
|
|
34
49
|
message: "No managed installation exists."
|
|
35
50
|
});
|
|
36
51
|
}
|
|
52
|
+
const trust = await trustChange(options, plan);
|
|
37
53
|
if (options.args.dryRun) {
|
|
38
54
|
return okEnvelope("UNINSTALL_PLAN_READY", {
|
|
39
55
|
applied: false,
|
|
40
|
-
plan: toPublicUninstallPlan(plan),
|
|
56
|
+
plan: toPublicUninstallPlan(plan, trust),
|
|
41
57
|
message: "Uninstall plan calculated; no files were changed.",
|
|
42
|
-
text: formatUninstallPlan(plan)
|
|
58
|
+
text: formatUninstallPlan(plan, trust)
|
|
43
59
|
});
|
|
44
60
|
}
|
|
45
61
|
if (!options.args.yes && !options.isTTY) {
|
|
46
|
-
return uninstallError("UNINSTALL_CONFIRMATION_REQUIRED", "Non-interactive uninstall requires --yes.", plan);
|
|
62
|
+
return uninstallError("UNINSTALL_CONFIRMATION_REQUIRED", "Non-interactive uninstall requires --yes.", plan, trust);
|
|
47
63
|
}
|
|
48
64
|
if (!options.args.yes &&
|
|
49
|
-
!(await options.confirm(plan))) {
|
|
50
|
-
return uninstallError("UNINSTALL_CANCELLED", "Uninstall was cancelled; no files were changed.", plan);
|
|
65
|
+
!(await options.confirm(plan, trust))) {
|
|
66
|
+
return uninstallError("UNINSTALL_CANCELLED", "Uninstall was cancelled; no files were changed.", plan, trust);
|
|
51
67
|
}
|
|
52
68
|
await applyUninstallPlan(options.root, plan);
|
|
69
|
+
if (trust.action === "revoke") {
|
|
70
|
+
try {
|
|
71
|
+
if (options.trustStore === undefined ||
|
|
72
|
+
!(await options.trustStore.revoke(trust.binding))) {
|
|
73
|
+
throw new Error("Trust record changed during uninstall.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return uninstallError("UNINSTALL_TRUST_FAILED", "Uninstall was applied, but repository trust was not revoked. Run `agent-ops trust revoke`.", plan, trust, true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
53
80
|
return okEnvelope("UNINSTALL_APPLIED", {
|
|
54
81
|
applied: true,
|
|
55
|
-
plan: toPublicUninstallPlan(plan),
|
|
82
|
+
plan: toPublicUninstallPlan(plan, trust),
|
|
56
83
|
message: "Managed installation content was removed."
|
|
57
84
|
});
|
|
58
85
|
}
|