@kylecheng3146/agent-ops 0.1.1 → 0.1.2
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.
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { commonHarnessAdapters } from "../../../runtime/src/install/harness.js";
|
|
8
|
+
import { hookRegistrationSatisfied, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
|
|
9
|
+
import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
|
|
7
10
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
8
11
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
9
12
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
@@ -28,7 +31,7 @@ import { runTrustCommand } from "./commands/trust.js";
|
|
|
28
31
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
29
32
|
import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
30
33
|
import { errorEnvelope } from "./output.js";
|
|
31
|
-
const CLI_VERSION = "0.1.
|
|
34
|
+
const CLI_VERSION = "0.1.2";
|
|
32
35
|
const DEFAULT_CONFIG = {
|
|
33
36
|
schemaVersion: 1,
|
|
34
37
|
profiles: [],
|
|
@@ -95,6 +98,23 @@ async function loadEffectiveConfig(root, scope) {
|
|
|
95
98
|
}
|
|
96
99
|
return mergeConfigLayers(layers);
|
|
97
100
|
}
|
|
101
|
+
async function readOptionalJson(path) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function installedHarness(root) {
|
|
110
|
+
try {
|
|
111
|
+
return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8")).harness;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ponytail: no readable manifest means demand hooks for both harnesses.
|
|
115
|
+
return "both";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
98
118
|
async function repositoryTrust(root, config) {
|
|
99
119
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
100
120
|
const state = localStatePaths(home);
|
|
@@ -175,7 +195,20 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
175
195
|
});
|
|
176
196
|
}
|
|
177
197
|
if (args.command === "doctor") {
|
|
178
|
-
|
|
198
|
+
const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
199
|
+
return await runDoctorCommand({
|
|
200
|
+
root,
|
|
201
|
+
probes: {
|
|
202
|
+
hookRegistration: async () => hookRegistrationSatisfied({
|
|
203
|
+
harness: await installedHarness(root),
|
|
204
|
+
profiles: config.profiles,
|
|
205
|
+
claudeSettings: await readOptionalJson(join(root, ".claude", "settings.json")),
|
|
206
|
+
codexHooks: await readOptionalJson(join(root, ".codex", "hooks.json"))
|
|
207
|
+
}),
|
|
208
|
+
repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config)),
|
|
209
|
+
smokeAvailability: () => smokeAvailabilityStatus(config)
|
|
210
|
+
}
|
|
211
|
+
});
|
|
179
212
|
}
|
|
180
213
|
if (args.command === "uninstall") {
|
|
181
214
|
return await runUninstallCommand({
|
|
@@ -6,6 +6,17 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { sha256 } from "./hash.js";
|
|
7
7
|
import { AgentOpsError, resolveContainedPath } from "./paths.js";
|
|
8
8
|
export { AgentOpsError } from "./paths.js";
|
|
9
|
+
/**
|
|
10
|
+
* Identity must come from bigint stats. Windows file indexes pack a sequence
|
|
11
|
+
* number above the 2^53 boundary, so a numeric `ino` silently rounds and an
|
|
12
|
+
* untouched file looks like it changed.
|
|
13
|
+
*/
|
|
14
|
+
export function fileIdentity(status) {
|
|
15
|
+
return {
|
|
16
|
+
device: status.dev.toString(),
|
|
17
|
+
inode: status.ino.toString()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
const MUTATION_WORKER_PATH = fileURLToPath(new URL("./mutation-worker.js", import.meta.url));
|
|
10
21
|
function isMissing(error) {
|
|
11
22
|
return (typeof error === "object" &&
|
|
@@ -25,10 +36,11 @@ async function captureParentGuard(parent) {
|
|
|
25
36
|
normalizedPath(canonicalPath) !== normalizedPath(parent)) {
|
|
26
37
|
throw new AgentOpsError("PRECONDITION_CHANGED", `Destination directory changed before mutation: ${parent}`);
|
|
27
38
|
}
|
|
39
|
+
const identity = fileIdentity(status);
|
|
28
40
|
return {
|
|
29
41
|
expectedParentPath: canonicalPath,
|
|
30
|
-
parentDevice:
|
|
31
|
-
parentInode:
|
|
42
|
+
parentDevice: identity.device,
|
|
43
|
+
parentInode: identity.inode
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
46
|
async function runAnchoredMutation(targetPath, action, expectedHash, content, mode) {
|
|
@@ -162,20 +174,21 @@ async function createBackup(snapshot, recoveryDirectory) {
|
|
|
162
174
|
async function snapshotOperation(root, operation) {
|
|
163
175
|
const targetPath = await resolveContainedPath(root, operation.path);
|
|
164
176
|
try {
|
|
165
|
-
const status = await lstat(targetPath);
|
|
177
|
+
const status = await lstat(targetPath, { bigint: true });
|
|
166
178
|
if (!status.isFile()) {
|
|
167
179
|
throw new AgentOpsError("UNSUPPORTED_FILE_TYPE", `Managed target must be a regular file: ${operation.path}`);
|
|
168
180
|
}
|
|
169
181
|
const content = await readFile(targetPath);
|
|
182
|
+
const identity = fileIdentity(status);
|
|
170
183
|
return {
|
|
171
184
|
operation,
|
|
172
185
|
targetPath,
|
|
173
186
|
existed: true,
|
|
174
187
|
content,
|
|
175
|
-
mode: status.mode & 0o777,
|
|
188
|
+
mode: Number(status.mode) & 0o777,
|
|
176
189
|
actualHash: sha256(content),
|
|
177
|
-
device:
|
|
178
|
-
inode:
|
|
190
|
+
device: identity.device,
|
|
191
|
+
inode: identity.inode,
|
|
179
192
|
backupPath: null,
|
|
180
193
|
createdDirectories: []
|
|
181
194
|
};
|
|
@@ -219,10 +232,7 @@ async function currentIdentity(path) {
|
|
|
219
232
|
if (!status.isFile() || status.isSymbolicLink()) {
|
|
220
233
|
throw new AgentOpsError("PRECONDITION_CHANGED", `A managed path is no longer a regular file: ${path}`);
|
|
221
234
|
}
|
|
222
|
-
return
|
|
223
|
-
device: status.dev.toString(),
|
|
224
|
-
inode: status.ino.toString()
|
|
225
|
-
};
|
|
235
|
+
return fileIdentity(status);
|
|
226
236
|
}
|
|
227
237
|
catch (error) {
|
|
228
238
|
if (isMissing(error)) {
|
|
@@ -165,9 +165,13 @@ async function checkProbe(id, probe) {
|
|
|
165
165
|
return check(id, "UNKNOWN", "No probe was provided.");
|
|
166
166
|
}
|
|
167
167
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const result = await probe();
|
|
169
|
+
const status = typeof result === "boolean" ? (result ? "PASS" : "FAIL") : result;
|
|
170
|
+
return check(id, status, status === "PASS"
|
|
171
|
+
? "Probe passed."
|
|
172
|
+
: status === "FAIL"
|
|
173
|
+
? "Probe failed."
|
|
174
|
+
: "Probe has nothing to verify yet.");
|
|
171
175
|
}
|
|
172
176
|
catch {
|
|
173
177
|
return check(id, "FAIL", "Probe failed.");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildClaudeHookSettings } from "../adapters/claude/config.js";
|
|
2
|
+
import { buildCodexHookConfig } from "../adapters/codex/config.js";
|
|
3
|
+
import { resolveProfiles } from "./profiles.js";
|
|
4
|
+
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
5
|
+
const CODEX_COMMAND_PREFIX = "agent-ops hook codex ";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function hasManagedHandler(source, events, isManaged) {
|
|
10
|
+
if (!isRecord(source) || !isRecord(source.hooks)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const registered = source.hooks;
|
|
14
|
+
return events.every((event) => {
|
|
15
|
+
const groups = registered[event];
|
|
16
|
+
return (Array.isArray(groups) &&
|
|
17
|
+
groups.some((group) => isRecord(group) &&
|
|
18
|
+
Array.isArray(group.hooks) &&
|
|
19
|
+
group.hooks.some(isManaged)));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isManagedClaudeHandler(handler) {
|
|
23
|
+
return (isRecord(handler) &&
|
|
24
|
+
Array.isArray(handler.args) &&
|
|
25
|
+
handler.args.includes(CLAUDE_HOOK_MARKER));
|
|
26
|
+
}
|
|
27
|
+
function isManagedCodexHandler(handler) {
|
|
28
|
+
return (isRecord(handler) &&
|
|
29
|
+
typeof handler.command === "string" &&
|
|
30
|
+
handler.command.startsWith(CODEX_COMMAND_PREFIX));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Hook registration is satisfied when every hook event implied by the
|
|
34
|
+
* installed profiles carries an agent-ops owned handler for every installed
|
|
35
|
+
* harness. Installations without hook capabilities have nothing to register.
|
|
36
|
+
*/
|
|
37
|
+
export function hookRegistrationSatisfied(input) {
|
|
38
|
+
const { capabilities } = resolveProfiles(input.profiles);
|
|
39
|
+
const claudeEvents = Object.keys(buildClaudeHookSettings(capabilities, "probe").hooks);
|
|
40
|
+
const codexEvents = Object.keys(buildCodexHookConfig(capabilities).hooks);
|
|
41
|
+
if (claudeEvents.length === 0 && codexEvents.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (input.harness !== "codex" &&
|
|
45
|
+
!hasManagedHandler(input.claudeSettings, claudeEvents, isManagedClaudeHandler)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return (input.harness === "claude" ||
|
|
49
|
+
hasManagedHandler(input.codexHooks, codexEvents, isManagedCodexHandler));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Smoke availability stays UNKNOWN until the repository declares a
|
|
53
|
+
* verification command; the toolkit never invents one.
|
|
54
|
+
*/
|
|
55
|
+
export function smokeAvailabilityStatus(config) {
|
|
56
|
+
return config.verification.commands.length > 0 ? "PASS" : "UNKNOWN";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Installation approval never grants trust, so an ungranted repository is
|
|
60
|
+
* unconfigured rather than broken. A stale binding is a real failure.
|
|
61
|
+
*/
|
|
62
|
+
export function repositoryTrustStatus(trust) {
|
|
63
|
+
return trust === "TRUSTED"
|
|
64
|
+
? "PASS"
|
|
65
|
+
: trust === "STALE"
|
|
66
|
+
? "FAIL"
|
|
67
|
+
: "UNKNOWN";
|
|
68
|
+
}
|