@kylecheng3146/agent-ops 0.1.4 → 0.1.5
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 +88 -13
- package/dist/packages/cli/src/args.js +39 -6
- package/dist/packages/cli/src/bin.js +33 -11
- package/dist/packages/cli/src/cli.js +6 -1
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +18 -16
- package/dist/packages/cli/src/commands/init.js +9 -5
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +13 -5
- package/dist/packages/cli/src/context.js +9 -3
- package/dist/packages/cli/src/hook-process.js +109 -7
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +27 -11
- package/dist/runtime/src/adapters/claude/config.js +0 -8
- package/dist/runtime/src/adapters/claude/events.js +26 -0
- package/dist/runtime/src/adapters/claude/output.js +6 -6
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +29 -11
- package/dist/runtime/src/adapters/codex/events.js +26 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/doctor.js +119 -9
- package/dist/runtime/src/install/harness.js +296 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +110 -34
- package/dist/runtime/src/install/plan.js +207 -28
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +8 -1
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +10 -3
- package/dist/runtime/src/install/update.js +8 -2
- package/dist/runtime/src/schema/validate.js +37 -18
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +83 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +66 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +77 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +57 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +4 -2
- package/schemas/config.schema.json +55 -1
- package/schemas/manifest.schema.json +7 -2
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- package/dist/runtime/src/review/codex-runner.js +0 -4
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
2
2
|
import { okEnvelope } from "../output.js";
|
|
3
3
|
import { formatOperationPlan } from "../plan-output.js";
|
|
4
|
+
import { toPublicUpdatePlan } from "../public-plan.js";
|
|
4
5
|
export function formatUpdatePlan(plan) {
|
|
5
6
|
return formatOperationPlan({
|
|
6
7
|
title: "Update plan",
|
|
7
8
|
metadata: [
|
|
8
9
|
`Target version: ${plan.targetVersion}`,
|
|
10
|
+
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
9
11
|
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
10
12
|
? "none"
|
|
11
13
|
: plan.migrationSteps
|
|
12
14
|
.map(({ fromVersion, toVersion }) => `${fromVersion} -> ${toVersion}`)
|
|
13
15
|
.join(", ")}`
|
|
14
16
|
],
|
|
15
|
-
operations: plan.installation.operations
|
|
17
|
+
operations: toPublicUpdatePlan(plan).installation.operations
|
|
16
18
|
});
|
|
17
19
|
}
|
|
18
20
|
function updateError(code, message, plan) {
|
|
19
21
|
return {
|
|
20
22
|
code,
|
|
21
23
|
status: "error",
|
|
22
|
-
data: { applied: false, plan, message },
|
|
24
|
+
data: { applied: false, plan: toPublicUpdatePlan(plan), message },
|
|
23
25
|
errors: [{ code, message }]
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -27,6 +29,9 @@ export async function runUpdateCommand(options) {
|
|
|
27
29
|
const plan = await createUpdatePlan({
|
|
28
30
|
root: options.root,
|
|
29
31
|
adapters: options.adapters,
|
|
32
|
+
...(options.args.harness === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { harness: options.args.harness }),
|
|
30
35
|
...(options.registry === undefined
|
|
31
36
|
? {}
|
|
32
37
|
: { registry: options.registry }),
|
|
@@ -35,12 +40,15 @@ export async function runUpdateCommand(options) {
|
|
|
35
40
|
: { targetVersion: options.targetVersion }),
|
|
36
41
|
...(options.hookRuntimePath === undefined
|
|
37
42
|
? {}
|
|
38
|
-
: { hookRuntimePath: options.hookRuntimePath })
|
|
43
|
+
: { hookRuntimePath: options.hookRuntimePath }),
|
|
44
|
+
...((options.hookTargets ?? options.args.hookTargets) === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { hookTargets: options.hookTargets ?? options.args.hookTargets })
|
|
39
47
|
});
|
|
40
48
|
if (options.args.dryRun) {
|
|
41
49
|
return okEnvelope("UPDATE_PLAN_READY", {
|
|
42
50
|
applied: false,
|
|
43
|
-
plan,
|
|
51
|
+
plan: toPublicUpdatePlan(plan),
|
|
44
52
|
message: "Update plan calculated; no files were changed.",
|
|
45
53
|
text: formatUpdatePlan(plan)
|
|
46
54
|
});
|
|
@@ -55,7 +63,7 @@ export async function runUpdateCommand(options) {
|
|
|
55
63
|
await applyUpdatePlan(options.root, plan);
|
|
56
64
|
return okEnvelope("UPDATE_APPLIED", {
|
|
57
65
|
applied: true,
|
|
58
|
-
plan,
|
|
66
|
+
plan: toPublicUpdatePlan(plan),
|
|
59
67
|
message: `Managed installation updated to ${plan.targetVersion}.`
|
|
60
68
|
});
|
|
61
69
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
5
|
+
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
4
6
|
import { loadConfigFile } from "../../../runtime/src/config/load.js";
|
|
5
7
|
import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
|
|
6
|
-
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
7
8
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
8
9
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
9
10
|
import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
10
11
|
export const DEFAULT_CONFIG = {
|
|
11
|
-
schemaVersion:
|
|
12
|
+
schemaVersion: 2,
|
|
12
13
|
profiles: [],
|
|
13
14
|
verification: { commands: [] },
|
|
15
|
+
features: {
|
|
16
|
+
stopVerification: {
|
|
17
|
+
enabled: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
14
20
|
pathMappings: [],
|
|
15
21
|
securityExceptions: []
|
|
16
22
|
};
|
|
@@ -91,7 +97,7 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
91
97
|
const binding = await calculateTrustBinding({
|
|
92
98
|
repositoryPath: root,
|
|
93
99
|
remoteUrl: repositoryRemoteUrl(root),
|
|
94
|
-
configHash:
|
|
100
|
+
configHash: calculateConfigHash(config),
|
|
95
101
|
runtimeHash: sha256(cliVersion)
|
|
96
102
|
});
|
|
97
103
|
return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
4
|
+
import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
|
|
5
|
+
import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
|
|
6
|
+
import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
|
|
7
|
+
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
1
8
|
import { runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
|
|
2
9
|
import { loadEffectiveConfig, repositoryTrust } from "./context.js";
|
|
3
|
-
const HARNESSES = new Set(
|
|
10
|
+
const HARNESSES = new Set(HARNESS_IDS);
|
|
4
11
|
const MAX_HOOK_INPUT_BYTES = 1024 * 1024;
|
|
12
|
+
const execFile = promisify(execFileCallback);
|
|
5
13
|
async function readStdin(stream) {
|
|
6
14
|
const chunks = [];
|
|
7
15
|
let total = 0;
|
|
@@ -17,28 +25,122 @@ async function readStdin(stream) {
|
|
|
17
25
|
}
|
|
18
26
|
return Buffer.concat(chunks, total).toString("utf8");
|
|
19
27
|
}
|
|
28
|
+
function parseInput(source) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(source);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function defaultGitRunner(root) {
|
|
37
|
+
return {
|
|
38
|
+
run: async (args) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await execFile("git", [...args], {
|
|
41
|
+
cwd: root,
|
|
42
|
+
encoding: "buffer"
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
stdout: result.stdout
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const failure = error;
|
|
51
|
+
return {
|
|
52
|
+
exitCode: failure.status ?? 1,
|
|
53
|
+
stdout: failure.stdout ?? new Uint8Array()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function stopVerificationOptions(input) {
|
|
60
|
+
const nativeRecursion = input.harness === "claude" &&
|
|
61
|
+
claudeStopRecursionMarker(input.rawInput);
|
|
62
|
+
const recursionActive = nativeRecursion || process.env[STOP_VERIFICATION_ENV] === "1";
|
|
63
|
+
const configHash = calculateConfigHash(input.config);
|
|
64
|
+
return {
|
|
65
|
+
confirmedConfig: true,
|
|
66
|
+
trusted: input.trusted,
|
|
67
|
+
scopeMapped: true,
|
|
68
|
+
recursionMarker: recursionActive,
|
|
69
|
+
configHash,
|
|
70
|
+
verify: async () => await new StopVerificationService({
|
|
71
|
+
root: input.root,
|
|
72
|
+
config: input.config,
|
|
73
|
+
trusted: input.trusted,
|
|
74
|
+
gitRunner: input.gitRunner,
|
|
75
|
+
processRunner: input.processRunner,
|
|
76
|
+
recursionActive,
|
|
77
|
+
configHash
|
|
78
|
+
}).verify()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function shouldBuildStopVerification(harness, event, config, rawInput) {
|
|
82
|
+
if (event !== "Stop" || !config.features.stopVerification.enabled) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
let normalized;
|
|
86
|
+
try {
|
|
87
|
+
normalized = harnessDescriptor(harness).runtime.normalizeInput(rawInput);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (normalized.event !== "stop") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return harnessDescriptor(harness).control.registrations.some(({ capability, support }) => capability === "optional-stop-verify" && support !== "unsupported");
|
|
96
|
+
}
|
|
20
97
|
/**
|
|
21
98
|
* Runs one hook invocation. Always resolves to exit code 0: a hook that
|
|
22
99
|
* cannot answer must never block the harness it advises.
|
|
23
100
|
*/
|
|
24
|
-
export async function runHookProcess(argv, io, cliVersion) {
|
|
101
|
+
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
25
102
|
const [harness, event] = argv;
|
|
26
103
|
if (harness === undefined ||
|
|
27
104
|
!HARNESSES.has(harness) ||
|
|
28
105
|
event === undefined ||
|
|
29
106
|
!HOOK_EVENTS.includes(event)) {
|
|
30
|
-
io.writeStderr(
|
|
107
|
+
io.writeStderr(`Usage: agent-ops hook <${HARNESS_IDS.join("|")}> <SessionStart|PreToolUse|Stop>\n`);
|
|
31
108
|
return 0;
|
|
32
109
|
}
|
|
33
110
|
try {
|
|
34
|
-
const root = process.cwd();
|
|
35
|
-
const config =
|
|
111
|
+
const root = dependencies.root ?? process.cwd();
|
|
112
|
+
const config = dependencies.loadConfig === undefined
|
|
113
|
+
? (await loadEffectiveConfig(root, "project")).config
|
|
114
|
+
: await dependencies.loadConfig(root);
|
|
115
|
+
const trustStatus = dependencies.trust === undefined
|
|
116
|
+
? await repositoryTrust(root, config, cliVersion)
|
|
117
|
+
: await dependencies.trust(root, config, cliVersion);
|
|
118
|
+
const rawInput = await readStdin(io.stdin);
|
|
119
|
+
const parsedInput = parseInput(rawInput);
|
|
120
|
+
const trusted = trustStatus === "TRUSTED";
|
|
121
|
+
const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
|
|
122
|
+
const processRunner = dependencies.processRunner ?? new NodeVerificationProcessRunner();
|
|
123
|
+
const stopVerification = shouldBuildStopVerification(harness, event, config, parsedInput)
|
|
124
|
+
? stopVerificationOptions({
|
|
125
|
+
harness: harness,
|
|
126
|
+
rawInput: parsedInput,
|
|
127
|
+
root,
|
|
128
|
+
config,
|
|
129
|
+
trusted,
|
|
130
|
+
gitRunner,
|
|
131
|
+
processRunner
|
|
132
|
+
})
|
|
133
|
+
: undefined;
|
|
36
134
|
const output = await runHookCommand({
|
|
37
135
|
harness: harness,
|
|
38
136
|
event: event,
|
|
39
|
-
stdin:
|
|
137
|
+
stdin: rawInput,
|
|
40
138
|
config,
|
|
41
|
-
trusted
|
|
139
|
+
trusted,
|
|
140
|
+
...(dependencies.advisory === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { advisory: dependencies.advisory }),
|
|
143
|
+
...(stopVerification === undefined ? {} : { stopVerification })
|
|
42
144
|
});
|
|
43
145
|
if (output.stdout.length > 0) {
|
|
44
146
|
io.writeStdout(output.stdout);
|
|
@@ -18,10 +18,15 @@ export function formatOperationPlan(options) {
|
|
|
18
18
|
for (const operation of options.operations) {
|
|
19
19
|
lines.push(`- ${operation.kind} ${escapeTerminalText(operation.path)}`, ` expected: ${operation.expectedHash ?? "<absent>"}`);
|
|
20
20
|
if (operation.kind === "write") {
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if ("contentHash" in operation) {
|
|
22
|
+
lines.push(` content: <opaque ${escapeTerminalText(operation.contentHash)}>`, ` summary: ${escapeTerminalText(operation.summary)}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(" content:", ...operation.content
|
|
26
|
+
.replace(/\n$/u, "")
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => ` ${escapeTerminalText(line)}`));
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
return `${lines.join("\n")}\n`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
2
|
+
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
3
|
+
const OPAQUE_SUMMARY = "Opaque managed settings content withheld.";
|
|
4
|
+
function publicPath(path) {
|
|
5
|
+
return redactSecrets(path);
|
|
6
|
+
}
|
|
7
|
+
export function disclosureOf(operation) {
|
|
8
|
+
return operation.disclosure ?? "full";
|
|
9
|
+
}
|
|
10
|
+
export function toPublicOperation(operation) {
|
|
11
|
+
const path = publicPath(operation.path);
|
|
12
|
+
if (operation.kind === "remove") {
|
|
13
|
+
return {
|
|
14
|
+
kind: "remove",
|
|
15
|
+
path,
|
|
16
|
+
expectedHash: operation.expectedHash
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (disclosureOf(operation) === "opaque") {
|
|
20
|
+
return {
|
|
21
|
+
kind: "write",
|
|
22
|
+
path,
|
|
23
|
+
expectedHash: operation.expectedHash,
|
|
24
|
+
contentHash: sha256(operation.content),
|
|
25
|
+
summary: redactSecrets(OPAQUE_SUMMARY)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: "write",
|
|
30
|
+
path,
|
|
31
|
+
expectedHash: operation.expectedHash,
|
|
32
|
+
content: operation.content
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function toPublicOperations(operations) {
|
|
36
|
+
return operations.map(toPublicOperation);
|
|
37
|
+
}
|
|
38
|
+
export function toPublicInstallPlan(plan) {
|
|
39
|
+
return {
|
|
40
|
+
scope: plan.scope,
|
|
41
|
+
harness: plan.harness,
|
|
42
|
+
profiles: plan.profiles,
|
|
43
|
+
capabilities: plan.capabilities,
|
|
44
|
+
manifest: plan.manifest,
|
|
45
|
+
operations: toPublicOperations(plan.operations)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function toPublicUpdatePlan(plan) {
|
|
49
|
+
return {
|
|
50
|
+
targetVersion: plan.targetVersion,
|
|
51
|
+
migrationSteps: plan.migrationSteps,
|
|
52
|
+
installation: toPublicInstallPlan(plan.installation)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function toPublicUninstallPlan(plan) {
|
|
56
|
+
return {
|
|
57
|
+
installed: plan.installed,
|
|
58
|
+
manifest: plan.manifest,
|
|
59
|
+
manifestHash: plan.manifestHash,
|
|
60
|
+
operations: toPublicOperations(plan.operations)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { CliArgumentError } from "./args.js";
|
|
2
|
+
import { HARNESS_IDS, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
|
|
2
3
|
import { selectOption, selectOptions } from "./ui.js";
|
|
3
4
|
const SCOPES = new Set(["project", "user"]);
|
|
4
|
-
const HARNESSES = new Set(["both", "claude", "codex"]);
|
|
5
5
|
const PROFILES = new Set(["advisory", "core", "guardrails"]);
|
|
6
|
+
const DEFAULT_HARNESS = [];
|
|
6
7
|
const SCOPE_CHOICES = [
|
|
7
8
|
{ label: "project", value: "project" },
|
|
8
9
|
{ label: "user", value: "user" }
|
|
9
10
|
];
|
|
10
|
-
const HARNESS_CHOICES =
|
|
11
|
-
{ label: "both", value: "both" },
|
|
12
|
-
{ label: "claude", value: "claude" },
|
|
13
|
-
{ label: "codex", value: "codex" }
|
|
14
|
-
];
|
|
11
|
+
const HARNESS_CHOICES = HARNESS_IDS.map((id) => ({ label: id, value: id }));
|
|
15
12
|
const PROFILE_CHOICES = [
|
|
16
13
|
{
|
|
17
14
|
label: "core",
|
|
@@ -26,10 +23,10 @@ const PROFILE_CHOICES = [
|
|
|
26
23
|
{
|
|
27
24
|
label: "guardrails",
|
|
28
25
|
value: "guardrails",
|
|
29
|
-
description: "Blocks high-confidence unsafe commands
|
|
26
|
+
description: "Blocks high-confidence unsafe commands; Stop verification is a separate opt-in feature."
|
|
30
27
|
}
|
|
31
28
|
];
|
|
32
|
-
const WIZARD_SUBTITLE = "Safe setup for Codex
|
|
29
|
+
const WIZARD_SUBTITLE = "Safe setup for Codex, Claude Code, and opencode with profile-driven rules, verification, and hooks.";
|
|
33
30
|
async function createPromptSession(io) {
|
|
34
31
|
if (!io.isTTY) {
|
|
35
32
|
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Missing init choices require an interactive terminal.");
|
|
@@ -60,6 +57,16 @@ function selectValue(raw, fallback, allowed, label) {
|
|
|
60
57
|
}
|
|
61
58
|
return value;
|
|
62
59
|
}
|
|
60
|
+
function selectHarness(raw) {
|
|
61
|
+
if (raw.trim() === "") {
|
|
62
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Choose at least one harness.");
|
|
63
|
+
}
|
|
64
|
+
const selection = resolveHarnessSelection(raw);
|
|
65
|
+
if (selection === null) {
|
|
66
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Harness must be a unique comma-separated list of ${HARNESS_IDS.join(", ")}.`);
|
|
67
|
+
}
|
|
68
|
+
return selection;
|
|
69
|
+
}
|
|
63
70
|
function selectProfiles(raw) {
|
|
64
71
|
const values = raw.trim() === ""
|
|
65
72
|
? ["core"]
|
|
@@ -81,14 +88,23 @@ export async function completeInitChoices(args, io) {
|
|
|
81
88
|
if (!io.isTTY) {
|
|
82
89
|
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Non-interactive init requires --scope, --harness, and at least one --profile.");
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
const canUseRawSelectors = io.input !== undefined &&
|
|
92
|
+
io.output !== undefined &&
|
|
93
|
+
typeof io.input.setRawMode === "function" &&
|
|
94
|
+
io.input.isTTY === true;
|
|
95
|
+
if (canUseRawSelectors) {
|
|
85
96
|
const selectorIo = {
|
|
86
97
|
input: io.input,
|
|
87
98
|
output: io.output
|
|
88
99
|
};
|
|
89
100
|
selectorIo.output.write(`${WIZARD_SUBTITLE}\n\n`);
|
|
90
101
|
const scope = args.scope ?? await selectOption("Scope", SCOPE_CHOICES, selectorIo);
|
|
91
|
-
const harness = args.harness ??
|
|
102
|
+
const harness = args.harness ??
|
|
103
|
+
await selectOptions("Harness (multi-select: ↑↓ move, Space toggle, Enter confirm)", HARNESS_CHOICES, selectorIo, [...DEFAULT_HARNESS], {
|
|
104
|
+
selectAll: true,
|
|
105
|
+
selectAllLabel: "Select all",
|
|
106
|
+
selectAllDescription: "Install for every supported harness."
|
|
107
|
+
});
|
|
92
108
|
const profiles = args.profiles.length > 0
|
|
93
109
|
? args.profiles
|
|
94
110
|
: await selectOptions("Profiles (multi-select: ↑↓ move, Space toggle, Enter confirm)", PROFILE_CHOICES, selectorIo, [], {
|
|
@@ -108,7 +124,7 @@ export async function completeInitChoices(args, io) {
|
|
|
108
124
|
const scope = args.scope ??
|
|
109
125
|
selectValue(await session.question("Scope (project/user) [project]: "), "project", SCOPES, "scope");
|
|
110
126
|
const harness = args.harness ??
|
|
111
|
-
|
|
127
|
+
selectHarness(await session.question(`Harness (${HARNESS_IDS.join(",")}): `));
|
|
112
128
|
const profiles = args.profiles.length > 0
|
|
113
129
|
? args.profiles
|
|
114
130
|
: selectProfiles(await session.question("Profiles (core,advisory,guardrails) [core]: "));
|
|
@@ -10,14 +10,6 @@ export function claudeSettingsTarget(scope) {
|
|
|
10
10
|
requiresWorkspaceTrust: false
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export function claudeRoutingBlock() {
|
|
14
|
-
return [
|
|
15
|
-
"## Loop Engineering",
|
|
16
|
-
"",
|
|
17
|
-
"Load `.agent-ops/CLAUDE.md` as concise project context.",
|
|
18
|
-
""
|
|
19
|
-
].join("\n");
|
|
20
|
-
}
|
|
21
13
|
function commandHook(event, runtimePath) {
|
|
22
14
|
return {
|
|
23
15
|
type: "command",
|
|
@@ -6,3 +6,29 @@ export const CLAUDE_SUPPORTED_EVENTS = [
|
|
|
6
6
|
export function claudeNonInteractiveTrust(printMode) {
|
|
7
7
|
return printMode ? "dialog-skipped" : "interactive-dialog";
|
|
8
8
|
}
|
|
9
|
+
export const CLAUDE_CAPABILITY_REGISTRATIONS = [
|
|
10
|
+
{
|
|
11
|
+
capability: "lifecycle-summary",
|
|
12
|
+
normalizedEvent: "session-start",
|
|
13
|
+
nativeEvent: "SessionStart",
|
|
14
|
+
surfaceId: "claude-settings",
|
|
15
|
+
support: "supported",
|
|
16
|
+
runtimeFailure: "fail-open"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capability: "command-policy",
|
|
20
|
+
normalizedEvent: "command",
|
|
21
|
+
nativeEvent: "PreToolUse",
|
|
22
|
+
surfaceId: "claude-settings",
|
|
23
|
+
support: "supported",
|
|
24
|
+
runtimeFailure: "fail-closed"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
capability: "optional-stop-verify",
|
|
28
|
+
normalizedEvent: "stop",
|
|
29
|
+
nativeEvent: "Stop",
|
|
30
|
+
surfaceId: "claude-settings",
|
|
31
|
+
support: "supported",
|
|
32
|
+
runtimeFailure: "fail-open"
|
|
33
|
+
}
|
|
34
|
+
];
|
|
@@ -6,6 +6,12 @@ function json(value) {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
export function claudeHookOutput(event, result) {
|
|
9
|
+
if (event === "Stop" && result.evidence !== undefined) {
|
|
10
|
+
return json({
|
|
11
|
+
systemMessage: `agent-ops: ${result.code}`,
|
|
12
|
+
evidence: result.evidence
|
|
13
|
+
});
|
|
14
|
+
}
|
|
9
15
|
if (result.action === "continue" && result.status === "PASS") {
|
|
10
16
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
11
17
|
}
|
|
@@ -18,12 +24,6 @@ export function claudeHookOutput(event, result) {
|
|
|
18
24
|
}
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
|
-
if (event === "Stop" && result.action === "block") {
|
|
22
|
-
return json({
|
|
23
|
-
decision: "block",
|
|
24
|
-
reason: result.code
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
27
|
return json({
|
|
28
28
|
systemMessage: `agent-ops: ${result.code}`
|
|
29
29
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const CLAUDE_SURFACE_IDS = {
|
|
2
|
+
settings: "claude-settings",
|
|
3
|
+
projectLocal: "project-local",
|
|
4
|
+
userSettings: "user-settings",
|
|
5
|
+
plugin: "plugin"
|
|
6
|
+
};
|
|
7
|
+
export function claudeSurfaces(scope) {
|
|
8
|
+
if (scope === "project") {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
id: CLAUDE_SURFACE_IDS.settings,
|
|
12
|
+
path: ".claude/settings.json",
|
|
13
|
+
scope: "project",
|
|
14
|
+
access: "managed-default",
|
|
15
|
+
representation: "json"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: CLAUDE_SURFACE_IDS.projectLocal,
|
|
19
|
+
path: ".claude/settings.local.json",
|
|
20
|
+
scope: "project",
|
|
21
|
+
access: "managed-opt-in",
|
|
22
|
+
representation: "json"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: CLAUDE_SURFACE_IDS.userSettings,
|
|
26
|
+
path: "~/.claude/settings.json",
|
|
27
|
+
scope: "external",
|
|
28
|
+
access: "inspect-only",
|
|
29
|
+
representation: "json"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: CLAUDE_SURFACE_IDS.plugin,
|
|
33
|
+
path: "~/.claude/plugins",
|
|
34
|
+
scope: "external",
|
|
35
|
+
access: "inspect-only",
|
|
36
|
+
representation: "javascript"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
id: CLAUDE_SURFACE_IDS.settings,
|
|
43
|
+
path: ".claude/settings.json",
|
|
44
|
+
scope: "user",
|
|
45
|
+
access: "managed-default",
|
|
46
|
+
representation: "json"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: CLAUDE_SURFACE_IDS.projectLocal,
|
|
50
|
+
path: "project/.claude/settings.local.json",
|
|
51
|
+
scope: "external",
|
|
52
|
+
access: "inspect-only",
|
|
53
|
+
representation: "json"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: CLAUDE_SURFACE_IDS.userSettings,
|
|
57
|
+
path: "project/.claude/settings.json",
|
|
58
|
+
scope: "external",
|
|
59
|
+
access: "inspect-only",
|
|
60
|
+
representation: "json"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: CLAUDE_SURFACE_IDS.plugin,
|
|
64
|
+
path: "~/.claude/plugins",
|
|
65
|
+
scope: "external",
|
|
66
|
+
access: "inspect-only",
|
|
67
|
+
representation: "javascript"
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { AgentOpsError } from "../../fs/paths.js";
|
|
2
|
-
const
|
|
2
|
+
export const CODEX_MANAGED_MARKER = "--managed-by=agent-ops";
|
|
3
|
+
/**
|
|
4
|
+
* Releases up to 0.1.4 registered a bare `agent-ops` command resolved through
|
|
5
|
+
* PATH. Detection still recognizes it so an update replaces it instead of
|
|
6
|
+
* leaving the hijackable handler behind.
|
|
7
|
+
*/
|
|
8
|
+
const LEGACY_COMMAND_PREFIX = "agent-ops hook codex ";
|
|
3
9
|
function isRecord(value) {
|
|
4
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
11
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
12
|
+
function assertRuntimePath(runtimePath) {
|
|
13
|
+
if (runtimePath.length === 0 ||
|
|
14
|
+
runtimePath.length > 4096 ||
|
|
15
|
+
runtimePath.includes("\0") ||
|
|
16
|
+
// The command is a single shell string, so a quote would break the
|
|
17
|
+
// argument boundary the surrounding quotes establish.
|
|
18
|
+
runtimePath.includes('"')) {
|
|
19
|
+
throw new AgentOpsError("CODEX_HOOK_PATH_INVALID", "Codex hook runtime path is invalid.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function commandHook(event, runtimePath) {
|
|
23
|
+
const command = `node "${runtimePath}" codex ${event} ${CODEX_MANAGED_MARKER}`;
|
|
8
24
|
return {
|
|
9
25
|
type: "command",
|
|
10
26
|
command,
|
|
@@ -13,10 +29,10 @@ function commandHook(event) {
|
|
|
13
29
|
statusMessage: `Running agent-ops ${event}`
|
|
14
30
|
};
|
|
15
31
|
}
|
|
16
|
-
function matcherGroup(event) {
|
|
32
|
+
function matcherGroup(event, runtimePath) {
|
|
17
33
|
return {
|
|
18
34
|
...(event === "PreToolUse" ? { matcher: "^Bash$" } : {}),
|
|
19
|
-
hooks: [commandHook(event)]
|
|
35
|
+
hooks: [commandHook(event, runtimePath)]
|
|
20
36
|
};
|
|
21
37
|
}
|
|
22
38
|
export function codexHookTarget(scope) {
|
|
@@ -26,26 +42,28 @@ export function codexHookTarget(scope) {
|
|
|
26
42
|
requiresProjectTrust: scope === "project"
|
|
27
43
|
};
|
|
28
44
|
}
|
|
29
|
-
export function buildCodexHookConfig(capabilities) {
|
|
45
|
+
export function buildCodexHookConfig(capabilities, runtimePath) {
|
|
46
|
+
assertRuntimePath(runtimePath);
|
|
30
47
|
const hooks = {};
|
|
31
48
|
if (capabilities.includes("lifecycle-summary")) {
|
|
32
|
-
hooks.SessionStart = [matcherGroup("SessionStart")];
|
|
49
|
+
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
33
50
|
}
|
|
34
51
|
if (capabilities.includes("command-policy")) {
|
|
35
|
-
hooks.PreToolUse = [matcherGroup("PreToolUse")];
|
|
52
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
36
53
|
}
|
|
37
54
|
if (capabilities.includes("optional-stop-verify")) {
|
|
38
|
-
hooks.Stop = [matcherGroup("Stop")];
|
|
55
|
+
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
39
56
|
}
|
|
40
57
|
return {
|
|
41
|
-
description: "
|
|
58
|
+
description: "agent-ops lifecycle hooks.",
|
|
42
59
|
hooks
|
|
43
60
|
};
|
|
44
61
|
}
|
|
45
62
|
function isOwnedHandler(hook) {
|
|
46
63
|
return (isRecord(hook) &&
|
|
47
64
|
typeof hook.command === "string" &&
|
|
48
|
-
hook.command.
|
|
65
|
+
(hook.command.includes(CODEX_MANAGED_MARKER) ||
|
|
66
|
+
hook.command.startsWith(LEGACY_COMMAND_PREFIX)));
|
|
49
67
|
}
|
|
50
68
|
function withoutOwnedHandlers(value) {
|
|
51
69
|
if (!isRecord(value) || !Array.isArray(value.hooks)) {
|
|
@@ -6,3 +6,29 @@ export const CODEX_SUPPORTED_EVENTS = [
|
|
|
6
6
|
export function codexMatcherSupport(event) {
|
|
7
7
|
return event === "PreToolUse" ? "tool-name" : "unsupported";
|
|
8
8
|
}
|
|
9
|
+
export const CODEX_CAPABILITY_REGISTRATIONS = [
|
|
10
|
+
{
|
|
11
|
+
capability: "lifecycle-summary",
|
|
12
|
+
normalizedEvent: "session-start",
|
|
13
|
+
nativeEvent: "SessionStart",
|
|
14
|
+
surfaceId: "codex-hooks",
|
|
15
|
+
support: "supported",
|
|
16
|
+
runtimeFailure: "fail-open"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capability: "command-policy",
|
|
20
|
+
normalizedEvent: "command",
|
|
21
|
+
nativeEvent: "PreToolUse",
|
|
22
|
+
surfaceId: "codex-hooks",
|
|
23
|
+
support: "unknown",
|
|
24
|
+
runtimeFailure: "native-unknown"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
capability: "optional-stop-verify",
|
|
28
|
+
normalizedEvent: "stop",
|
|
29
|
+
nativeEvent: "Stop",
|
|
30
|
+
surfaceId: "codex-hooks",
|
|
31
|
+
support: "unsupported",
|
|
32
|
+
runtimeFailure: "fail-open"
|
|
33
|
+
}
|
|
34
|
+
];
|