@kylecheng3146/agent-ops 0.1.4 → 0.1.6
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 +164 -13
- package/dist/packages/cli/src/args.js +40 -7
- package/dist/packages/cli/src/bin.js +35 -11
- package/dist/packages/cli/src/cli.js +7 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +33 -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 +16 -5
- package/dist/packages/cli/src/context.js +69 -3
- package/dist/packages/cli/src/hook-process.js +231 -16
- package/dist/packages/cli/src/loop-entry.js +8 -0
- 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 +36 -15
- package/dist/runtime/src/adapters/claude/config.js +57 -19
- package/dist/runtime/src/adapters/claude/events.js +33 -0
- package/dist/runtime/src/adapters/claude/output.js +8 -7
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +66 -13
- package/dist/runtime/src/adapters/codex/events.js +33 -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/managed-block.js +35 -18
- 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/codex-loop.js +439 -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/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +182 -14
- package/dist/runtime/src/install/harness.js +294 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +146 -35
- package/dist/runtime/src/install/plan.js +276 -31
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +12 -3
- 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 +11 -4
- package/dist/runtime/src/install/update.js +13 -3
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/schema/validate.js +45 -19
- 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 +159 -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 +104 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +145 -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 +89 -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 +56 -2
- package/schemas/manifest.schema.json +19 -3
- 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
|
@@ -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
|
|
5
|
-
const
|
|
5
|
+
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
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,15 @@ 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."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: "loop",
|
|
30
|
+
value: "loop",
|
|
31
|
+
description: "Installs the shared Codex and Claude Code local loop with secret and destructive-command interception."
|
|
30
32
|
}
|
|
31
33
|
];
|
|
32
|
-
const WIZARD_SUBTITLE = "Safe setup for Codex
|
|
34
|
+
const WIZARD_SUBTITLE = "Safe setup for Codex, Claude Code, and opencode with profile-driven rules, verification, and hooks.";
|
|
33
35
|
async function createPromptSession(io) {
|
|
34
36
|
if (!io.isTTY) {
|
|
35
37
|
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Missing init choices require an interactive terminal.");
|
|
@@ -60,6 +62,16 @@ function selectValue(raw, fallback, allowed, label) {
|
|
|
60
62
|
}
|
|
61
63
|
return value;
|
|
62
64
|
}
|
|
65
|
+
function selectHarness(raw) {
|
|
66
|
+
if (raw.trim() === "") {
|
|
67
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Choose at least one harness.");
|
|
68
|
+
}
|
|
69
|
+
const selection = resolveHarnessSelection(raw);
|
|
70
|
+
if (selection === null) {
|
|
71
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Harness must be a unique comma-separated list of ${HARNESS_IDS.join(", ")}.`);
|
|
72
|
+
}
|
|
73
|
+
return selection;
|
|
74
|
+
}
|
|
63
75
|
function selectProfiles(raw) {
|
|
64
76
|
const values = raw.trim() === ""
|
|
65
77
|
? ["core"]
|
|
@@ -67,7 +79,7 @@ function selectProfiles(raw) {
|
|
|
67
79
|
if (values.length === 0 ||
|
|
68
80
|
values.some((value) => !PROFILES.has(value)) ||
|
|
69
81
|
new Set(values).size !== values.length) {
|
|
70
|
-
throw new CliArgumentError("CLI_INVALID_VALUE", "Profiles must be a unique comma-separated list of core, advisory, or
|
|
82
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Profiles must be a unique comma-separated list of core, advisory, guardrails, or loop.");
|
|
71
83
|
}
|
|
72
84
|
return values;
|
|
73
85
|
}
|
|
@@ -81,20 +93,29 @@ export async function completeInitChoices(args, io) {
|
|
|
81
93
|
if (!io.isTTY) {
|
|
82
94
|
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Non-interactive init requires --scope, --harness, and at least one --profile.");
|
|
83
95
|
}
|
|
84
|
-
|
|
96
|
+
const canUseRawSelectors = io.input !== undefined &&
|
|
97
|
+
io.output !== undefined &&
|
|
98
|
+
typeof io.input.setRawMode === "function" &&
|
|
99
|
+
io.input.isTTY === true;
|
|
100
|
+
if (canUseRawSelectors) {
|
|
85
101
|
const selectorIo = {
|
|
86
102
|
input: io.input,
|
|
87
103
|
output: io.output
|
|
88
104
|
};
|
|
89
105
|
selectorIo.output.write(`${WIZARD_SUBTITLE}\n\n`);
|
|
90
106
|
const scope = args.scope ?? await selectOption("Scope", SCOPE_CHOICES, selectorIo);
|
|
91
|
-
const harness = args.harness ??
|
|
107
|
+
const harness = args.harness ??
|
|
108
|
+
await selectOptions("Harness (multi-select: ↑↓ move, Space toggle, Enter confirm)", HARNESS_CHOICES, selectorIo, [...DEFAULT_HARNESS], {
|
|
109
|
+
selectAll: true,
|
|
110
|
+
selectAllLabel: "Select all",
|
|
111
|
+
selectAllDescription: "Install for every supported harness."
|
|
112
|
+
});
|
|
92
113
|
const profiles = args.profiles.length > 0
|
|
93
114
|
? args.profiles
|
|
94
115
|
: await selectOptions("Profiles (multi-select: ↑↓ move, Space toggle, Enter confirm)", PROFILE_CHOICES, selectorIo, [], {
|
|
95
116
|
selectAll: true,
|
|
96
117
|
selectAllLabel: "Select all",
|
|
97
|
-
selectAllDescription: "Enable core, advisory, and
|
|
118
|
+
selectAllDescription: "Enable core, advisory, guardrails, and loop together."
|
|
98
119
|
});
|
|
99
120
|
return {
|
|
100
121
|
...args,
|
|
@@ -108,10 +129,10 @@ export async function completeInitChoices(args, io) {
|
|
|
108
129
|
const scope = args.scope ??
|
|
109
130
|
selectValue(await session.question("Scope (project/user) [project]: "), "project", SCOPES, "scope");
|
|
110
131
|
const harness = args.harness ??
|
|
111
|
-
|
|
132
|
+
selectHarness(await session.question(`Harness (${HARNESS_IDS.join(",")}): `));
|
|
112
133
|
const profiles = args.profiles.length > 0
|
|
113
134
|
? args.profiles
|
|
114
|
-
: selectProfiles(await session.question("Profiles (core,advisory,guardrails) [core]: "));
|
|
135
|
+
: selectProfiles(await session.question("Profiles (core,advisory,guardrails,loop) [core]: "));
|
|
115
136
|
return {
|
|
116
137
|
...args,
|
|
117
138
|
scope,
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { AgentOpsError } from "../../fs/paths.js";
|
|
2
|
+
const CLAUDE_LOOP_EVENTS = [
|
|
3
|
+
"SessionStart",
|
|
4
|
+
"UserPromptSubmit",
|
|
5
|
+
"PreToolUse",
|
|
6
|
+
"PermissionRequest",
|
|
7
|
+
"PostToolUse",
|
|
8
|
+
"PreCompact",
|
|
9
|
+
"PostCompact",
|
|
10
|
+
"SubagentStart",
|
|
11
|
+
"SubagentStop"
|
|
12
|
+
];
|
|
13
|
+
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
14
|
+
const CLAUDE_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.sh";
|
|
2
15
|
export function claudeSettingsTarget(scope) {
|
|
3
16
|
return scope === "project"
|
|
4
17
|
? {
|
|
@@ -10,14 +23,6 @@ export function claudeSettingsTarget(scope) {
|
|
|
10
23
|
requiresWorkspaceTrust: false
|
|
11
24
|
};
|
|
12
25
|
}
|
|
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
26
|
function commandHook(event, runtimePath) {
|
|
22
27
|
return {
|
|
23
28
|
type: "command",
|
|
@@ -26,7 +31,7 @@ function commandHook(event, runtimePath) {
|
|
|
26
31
|
runtimePath,
|
|
27
32
|
"claude",
|
|
28
33
|
event,
|
|
29
|
-
|
|
34
|
+
CLAUDE_HOOK_MARKER
|
|
30
35
|
],
|
|
31
36
|
timeout: 30
|
|
32
37
|
};
|
|
@@ -37,6 +42,25 @@ function matcherGroup(event, runtimePath) {
|
|
|
37
42
|
hooks: [commandHook(event, runtimePath)]
|
|
38
43
|
};
|
|
39
44
|
}
|
|
45
|
+
function loopMatcherGroup(event) {
|
|
46
|
+
return {
|
|
47
|
+
...(event === "PreToolUse" || event === "PermissionRequest"
|
|
48
|
+
? { matcher: "Bash" }
|
|
49
|
+
: {}),
|
|
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
|
+
]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
40
64
|
export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
41
65
|
if (runtimePath.length === 0 ||
|
|
42
66
|
runtimePath.length > 4096 ||
|
|
@@ -44,11 +68,18 @@ export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
|
44
68
|
throw new AgentOpsError("CLAUDE_HOOK_PATH_INVALID", "Claude hook runtime path is invalid.");
|
|
45
69
|
}
|
|
46
70
|
const hooks = {};
|
|
47
|
-
if (capabilities.includes("
|
|
48
|
-
|
|
71
|
+
if (capabilities.includes("project-loop")) {
|
|
72
|
+
for (const event of CLAUDE_LOOP_EVENTS) {
|
|
73
|
+
hooks[event] = [loopMatcherGroup(event)];
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
else {
|
|
77
|
+
if (capabilities.includes("lifecycle-summary")) {
|
|
78
|
+
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
79
|
+
}
|
|
80
|
+
if (capabilities.includes("command-policy")) {
|
|
81
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
82
|
+
}
|
|
52
83
|
}
|
|
53
84
|
if (capabilities.includes("optional-stop-verify")) {
|
|
54
85
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
@@ -58,19 +89,26 @@ export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
|
58
89
|
function isRecord(value) {
|
|
59
90
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
91
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Matches only the two command shapes agent-ops actually generates. This is
|
|
94
|
+
* reused by installation inspection so a foreign hook cannot masquerade as
|
|
95
|
+
* ours merely by carrying the marker string.
|
|
96
|
+
*/
|
|
97
|
+
export function isClaudeManagedHandler(handler) {
|
|
98
|
+
if (!isRecord(handler) || !Array.isArray(handler.args)) {
|
|
65
99
|
return false;
|
|
66
100
|
}
|
|
67
|
-
return handler.
|
|
101
|
+
return ((handler.command === "node" &&
|
|
102
|
+
handler.args[3] === CLAUDE_HOOK_MARKER) ||
|
|
103
|
+
(handler.command === "bash" &&
|
|
104
|
+
handler.args[0] === CLAUDE_LOOP_LAUNCHER &&
|
|
105
|
+
handler.args[2] === CLAUDE_HOOK_MARKER));
|
|
68
106
|
}
|
|
69
107
|
function withoutOwnedHandlers(value) {
|
|
70
108
|
if (!isRecord(value) || !Array.isArray(value.hooks)) {
|
|
71
109
|
return value;
|
|
72
110
|
}
|
|
73
|
-
const hooks = value.hooks.filter((handler) => !
|
|
111
|
+
const hooks = value.hooks.filter((handler) => !isClaudeManagedHandler(handler));
|
|
74
112
|
return hooks.length === 0 ? null : { ...value, hooks };
|
|
75
113
|
}
|
|
76
114
|
function hookRecord(settings) {
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
export const CLAUDE_SUPPORTED_EVENTS = [
|
|
2
2
|
"SessionStart",
|
|
3
|
+
"UserPromptSubmit",
|
|
3
4
|
"PreToolUse",
|
|
5
|
+
"PermissionRequest",
|
|
6
|
+
"PostToolUse",
|
|
7
|
+
"PreCompact",
|
|
8
|
+
"PostCompact",
|
|
9
|
+
"SubagentStart",
|
|
10
|
+
"SubagentStop",
|
|
4
11
|
"Stop"
|
|
5
12
|
];
|
|
6
13
|
export function claudeNonInteractiveTrust(printMode) {
|
|
7
14
|
return printMode ? "dialog-skipped" : "interactive-dialog";
|
|
8
15
|
}
|
|
16
|
+
export const CLAUDE_CAPABILITY_REGISTRATIONS = [
|
|
17
|
+
{
|
|
18
|
+
capability: "lifecycle-summary",
|
|
19
|
+
normalizedEvent: "session-start",
|
|
20
|
+
nativeEvent: "SessionStart",
|
|
21
|
+
surfaceId: "claude-settings",
|
|
22
|
+
support: "supported",
|
|
23
|
+
runtimeFailure: "fail-open"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capability: "command-policy",
|
|
27
|
+
normalizedEvent: "command",
|
|
28
|
+
nativeEvent: "PreToolUse",
|
|
29
|
+
surfaceId: "claude-settings",
|
|
30
|
+
support: "supported",
|
|
31
|
+
runtimeFailure: "fail-closed"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
capability: "optional-stop-verify",
|
|
35
|
+
normalizedEvent: "stop",
|
|
36
|
+
nativeEvent: "Stop",
|
|
37
|
+
surfaceId: "claude-settings",
|
|
38
|
+
support: "supported",
|
|
39
|
+
runtimeFailure: "fail-open"
|
|
40
|
+
}
|
|
41
|
+
];
|
|
@@ -6,6 +6,13 @@ function json(value) {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
export function claudeHookOutput(event, result) {
|
|
9
|
+
const denialReason = result.remedy === undefined ? result.code : `${result.code}: ${result.remedy}`;
|
|
10
|
+
if (event === "Stop" && result.evidence !== undefined) {
|
|
11
|
+
return json({
|
|
12
|
+
systemMessage: `agent-ops: ${result.code}`,
|
|
13
|
+
evidence: result.evidence
|
|
14
|
+
});
|
|
15
|
+
}
|
|
9
16
|
if (result.action === "continue" && result.status === "PASS") {
|
|
10
17
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
11
18
|
}
|
|
@@ -14,16 +21,10 @@ export function claudeHookOutput(event, result) {
|
|
|
14
21
|
hookSpecificOutput: {
|
|
15
22
|
hookEventName: "PreToolUse",
|
|
16
23
|
permissionDecision: "deny",
|
|
17
|
-
permissionDecisionReason:
|
|
24
|
+
permissionDecisionReason: denialReason
|
|
18
25
|
}
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
|
-
if (event === "Stop" && result.action === "block") {
|
|
22
|
-
return json({
|
|
23
|
-
decision: "block",
|
|
24
|
-
reason: result.code
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
28
|
return json({
|
|
28
29
|
systemMessage: `agent-ops: ${result.code}`
|
|
29
30
|
});
|
|
@@ -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,37 @@
|
|
|
1
1
|
import { AgentOpsError } from "../../fs/paths.js";
|
|
2
|
-
const
|
|
2
|
+
export const CODEX_MANAGED_MARKER = "--managed-by=agent-ops";
|
|
3
|
+
const CODEX_LOOP_EVENTS = [
|
|
4
|
+
"SessionStart",
|
|
5
|
+
"UserPromptSubmit",
|
|
6
|
+
"PreToolUse",
|
|
7
|
+
"PermissionRequest",
|
|
8
|
+
"PostToolUse",
|
|
9
|
+
"PreCompact",
|
|
10
|
+
"PostCompact",
|
|
11
|
+
"SubagentStart",
|
|
12
|
+
"SubagentStop"
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Releases up to 0.1.4 registered a bare `agent-ops` command resolved through
|
|
16
|
+
* PATH. Detection still recognizes it so an update replaces it instead of
|
|
17
|
+
* leaving the hijackable handler behind.
|
|
18
|
+
*/
|
|
19
|
+
const LEGACY_COMMAND_PREFIX = "agent-ops hook codex ";
|
|
3
20
|
function isRecord(value) {
|
|
4
21
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
22
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
23
|
+
function assertRuntimePath(runtimePath) {
|
|
24
|
+
if (runtimePath.length === 0 ||
|
|
25
|
+
runtimePath.length > 4096 ||
|
|
26
|
+
runtimePath.includes("\0") ||
|
|
27
|
+
// The command is a single shell string, so a quote would break the
|
|
28
|
+
// argument boundary the surrounding quotes establish.
|
|
29
|
+
runtimePath.includes('"')) {
|
|
30
|
+
throw new AgentOpsError("CODEX_HOOK_PATH_INVALID", "Codex hook runtime path is invalid.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function commandHook(event, runtimePath) {
|
|
34
|
+
const command = `node "${runtimePath}" codex ${event} ${CODEX_MANAGED_MARKER}`;
|
|
8
35
|
return {
|
|
9
36
|
type: "command",
|
|
10
37
|
command,
|
|
@@ -13,10 +40,27 @@ function commandHook(event) {
|
|
|
13
40
|
statusMessage: `Running agent-ops ${event}`
|
|
14
41
|
};
|
|
15
42
|
}
|
|
16
|
-
function matcherGroup(event) {
|
|
43
|
+
function matcherGroup(event, runtimePath) {
|
|
17
44
|
return {
|
|
18
45
|
...(event === "PreToolUse" ? { matcher: "^Bash$" } : {}),
|
|
19
|
-
hooks: [commandHook(event)]
|
|
46
|
+
hooks: [commandHook(event, runtimePath)]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function loopMatcherGroup(event) {
|
|
50
|
+
const command = `bash "$(git rev-parse --show-toplevel)/.codex/hooks/agent-ops-loop.sh" ${event} ${CODEX_MANAGED_MARKER}`;
|
|
51
|
+
return {
|
|
52
|
+
...(event === "PreToolUse" || event === "PermissionRequest"
|
|
53
|
+
? { matcher: "^Bash$" }
|
|
54
|
+
: {}),
|
|
55
|
+
hooks: [
|
|
56
|
+
{
|
|
57
|
+
type: "command",
|
|
58
|
+
command,
|
|
59
|
+
commandWindows: command,
|
|
60
|
+
timeout: 30,
|
|
61
|
+
statusMessage: `Running agent-ops ${event}`
|
|
62
|
+
}
|
|
63
|
+
]
|
|
20
64
|
};
|
|
21
65
|
}
|
|
22
66
|
export function codexHookTarget(scope) {
|
|
@@ -26,26 +70,35 @@ export function codexHookTarget(scope) {
|
|
|
26
70
|
requiresProjectTrust: scope === "project"
|
|
27
71
|
};
|
|
28
72
|
}
|
|
29
|
-
export function buildCodexHookConfig(capabilities) {
|
|
73
|
+
export function buildCodexHookConfig(capabilities, runtimePath) {
|
|
74
|
+
assertRuntimePath(runtimePath);
|
|
30
75
|
const hooks = {};
|
|
31
|
-
if (capabilities.includes("
|
|
32
|
-
|
|
76
|
+
if (capabilities.includes("project-loop")) {
|
|
77
|
+
for (const event of CODEX_LOOP_EVENTS) {
|
|
78
|
+
hooks[event] = [loopMatcherGroup(event)];
|
|
79
|
+
}
|
|
33
80
|
}
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
else {
|
|
82
|
+
if (capabilities.includes("lifecycle-summary")) {
|
|
83
|
+
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
84
|
+
}
|
|
85
|
+
if (capabilities.includes("command-policy")) {
|
|
86
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
87
|
+
}
|
|
36
88
|
}
|
|
37
89
|
if (capabilities.includes("optional-stop-verify")) {
|
|
38
|
-
hooks.Stop = [matcherGroup("Stop")];
|
|
90
|
+
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
39
91
|
}
|
|
40
92
|
return {
|
|
41
|
-
description: "
|
|
93
|
+
description: "agent-ops lifecycle hooks.",
|
|
42
94
|
hooks
|
|
43
95
|
};
|
|
44
96
|
}
|
|
45
97
|
function isOwnedHandler(hook) {
|
|
46
98
|
return (isRecord(hook) &&
|
|
47
99
|
typeof hook.command === "string" &&
|
|
48
|
-
hook.command.
|
|
100
|
+
(hook.command.includes(CODEX_MANAGED_MARKER) ||
|
|
101
|
+
hook.command.startsWith(LEGACY_COMMAND_PREFIX)));
|
|
49
102
|
}
|
|
50
103
|
function withoutOwnedHandlers(value) {
|
|
51
104
|
if (!isRecord(value) || !Array.isArray(value.hooks)) {
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
export const CODEX_SUPPORTED_EVENTS = [
|
|
2
2
|
"SessionStart",
|
|
3
|
+
"UserPromptSubmit",
|
|
3
4
|
"PreToolUse",
|
|
5
|
+
"PermissionRequest",
|
|
6
|
+
"PostToolUse",
|
|
7
|
+
"PreCompact",
|
|
8
|
+
"PostCompact",
|
|
9
|
+
"SubagentStart",
|
|
10
|
+
"SubagentStop",
|
|
4
11
|
"Stop"
|
|
5
12
|
];
|
|
6
13
|
export function codexMatcherSupport(event) {
|
|
7
14
|
return event === "PreToolUse" ? "tool-name" : "unsupported";
|
|
8
15
|
}
|
|
16
|
+
export const CODEX_CAPABILITY_REGISTRATIONS = [
|
|
17
|
+
{
|
|
18
|
+
capability: "lifecycle-summary",
|
|
19
|
+
normalizedEvent: "session-start",
|
|
20
|
+
nativeEvent: "SessionStart",
|
|
21
|
+
surfaceId: "codex-hooks",
|
|
22
|
+
support: "supported",
|
|
23
|
+
runtimeFailure: "fail-open"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capability: "command-policy",
|
|
27
|
+
normalizedEvent: "command",
|
|
28
|
+
nativeEvent: "PreToolUse",
|
|
29
|
+
surfaceId: "codex-hooks",
|
|
30
|
+
support: "unknown",
|
|
31
|
+
runtimeFailure: "native-unknown"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
capability: "optional-stop-verify",
|
|
35
|
+
normalizedEvent: "stop",
|
|
36
|
+
nativeEvent: "Stop",
|
|
37
|
+
surfaceId: "codex-hooks",
|
|
38
|
+
support: "unsupported",
|
|
39
|
+
runtimeFailure: "fail-open"
|
|
40
|
+
}
|
|
41
|
+
];
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export const CODEX_NON_ZERO_EXIT_BEHAVIOR = "UNKNOWN";
|
|
2
2
|
export const CODEX_PRE_TOOL_BLOCKING = "UNKNOWN";
|
|
3
3
|
export function codexHookOutput(event, result) {
|
|
4
|
+
if (event === "Stop" && result.evidence !== undefined) {
|
|
5
|
+
return {
|
|
6
|
+
exitCode: 0,
|
|
7
|
+
stdout: JSON.stringify({
|
|
8
|
+
continue: true,
|
|
9
|
+
systemMessage: `agent-ops: ${result.code}`,
|
|
10
|
+
evidence: result.evidence
|
|
11
|
+
})
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
if (result.action === "continue" && result.status === "PASS") {
|
|
5
15
|
return { exitCode: 0, stdout: "" };
|
|
6
16
|
}
|