@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
|
@@ -2,13 +2,15 @@ import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/insta
|
|
|
2
2
|
import { okEnvelope } from "../output.js";
|
|
3
3
|
import { formatOperationPlan } from "../plan-output.js";
|
|
4
4
|
import { toPublicUpdatePlan } from "../public-plan.js";
|
|
5
|
-
|
|
5
|
+
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
6
|
+
export function formatUpdatePlan(plan, trust) {
|
|
6
7
|
const detectedVerification = plan.installation.detectedVerification;
|
|
7
8
|
return formatOperationPlan({
|
|
8
9
|
title: "Update plan",
|
|
9
10
|
metadata: [
|
|
10
11
|
`Target version: ${plan.targetVersion}`,
|
|
11
12
|
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
13
|
+
...(trust === undefined ? [] : formatTrustChange(trust)),
|
|
12
14
|
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
13
15
|
? "none"
|
|
14
16
|
: plan.migrationSteps
|
|
@@ -24,14 +26,27 @@ export function formatUpdatePlan(plan) {
|
|
|
24
26
|
operations: toPublicUpdatePlan(plan).installation.operations
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
|
-
function updateError(code, message, plan) {
|
|
29
|
+
function updateError(code, message, plan, trust, applied = false) {
|
|
28
30
|
return {
|
|
29
31
|
code,
|
|
30
32
|
status: "error",
|
|
31
|
-
data: { applied
|
|
33
|
+
data: { applied, plan: toPublicUpdatePlan(plan, trust), message },
|
|
32
34
|
errors: [{ code, message }]
|
|
33
35
|
};
|
|
34
36
|
}
|
|
37
|
+
async function trustChange(options, plan) {
|
|
38
|
+
if (plan.installation.scope === "user") {
|
|
39
|
+
return { action: "skipped", reason: "user-scope" };
|
|
40
|
+
}
|
|
41
|
+
if (options.calculateTrustBinding === undefined ||
|
|
42
|
+
options.trustStore === undefined) {
|
|
43
|
+
return { action: "skipped", reason: "not-configured" };
|
|
44
|
+
}
|
|
45
|
+
const binding = await options.calculateTrustBinding(plan.installation.config);
|
|
46
|
+
return binding === null
|
|
47
|
+
? { action: "skipped", reason: "no-verification-commands" }
|
|
48
|
+
: await planTrustGrant(binding, options.trustStore);
|
|
49
|
+
}
|
|
35
50
|
export async function runUpdateCommand(options) {
|
|
36
51
|
const plan = await createUpdatePlan({
|
|
37
52
|
root: options.root,
|
|
@@ -55,25 +70,37 @@ export async function runUpdateCommand(options) {
|
|
|
55
70
|
? {}
|
|
56
71
|
: { hookTargets: options.hookTargets ?? options.args.hookTargets })
|
|
57
72
|
});
|
|
73
|
+
const trust = await trustChange(options, plan);
|
|
58
74
|
if (options.args.dryRun) {
|
|
59
75
|
return okEnvelope("UPDATE_PLAN_READY", {
|
|
60
76
|
applied: false,
|
|
61
|
-
plan: toPublicUpdatePlan(plan),
|
|
77
|
+
plan: toPublicUpdatePlan(plan, trust),
|
|
62
78
|
message: "Update plan calculated; no files were changed.",
|
|
63
|
-
text: formatUpdatePlan(plan)
|
|
79
|
+
text: formatUpdatePlan(plan, trust)
|
|
64
80
|
});
|
|
65
81
|
}
|
|
66
82
|
if (!options.args.yes && !options.isTTY) {
|
|
67
|
-
return updateError("UPDATE_CONFIRMATION_REQUIRED", "Non-interactive update requires --yes.", plan);
|
|
83
|
+
return updateError("UPDATE_CONFIRMATION_REQUIRED", "Non-interactive update requires --yes.", plan, trust);
|
|
68
84
|
}
|
|
69
85
|
if (!options.args.yes &&
|
|
70
|
-
!(await options.confirm(plan))) {
|
|
71
|
-
return updateError("UPDATE_CANCELLED", "Update was cancelled; no files were changed.", plan);
|
|
86
|
+
!(await options.confirm(plan, trust))) {
|
|
87
|
+
return updateError("UPDATE_CANCELLED", "Update was cancelled; no files were changed.", plan, trust);
|
|
72
88
|
}
|
|
73
89
|
await applyUpdatePlan(options.root, plan);
|
|
90
|
+
if (trust.action === "grant") {
|
|
91
|
+
try {
|
|
92
|
+
if (options.trustStore === undefined) {
|
|
93
|
+
throw new Error("Trust store is unavailable.");
|
|
94
|
+
}
|
|
95
|
+
await options.trustStore.grant(trust.binding);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return updateError("UPDATE_TRUST_FAILED", "Update was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
74
101
|
return okEnvelope("UPDATE_APPLIED", {
|
|
75
102
|
applied: true,
|
|
76
|
-
plan: toPublicUpdatePlan(plan),
|
|
103
|
+
plan: toPublicUpdatePlan(plan, trust),
|
|
77
104
|
message: `Managed installation updated to ${plan.targetVersion}.`
|
|
78
105
|
});
|
|
79
106
|
}
|
|
@@ -43,7 +43,7 @@ async function loadOptionalConfig(path) {
|
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
export async function loadEffectiveConfig(root, scope) {
|
|
46
|
+
export async function loadEffectiveConfig(root, scope, projectOverride) {
|
|
47
47
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
48
48
|
const userPath = join(home, ".agent-ops", "config.json");
|
|
49
49
|
const projectPath = join(root, ".agent-ops", "config.json");
|
|
@@ -69,7 +69,9 @@ export async function loadEffectiveConfig(root, scope) {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
const project =
|
|
72
|
+
const project = projectOverride === undefined
|
|
73
|
+
? await loadOptionalConfig(projectPath)
|
|
74
|
+
: { sourcePath: projectPath, config: projectOverride };
|
|
73
75
|
if (project !== null) {
|
|
74
76
|
layers.push({
|
|
75
77
|
source: "project",
|
|
@@ -154,15 +156,18 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
154
156
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
155
157
|
const state = localStatePaths(home);
|
|
156
158
|
try {
|
|
157
|
-
const binding = await
|
|
158
|
-
repositoryPath: root,
|
|
159
|
-
remoteUrl: repositoryRemoteUrl(root),
|
|
160
|
-
configHash: calculateConfigHash(config),
|
|
161
|
-
runtimeHash: sha256(cliVersion)
|
|
162
|
-
});
|
|
159
|
+
const binding = await repositoryTrustBinding(root, config, cliVersion);
|
|
163
160
|
return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
|
|
164
161
|
}
|
|
165
162
|
catch {
|
|
166
163
|
return "UNTRUSTED";
|
|
167
164
|
}
|
|
168
165
|
}
|
|
166
|
+
export async function repositoryTrustBinding(root, config, cliVersion) {
|
|
167
|
+
return await calculateTrustBinding({
|
|
168
|
+
repositoryPath: root,
|
|
169
|
+
remoteUrl: repositoryRemoteUrl(root),
|
|
170
|
+
configHash: calculateConfigHash(config),
|
|
171
|
+
runtimeHash: sha256(cliVersion)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -35,7 +35,7 @@ export function toPublicOperation(operation) {
|
|
|
35
35
|
export function toPublicOperations(operations) {
|
|
36
36
|
return operations.map(toPublicOperation);
|
|
37
37
|
}
|
|
38
|
-
export function toPublicInstallPlan(plan) {
|
|
38
|
+
export function toPublicInstallPlan(plan, trust) {
|
|
39
39
|
return {
|
|
40
40
|
scope: plan.scope,
|
|
41
41
|
harness: plan.harness,
|
|
@@ -43,21 +43,23 @@ export function toPublicInstallPlan(plan) {
|
|
|
43
43
|
capabilities: plan.capabilities,
|
|
44
44
|
manifest: plan.manifest,
|
|
45
45
|
operations: toPublicOperations(plan.operations),
|
|
46
|
-
detectedVerification: plan.detectedVerification
|
|
46
|
+
detectedVerification: plan.detectedVerification,
|
|
47
|
+
...(trust === undefined ? {} : { trust })
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
|
-
export function toPublicUpdatePlan(plan) {
|
|
50
|
+
export function toPublicUpdatePlan(plan, trust) {
|
|
50
51
|
return {
|
|
51
52
|
targetVersion: plan.targetVersion,
|
|
52
53
|
migrationSteps: plan.migrationSteps,
|
|
53
|
-
installation: toPublicInstallPlan(plan.installation)
|
|
54
|
+
installation: toPublicInstallPlan(plan.installation, trust)
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
|
-
export function toPublicUninstallPlan(plan) {
|
|
57
|
+
export function toPublicUninstallPlan(plan, trust) {
|
|
57
58
|
return {
|
|
58
59
|
installed: plan.installed,
|
|
59
60
|
manifest: plan.manifest,
|
|
60
61
|
manifestHash: plan.manifestHash,
|
|
61
|
-
operations: toPublicOperations(plan.operations)
|
|
62
|
+
operations: toPublicOperations(plan.operations),
|
|
63
|
+
...(trust === undefined ? {} : { trust })
|
|
62
64
|
};
|
|
63
65
|
}
|
|
@@ -12,6 +12,7 @@ const CLAUDE_LOOP_EVENTS = [
|
|
|
12
12
|
];
|
|
13
13
|
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
14
14
|
const CLAUDE_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.sh";
|
|
15
|
+
const CLAUDE_WINDOWS_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.ps1";
|
|
15
16
|
export function claudeSettingsTarget(scope) {
|
|
16
17
|
return scope === "project"
|
|
17
18
|
? {
|
|
@@ -42,26 +43,35 @@ function matcherGroup(event, runtimePath) {
|
|
|
42
43
|
hooks: [commandHook(event, runtimePath)]
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
|
-
function
|
|
46
|
+
function powershellLoopCommand(event) {
|
|
47
|
+
return `& "${CLAUDE_WINDOWS_LOOP_LAUNCHER}" "${event}" "${CLAUDE_HOOK_MARKER}"`;
|
|
48
|
+
}
|
|
49
|
+
function loopMatcherGroup(event, platform) {
|
|
50
|
+
const hook = platform === "win32"
|
|
51
|
+
? {
|
|
52
|
+
type: "command",
|
|
53
|
+
shell: "powershell",
|
|
54
|
+
command: powershellLoopCommand(event),
|
|
55
|
+
timeout: 30
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
type: "command",
|
|
59
|
+
command: "bash",
|
|
60
|
+
args: [
|
|
61
|
+
CLAUDE_LOOP_LAUNCHER,
|
|
62
|
+
event,
|
|
63
|
+
CLAUDE_HOOK_MARKER
|
|
64
|
+
],
|
|
65
|
+
timeout: 30
|
|
66
|
+
};
|
|
46
67
|
return {
|
|
47
68
|
...(event === "PreToolUse" || event === "PermissionRequest"
|
|
48
69
|
? { matcher: "Bash" }
|
|
49
70
|
: {}),
|
|
50
|
-
hooks: [
|
|
51
|
-
{
|
|
52
|
-
type: "command",
|
|
53
|
-
command: "bash",
|
|
54
|
-
args: [
|
|
55
|
-
CLAUDE_LOOP_LAUNCHER,
|
|
56
|
-
event,
|
|
57
|
-
CLAUDE_HOOK_MARKER
|
|
58
|
-
],
|
|
59
|
-
timeout: 30
|
|
60
|
-
}
|
|
61
|
-
]
|
|
71
|
+
hooks: [hook]
|
|
62
72
|
};
|
|
63
73
|
}
|
|
64
|
-
export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
74
|
+
export function buildClaudeHookSettings(capabilities, runtimePath, platform = process.platform) {
|
|
65
75
|
if (runtimePath.length === 0 ||
|
|
66
76
|
runtimePath.length > 4096 ||
|
|
67
77
|
runtimePath.includes("\0")) {
|
|
@@ -70,7 +80,7 @@ export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
|
70
80
|
const hooks = {};
|
|
71
81
|
if (capabilities.includes("project-loop")) {
|
|
72
82
|
for (const event of CLAUDE_LOOP_EVENTS) {
|
|
73
|
-
hooks[event] = [loopMatcherGroup(event)];
|
|
83
|
+
hooks[event] = [loopMatcherGroup(event, platform)];
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
else {
|
|
@@ -95,14 +105,20 @@ function isRecord(value) {
|
|
|
95
105
|
* ours merely by carrying the marker string.
|
|
96
106
|
*/
|
|
97
107
|
export function isClaudeManagedHandler(handler) {
|
|
98
|
-
if (!isRecord(handler) ||
|
|
108
|
+
if (!isRecord(handler) || handler.type !== "command") {
|
|
99
109
|
return false;
|
|
100
110
|
}
|
|
101
111
|
return ((handler.command === "node" &&
|
|
112
|
+
Array.isArray(handler.args) &&
|
|
102
113
|
handler.args[3] === CLAUDE_HOOK_MARKER) ||
|
|
103
114
|
(handler.command === "bash" &&
|
|
115
|
+
Array.isArray(handler.args) &&
|
|
104
116
|
handler.args[0] === CLAUDE_LOOP_LAUNCHER &&
|
|
105
|
-
handler.args[2] === CLAUDE_HOOK_MARKER)
|
|
117
|
+
handler.args[2] === CLAUDE_HOOK_MARKER) ||
|
|
118
|
+
(handler.shell === "powershell" &&
|
|
119
|
+
handler.args === undefined &&
|
|
120
|
+
typeof handler.command === "string" &&
|
|
121
|
+
CLAUDE_LOOP_EVENTS.some((event) => handler.command === powershellLoopCommand(event))));
|
|
106
122
|
}
|
|
107
123
|
function withoutOwnedHandlers(value) {
|
|
108
124
|
if (!isRecord(value) || !Array.isArray(value.hooks)) {
|
|
@@ -8,6 +8,19 @@ function json(value) {
|
|
|
8
8
|
export function claudeHookOutput(event, result) {
|
|
9
9
|
const denialReason = result.remedy === undefined ? result.code : `${result.code}: ${result.remedy}`;
|
|
10
10
|
if (event === "Stop" && result.evidence !== undefined) {
|
|
11
|
+
if (result.status === "FAIL") {
|
|
12
|
+
const failed = result.evidence.commandResults
|
|
13
|
+
.filter(({ exitCode }) => exitCode !== 0)
|
|
14
|
+
.map(({ commandId }) => commandId);
|
|
15
|
+
return json({
|
|
16
|
+
decision: "block",
|
|
17
|
+
reason: `agent-ops: ${result.code} reported FAIL for ${failed.join(", ")}. ` +
|
|
18
|
+
"Resolve every failing item — an unsatisfied independent-review " +
|
|
19
|
+
"gate is cleared by running `agent-ops review` to a PASS — then " +
|
|
20
|
+
"stop again.",
|
|
21
|
+
evidence: result.evidence
|
|
22
|
+
});
|
|
23
|
+
}
|
|
11
24
|
return json({
|
|
12
25
|
systemMessage: `agent-ops: ${result.code}`,
|
|
13
26
|
evidence: result.evidence
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { calculateConfigHash } from "../config/hash.js";
|
|
2
2
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
3
|
+
import { findReviewAttestation } from "../review/attestation.js";
|
|
4
|
+
import { resolveReviewScope } from "../review/scope.js";
|
|
5
|
+
import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
|
|
3
6
|
import { aggregateVerificationStatus, executeConfiguredCommand } from "../verify/command-executor.js";
|
|
4
7
|
import { collectChangeSurface } from "../verify/change-surface.js";
|
|
5
8
|
import { selectVerificationScope } from "../verify/scope.js";
|
|
@@ -15,6 +18,7 @@ function commandById(config, commandId) {
|
|
|
15
18
|
}
|
|
16
19
|
return command;
|
|
17
20
|
}
|
|
21
|
+
export const REVIEW_GATE_COMMAND_ID = "independent-review";
|
|
18
22
|
export class StopVerificationService {
|
|
19
23
|
#options;
|
|
20
24
|
constructor(options) {
|
|
@@ -39,6 +43,35 @@ export class StopVerificationService {
|
|
|
39
43
|
throw stopError("STOP_VERIFICATION_UNCONFIRMED", "Stop verification configuration is stale or unconfirmed.");
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Reports whether the current source state carries a passing independent
|
|
48
|
+
* review. Returns null when no gate applies: an unchanged tree, or a
|
|
49
|
+
* repository this service cannot inspect. Infrastructure that cannot answer
|
|
50
|
+
* stays fail-open — only a resolvable change surface with no attestation
|
|
51
|
+
* fails closed.
|
|
52
|
+
*/
|
|
53
|
+
async #reviewGate() {
|
|
54
|
+
if ((this.#options.config.reviewRoles ?? []).length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
let fingerprint;
|
|
58
|
+
try {
|
|
59
|
+
const scope = await resolveReviewScope({
|
|
60
|
+
root: this.#options.root,
|
|
61
|
+
runner: this.#options.gitRunner
|
|
62
|
+
});
|
|
63
|
+
fingerprint = await calculateSourceFingerprint(this.#options.root, scope, this.#options.gitRunner);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const attestation = await findReviewAttestation(this.#options.root, fingerprint);
|
|
69
|
+
return {
|
|
70
|
+
commandId: REVIEW_GATE_COMMAND_ID,
|
|
71
|
+
exitCode: attestation === null ? 1 : 0,
|
|
72
|
+
testCount: null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
42
75
|
async verify() {
|
|
43
76
|
this.#assertReady();
|
|
44
77
|
const surface = await collectChangeSurface(this.#options.gitRunner);
|
|
@@ -57,13 +90,19 @@ export class StopVerificationService {
|
|
|
57
90
|
});
|
|
58
91
|
executions.push(result);
|
|
59
92
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
exitCode,
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
const reviewGate = await this.#reviewGate();
|
|
94
|
+
const results = [
|
|
95
|
+
...executions.map(({ commandId, exitCode, testCount }) => ({
|
|
96
|
+
commandId,
|
|
97
|
+
exitCode,
|
|
98
|
+
testCount
|
|
99
|
+
})),
|
|
100
|
+
...(reviewGate === null ? [] : [reviewGate])
|
|
101
|
+
];
|
|
65
102
|
return {
|
|
66
|
-
status:
|
|
103
|
+
status: reviewGate !== null && reviewGate.exitCode !== 0
|
|
104
|
+
? "FAIL"
|
|
105
|
+
: aggregateVerificationStatus(executions),
|
|
67
106
|
results
|
|
68
107
|
};
|
|
69
108
|
}
|
|
@@ -36,6 +36,12 @@ export function loopLauncherPath(harness) {
|
|
|
36
36
|
export function loopLauncherArtifactId(harness) {
|
|
37
37
|
return `${harness}-loop-launcher`;
|
|
38
38
|
}
|
|
39
|
+
export function loopWindowsLauncherPath(harness) {
|
|
40
|
+
return `${loopRoot(harness)}/hooks/agent-ops-loop.ps1`;
|
|
41
|
+
}
|
|
42
|
+
export function loopWindowsLauncherArtifactId(harness) {
|
|
43
|
+
return `${harness}-loop-launcher-windows`;
|
|
44
|
+
}
|
|
39
45
|
function assertRuntimePath(runtimePath) {
|
|
40
46
|
if (runtimePath.length === 0 ||
|
|
41
47
|
runtimePath.length > 4096 ||
|
|
@@ -47,6 +53,9 @@ function assertRuntimePath(runtimePath) {
|
|
|
47
53
|
function shellQuote(value) {
|
|
48
54
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
49
55
|
}
|
|
56
|
+
function powershellQuote(value) {
|
|
57
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
58
|
+
}
|
|
50
59
|
function loopEntryPath(hookRuntimePath) {
|
|
51
60
|
assertRuntimePath(hookRuntimePath);
|
|
52
61
|
return `${hookRuntimePath.slice(0, -"hook-entry.js".length)}loop-entry.js`;
|
|
@@ -61,6 +70,17 @@ export function buildLoopLauncher(harness, hookRuntimePath) {
|
|
|
61
70
|
""
|
|
62
71
|
].join("\n");
|
|
63
72
|
}
|
|
73
|
+
export function buildPowerShellLoopLauncher(harness, hookRuntimePath) {
|
|
74
|
+
const runtimePath = loopEntryPath(hookRuntimePath);
|
|
75
|
+
return [
|
|
76
|
+
`# agent-ops: generated ${harness} Windows loop v1`,
|
|
77
|
+
"$ErrorActionPreference = \"Stop\"",
|
|
78
|
+
`$runtimePath = ${powershellQuote(runtimePath)}`,
|
|
79
|
+
`& node $runtimePath ${harness} @args`,
|
|
80
|
+
"exit $LASTEXITCODE",
|
|
81
|
+
""
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
64
84
|
function statePaths(harness) {
|
|
65
85
|
const root = loopRoot(harness);
|
|
66
86
|
return [
|
|
@@ -120,12 +140,22 @@ export function planLoopContribution(options) {
|
|
|
120
140
|
if (options.hookRuntimePath === undefined) {
|
|
121
141
|
throw new AgentOpsError("LOOP_RUNTIME_REQUIRED", "The loop profile requires the installed hook runtime path.");
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
const artifacts = harnesses.flatMap((harness) => [
|
|
144
|
+
{
|
|
125
145
|
id: loopLauncherArtifactId(harness),
|
|
126
146
|
path: loopLauncherPath(harness),
|
|
127
147
|
content: buildLoopLauncher(harness, options.hookRuntimePath ?? "")
|
|
128
|
-
}
|
|
148
|
+
},
|
|
149
|
+
...(harness === "claude"
|
|
150
|
+
? [{
|
|
151
|
+
id: loopWindowsLauncherArtifactId(harness),
|
|
152
|
+
path: loopWindowsLauncherPath(harness),
|
|
153
|
+
content: buildPowerShellLoopLauncher(harness, options.hookRuntimePath ?? "")
|
|
154
|
+
}]
|
|
155
|
+
: [])
|
|
156
|
+
]);
|
|
157
|
+
return {
|
|
158
|
+
artifacts,
|
|
129
159
|
blocks: [
|
|
130
160
|
{
|
|
131
161
|
id: LOOP_MARKER_ID,
|
|
@@ -136,7 +136,7 @@ const DESCRIPTORS = {
|
|
|
136
136
|
hookPath: ".claude/settings.json",
|
|
137
137
|
surfaces: claudeSurfaces,
|
|
138
138
|
ownSettingsKeys: ["hooks"],
|
|
139
|
-
buildHooks: (capabilities, runtimePath) => buildClaudeHookSettings(capabilities, runtimePath),
|
|
139
|
+
buildHooks: (capabilities, runtimePath, platform) => buildClaudeHookSettings(capabilities, runtimePath, platform),
|
|
140
140
|
mergeHooks: (existing, managed) => mergeClaudeSettings(existing, managed),
|
|
141
141
|
stripHooks: (existing) => stripClaudeManagedHooks(existing),
|
|
142
142
|
isManagedHandler: isClaudeManagedHandler,
|
|
@@ -244,7 +244,7 @@ export function managedRules(descriptor, context) {
|
|
|
244
244
|
""
|
|
245
245
|
];
|
|
246
246
|
if (context.capabilities.includes("rules")) {
|
|
247
|
-
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` (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.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "
|
|
247
|
+
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.", "", "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.", "");
|
|
248
248
|
}
|
|
249
249
|
if (context.capabilities.includes("lifecycle-summary")) {
|
|
250
250
|
lines.push("Advisory lifecycle summaries and local logs are informational. Advisory", "failures must remain fail-open and cannot become verification evidence.", "");
|
|
@@ -34,7 +34,7 @@ export function planHookRegistration(options) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
const path = options.path ?? hookRegistrationPath(options.harness, options.scope);
|
|
37
|
-
const managed = descriptor.control.buildHooks(options.capabilities, options.runtimePath);
|
|
37
|
+
const managed = descriptor.control.buildHooks(options.capabilities, options.runtimePath, options.platform);
|
|
38
38
|
const events = Object.keys(managed.hooks);
|
|
39
39
|
if (events.length === 0) {
|
|
40
40
|
return null;
|
|
@@ -2,7 +2,7 @@ import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
3
|
import { harnessDescriptor, harnessHookPath, routingBlockId, selectHarnessHookSurface, rulesArtifactId } from "./harness.js";
|
|
4
4
|
import { isOpencodePluginPath } from "../adapters/opencode/config.js";
|
|
5
|
-
import { LOOP_MARKER_ID, LOOP_MARKER_VERSION, loopIgnoreContent, loopLauncherArtifactId, loopLauncherPath, selectedLoopHarnesses } from "./codex-loop.js";
|
|
5
|
+
import { LOOP_MARKER_ID, LOOP_MARKER_VERSION, loopIgnoreContent, loopLauncherArtifactId, loopWindowsLauncherArtifactId, loopWindowsLauncherPath, loopLauncherPath, selectedLoopHarnesses } from "./codex-loop.js";
|
|
6
6
|
function expectedMarker(manifest, id, markerId) {
|
|
7
7
|
const descriptor = harnessDescriptor(id);
|
|
8
8
|
const markers = managedBlockMarkers(markerId, 1, "html");
|
|
@@ -76,10 +76,13 @@ export function assertSupportedManifestOwnership(manifest, root) {
|
|
|
76
76
|
const requiredArtifactPaths = new Set([
|
|
77
77
|
pathKey(".agent-ops/config.json")
|
|
78
78
|
]);
|
|
79
|
+
const optionalArtifactPaths = new Set();
|
|
79
80
|
const expectedMarkers = new Map();
|
|
80
81
|
const expectedMarkerPaths = new Set();
|
|
81
82
|
const loopHarnesses = selectedLoopHarnesses(harnesses);
|
|
82
|
-
const hasLoopArtifacts = manifest.artifacts.some(({ id }) => loopHarnesses.some((harness) => id === loopLauncherArtifactId(harness)
|
|
83
|
+
const hasLoopArtifacts = manifest.artifacts.some(({ id }) => loopHarnesses.some((harness) => id === loopLauncherArtifactId(harness) ||
|
|
84
|
+
(harness === "claude" &&
|
|
85
|
+
id === loopWindowsLauncherArtifactId(harness))));
|
|
83
86
|
const hasLoopMarker = manifest.markers.some(({ id }) => id === LOOP_MARKER_ID);
|
|
84
87
|
const hasLoop = hasLoopArtifacts || hasLoopMarker;
|
|
85
88
|
if (hasLoop &&
|
|
@@ -140,6 +143,16 @@ export function assertSupportedManifestOwnership(manifest, root) {
|
|
|
140
143
|
ids: new Set([loopLauncherArtifactId(harness)])
|
|
141
144
|
});
|
|
142
145
|
requiredArtifactPaths.add(pathKey(path));
|
|
146
|
+
if (harness === "claude") {
|
|
147
|
+
const windowsPath = loopWindowsLauncherPath(harness);
|
|
148
|
+
expectedArtifactPaths.set(pathKey(windowsPath), {
|
|
149
|
+
path: windowsPath,
|
|
150
|
+
ids: new Set([loopWindowsLauncherArtifactId(harness)])
|
|
151
|
+
});
|
|
152
|
+
// Older loop manifests predate the Windows launcher. Accept them so
|
|
153
|
+
// update and uninstall remain backward-compatible.
|
|
154
|
+
optionalArtifactPaths.add(pathKey(windowsPath));
|
|
155
|
+
}
|
|
143
156
|
}
|
|
144
157
|
expectedMarkerPaths.add(pathKey(".gitignore"));
|
|
145
158
|
expectedMarkers.set(LOOP_MARKER_ID, expectedLoopMarker(manifest));
|
|
@@ -148,13 +161,16 @@ export function assertSupportedManifestOwnership(manifest, root) {
|
|
|
148
161
|
? recordedOpencodePluginPath ??
|
|
149
162
|
harnessHookPath("opencode", manifest.scope, root)
|
|
150
163
|
: null;
|
|
151
|
-
|
|
152
|
-
expectedArtifactPaths.has(pathKey(opencodePluginPath))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
if (opencodePluginPath !== null &&
|
|
165
|
+
expectedArtifactPaths.has(pathKey(opencodePluginPath))) {
|
|
166
|
+
optionalArtifactPaths.add(pathKey(opencodePluginPath));
|
|
167
|
+
}
|
|
168
|
+
const optionalArtifactCount = [...optionalArtifactPaths].filter((path) => expectedArtifactPaths.has(path)).length;
|
|
169
|
+
const manifestArtifactPaths = new Set(manifest.artifacts.map(({ path }) => pathKey(path)));
|
|
170
|
+
if ([...requiredArtifactPaths].some((path) => !manifestArtifactPaths.has(path)) ||
|
|
171
|
+
manifest.artifacts.length < requiredArtifactPaths.size ||
|
|
172
|
+
manifest.artifacts.length >
|
|
173
|
+
requiredArtifactPaths.size + optionalArtifactCount ||
|
|
158
174
|
manifest.markers.length !== expectedMarkerPaths.size) {
|
|
159
175
|
throw manifestOwnershipError();
|
|
160
176
|
}
|
|
@@ -71,7 +71,7 @@ async function detectVerificationCommands(root) {
|
|
|
71
71
|
.filter((proposal) => proposal.confidence === "high")
|
|
72
72
|
.map(verificationCommandFromProposal);
|
|
73
73
|
}
|
|
74
|
-
function
|
|
74
|
+
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = []) {
|
|
75
75
|
// Absent reviewRoles means external review is disabled; an empty selection
|
|
76
76
|
// must therefore omit the field rather than write an empty array.
|
|
77
77
|
const reviewRoles = reviewTargets.length > 0
|
|
@@ -80,10 +80,10 @@ function formatConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
80
80
|
const verification = existing?.verification !== undefined &&
|
|
81
81
|
existing.verification.commands.length > 0
|
|
82
82
|
? existing.verification
|
|
83
|
-
: { commands: detectedCommands };
|
|
84
|
-
return
|
|
83
|
+
: { commands: [...detectedCommands] };
|
|
84
|
+
return {
|
|
85
85
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
86
|
-
profiles,
|
|
86
|
+
profiles: [...profiles],
|
|
87
87
|
verification,
|
|
88
88
|
features: existing?.features ?? {
|
|
89
89
|
stopVerification: {
|
|
@@ -92,8 +92,8 @@ function formatConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
92
92
|
},
|
|
93
93
|
pathMappings: existing?.pathMappings ?? [],
|
|
94
94
|
securityExceptions: existing?.securityExceptions ?? [],
|
|
95
|
-
...(reviewRoles === undefined ? {} : { reviewRoles })
|
|
96
|
-
}
|
|
95
|
+
...(reviewRoles === undefined ? {} : { reviewRoles: [...reviewRoles] })
|
|
96
|
+
};
|
|
97
97
|
}
|
|
98
98
|
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = []) {
|
|
99
99
|
const current = await readCurrentFile(root, CONFIG_PATH);
|
|
@@ -131,7 +131,8 @@ async function planConfig(root, profiles, existingManifest, suppliedConfig, revi
|
|
|
131
131
|
existingConfig.verification.commands.length === 0
|
|
132
132
|
? await detectVerificationCommands(root)
|
|
133
133
|
: [];
|
|
134
|
-
const
|
|
134
|
+
const config = buildConfig(profiles, existingConfig, reviewTargets, detectedCommands);
|
|
135
|
+
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
135
136
|
return {
|
|
136
137
|
operation: {
|
|
137
138
|
kind: "write",
|
|
@@ -145,6 +146,7 @@ async function planConfig(root, profiles, existingManifest, suppliedConfig, revi
|
|
|
145
146
|
hash: sha256(content),
|
|
146
147
|
owner: "agent-ops"
|
|
147
148
|
},
|
|
149
|
+
config,
|
|
148
150
|
detectedVerification: detectedCommands
|
|
149
151
|
};
|
|
150
152
|
}
|
|
@@ -502,6 +504,7 @@ export async function createInstallPlan(options) {
|
|
|
502
504
|
path: hookPath,
|
|
503
505
|
capabilities: resolved.capabilities,
|
|
504
506
|
runtimePath: options.hookRuntimePath,
|
|
507
|
+
platform: options.platform,
|
|
505
508
|
currentSource: current?.content ?? null
|
|
506
509
|
});
|
|
507
510
|
if (planned === null) {
|
|
@@ -567,6 +570,7 @@ export async function createInstallPlan(options) {
|
|
|
567
570
|
harness: options.harness,
|
|
568
571
|
profiles: resolved.profiles,
|
|
569
572
|
capabilities: resolved.capabilities,
|
|
573
|
+
config: config.config,
|
|
570
574
|
manifest,
|
|
571
575
|
operations,
|
|
572
576
|
detectedVerification: config.detectedVerification
|
|
@@ -30,8 +30,8 @@ export function smokeAvailabilityStatus(config) {
|
|
|
30
30
|
return config.verification.commands.length > 0 ? "PASS" : "UNKNOWN";
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* User-scope installs and projects without verification commands can remain
|
|
34
|
+
* untrusted without being broken. A stale binding is a real failure.
|
|
35
35
|
*/
|
|
36
36
|
export function repositoryTrustStatus(trust) {
|
|
37
37
|
return trust === "TRUSTED"
|