@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,296 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import { resolveContainedPath } from "../fs/paths.js";
|
|
4
|
+
import { harnessDescriptor, harnessSurfaceByPath, harnessSurfaces } from "./harness.js";
|
|
5
|
+
import { resolveCapabilities, resolveProfiles } from "./profiles.js";
|
|
6
|
+
import { isWritableSurface } from "./surfaces.js";
|
|
7
|
+
const MAX_SURFACE_FILE_BYTES = 1024 * 1024;
|
|
8
|
+
function isMissing(error) {
|
|
9
|
+
return (typeof error === "object" &&
|
|
10
|
+
error !== null &&
|
|
11
|
+
"code" in error &&
|
|
12
|
+
error.code === "ENOENT");
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
async function readSurface(root, surface) {
|
|
18
|
+
if (surface.scope === "external") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let resolvedPath;
|
|
22
|
+
try {
|
|
23
|
+
resolvedPath = await resolveContainedPath(root, surface.path);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (isMissing(error)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
let before;
|
|
32
|
+
try {
|
|
33
|
+
before = await lstat(resolvedPath, { bigint: true });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (isMissing(error)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (!before.isFile() ||
|
|
42
|
+
before.size > BigInt(MAX_SURFACE_FILE_BYTES)) {
|
|
43
|
+
throw new Error("Surface is not a bounded regular file.");
|
|
44
|
+
}
|
|
45
|
+
const handle = await open(resolvedPath, constants.O_RDONLY |
|
|
46
|
+
constants.O_NOFOLLOW |
|
|
47
|
+
constants.O_NONBLOCK);
|
|
48
|
+
try {
|
|
49
|
+
const opened = await handle.stat({ bigint: true });
|
|
50
|
+
const resolvedAgain = await resolveContainedPath(root, surface.path);
|
|
51
|
+
const after = await lstat(resolvedAgain, { bigint: true });
|
|
52
|
+
if (!opened.isFile() ||
|
|
53
|
+
opened.size > BigInt(MAX_SURFACE_FILE_BYTES) ||
|
|
54
|
+
before.dev !== after.dev ||
|
|
55
|
+
before.ino !== after.ino) {
|
|
56
|
+
throw new Error("Surface identity changed during inspection.");
|
|
57
|
+
}
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let totalBytes = 0;
|
|
60
|
+
while (totalBytes <= MAX_SURFACE_FILE_BYTES) {
|
|
61
|
+
const remaining = MAX_SURFACE_FILE_BYTES + 1 - totalBytes;
|
|
62
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, remaining));
|
|
63
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
64
|
+
if (bytesRead === 0) {
|
|
65
|
+
return {
|
|
66
|
+
source: Buffer.concat(chunks, totalBytes).toString("utf8")
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
70
|
+
totalBytes += bytesRead;
|
|
71
|
+
}
|
|
72
|
+
throw new Error("Surface exceeded its inspection limit.");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await handle.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function sameEvents(left, right) {
|
|
79
|
+
return (left.length === right.length &&
|
|
80
|
+
left.every((event) => right.includes(event)));
|
|
81
|
+
}
|
|
82
|
+
function desiredCapabilities(config) {
|
|
83
|
+
return config.profiles.length === 0
|
|
84
|
+
? []
|
|
85
|
+
: resolveCapabilities(config).capabilities;
|
|
86
|
+
}
|
|
87
|
+
function managedJsonCount(source, isManagedHandler) {
|
|
88
|
+
if (source === null) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
return jsonHandlerCounts(source, isManagedHandler)?.managed ?? 0;
|
|
92
|
+
}
|
|
93
|
+
export async function inspectHarnessRegistrations(options) {
|
|
94
|
+
const capabilities = desiredCapabilities(options.config);
|
|
95
|
+
const statuses = [];
|
|
96
|
+
for (const harness of options.manifest.harness) {
|
|
97
|
+
const descriptor = harnessDescriptor(harness);
|
|
98
|
+
const control = descriptor.control;
|
|
99
|
+
const hookRecord = options.manifest.hooks?.find(({ harness: recordHarness }) => recordHarness === harness);
|
|
100
|
+
const recordedEvents = hookRecord?.events ?? [];
|
|
101
|
+
const desiredEvents = control.buildHooks === undefined
|
|
102
|
+
? []
|
|
103
|
+
: Object.keys(control.buildHooks(capabilities, "probe").hooks);
|
|
104
|
+
if (control.buildHooks !== undefined) {
|
|
105
|
+
const surfaces = harnessSurfaces(harness, options.manifest.scope, options.root);
|
|
106
|
+
const writableJsonSurfaces = surfaces.filter((surface) => isWritableSurface(surface) && surface.representation === "json");
|
|
107
|
+
let managedCount = 0;
|
|
108
|
+
for (const surface of writableJsonSurfaces) {
|
|
109
|
+
try {
|
|
110
|
+
const read = await readSurface(options.root, surface);
|
|
111
|
+
managedCount += managedJsonCount(read?.source ?? null, control.isManagedHandler);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// An unsafe surface is treated as drift below when it is selected.
|
|
115
|
+
managedCount += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const selectedSurface = hookRecord === undefined
|
|
119
|
+
? writableJsonSurfaces.find((surface) => surface.access === "managed-default")
|
|
120
|
+
: harnessSurfaceByPath(harness, options.manifest.scope, hookRecord.path, options.root);
|
|
121
|
+
let source = null;
|
|
122
|
+
if (selectedSurface !== undefined) {
|
|
123
|
+
try {
|
|
124
|
+
source = (await readSurface(options.root, selectedSurface))?.source ??
|
|
125
|
+
null;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
source = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const registered = hookRecord === undefined
|
|
132
|
+
? desiredEvents.length === 0 && managedCount === 0
|
|
133
|
+
: selectedSurface !== undefined &&
|
|
134
|
+
sameEvents(desiredEvents, recordedEvents) &&
|
|
135
|
+
(desiredEvents.length === 0
|
|
136
|
+
? managedCount === 0
|
|
137
|
+
: source !== null &&
|
|
138
|
+
control.hookRegistered(source, capabilities));
|
|
139
|
+
statuses.push({
|
|
140
|
+
harness,
|
|
141
|
+
registered,
|
|
142
|
+
desiredEvents,
|
|
143
|
+
recordedEvents
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const pluginArtifact = options.manifest.artifacts.find(({ id }) => id === "opencode-plugin");
|
|
148
|
+
const hasDesiredPlugin = control.registrations.some((registration) => capabilities.includes(registration.capability));
|
|
149
|
+
let registered = !hasDesiredPlugin && pluginArtifact === undefined;
|
|
150
|
+
if (hasDesiredPlugin && pluginArtifact !== undefined) {
|
|
151
|
+
const discovered = harnessSurfaces(harness, options.manifest.scope, options.root).find((surface) => surface.id === "opencode-plugin");
|
|
152
|
+
const selectedSurface = discovered === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: { ...discovered, path: pluginArtifact.path };
|
|
155
|
+
try {
|
|
156
|
+
const source = selectedSurface === undefined
|
|
157
|
+
? null
|
|
158
|
+
: (await readSurface(options.root, selectedSurface))?.source ?? null;
|
|
159
|
+
registered =
|
|
160
|
+
source !== null && control.hookRegistered(source, capabilities);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
registered = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!hasDesiredPlugin && pluginArtifact !== undefined) {
|
|
167
|
+
registered = false;
|
|
168
|
+
}
|
|
169
|
+
statuses.push({
|
|
170
|
+
harness,
|
|
171
|
+
registered,
|
|
172
|
+
desiredEvents,
|
|
173
|
+
recordedEvents
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return statuses;
|
|
177
|
+
}
|
|
178
|
+
function jsonHandlerCounts(source, isManagedHandler) {
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(source);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!isRecord(parsed)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const hooks = parsed.hooks;
|
|
190
|
+
if (hooks === undefined) {
|
|
191
|
+
return { managed: 0, foreign: 0 };
|
|
192
|
+
}
|
|
193
|
+
if (!isRecord(hooks)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
let managed = 0;
|
|
197
|
+
let foreign = 0;
|
|
198
|
+
for (const groups of Object.values(hooks)) {
|
|
199
|
+
if (!Array.isArray(groups)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
for (const group of groups) {
|
|
203
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
for (const handler of group.hooks) {
|
|
207
|
+
if (isManagedHandler?.(handler) === true) {
|
|
208
|
+
managed += 1;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
foreign += 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { managed, foreign };
|
|
217
|
+
}
|
|
218
|
+
function handlerCounts(surface, source, capabilities, isManagedHandler, hookRegistered) {
|
|
219
|
+
if (surface.representation === "json") {
|
|
220
|
+
return jsonHandlerCounts(source, isManagedHandler);
|
|
221
|
+
}
|
|
222
|
+
if (surface.representation === "javascript") {
|
|
223
|
+
return {
|
|
224
|
+
managed: hookRegistered(source, capabilities) ? 1 : 0,
|
|
225
|
+
foreign: 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { managed: 0, foreign: 0 };
|
|
229
|
+
}
|
|
230
|
+
export async function inspectHarnessSurfaces(options) {
|
|
231
|
+
const capabilities = options.profiles.length === 0
|
|
232
|
+
? []
|
|
233
|
+
: resolveProfiles(options.profiles).capabilities;
|
|
234
|
+
const statuses = [];
|
|
235
|
+
for (const harness of options.harness) {
|
|
236
|
+
const descriptor = harnessDescriptor(harness);
|
|
237
|
+
for (const surface of harnessSurfaces(harness, options.scope, options.root)) {
|
|
238
|
+
if (surface.scope === "external") {
|
|
239
|
+
statuses.push({
|
|
240
|
+
harness,
|
|
241
|
+
surfaceId: surface.id,
|
|
242
|
+
path: surface.path,
|
|
243
|
+
status: "unknown",
|
|
244
|
+
managedHandlerCount: 0,
|
|
245
|
+
foreignHandlerCount: 0
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const read = await readSurface(options.root, surface);
|
|
251
|
+
if (read === null) {
|
|
252
|
+
statuses.push({
|
|
253
|
+
harness,
|
|
254
|
+
surfaceId: surface.id,
|
|
255
|
+
path: surface.path,
|
|
256
|
+
status: "missing",
|
|
257
|
+
managedHandlerCount: 0,
|
|
258
|
+
foreignHandlerCount: 0
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const counts = handlerCounts(surface, read.source, capabilities, descriptor.control.isManagedHandler, descriptor.control.hookRegistered);
|
|
263
|
+
if (counts === null) {
|
|
264
|
+
statuses.push({
|
|
265
|
+
harness,
|
|
266
|
+
surfaceId: surface.id,
|
|
267
|
+
path: surface.path,
|
|
268
|
+
status: "unknown",
|
|
269
|
+
managedHandlerCount: 0,
|
|
270
|
+
foreignHandlerCount: 0
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
statuses.push({
|
|
275
|
+
harness,
|
|
276
|
+
surfaceId: surface.id,
|
|
277
|
+
path: surface.path,
|
|
278
|
+
status: counts.managed > 0 ? "managed" : "foreign",
|
|
279
|
+
managedHandlerCount: counts.managed,
|
|
280
|
+
foreignHandlerCount: counts.foreign
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
statuses.push({
|
|
285
|
+
harness,
|
|
286
|
+
surfaceId: surface.id,
|
|
287
|
+
path: surface.path,
|
|
288
|
+
status: "unknown",
|
|
289
|
+
managedHandlerCount: 0,
|
|
290
|
+
foreignHandlerCount: 0
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return statuses;
|
|
296
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function findSurfaceById(surfaces, id) {
|
|
2
|
+
return surfaces.find((surface) => surface.id === id);
|
|
3
|
+
}
|
|
4
|
+
export function findSurfaceByPath(surfaces, path) {
|
|
5
|
+
return surfaces.find((surface) => surface.path === path);
|
|
6
|
+
}
|
|
7
|
+
export function isWritableSurface(surface) {
|
|
8
|
+
return (surface !== undefined &&
|
|
9
|
+
(surface.access === "managed-default" ||
|
|
10
|
+
surface.access === "managed-opt-in"));
|
|
11
|
+
}
|
|
@@ -7,6 +7,7 @@ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
|
7
7
|
import { FileTransaction } from "../fs/transaction.js";
|
|
8
8
|
import { planHookRemoval } from "./hooks.js";
|
|
9
9
|
import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
|
|
10
|
+
import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
|
|
10
11
|
const MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
11
12
|
const MAX_UNINSTALL_FILE_BYTES = 1024 * 1024;
|
|
12
13
|
function isMissing(error) {
|
|
@@ -85,7 +86,7 @@ async function planMarkerFiles(root, markers, expectedMarkers) {
|
|
|
85
86
|
}
|
|
86
87
|
assertExpectedManagedBlock(content, marker, expected);
|
|
87
88
|
try {
|
|
88
|
-
content = removeManagedBlock(content, marker.id);
|
|
89
|
+
content = removeManagedBlock(content, marker.id, expected.markerStyle);
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block cannot be removed safely: ${path}`, { cause: error });
|
|
@@ -117,13 +118,17 @@ export async function createUninstallPlan(root) {
|
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
const manifest = parseInstallManifest(currentManifest.content);
|
|
120
|
-
const expectedMarkers = assertSupportedManifestOwnership(manifest);
|
|
121
|
+
const expectedMarkers = assertSupportedManifestOwnership(manifest, root);
|
|
121
122
|
const operations = [];
|
|
122
123
|
for (const artifact of manifest.artifacts) {
|
|
123
124
|
const current = await readCurrentFile(root, artifact.path);
|
|
124
125
|
if (current === null || current.hash !== artifact.hash) {
|
|
125
126
|
throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
|
|
126
127
|
}
|
|
128
|
+
if (artifact.id === "opencode-plugin" &&
|
|
129
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
130
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
131
|
+
}
|
|
127
132
|
operations.push({
|
|
128
133
|
kind: "remove",
|
|
129
134
|
path: artifact.path,
|
|
@@ -141,13 +146,15 @@ export async function createUninstallPlan(root) {
|
|
|
141
146
|
? {
|
|
142
147
|
kind: "remove",
|
|
143
148
|
path: hook.path,
|
|
144
|
-
expectedHash: current.hash
|
|
149
|
+
expectedHash: current.hash,
|
|
150
|
+
disclosure: removal.disclosure
|
|
145
151
|
}
|
|
146
152
|
: {
|
|
147
153
|
kind: "write",
|
|
148
154
|
path: hook.path,
|
|
149
155
|
content: removal.content,
|
|
150
|
-
expectedHash: current.hash
|
|
156
|
+
expectedHash: current.hash,
|
|
157
|
+
disclosure: removal.disclosure
|
|
151
158
|
});
|
|
152
159
|
}
|
|
153
160
|
operations.push({
|
|
@@ -9,6 +9,7 @@ import { createInstallPlan } from "./plan.js";
|
|
|
9
9
|
const PACKAGE_NAME = "@kylecheng3146/agent-ops";
|
|
10
10
|
const CONFIG_PATH = ".agent-ops/config.json";
|
|
11
11
|
const MAX_UPDATE_CONFIG_BYTES = 1024 * 1024;
|
|
12
|
+
const TOOLKIT_VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
12
13
|
async function readBoundedConfig(root) {
|
|
13
14
|
const resolvedPath = await resolveContainedPath(root, CONFIG_PATH);
|
|
14
15
|
const before = await lstat(resolvedPath, { bigint: true });
|
|
@@ -82,13 +83,18 @@ export async function createUpdatePlan(options) {
|
|
|
82
83
|
if (targetVersion === undefined) {
|
|
83
84
|
throw new AgentOpsError("UPDATE_TARGET_REQUIRED", "Update requires a target version or an explicit registry client.");
|
|
84
85
|
}
|
|
86
|
+
if (!TOOLKIT_VERSION_PATTERN.test(targetVersion)) {
|
|
87
|
+
throw new AgentOpsError("INVALID_TOOLKIT_VERSION", "Toolkit version must be a valid semantic version.");
|
|
88
|
+
}
|
|
85
89
|
const report = await doctorInstallation({ root: options.root });
|
|
86
90
|
for (const id of [
|
|
87
91
|
"node-version",
|
|
88
92
|
"manifest",
|
|
89
93
|
"markers"
|
|
90
94
|
]) {
|
|
91
|
-
|
|
95
|
+
const status = statusOf(report, id);
|
|
96
|
+
if (status !== "PASS" &&
|
|
97
|
+
!(id === "markers" && status === "DEGRADED")) {
|
|
92
98
|
throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", `Update requires a passing ${id} doctor check.`);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
@@ -103,13 +109,17 @@ export async function createUpdatePlan(options) {
|
|
|
103
109
|
const installation = await createInstallPlan({
|
|
104
110
|
root: options.root,
|
|
105
111
|
scope: report.manifest.scope,
|
|
106
|
-
harness: report.manifest.harness,
|
|
112
|
+
harness: options.harness ?? report.manifest.harness,
|
|
107
113
|
profiles: configPreview.migrated.profiles,
|
|
108
114
|
adapters: options.adapters,
|
|
109
|
-
toolkitVersion: targetVersion,
|
|
115
|
+
toolkitVersion: options.toolkitVersion ?? targetVersion,
|
|
116
|
+
allowHarnessChange: true,
|
|
110
117
|
...(options.hookRuntimePath === undefined
|
|
111
118
|
? {}
|
|
112
119
|
: { hookRuntimePath: options.hookRuntimePath }),
|
|
120
|
+
...(options.hookTargets === undefined
|
|
121
|
+
? {}
|
|
122
|
+
: { hookTargets: options.hookTargets }),
|
|
113
123
|
existingConfig: {
|
|
114
124
|
value: configPreview.migrated,
|
|
115
125
|
sourceHash: configPreview.sourceHash
|
|
@@ -83,6 +83,31 @@ function sanitizeEvent(value) {
|
|
|
83
83
|
result: value.result
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
if (value.type === "loop-event") {
|
|
87
|
+
if (!hasExactKeys(value, ["code", "event", "outcome", "type"]) ||
|
|
88
|
+
typeof value.code !== "string" ||
|
|
89
|
+
!ID_PATTERN.test(value.code) ||
|
|
90
|
+
![
|
|
91
|
+
"permission-request",
|
|
92
|
+
"post-compact",
|
|
93
|
+
"post-tool-use",
|
|
94
|
+
"pre-compact",
|
|
95
|
+
"pre-tool-use",
|
|
96
|
+
"session-start",
|
|
97
|
+
"subagent-start",
|
|
98
|
+
"subagent-stop",
|
|
99
|
+
"user-prompt-submit"
|
|
100
|
+
].includes(String(value.event)) ||
|
|
101
|
+
!["allowed", "blocked", "observed"].includes(String(value.outcome))) {
|
|
102
|
+
return invalidEvent();
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
type: "loop-event",
|
|
106
|
+
event: value.event,
|
|
107
|
+
outcome: value.outcome,
|
|
108
|
+
code: value.code
|
|
109
|
+
};
|
|
110
|
+
}
|
|
86
111
|
return invalidEvent();
|
|
87
112
|
}
|
|
88
113
|
function serialize(stored) {
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFIG_SCHEMA_VERSION, EVIDENCE_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION, TASK_SCHEMA_VERSION } from "../contracts.js";
|
|
2
2
|
const ID_PATTERN = /^[a-z][a-z0-9-]{0,127}$/;
|
|
3
3
|
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
4
4
|
const WINDOWS_RESERVED_SEGMENT = /^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)(?:\..*)?$/i;
|
|
5
|
-
const PROFILE_VALUES = new Set(["advisory", "core", "guardrails"]);
|
|
5
|
+
const PROFILE_VALUES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
6
6
|
const EVIDENCE_KINDS = new Set(["exit-code", "file", "test-count"]);
|
|
7
7
|
const SCOPE_VALUES = new Set(["project", "user"]);
|
|
8
|
-
const HARNESS_VALUES = new Set(["
|
|
8
|
+
const HARNESS_VALUES = new Set(["claude", "codex", "opencode"]);
|
|
9
|
+
// opencode's plugin is a managed artifact, not a ManagedHookRecord entry.
|
|
9
10
|
const HOOK_HARNESS_VALUES = new Set(["claude", "codex"]);
|
|
10
11
|
const HOOK_EVENT_VALUES = new Set([
|
|
11
12
|
"SessionStart",
|
|
13
|
+
"UserPromptSubmit",
|
|
12
14
|
"PreToolUse",
|
|
15
|
+
"PermissionRequest",
|
|
16
|
+
"PostToolUse",
|
|
17
|
+
"PreCompact",
|
|
18
|
+
"PostCompact",
|
|
19
|
+
"SubagentStart",
|
|
20
|
+
"SubagentStop",
|
|
13
21
|
"Stop"
|
|
14
22
|
]);
|
|
15
23
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
@@ -38,12 +46,12 @@ function unknownFieldFailure(value, allowed, path) {
|
|
|
38
46
|
? undefined
|
|
39
47
|
: failure("UNKNOWN_FIELD", `${path}.${unknown}`, `Unknown field: ${unknown}`);
|
|
40
48
|
}
|
|
41
|
-
function validateRoot(value, allowed) {
|
|
49
|
+
function validateRoot(value, allowed, expectedVersion) {
|
|
42
50
|
if (!isRecord(value)) {
|
|
43
51
|
return failure("INVALID_TYPE", "$", "Expected an object.");
|
|
44
52
|
}
|
|
45
|
-
if (value.schemaVersion !==
|
|
46
|
-
return failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion", `Expected schemaVersion ${
|
|
53
|
+
if (value.schemaVersion !== expectedVersion) {
|
|
54
|
+
return failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion", `Expected schemaVersion ${expectedVersion}.`);
|
|
47
55
|
}
|
|
48
56
|
return unknownFieldFailure(value, allowed, "$") ?? value;
|
|
49
57
|
}
|
|
@@ -277,12 +285,13 @@ function validateSecurityException(value, path) {
|
|
|
277
285
|
}
|
|
278
286
|
export function validateConfig(value) {
|
|
279
287
|
const root = validateRoot(value, [
|
|
288
|
+
"features",
|
|
280
289
|
"pathMappings",
|
|
281
290
|
"profiles",
|
|
282
291
|
"schemaVersion",
|
|
283
292
|
"securityExceptions",
|
|
284
293
|
"verification"
|
|
285
|
-
]);
|
|
294
|
+
], CONFIG_SCHEMA_VERSION);
|
|
286
295
|
if (isFailure(root)) {
|
|
287
296
|
return root;
|
|
288
297
|
}
|
|
@@ -306,6 +315,23 @@ export function validateConfig(value) {
|
|
|
306
315
|
if (!hasUniqueStrings(root.profiles)) {
|
|
307
316
|
return failure("DUPLICATE_ID", "$.profiles", "Profiles must be unique.");
|
|
308
317
|
}
|
|
318
|
+
if (!isRecord(root.features)) {
|
|
319
|
+
return failure("INVALID_TYPE", "$.features", "features must be an object.");
|
|
320
|
+
}
|
|
321
|
+
const featuresUnknown = unknownFieldFailure(root.features, ["stopVerification"], "$.features");
|
|
322
|
+
if (featuresUnknown !== undefined) {
|
|
323
|
+
return featuresUnknown;
|
|
324
|
+
}
|
|
325
|
+
if (!isRecord(root.features.stopVerification)) {
|
|
326
|
+
return failure("INVALID_TYPE", "$.features.stopVerification", "stopVerification must be an object.");
|
|
327
|
+
}
|
|
328
|
+
const stopVerificationUnknown = unknownFieldFailure(root.features.stopVerification, ["enabled"], "$.features.stopVerification");
|
|
329
|
+
if (stopVerificationUnknown !== undefined) {
|
|
330
|
+
return stopVerificationUnknown;
|
|
331
|
+
}
|
|
332
|
+
if (typeof root.features.stopVerification.enabled !== "boolean") {
|
|
333
|
+
return failure("INVALID_FEATURE", "$.features.stopVerification.enabled", "stopVerification.enabled must be a boolean.");
|
|
334
|
+
}
|
|
309
335
|
if (!isRecord(root.verification)) {
|
|
310
336
|
return failure("INVALID_TYPE", "$.verification", "verification must be an object.");
|
|
311
337
|
}
|
|
@@ -327,6 +353,10 @@ export function validateConfig(value) {
|
|
|
327
353
|
}
|
|
328
354
|
commandIds.add(command.value.id);
|
|
329
355
|
}
|
|
356
|
+
if (root.features.stopVerification.enabled === true &&
|
|
357
|
+
root.verification.commands.length === 0) {
|
|
358
|
+
return failure("STOP_VERIFICATION_COMMANDS_REQUIRED", "$.features.stopVerification.enabled", "Stop verification requires at least one verification command.");
|
|
359
|
+
}
|
|
330
360
|
if (!Array.isArray(root.pathMappings)) {
|
|
331
361
|
return failure("INVALID_TYPE", "$.pathMappings", "pathMappings must be an array.");
|
|
332
362
|
}
|
|
@@ -372,7 +402,7 @@ function validateCriterion(value, path) {
|
|
|
372
402
|
return success(value);
|
|
373
403
|
}
|
|
374
404
|
export function validateTask(value) {
|
|
375
|
-
const root = validateRoot(value, ["criteria", "id", "schemaVersion", "title"]);
|
|
405
|
+
const root = validateRoot(value, ["criteria", "id", "schemaVersion", "title"], TASK_SCHEMA_VERSION);
|
|
376
406
|
if (isFailure(root)) {
|
|
377
407
|
return root;
|
|
378
408
|
}
|
|
@@ -433,7 +463,7 @@ export function validateEvidence(value) {
|
|
|
433
463
|
"taskId",
|
|
434
464
|
"testCount",
|
|
435
465
|
"toolVersions"
|
|
436
|
-
]);
|
|
466
|
+
], EVIDENCE_SCHEMA_VERSION);
|
|
437
467
|
if (isFailure(root)) {
|
|
438
468
|
return root;
|
|
439
469
|
}
|
|
@@ -556,22 +586,18 @@ function validateManagedHook(value, path) {
|
|
|
556
586
|
return success(value);
|
|
557
587
|
}
|
|
558
588
|
export function validateManifest(value) {
|
|
559
|
-
const root = validateRoot(value, [
|
|
560
|
-
"artifacts",
|
|
561
|
-
"harness",
|
|
562
|
-
"hooks",
|
|
563
|
-
"markers",
|
|
564
|
-
"schemaVersion",
|
|
565
|
-
"scope"
|
|
566
|
-
]);
|
|
589
|
+
const root = validateRoot(value, ["artifacts", "harness", "hooks", "markers", "schemaVersion", "scope"], MANIFEST_SCHEMA_VERSION);
|
|
567
590
|
if (isFailure(root)) {
|
|
568
591
|
return root;
|
|
569
592
|
}
|
|
570
593
|
if (typeof root.scope !== "string" || !SCOPE_VALUES.has(root.scope)) {
|
|
571
594
|
return failure("INVALID_SCOPE", "$.scope", "Unsupported install scope.");
|
|
572
595
|
}
|
|
573
|
-
if (
|
|
574
|
-
|
|
596
|
+
if (!Array.isArray(root.harness) ||
|
|
597
|
+
root.harness.length === 0 ||
|
|
598
|
+
!root.harness.every((id) => typeof id === "string" && HARNESS_VALUES.has(id)) ||
|
|
599
|
+
new Set(root.harness).size !== root.harness.length) {
|
|
600
|
+
return failure("INVALID_HARNESS", "$.harness", "Harness must be a non-empty list of unique supported harnesses.");
|
|
575
601
|
}
|
|
576
602
|
if (!Array.isArray(root.artifacts) || !Array.isArray(root.markers)) {
|
|
577
603
|
return failure("INVALID_TYPE", "$.artifacts", "artifacts and markers must be arrays.");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { TASK_SCHEMA_VERSION } from "../contracts.js";
|
|
3
3
|
import { AgentOpsError } from "../fs/paths.js";
|
|
4
4
|
import { validateTask } from "../schema/validate.js";
|
|
5
5
|
import { renderTaskMarkdown } from "./render.js";
|
|
@@ -74,7 +74,7 @@ export class TaskService {
|
|
|
74
74
|
}
|
|
75
75
|
async create(input) {
|
|
76
76
|
const task = {
|
|
77
|
-
schemaVersion:
|
|
77
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
78
78
|
id: this.#generateId(),
|
|
79
79
|
title: input.title,
|
|
80
80
|
criteria: [...input.criteria]
|
|
@@ -117,7 +117,7 @@ export class TaskService {
|
|
|
117
117
|
const state = await this.#store.read();
|
|
118
118
|
if (query.taskId !== undefined) {
|
|
119
119
|
return cloneRecord(findTask({
|
|
120
|
-
schemaVersion:
|
|
120
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
121
121
|
tasks: [...state.tasks],
|
|
122
122
|
sessions: [...state.sessions]
|
|
123
123
|
}, query.taskId));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { TASK_SCHEMA_VERSION } from "../contracts.js";
|
|
2
3
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
4
|
import { validateTask } from "../schema/validate.js";
|
|
4
5
|
import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../security/permissions.js";
|
|
@@ -154,7 +155,11 @@ function parseSession(value) {
|
|
|
154
155
|
}
|
|
155
156
|
function parseState(source) {
|
|
156
157
|
if (source === null) {
|
|
157
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
160
|
+
tasks: [],
|
|
161
|
+
sessions: []
|
|
162
|
+
};
|
|
158
163
|
}
|
|
159
164
|
let value;
|
|
160
165
|
try {
|
|
@@ -165,7 +170,7 @@ function parseState(source) {
|
|
|
165
170
|
}
|
|
166
171
|
if (!isRecord(value) ||
|
|
167
172
|
!hasExactKeys(value, ["schemaVersion", "sessions", "tasks"]) ||
|
|
168
|
-
value.schemaVersion !==
|
|
173
|
+
value.schemaVersion !== TASK_SCHEMA_VERSION ||
|
|
169
174
|
!Array.isArray(value.tasks) ||
|
|
170
175
|
!Array.isArray(value.sessions)) {
|
|
171
176
|
return invalidState("Task state has an unsupported structure.");
|
|
@@ -184,7 +189,11 @@ function parseState(source) {
|
|
|
184
189
|
})) {
|
|
185
190
|
return invalidState("Session attachments must reference known tasks.");
|
|
186
191
|
}
|
|
187
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
194
|
+
tasks,
|
|
195
|
+
sessions
|
|
196
|
+
};
|
|
188
197
|
}
|
|
189
198
|
function positiveMaxBytes(value) {
|
|
190
199
|
if (value === undefined) {
|