@kylecheng3146/agent-ops 0.0.1

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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/SECURITY.md +22 -0
  4. package/dist/packages/cli/src/args.js +322 -0
  5. package/dist/packages/cli/src/bin.js +290 -0
  6. package/dist/packages/cli/src/cli.js +80 -0
  7. package/dist/packages/cli/src/commands/config.js +8 -0
  8. package/dist/packages/cli/src/commands/doctor.js +32 -0
  9. package/dist/packages/cli/src/commands/index.js +16 -0
  10. package/dist/packages/cli/src/commands/init.js +65 -0
  11. package/dist/packages/cli/src/commands/review.js +71 -0
  12. package/dist/packages/cli/src/commands/task.js +141 -0
  13. package/dist/packages/cli/src/commands/trust.js +48 -0
  14. package/dist/packages/cli/src/commands/uninstall.js +58 -0
  15. package/dist/packages/cli/src/commands/update.js +58 -0
  16. package/dist/packages/cli/src/commands/verify.js +108 -0
  17. package/dist/packages/cli/src/output.js +53 -0
  18. package/dist/packages/cli/src/plan-output.js +28 -0
  19. package/dist/packages/cli/src/wizard.js +75 -0
  20. package/dist/runtime/src/adapters/claude/config.js +109 -0
  21. package/dist/runtime/src/adapters/claude/events.js +8 -0
  22. package/dist/runtime/src/adapters/claude/input.js +38 -0
  23. package/dist/runtime/src/adapters/claude/output.js +30 -0
  24. package/dist/runtime/src/adapters/codex/config.js +93 -0
  25. package/dist/runtime/src/adapters/codex/events.js +8 -0
  26. package/dist/runtime/src/adapters/codex/input.js +41 -0
  27. package/dist/runtime/src/adapters/codex/output.js +24 -0
  28. package/dist/runtime/src/config/explain.js +34 -0
  29. package/dist/runtime/src/config/load.js +25 -0
  30. package/dist/runtime/src/config/merge.js +170 -0
  31. package/dist/runtime/src/config/migrate.js +61 -0
  32. package/dist/runtime/src/contracts.js +1 -0
  33. package/dist/runtime/src/discovery/go.js +145 -0
  34. package/dist/runtime/src/discovery/index.js +40 -0
  35. package/dist/runtime/src/discovery/make.js +162 -0
  36. package/dist/runtime/src/discovery/node.js +175 -0
  37. package/dist/runtime/src/discovery/python.js +163 -0
  38. package/dist/runtime/src/discovery/rust.js +159 -0
  39. package/dist/runtime/src/discovery/types.js +1 -0
  40. package/dist/runtime/src/fs/hash.js +19 -0
  41. package/dist/runtime/src/fs/managed-block.js +90 -0
  42. package/dist/runtime/src/fs/manifest.js +24 -0
  43. package/dist/runtime/src/fs/mutation-worker.js +185 -0
  44. package/dist/runtime/src/fs/paths.js +96 -0
  45. package/dist/runtime/src/fs/transaction.js +498 -0
  46. package/dist/runtime/src/guardrails/destructive.js +207 -0
  47. package/dist/runtime/src/guardrails/evaluate.js +9 -0
  48. package/dist/runtime/src/guardrails/exceptions.js +49 -0
  49. package/dist/runtime/src/guardrails/secrets.js +97 -0
  50. package/dist/runtime/src/guardrails/types.js +9 -0
  51. package/dist/runtime/src/hooks/dispatch.js +78 -0
  52. package/dist/runtime/src/hooks/events.js +1 -0
  53. package/dist/runtime/src/hooks/hook-entry.js +19 -0
  54. package/dist/runtime/src/hooks/normalize.js +59 -0
  55. package/dist/runtime/src/hooks/output.js +12 -0
  56. package/dist/runtime/src/hooks/shell.js +138 -0
  57. package/dist/runtime/src/hooks/stop-verify.js +70 -0
  58. package/dist/runtime/src/install/apply.js +70 -0
  59. package/dist/runtime/src/install/doctor.js +196 -0
  60. package/dist/runtime/src/install/harness.js +87 -0
  61. package/dist/runtime/src/install/ownership.js +84 -0
  62. package/dist/runtime/src/install/plan.js +257 -0
  63. package/dist/runtime/src/install/profiles.js +28 -0
  64. package/dist/runtime/src/install/types.js +1 -0
  65. package/dist/runtime/src/install/uninstall.js +206 -0
  66. package/dist/runtime/src/install/update.js +123 -0
  67. package/dist/runtime/src/logging/local-log.js +158 -0
  68. package/dist/runtime/src/registry/npm.js +141 -0
  69. package/dist/runtime/src/review/claude-runner.js +4 -0
  70. package/dist/runtime/src/review/codex-runner.js +4 -0
  71. package/dist/runtime/src/review/packet.js +10 -0
  72. package/dist/runtime/src/review/result.js +24 -0
  73. package/dist/runtime/src/review/roles.js +3 -0
  74. package/dist/runtime/src/review/runner.js +45 -0
  75. package/dist/runtime/src/schema/validate.js +584 -0
  76. package/dist/runtime/src/security/permissions.js +654 -0
  77. package/dist/runtime/src/security/redact.js +41 -0
  78. package/dist/runtime/src/security/trust.js +209 -0
  79. package/dist/runtime/src/task/render.js +43 -0
  80. package/dist/runtime/src/task/service.js +235 -0
  81. package/dist/runtime/src/task/store.js +265 -0
  82. package/dist/runtime/src/verify/change-surface.js +86 -0
  83. package/dist/runtime/src/verify/evidence.js +89 -0
  84. package/dist/runtime/src/verify/fingerprint.js +67 -0
  85. package/dist/runtime/src/verify/scope.js +69 -0
  86. package/dist/runtime/src/verify/service.js +217 -0
  87. package/dist/runtime/src/verify/spawn.js +326 -0
  88. package/dist/runtime/src/verify/test-count.js +148 -0
  89. package/docs/en/spec/README.md +13 -0
  90. package/docs/en/spec/acceptance-and-evidence.md +21 -0
  91. package/docs/en/spec/delegation.md +21 -0
  92. package/docs/en/spec/guardrails.md +21 -0
  93. package/docs/en/spec/harness-adapters.md +21 -0
  94. package/docs/en/spec/judgment.md +21 -0
  95. package/docs/en/spec/loop-engineering.md +23 -0
  96. package/docs/en/spec/maintenance.md +21 -0
  97. package/docs/en/spec/review.md +21 -0
  98. package/docs/en/spec/troubleshooting.md +21 -0
  99. package/docs/zh-TW/spec/README.md +13 -0
  100. package/docs/zh-TW/spec/acceptance-and-evidence.md +23 -0
  101. package/docs/zh-TW/spec/delegation.md +23 -0
  102. package/docs/zh-TW/spec/guardrails.md +23 -0
  103. package/docs/zh-TW/spec/harness-adapters.md +23 -0
  104. package/docs/zh-TW/spec/judgment.md +23 -0
  105. package/docs/zh-TW/spec/loop-engineering.md +23 -0
  106. package/docs/zh-TW/spec/maintenance.md +23 -0
  107. package/docs/zh-TW/spec/review.md +23 -0
  108. package/docs/zh-TW/spec/troubleshooting.md +23 -0
  109. package/package.json +41 -0
  110. package/schemas/config.schema.json +231 -0
  111. package/schemas/evidence.schema.json +115 -0
  112. package/schemas/manifest.schema.json +116 -0
  113. package/schemas/task.schema.json +59 -0
  114. package/templates/common/AGENTS.block.md +3 -0
  115. package/templates/common/CLAUDE.block.md +3 -0
@@ -0,0 +1,45 @@
1
+ import { aggregateReviewResults } from "./result.js";
2
+ import { redactSecrets } from "../security/redact.js";
3
+ import { safeTaskText } from "../task/render.js";
4
+ function promptFor(invocation) {
5
+ return [
6
+ "Review the requested criteria in read-only mode.",
7
+ `Harness: ${invocation.harness}; model: ${invocation.model}; effort: ${invocation.effort}.`,
8
+ `Artifacts: ${invocation.packet.artifactRefs.join(", ") || "none"}.`,
9
+ `Criteria: ${invocation.packet.criteria.map((criterion) => criterion.id).join(", ") || "none"}.`
10
+ ].join("\n");
11
+ }
12
+ function safeResult(result) {
13
+ return {
14
+ criterionId: safeTaskText(redactSecrets(result.criterionId)),
15
+ status: result.status,
16
+ evidence: result.evidence.map((reference) => safeTaskText(redactSecrets(reference)))
17
+ };
18
+ }
19
+ export async function runIndependentReview(options) {
20
+ const base = {
21
+ harness: options.invocation.harness,
22
+ model: options.invocation.model,
23
+ effort: options.invocation.effort,
24
+ prompt: promptFor(options.invocation)
25
+ };
26
+ if (!options.authorized) {
27
+ return { ...base, status: "NOT_RUN", reason: "authorization-required" };
28
+ }
29
+ const result = await options.execute({
30
+ invocation: options.invocation,
31
+ readOnly: true
32
+ });
33
+ if (result.status === "NOT_RUN") {
34
+ return { ...base, status: result.status, reason: result.reason };
35
+ }
36
+ if (result.status === "FAIL") {
37
+ return { ...base, status: result.status, results: result.results };
38
+ }
39
+ const summary = aggregateReviewResults(options.invocation.packet.criteria.map((criterion) => criterion.id), result.results);
40
+ return {
41
+ ...base,
42
+ status: summary.status,
43
+ results: summary.results.map(safeResult)
44
+ };
45
+ }
@@ -0,0 +1,584 @@
1
+ import { SCHEMA_VERSION } from "../contracts.js";
2
+ const ID_PATTERN = /^[a-z][a-z0-9-]{0,127}$/;
3
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
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"]);
6
+ const EVIDENCE_KINDS = new Set(["exit-code", "file", "test-count"]);
7
+ const SCOPE_VALUES = new Set(["project", "user"]);
8
+ const HARNESS_VALUES = new Set(["both", "claude", "codex"]);
9
+ const MAX_TIMEOUT_MS = 2_147_483_647;
10
+ const MAX_EXIT_CODE = 4_294_967_295;
11
+ function failure(code, path, message) {
12
+ return {
13
+ ok: false,
14
+ errors: [{ code, path, message }]
15
+ };
16
+ }
17
+ function success(value) {
18
+ return { ok: true, value, errors: [] };
19
+ }
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function firstUnknownField(value, allowed) {
24
+ const allowedSet = new Set(allowed);
25
+ return Object.keys(value)
26
+ .filter((key) => !allowedSet.has(key))
27
+ .sort()[0];
28
+ }
29
+ function unknownFieldFailure(value, allowed, path) {
30
+ const unknown = firstUnknownField(value, allowed);
31
+ return unknown === undefined
32
+ ? undefined
33
+ : failure("UNKNOWN_FIELD", `${path}.${unknown}`, `Unknown field: ${unknown}`);
34
+ }
35
+ function validateRoot(value, allowed) {
36
+ if (!isRecord(value)) {
37
+ return failure("INVALID_TYPE", "$", "Expected an object.");
38
+ }
39
+ if (value.schemaVersion !== SCHEMA_VERSION) {
40
+ return failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion", `Expected schemaVersion ${SCHEMA_VERSION}.`);
41
+ }
42
+ return unknownFieldFailure(value, allowed, "$") ?? value;
43
+ }
44
+ function isFailure(value) {
45
+ return "ok" in value && value.ok === false;
46
+ }
47
+ function isNonEmptyString(value) {
48
+ return (typeof value === "string" &&
49
+ !value.includes("\0") &&
50
+ value.trim().length > 0);
51
+ }
52
+ function isIdentifier(value) {
53
+ return typeof value === "string" && ID_PATTERN.test(value);
54
+ }
55
+ function isStringArray(value) {
56
+ if (!Array.isArray(value)) {
57
+ return false;
58
+ }
59
+ for (let index = 0; index < value.length; index += 1) {
60
+ if (typeof value[index] !== "string") {
61
+ return false;
62
+ }
63
+ }
64
+ return true;
65
+ }
66
+ function isSafeRelativePath(value) {
67
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
68
+ return false;
69
+ }
70
+ if (value.startsWith("/") ||
71
+ value.startsWith("\\") ||
72
+ value.includes("\\") ||
73
+ /^[A-Za-z]:/.test(value)) {
74
+ return false;
75
+ }
76
+ if (value === ".") {
77
+ return true;
78
+ }
79
+ const segments = value.split("/");
80
+ return (segments.every((segment) => segment.length > 0 &&
81
+ segment !== "." &&
82
+ segment !== ".." &&
83
+ !segment.endsWith(".") &&
84
+ !WINDOWS_RESERVED_SEGMENT.test(segment) &&
85
+ /^[A-Za-z0-9._-]+$/.test(segment)) && !value.startsWith("//"));
86
+ }
87
+ function isIsoTimestamp(value) {
88
+ // Deliberately excludes leap-second values because JavaScript Date cannot
89
+ // order them reliably for startedAt/finishedAt evidence comparisons.
90
+ if (typeof value !== "string") {
91
+ return false;
92
+ }
93
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
94
+ if (match === null) {
95
+ return false;
96
+ }
97
+ const year = Number(match[1]);
98
+ const month = Number(match[2]);
99
+ const day = Number(match[3]);
100
+ const hour = Number(match[4]);
101
+ const minute = Number(match[5]);
102
+ const second = Number(match[6]);
103
+ const offsetHour = match[8] === undefined ? 0 : Number(match[9]);
104
+ const offsetMinute = match[8] === undefined ? 0 : Number(match[10]);
105
+ const isLeapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
106
+ const daysInMonth = [
107
+ 31,
108
+ isLeapYear ? 29 : 28,
109
+ 31,
110
+ 30,
111
+ 31,
112
+ 30,
113
+ 31,
114
+ 31,
115
+ 30,
116
+ 31,
117
+ 30,
118
+ 31
119
+ ][month - 1];
120
+ return (Number.isInteger(year) &&
121
+ year >= 1 &&
122
+ month >= 1 &&
123
+ month <= 12 &&
124
+ daysInMonth !== undefined &&
125
+ day >= 1 &&
126
+ day <= daysInMonth &&
127
+ hour >= 0 &&
128
+ hour <= 23 &&
129
+ minute >= 0 &&
130
+ minute <= 59 &&
131
+ second >= 0 &&
132
+ second <= 59 &&
133
+ offsetHour >= 0 &&
134
+ offsetHour <= 23 &&
135
+ offsetMinute >= 0 &&
136
+ offsetMinute <= 59 &&
137
+ Number.isFinite(Date.parse(value)));
138
+ }
139
+ function hasUniqueStrings(values) {
140
+ return new Set(values).size === values.length;
141
+ }
142
+ function validateEvidenceRequirement(value, path) {
143
+ if (!isRecord(value)) {
144
+ return failure("INVALID_TYPE", path, "Expected an evidence object.");
145
+ }
146
+ const unknown = unknownFieldFailure(value, ["kind", "minimum"], path);
147
+ if (unknown !== undefined) {
148
+ return unknown;
149
+ }
150
+ if (typeof value.kind !== "string" || !EVIDENCE_KINDS.has(value.kind)) {
151
+ return failure("INVALID_EVIDENCE_KIND", `${path}.kind`, "Unsupported evidence kind.");
152
+ }
153
+ if (value.minimum !== undefined &&
154
+ (!Number.isSafeInteger(value.minimum) || value.minimum < 0)) {
155
+ return failure("INVALID_MINIMUM", `${path}.minimum`, "minimum must be a non-negative integer.");
156
+ }
157
+ return success(value);
158
+ }
159
+ function validateCommand(value, path) {
160
+ if (!isRecord(value)) {
161
+ return failure("INVALID_TYPE", path, "Expected a verifier command object.");
162
+ }
163
+ const isShell = value.shell === true;
164
+ const allowed = isShell
165
+ ? [
166
+ "acknowledgeRisk",
167
+ "args",
168
+ "command",
169
+ "cwd",
170
+ "evidence",
171
+ "id",
172
+ "required",
173
+ "shell",
174
+ "timeoutMs"
175
+ ]
176
+ : [
177
+ "args",
178
+ "command",
179
+ "cwd",
180
+ "evidence",
181
+ "id",
182
+ "required",
183
+ "shell",
184
+ "timeoutMs"
185
+ ];
186
+ const unknown = unknownFieldFailure(value, allowed, path);
187
+ if (unknown !== undefined) {
188
+ return unknown;
189
+ }
190
+ if (isShell && value.acknowledgeRisk !== true) {
191
+ return failure("SHELL_ACK_REQUIRED", `${path}.acknowledgeRisk`, "Shell execution requires acknowledgeRisk: true.");
192
+ }
193
+ if (value.shell !== undefined && value.shell !== false && value.shell !== true) {
194
+ return failure("INVALID_SHELL_MODE", `${path}.shell`, "shell must be true or false.");
195
+ }
196
+ if (!isIdentifier(value.id)) {
197
+ return failure("INVALID_ID", `${path}.id`, "Invalid command ID.");
198
+ }
199
+ if (!isNonEmptyString(value.command)) {
200
+ return failure("INVALID_COMMAND", `${path}.command`, "command must be a non-empty string.");
201
+ }
202
+ if (!isStringArray(value.args) ||
203
+ value.args.some((argument) => argument.includes("\0"))) {
204
+ return failure("INVALID_ARGS", `${path}.args`, "args must be an array of strings.");
205
+ }
206
+ if (!isSafeRelativePath(value.cwd)) {
207
+ return failure("INVALID_RELATIVE_PATH", `${path}.cwd`, "cwd must be a portable project-relative path.");
208
+ }
209
+ if (typeof value.required !== "boolean") {
210
+ return failure("INVALID_TYPE", `${path}.required`, "required must be boolean.");
211
+ }
212
+ if (value.timeoutMs !== undefined &&
213
+ (!Number.isSafeInteger(value.timeoutMs) ||
214
+ value.timeoutMs <= 0 ||
215
+ value.timeoutMs > MAX_TIMEOUT_MS)) {
216
+ return failure("INVALID_TIMEOUT", `${path}.timeoutMs`, "timeoutMs must be a positive integer.");
217
+ }
218
+ const evidence = validateEvidenceRequirement(value.evidence, `${path}.evidence`);
219
+ if (!evidence.ok) {
220
+ return evidence;
221
+ }
222
+ return success(value);
223
+ }
224
+ function validatePathMapping(value, path, commandIds) {
225
+ if (!isRecord(value)) {
226
+ return failure("INVALID_TYPE", path, "Expected a path mapping object.");
227
+ }
228
+ const unknown = unknownFieldFailure(value, ["path", "verifierIds"], path);
229
+ if (unknown !== undefined) {
230
+ return unknown;
231
+ }
232
+ if (!isSafeRelativePath(value.path)) {
233
+ return failure("INVALID_RELATIVE_PATH", `${path}.path`, "Mapping path must be project-relative.");
234
+ }
235
+ if (!isStringArray(value.verifierIds) ||
236
+ !value.verifierIds.every(isIdentifier)) {
237
+ return failure("INVALID_VERIFIER_REFERENCE", `${path}.verifierIds`, "verifierIds must contain valid IDs.");
238
+ }
239
+ if (!hasUniqueStrings(value.verifierIds)) {
240
+ return failure("DUPLICATE_ID", `${path}.verifierIds`, "verifierIds must be unique.");
241
+ }
242
+ const unknownReference = value.verifierIds.find((id) => !commandIds.has(id));
243
+ if (unknownReference !== undefined) {
244
+ return failure("UNKNOWN_VERIFIER_REFERENCE", `${path}.verifierIds`, `Unknown verifier reference: ${unknownReference}`);
245
+ }
246
+ return success(value);
247
+ }
248
+ function validateSecurityException(value, path) {
249
+ if (!isRecord(value)) {
250
+ return failure("INVALID_TYPE", path, "Expected a security exception object.");
251
+ }
252
+ const unknown = unknownFieldFailure(value, ["expiresAt", "reason", "ruleId", "scope"], path);
253
+ if (unknown !== undefined) {
254
+ return unknown;
255
+ }
256
+ if (!isIdentifier(value.ruleId)) {
257
+ return failure("INVALID_ID", `${path}.ruleId`, "Invalid rule ID.");
258
+ }
259
+ if (!isSafeRelativePath(value.scope) ||
260
+ value.scope === "." ||
261
+ value.scope.includes("*")) {
262
+ return failure("UNSAFE_EXCEPTION_SCOPE", `${path}.scope`, "Exception scope must be a bounded project-relative path.");
263
+ }
264
+ if (!isIsoTimestamp(value.expiresAt)) {
265
+ return failure("INVALID_TIMESTAMP", `${path}.expiresAt`, "expiresAt must be an ISO timestamp.");
266
+ }
267
+ if (!isNonEmptyString(value.reason)) {
268
+ return failure("INVALID_REASON", `${path}.reason`, "Exception reason is required.");
269
+ }
270
+ return success(value);
271
+ }
272
+ export function validateConfig(value) {
273
+ const root = validateRoot(value, [
274
+ "pathMappings",
275
+ "profiles",
276
+ "schemaVersion",
277
+ "securityExceptions",
278
+ "verification"
279
+ ]);
280
+ if (isFailure(root)) {
281
+ return root;
282
+ }
283
+ if (isRecord(root.verification) && Array.isArray(root.verification.commands)) {
284
+ for (const [index, command] of root.verification.commands.entries()) {
285
+ if (isRecord(command) &&
286
+ command.shell === true &&
287
+ command.acknowledgeRisk !== true) {
288
+ return failure("SHELL_ACK_REQUIRED", `$.verification.commands[${index}].acknowledgeRisk`, "Shell execution requires acknowledgeRisk: true.");
289
+ }
290
+ }
291
+ }
292
+ if (!Array.isArray(root.profiles)) {
293
+ return failure("INVALID_TYPE", "$.profiles", "profiles must be an array.");
294
+ }
295
+ for (const [index, profile] of root.profiles.entries()) {
296
+ if (typeof profile !== "string" || !PROFILE_VALUES.has(profile)) {
297
+ return failure("INVALID_PROFILE", `$.profiles[${index}]`, `Unsupported profile: ${String(profile)}`);
298
+ }
299
+ }
300
+ if (!hasUniqueStrings(root.profiles)) {
301
+ return failure("DUPLICATE_ID", "$.profiles", "Profiles must be unique.");
302
+ }
303
+ if (!isRecord(root.verification)) {
304
+ return failure("INVALID_TYPE", "$.verification", "verification must be an object.");
305
+ }
306
+ const verificationUnknown = unknownFieldFailure(root.verification, ["commands"], "$.verification");
307
+ if (verificationUnknown !== undefined) {
308
+ return verificationUnknown;
309
+ }
310
+ if (!Array.isArray(root.verification.commands)) {
311
+ return failure("INVALID_TYPE", "$.verification.commands", "commands must be an array.");
312
+ }
313
+ const commandIds = new Set();
314
+ for (const [index, commandValue] of root.verification.commands.entries()) {
315
+ const command = validateCommand(commandValue, `$.verification.commands[${index}]`);
316
+ if (!command.ok) {
317
+ return command;
318
+ }
319
+ if (commandIds.has(command.value.id)) {
320
+ return failure("DUPLICATE_ID", `$.verification.commands[${index}].id`, `Duplicate command ID: ${command.value.id}`);
321
+ }
322
+ commandIds.add(command.value.id);
323
+ }
324
+ if (!Array.isArray(root.pathMappings)) {
325
+ return failure("INVALID_TYPE", "$.pathMappings", "pathMappings must be an array.");
326
+ }
327
+ for (const [index, mappingValue] of root.pathMappings.entries()) {
328
+ const mapping = validatePathMapping(mappingValue, `$.pathMappings[${index}]`, commandIds);
329
+ if (!mapping.ok) {
330
+ return mapping;
331
+ }
332
+ }
333
+ if (!Array.isArray(root.securityExceptions)) {
334
+ return failure("INVALID_TYPE", "$.securityExceptions", "securityExceptions must be an array.");
335
+ }
336
+ for (const [index, exceptionValue] of root.securityExceptions.entries()) {
337
+ const exception = validateSecurityException(exceptionValue, `$.securityExceptions[${index}]`);
338
+ if (!exception.ok) {
339
+ return exception;
340
+ }
341
+ }
342
+ return success(root);
343
+ }
344
+ function validateCriterion(value, path) {
345
+ if (!isRecord(value)) {
346
+ return failure("INVALID_TYPE", path, "Expected a criterion object.");
347
+ }
348
+ const unknown = unknownFieldFailure(value, ["description", "id", "verifierIds"], path);
349
+ if (unknown !== undefined) {
350
+ return unknown;
351
+ }
352
+ if (!isIdentifier(value.id)) {
353
+ return failure("INVALID_ID", `${path}.id`, "Invalid criterion ID.");
354
+ }
355
+ if (!isNonEmptyString(value.description)) {
356
+ return failure("INVALID_DESCRIPTION", `${path}.description`, "Criterion description is required.");
357
+ }
358
+ if (!isStringArray(value.verifierIds) ||
359
+ value.verifierIds.length === 0 ||
360
+ !value.verifierIds.every(isIdentifier)) {
361
+ return failure("INVALID_VERIFIER_REFERENCE", `${path}.verifierIds`, "A criterion requires at least one valid verifier ID.");
362
+ }
363
+ if (!hasUniqueStrings(value.verifierIds)) {
364
+ return failure("DUPLICATE_ID", `${path}.verifierIds`, "Criterion verifier IDs must be unique.");
365
+ }
366
+ return success(value);
367
+ }
368
+ export function validateTask(value) {
369
+ const root = validateRoot(value, ["criteria", "id", "schemaVersion", "title"]);
370
+ if (isFailure(root)) {
371
+ return root;
372
+ }
373
+ if (!isIdentifier(root.id)) {
374
+ return failure("INVALID_ID", "$.id", "Invalid task ID.");
375
+ }
376
+ if (!isNonEmptyString(root.title)) {
377
+ return failure("INVALID_TITLE", "$.title", "Task title is required.");
378
+ }
379
+ if (!Array.isArray(root.criteria) ||
380
+ root.criteria.length < 2 ||
381
+ root.criteria.length > 5) {
382
+ return failure("CRITERIA_COUNT_INVALID", "$.criteria", "A task requires two to five criteria.");
383
+ }
384
+ const criterionIds = new Set();
385
+ for (const [index, criterionValue] of root.criteria.entries()) {
386
+ const criterion = validateCriterion(criterionValue, `$.criteria[${index}]`);
387
+ if (!criterion.ok) {
388
+ return criterion;
389
+ }
390
+ if (criterionIds.has(criterion.value.id)) {
391
+ return failure("DUPLICATE_ID", `$.criteria[${index}].id`, `Duplicate criterion ID: ${criterion.value.id}`);
392
+ }
393
+ criterionIds.add(criterion.value.id);
394
+ }
395
+ return success(root);
396
+ }
397
+ export function validateTaskAgainstConfig(taskValue, configValue) {
398
+ const config = validateConfig(configValue);
399
+ if (!config.ok) {
400
+ return config;
401
+ }
402
+ const task = validateTask(taskValue);
403
+ if (!task.ok) {
404
+ return task;
405
+ }
406
+ const commandIds = new Set(config.value.verification.commands.map((command) => command.id));
407
+ for (const [criterionIndex, criterion] of task.value.criteria.entries()) {
408
+ const unknownReference = criterion.verifierIds.find((verifierId) => !commandIds.has(verifierId));
409
+ if (unknownReference !== undefined) {
410
+ return failure("UNKNOWN_VERIFIER_REFERENCE", `$.criteria[${criterionIndex}].verifierIds`, `Unknown verifier reference: ${unknownReference}`);
411
+ }
412
+ }
413
+ return task;
414
+ }
415
+ export function validateEvidence(value) {
416
+ const root = validateRoot(value, [
417
+ "argv",
418
+ "commandId",
419
+ "configHash",
420
+ "criterionId",
421
+ "cwd",
422
+ "exitCode",
423
+ "finishedAt",
424
+ "schemaVersion",
425
+ "scope",
426
+ "startedAt",
427
+ "taskId",
428
+ "testCount",
429
+ "toolVersions"
430
+ ]);
431
+ if (isFailure(root)) {
432
+ return root;
433
+ }
434
+ for (const field of ["taskId", "criterionId", "commandId"]) {
435
+ if (!isIdentifier(root[field])) {
436
+ return failure("INVALID_ID", `$.${field}`, `Invalid ${field}.`);
437
+ }
438
+ }
439
+ if (!isStringArray(root.argv) ||
440
+ root.argv.length === 0 ||
441
+ root.argv.some((argument) => argument.includes("\0"))) {
442
+ return failure("INVALID_ARGS", "$.argv", "argv must contain safe strings.");
443
+ }
444
+ if (!isSafeRelativePath(root.cwd)) {
445
+ return failure("INVALID_RELATIVE_PATH", "$.cwd", "cwd must be project-relative.");
446
+ }
447
+ if (typeof root.scope !== "string" || !SCOPE_VALUES.has(root.scope)) {
448
+ return failure("INVALID_SCOPE", "$.scope", "Unsupported evidence scope.");
449
+ }
450
+ if (!isIsoTimestamp(root.startedAt)) {
451
+ return failure("INVALID_TIMESTAMP", "$.startedAt", "startedAt must be a supported RFC 3339 timestamp.");
452
+ }
453
+ if (!isIsoTimestamp(root.finishedAt)) {
454
+ return failure("INVALID_TIMESTAMP", "$.finishedAt", "finishedAt must be a supported RFC 3339 timestamp.");
455
+ }
456
+ if (Date.parse(root.finishedAt) < Date.parse(root.startedAt)) {
457
+ return failure("INVALID_TIMESTAMP_ORDER", "$.finishedAt", "finishedAt must not precede startedAt.");
458
+ }
459
+ if (root.exitCode !== null &&
460
+ (!Number.isSafeInteger(root.exitCode) ||
461
+ root.exitCode < 0 ||
462
+ root.exitCode > MAX_EXIT_CODE)) {
463
+ return failure("INVALID_EXIT_CODE", "$.exitCode", "exitCode must be null or a non-negative integer.");
464
+ }
465
+ if (root.testCount !== null &&
466
+ (!Number.isSafeInteger(root.testCount) || root.testCount < 0)) {
467
+ return failure("INVALID_TEST_COUNT", "$.testCount", "testCount must be null or a non-negative integer.");
468
+ }
469
+ if (!isRecord(root.toolVersions) ||
470
+ !Object.values(root.toolVersions).every(isNonEmptyString)) {
471
+ return failure("INVALID_TOOL_VERSIONS", "$.toolVersions", "toolVersions must map tool names to versions.");
472
+ }
473
+ if (typeof root.configHash !== "string" || !HASH_PATTERN.test(root.configHash)) {
474
+ return failure("INVALID_HASH", "$.configHash", "configHash must be a lowercase SHA-256 digest.");
475
+ }
476
+ return success(root);
477
+ }
478
+ function validateManagedPath(value, path, allowedFields) {
479
+ if (!isRecord(value)) {
480
+ return failure("INVALID_TYPE", path, "Expected a managed path record.");
481
+ }
482
+ const unknown = unknownFieldFailure(value, allowedFields, path);
483
+ if (unknown !== undefined) {
484
+ return unknown;
485
+ }
486
+ if (!isIdentifier(value.id)) {
487
+ return failure("INVALID_ID", `${path}.id`, "Invalid managed entry ID.");
488
+ }
489
+ if (!isSafeRelativePath(value.path)) {
490
+ return failure("INVALID_RELATIVE_PATH", `${path}.path`, "Managed paths must be project-relative.");
491
+ }
492
+ if (value.path === ".") {
493
+ return failure("INVALID_RELATIVE_PATH", `${path}.path`, "Managed paths must not target the project root.");
494
+ }
495
+ if (typeof value.hash !== "string" || !HASH_PATTERN.test(value.hash)) {
496
+ return failure("INVALID_HASH", `${path}.hash`, "Managed path hash must be a lowercase SHA-256 digest.");
497
+ }
498
+ if (value.owner !== "agent-ops") {
499
+ return failure("INVALID_OWNER", `${path}.owner`, "Managed entries must be owned by agent-ops.");
500
+ }
501
+ return success(value);
502
+ }
503
+ function validateManagedMarker(value, path) {
504
+ const base = validateManagedPath(value, path, [
505
+ "endMarker",
506
+ "hash",
507
+ "id",
508
+ "owner",
509
+ "path",
510
+ "startMarker"
511
+ ]);
512
+ if (!base.ok) {
513
+ return base;
514
+ }
515
+ const marker = value;
516
+ if (!isNonEmptyString(marker.startMarker) ||
517
+ !isNonEmptyString(marker.endMarker) ||
518
+ marker.startMarker === marker.endMarker) {
519
+ return failure("INVALID_MARKER_BOUNDARY", path, "Marker boundaries must be distinct non-empty strings.");
520
+ }
521
+ return success(value);
522
+ }
523
+ export function validateManifest(value) {
524
+ const root = validateRoot(value, [
525
+ "artifacts",
526
+ "harness",
527
+ "markers",
528
+ "schemaVersion",
529
+ "scope"
530
+ ]);
531
+ if (isFailure(root)) {
532
+ return root;
533
+ }
534
+ if (typeof root.scope !== "string" || !SCOPE_VALUES.has(root.scope)) {
535
+ return failure("INVALID_SCOPE", "$.scope", "Unsupported install scope.");
536
+ }
537
+ if (typeof root.harness !== "string" || !HARNESS_VALUES.has(root.harness)) {
538
+ return failure("INVALID_HARNESS", "$.harness", "Unsupported harness.");
539
+ }
540
+ if (!Array.isArray(root.artifacts) || !Array.isArray(root.markers)) {
541
+ return failure("INVALID_TYPE", "$.artifacts", "artifacts and markers must be arrays.");
542
+ }
543
+ const entryIds = new Set();
544
+ const artifactPaths = new Set();
545
+ for (const [index, artifactValue] of root.artifacts.entries()) {
546
+ const artifact = validateManagedPath(artifactValue, `$.artifacts[${index}]`, ["hash", "id", "owner", "path"]);
547
+ if (!artifact.ok) {
548
+ return artifact;
549
+ }
550
+ if (entryIds.has(artifact.value.id)) {
551
+ return failure("DUPLICATE_ID", `$.artifacts[${index}].id`, `Duplicate manifest entry ID: ${artifact.value.id}`);
552
+ }
553
+ const artifactPathKey = artifact.value.path.toLowerCase();
554
+ if (artifactPaths.has(artifactPathKey)) {
555
+ return failure("DUPLICATE_OWNERSHIP", `$.artifacts[${index}].path`, `Artifact path is owned more than once: ${artifact.value.path}`);
556
+ }
557
+ entryIds.add(artifact.value.id);
558
+ artifactPaths.add(artifactPathKey);
559
+ }
560
+ const markerBoundaries = new Set();
561
+ for (const [index, markerValue] of root.markers.entries()) {
562
+ const marker = validateManagedMarker(markerValue, `$.markers[${index}]`);
563
+ if (!marker.ok) {
564
+ return marker;
565
+ }
566
+ if (entryIds.has(marker.value.id)) {
567
+ return failure("DUPLICATE_ID", `$.markers[${index}].id`, `Duplicate manifest entry ID: ${marker.value.id}`);
568
+ }
569
+ const markerPathKey = marker.value.path.toLowerCase();
570
+ if (artifactPaths.has(markerPathKey)) {
571
+ return failure("DUPLICATE_OWNERSHIP", `$.markers[${index}].path`, `A whole-file artifact and marker cannot own the same path: ${marker.value.path}`);
572
+ }
573
+ const startBoundary = [markerPathKey, marker.value.startMarker].join("\0");
574
+ const endBoundary = [markerPathKey, marker.value.endMarker].join("\0");
575
+ if (markerBoundaries.has(startBoundary) ||
576
+ markerBoundaries.has(endBoundary)) {
577
+ return failure("DUPLICATE_OWNERSHIP", `$.markers[${index}]`, `Marker boundary is owned more than once: ${marker.value.path}`);
578
+ }
579
+ entryIds.add(marker.value.id);
580
+ markerBoundaries.add(startBoundary);
581
+ markerBoundaries.add(endBoundary);
582
+ }
583
+ return success(root);
584
+ }