@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,206 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { sha256 } from "../fs/hash.js";
4
+ import { removeManagedBlock } from "../fs/managed-block.js";
5
+ import { parseInstallManifest } from "../fs/manifest.js";
6
+ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
7
+ import { FileTransaction } from "../fs/transaction.js";
8
+ import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
9
+ const MANIFEST_PATH = ".agent-ops/manifest.json";
10
+ const MAX_UNINSTALL_FILE_BYTES = 1024 * 1024;
11
+ function isMissing(error) {
12
+ return (typeof error === "object" &&
13
+ error !== null &&
14
+ "code" in error &&
15
+ error.code === "ENOENT");
16
+ }
17
+ async function readCurrentFile(root, path) {
18
+ const resolvedPath = await resolveContainedPath(root, path);
19
+ let handle;
20
+ try {
21
+ const before = await lstat(resolvedPath, { bigint: true });
22
+ if (!before.isFile() ||
23
+ before.size > BigInt(MAX_UNINSTALL_FILE_BYTES)) {
24
+ throw new AgentOpsError("UNINSTALL_TARGET_INVALID", `Uninstall target must be a bounded regular file: ${path}`);
25
+ }
26
+ handle = await open(resolvedPath, constants.O_RDONLY |
27
+ constants.O_NOFOLLOW |
28
+ constants.O_NONBLOCK);
29
+ const opened = await handle.stat({ bigint: true });
30
+ const resolvedAgain = await resolveContainedPath(root, path);
31
+ const after = await lstat(resolvedAgain, { bigint: true });
32
+ if (!opened.isFile() ||
33
+ opened.size > BigInt(MAX_UNINSTALL_FILE_BYTES) ||
34
+ after.dev !== before.dev ||
35
+ after.ino !== before.ino) {
36
+ throw new AgentOpsError("UNINSTALL_TARGET_INVALID", `Uninstall target changed during inspection: ${path}`);
37
+ }
38
+ const chunks = [];
39
+ let totalBytes = 0;
40
+ while (totalBytes <= MAX_UNINSTALL_FILE_BYTES) {
41
+ const remaining = MAX_UNINSTALL_FILE_BYTES + 1 - totalBytes;
42
+ const chunk = Buffer.alloc(Math.min(64 * 1024, remaining));
43
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
44
+ if (bytesRead === 0) {
45
+ break;
46
+ }
47
+ chunks.push(chunk.subarray(0, bytesRead));
48
+ totalBytes += bytesRead;
49
+ }
50
+ if (totalBytes > MAX_UNINSTALL_FILE_BYTES) {
51
+ throw new AgentOpsError("UNINSTALL_TARGET_TOO_LARGE", `Uninstall target exceeds the planning limit: ${path}`);
52
+ }
53
+ const content = Buffer.concat(chunks, totalBytes).toString("utf8");
54
+ return { content, hash: sha256(content) };
55
+ }
56
+ catch (error) {
57
+ if (isMissing(error)) {
58
+ return null;
59
+ }
60
+ throw error;
61
+ }
62
+ finally {
63
+ await handle?.close();
64
+ }
65
+ }
66
+ async function planMarkerFiles(root, markers, expectedMarkers) {
67
+ const grouped = new Map();
68
+ for (const marker of markers) {
69
+ const existing = grouped.get(marker.path) ?? [];
70
+ existing.push(marker);
71
+ grouped.set(marker.path, existing);
72
+ }
73
+ const operations = [];
74
+ for (const [path, pathMarkers] of grouped) {
75
+ const current = await readCurrentFile(root, path);
76
+ if (current === null) {
77
+ throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block file is missing: ${path}`);
78
+ }
79
+ let content = current.content;
80
+ for (const marker of pathMarkers) {
81
+ const expected = expectedMarkers.get(marker.id);
82
+ if (expected === undefined) {
83
+ throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest contains an unsupported managed block.");
84
+ }
85
+ assertExpectedManagedBlock(content, marker, expected);
86
+ try {
87
+ content = removeManagedBlock(content, marker.id);
88
+ }
89
+ catch (error) {
90
+ throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block cannot be removed safely: ${path}`, { cause: error });
91
+ }
92
+ }
93
+ operations.push(content.length === 0
94
+ ? {
95
+ kind: "remove",
96
+ path,
97
+ expectedHash: current.hash
98
+ }
99
+ : {
100
+ kind: "write",
101
+ path,
102
+ content,
103
+ expectedHash: current.hash
104
+ });
105
+ }
106
+ return operations;
107
+ }
108
+ export async function createUninstallPlan(root) {
109
+ const currentManifest = await readCurrentFile(root, MANIFEST_PATH);
110
+ if (currentManifest === null) {
111
+ return {
112
+ installed: false,
113
+ manifest: null,
114
+ manifestHash: null,
115
+ operations: []
116
+ };
117
+ }
118
+ const manifest = parseInstallManifest(currentManifest.content);
119
+ const expectedMarkers = assertSupportedManifestOwnership(manifest);
120
+ const operations = [];
121
+ for (const artifact of manifest.artifacts) {
122
+ const current = await readCurrentFile(root, artifact.path);
123
+ if (current === null || current.hash !== artifact.hash) {
124
+ throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
125
+ }
126
+ operations.push({
127
+ kind: "remove",
128
+ path: artifact.path,
129
+ expectedHash: current.hash
130
+ });
131
+ }
132
+ operations.push(...await planMarkerFiles(root, manifest.markers, expectedMarkers));
133
+ operations.push({
134
+ kind: "remove",
135
+ path: MANIFEST_PATH,
136
+ expectedHash: currentManifest.hash
137
+ });
138
+ return {
139
+ installed: true,
140
+ manifest,
141
+ manifestHash: currentManifest.hash,
142
+ operations
143
+ };
144
+ }
145
+ function allowedPaths(plan) {
146
+ if (plan.manifest === null) {
147
+ return new Set();
148
+ }
149
+ return new Set([
150
+ ...plan.manifest.artifacts.map(({ path }) => path.toLowerCase()),
151
+ ...plan.manifest.markers.map(({ path }) => path.toLowerCase()),
152
+ MANIFEST_PATH
153
+ ]);
154
+ }
155
+ function assertUninstallPlan(plan) {
156
+ if (!plan.installed) {
157
+ if (plan.manifest !== null ||
158
+ plan.manifestHash !== null ||
159
+ plan.operations.length !== 0) {
160
+ throw new AgentOpsError("INVALID_UNINSTALL_PLAN", "An absent installation must have an empty uninstall plan.");
161
+ }
162
+ return;
163
+ }
164
+ if (plan.manifest === null || plan.manifestHash === null) {
165
+ throw new AgentOpsError("INVALID_UNINSTALL_PLAN", "Installed uninstall plans require a manifest.");
166
+ }
167
+ const allowed = allowedPaths(plan);
168
+ const manifestRemovals = plan.operations.filter((operation) => operation.kind === "remove" &&
169
+ operation.path === MANIFEST_PATH &&
170
+ operation.expectedHash === plan.manifestHash);
171
+ if (manifestRemovals.length !== 1 ||
172
+ plan.operations.some(({ path }) => !allowed.has(path.toLowerCase()))) {
173
+ throw new AgentOpsError("INVALID_UNINSTALL_PLAN", "Uninstall plan contains an unowned path or manifest mutation.");
174
+ }
175
+ }
176
+ async function validateUninstalled(root, manifest) {
177
+ for (const artifact of manifest.artifacts) {
178
+ if (await readCurrentFile(root, artifact.path) !== null) {
179
+ throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", `Managed artifact still exists: ${artifact.path}`);
180
+ }
181
+ }
182
+ for (const marker of manifest.markers) {
183
+ const current = await readCurrentFile(root, marker.path);
184
+ if (current !== null &&
185
+ (current.content.includes(marker.startMarker) ||
186
+ current.content.includes(marker.endMarker))) {
187
+ throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", `Managed block still exists: ${marker.path}`);
188
+ }
189
+ }
190
+ if (await readCurrentFile(root, MANIFEST_PATH) !== null) {
191
+ throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", "Installation manifest still exists.");
192
+ }
193
+ }
194
+ export async function applyUninstallPlan(root, plan) {
195
+ assertUninstallPlan(plan);
196
+ const currentPlan = await createUninstallPlan(root);
197
+ if (JSON.stringify(currentPlan) !== JSON.stringify(plan)) {
198
+ throw new AgentOpsError("INVALID_UNINSTALL_PLAN", "Uninstall plan no longer matches the current managed installation.");
199
+ }
200
+ const manifest = plan.manifest;
201
+ if (!plan.installed || manifest === null) {
202
+ return;
203
+ }
204
+ const transaction = new FileTransaction(root);
205
+ await transaction.apply({ operations: plan.operations }, async () => await validateUninstalled(root, manifest));
206
+ }
@@ -0,0 +1,123 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { previewConfigMigration } from "../config/migrate.js";
4
+ import { sha256 } from "../fs/hash.js";
5
+ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
6
+ import { applyInstallPlan } from "./apply.js";
7
+ import { doctorInstallation } from "./doctor.js";
8
+ import { createInstallPlan } from "./plan.js";
9
+ const PACKAGE_NAME = "@kylecheng3146/agent-ops";
10
+ const CONFIG_PATH = ".agent-ops/config.json";
11
+ const MAX_UPDATE_CONFIG_BYTES = 1024 * 1024;
12
+ async function readBoundedConfig(root) {
13
+ const resolvedPath = await resolveContainedPath(root, CONFIG_PATH);
14
+ const before = await lstat(resolvedPath, { bigint: true });
15
+ if (!before.isFile() ||
16
+ before.size > BigInt(MAX_UPDATE_CONFIG_BYTES)) {
17
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Update requires a bounded regular configuration file.");
18
+ }
19
+ const handle = await open(resolvedPath, constants.O_RDONLY |
20
+ constants.O_NOFOLLOW |
21
+ constants.O_NONBLOCK);
22
+ try {
23
+ const opened = await handle.stat({ bigint: true });
24
+ const resolvedAgain = await resolveContainedPath(root, CONFIG_PATH);
25
+ const after = await lstat(resolvedAgain, { bigint: true });
26
+ if (!opened.isFile() ||
27
+ opened.size > BigInt(MAX_UPDATE_CONFIG_BYTES) ||
28
+ after.dev !== before.dev ||
29
+ after.ino !== before.ino) {
30
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Configuration identity changed during update planning.");
31
+ }
32
+ const chunks = [];
33
+ let totalBytes = 0;
34
+ while (totalBytes <= MAX_UPDATE_CONFIG_BYTES) {
35
+ const remaining = MAX_UPDATE_CONFIG_BYTES + 1 - totalBytes;
36
+ const chunk = Buffer.alloc(Math.min(64 * 1024, remaining));
37
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
38
+ if (bytesRead === 0) {
39
+ return Buffer.concat(chunks, totalBytes);
40
+ }
41
+ chunks.push(chunk.subarray(0, bytesRead));
42
+ totalBytes += bytesRead;
43
+ }
44
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Configuration exceeded the update planning limit.");
45
+ }
46
+ finally {
47
+ await handle.close();
48
+ }
49
+ }
50
+ async function previewManagedConfig(root) {
51
+ let source;
52
+ let parsed;
53
+ try {
54
+ source = await readBoundedConfig(root);
55
+ parsed = JSON.parse(source.toString("utf8"));
56
+ }
57
+ catch (error) {
58
+ if (error instanceof AgentOpsError &&
59
+ error.code === "UPDATE_INSTALLATION_INVALID") {
60
+ throw error;
61
+ }
62
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Update requires valid configuration JSON.");
63
+ }
64
+ try {
65
+ return {
66
+ ...previewConfigMigration(parsed),
67
+ sourceHash: sha256(source)
68
+ };
69
+ }
70
+ catch {
71
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Update requires a valid or migratable configuration.");
72
+ }
73
+ }
74
+ function statusOf(report, id) {
75
+ return report.checks.find((candidate) => candidate.id === id)?.status;
76
+ }
77
+ export async function createUpdatePlan(options) {
78
+ const targetVersion = options.targetVersion ??
79
+ (options.registry === undefined
80
+ ? undefined
81
+ : await options.registry.latestVersion(options.packageName ?? PACKAGE_NAME));
82
+ if (targetVersion === undefined) {
83
+ throw new AgentOpsError("UPDATE_TARGET_REQUIRED", "Update requires a target version or an explicit registry client.");
84
+ }
85
+ const report = await doctorInstallation({ root: options.root });
86
+ for (const id of [
87
+ "node-version",
88
+ "manifest",
89
+ "markers"
90
+ ]) {
91
+ if (statusOf(report, id) !== "PASS") {
92
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", `Update requires a passing ${id} doctor check.`);
93
+ }
94
+ }
95
+ if (report.manifest === undefined) {
96
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Update requires a valid manifest.");
97
+ }
98
+ const configPreview = await previewManagedConfig(options.root);
99
+ if (statusOf(report, "config") !== "PASS" &&
100
+ configPreview.steps.length === 0) {
101
+ throw new AgentOpsError("UPDATE_INSTALLATION_INVALID", "Update requires a passing config doctor check.");
102
+ }
103
+ const installation = await createInstallPlan({
104
+ root: options.root,
105
+ scope: report.manifest.scope,
106
+ harness: report.manifest.harness,
107
+ profiles: configPreview.migrated.profiles,
108
+ adapters: options.adapters,
109
+ toolkitVersion: targetVersion,
110
+ existingConfig: {
111
+ value: configPreview.migrated,
112
+ sourceHash: configPreview.sourceHash
113
+ }
114
+ });
115
+ return {
116
+ targetVersion,
117
+ migrationSteps: configPreview.steps,
118
+ installation
119
+ };
120
+ }
121
+ export async function applyUpdatePlan(root, plan) {
122
+ await applyInstallPlan(root, plan.installation);
123
+ }
@@ -0,0 +1,158 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../security/permissions.js";
3
+ import { redactSecrets } from "../security/redact.js";
4
+ const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
5
+ const DEFAULT_MAX_BYTES = 1024 * 1024;
6
+ const ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function hasExactKeys(value, expected) {
11
+ const actual = Object.keys(value).sort();
12
+ return (actual.length === expected.length &&
13
+ [...expected]
14
+ .sort()
15
+ .every((key, index) => actual[index] === key));
16
+ }
17
+ function isBoundedString(value) {
18
+ return (typeof value === "string" &&
19
+ !value.includes("\0") &&
20
+ value.length > 0 &&
21
+ value.length <= 4096);
22
+ }
23
+ function invalidEvent() {
24
+ throw new AgentOpsError("LOG_EVENT_INVALID", "Local log event contains unsupported or invalid fields.");
25
+ }
26
+ function sanitizeEvent(value) {
27
+ if (!isRecord(value) || typeof value.type !== "string") {
28
+ return invalidEvent();
29
+ }
30
+ if (value.type === "diagnostic") {
31
+ if (!hasExactKeys(value, ["code", "type"]) ||
32
+ typeof value.code !== "string" ||
33
+ !ID_PATTERN.test(value.code)) {
34
+ return invalidEvent();
35
+ }
36
+ return {
37
+ type: "diagnostic",
38
+ code: value.code
39
+ };
40
+ }
41
+ if (value.type === "command-result") {
42
+ if (!hasExactKeys(value, [
43
+ "commandId",
44
+ "durationMs",
45
+ "exitCode",
46
+ "status",
47
+ "type"
48
+ ]) ||
49
+ typeof value.commandId !== "string" ||
50
+ !ID_PATTERN.test(value.commandId) ||
51
+ !["FAIL", "PASS", "UNKNOWN"].includes(String(value.status)) ||
52
+ (value.exitCode !== null &&
53
+ (!Number.isSafeInteger(value.exitCode) ||
54
+ value.exitCode < 0)) ||
55
+ !Number.isSafeInteger(value.durationMs) ||
56
+ value.durationMs < 0) {
57
+ return invalidEvent();
58
+ }
59
+ return {
60
+ type: "command-result",
61
+ commandId: value.commandId,
62
+ status: value.status,
63
+ exitCode: value.exitCode,
64
+ durationMs: value.durationMs
65
+ };
66
+ }
67
+ if (value.type === "trust-change") {
68
+ if (!hasExactKeys(value, [
69
+ "action",
70
+ "remoteIdentity",
71
+ "result",
72
+ "type"
73
+ ]) ||
74
+ !["grant", "revoke"].includes(String(value.action)) ||
75
+ !isBoundedString(value.remoteIdentity) ||
76
+ !["changed", "unchanged"].includes(String(value.result))) {
77
+ return invalidEvent();
78
+ }
79
+ return {
80
+ type: "trust-change",
81
+ action: value.action,
82
+ remoteIdentity: redactSecrets(value.remoteIdentity),
83
+ result: value.result
84
+ };
85
+ }
86
+ return invalidEvent();
87
+ }
88
+ function serialize(stored) {
89
+ return `${JSON.stringify({
90
+ timestamp: stored.timestamp,
91
+ ...stored.event
92
+ })}\n`;
93
+ }
94
+ function parseStoredLine(line) {
95
+ let value;
96
+ try {
97
+ value = JSON.parse(line);
98
+ }
99
+ catch (error) {
100
+ throw new AgentOpsError("LOG_CORRUPT", "Local log contains invalid JSON.", { cause: error });
101
+ }
102
+ if (!isRecord(value) ||
103
+ typeof value.timestamp !== "string" ||
104
+ !Number.isFinite(Date.parse(value.timestamp))) {
105
+ throw new AgentOpsError("LOG_CORRUPT", "Local log contains an invalid timestamp.");
106
+ }
107
+ const { timestamp, ...event } = value;
108
+ return {
109
+ timestamp,
110
+ event: sanitizeEvent(event)
111
+ };
112
+ }
113
+ function positiveOption(value, fallback) {
114
+ if (value === undefined) {
115
+ return fallback;
116
+ }
117
+ if (!Number.isSafeInteger(value) || value <= 0) {
118
+ throw new AgentOpsError("LOG_RETENTION_INVALID", "Local log retention limits must be positive integers.");
119
+ }
120
+ return value;
121
+ }
122
+ export async function appendLocalLog(path, event, options) {
123
+ const now = options.now ?? new Date().toISOString();
124
+ const nowMs = Date.parse(now);
125
+ if (!Number.isFinite(nowMs)) {
126
+ throw new AgentOpsError("LOG_TIMESTAMP_INVALID", "Local log timestamp must be ISO-compatible.");
127
+ }
128
+ const maxAgeMs = positiveOption(options.maxAgeMs, DEFAULT_MAX_AGE_MS);
129
+ const maxBytes = positiveOption(options.maxBytes, DEFAULT_MAX_BYTES);
130
+ const storedEvent = {
131
+ timestamp: now,
132
+ event: sanitizeEvent(event)
133
+ };
134
+ const newLine = serialize(storedEvent);
135
+ if (Buffer.byteLength(newLine) > maxBytes) {
136
+ throw new AgentOpsError("LOG_EVENT_TOO_LARGE", "Local log event exceeds the configured byte limit.");
137
+ }
138
+ await withPrivateFileLock(path, options.anchorDirectory, async () => {
139
+ const source = await readPrivateFile(path, options.anchorDirectory);
140
+ const retained = source === null || source.length === 0
141
+ ? []
142
+ : source
143
+ .split("\n")
144
+ .filter((line) => line.length > 0)
145
+ .map(parseStoredLine)
146
+ .filter((entry) => nowMs - Date.parse(entry.timestamp) <= maxAgeMs);
147
+ retained.push(storedEvent);
148
+ const serialized = retained.map(serialize);
149
+ let totalBytes = serialized.reduce((total, line) => total + Buffer.byteLength(line), 0);
150
+ while (serialized.length > 1 && totalBytes > maxBytes) {
151
+ const removed = serialized.shift();
152
+ if (removed !== undefined) {
153
+ totalBytes -= Buffer.byteLength(removed);
154
+ }
155
+ }
156
+ await writePrivateFile(path, serialized.join(""), options.anchorDirectory);
157
+ });
158
+ }
@@ -0,0 +1,141 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/";
3
+ const DEFAULT_TIMEOUT_MS = 10_000;
4
+ const DEFAULT_MAX_RESPONSE_BYTES = 1_048_576;
5
+ const SEMVER_LIKE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
6
+ function defaultFetch(url, init) {
7
+ return globalThis.fetch(url, init);
8
+ }
9
+ function positiveBound(value, name) {
10
+ if (!Number.isSafeInteger(value) || value < 1) {
11
+ throw new AgentOpsError("REGISTRY_OPTIONS_INVALID", `${name} must be a positive safe integer.`);
12
+ }
13
+ return value;
14
+ }
15
+ function responseTooLarge() {
16
+ return new AgentOpsError("REGISTRY_RESPONSE_TOO_LARGE", "The npm registry response exceeded the allowed size.");
17
+ }
18
+ async function readBoundedResponse(response, maxResponseBytes, controller) {
19
+ const declaredLength = response.headers.get("content-length");
20
+ if (declaredLength !== null &&
21
+ /^\d+$/.test(declaredLength) &&
22
+ BigInt(declaredLength) > BigInt(maxResponseBytes)) {
23
+ controller.abort();
24
+ throw responseTooLarge();
25
+ }
26
+ if (response.body === null) {
27
+ return new Uint8Array();
28
+ }
29
+ const reader = response.body.getReader();
30
+ const chunks = [];
31
+ let byteLength = 0;
32
+ try {
33
+ while (true) {
34
+ const { done, value } = await reader.read();
35
+ if (done) {
36
+ break;
37
+ }
38
+ if (value === undefined) {
39
+ continue;
40
+ }
41
+ byteLength += value.byteLength;
42
+ if (byteLength > maxResponseBytes) {
43
+ controller.abort();
44
+ throw responseTooLarge();
45
+ }
46
+ chunks.push(value);
47
+ }
48
+ }
49
+ finally {
50
+ reader.releaseLock();
51
+ }
52
+ const combined = new Uint8Array(byteLength);
53
+ let offset = 0;
54
+ for (const chunk of chunks) {
55
+ combined.set(chunk, offset);
56
+ offset += chunk.byteLength;
57
+ }
58
+ return combined;
59
+ }
60
+ function isRecord(value) {
61
+ return (typeof value === "object" &&
62
+ value !== null &&
63
+ !Array.isArray(value));
64
+ }
65
+ function parseLatestVersion(bytes) {
66
+ let parsed;
67
+ try {
68
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
69
+ parsed = JSON.parse(source);
70
+ }
71
+ catch {
72
+ throw new AgentOpsError("REGISTRY_RESPONSE_INVALID", "The npm registry response is not valid JSON.");
73
+ }
74
+ if (!isRecord(parsed)) {
75
+ throw new AgentOpsError("REGISTRY_RESPONSE_INVALID", "The npm registry response must be a JSON object.");
76
+ }
77
+ const distTags = parsed["dist-tags"];
78
+ const latest = isRecord(distTags) ? distTags.latest : undefined;
79
+ if (typeof latest !== "string" || !SEMVER_LIKE.test(latest)) {
80
+ throw new AgentOpsError("REGISTRY_RESPONSE_INVALID", "The npm registry response has no valid latest version.");
81
+ }
82
+ return latest;
83
+ }
84
+ export class NpmRegistryClient {
85
+ #fetch;
86
+ #timeoutMs;
87
+ #maxResponseBytes;
88
+ constructor(options = {}) {
89
+ this.#fetch = options.fetch ?? defaultFetch;
90
+ this.#timeoutMs = positiveBound(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, "timeoutMs");
91
+ this.#maxResponseBytes = positiveBound(options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES, "maxResponseBytes");
92
+ }
93
+ async #request(packageName, controller) {
94
+ const url = `${NPM_REGISTRY_URL}${encodeURIComponent(packageName)}`;
95
+ const response = await this.#fetch(url, {
96
+ method: "GET",
97
+ redirect: "error",
98
+ headers: { accept: "application/json" },
99
+ signal: controller.signal
100
+ });
101
+ if (response.status !== 200) {
102
+ throw new AgentOpsError("REGISTRY_HTTP_STATUS", `The npm registry returned HTTP ${response.status}.`);
103
+ }
104
+ return parseLatestVersion(await readBoundedResponse(response, this.#maxResponseBytes, controller));
105
+ }
106
+ async latestVersion(packageName) {
107
+ if (packageName.length === 0) {
108
+ throw new AgentOpsError("REGISTRY_PACKAGE_INVALID", "The npm package name must not be empty.");
109
+ }
110
+ const controller = new AbortController();
111
+ let timedOut = false;
112
+ let timeout;
113
+ const timeoutFailure = new Promise((_resolve, reject) => {
114
+ timeout = setTimeout(() => {
115
+ timedOut = true;
116
+ reject(new AgentOpsError("REGISTRY_TIMEOUT", "The npm registry request timed out."));
117
+ controller.abort();
118
+ }, this.#timeoutMs);
119
+ });
120
+ try {
121
+ return await Promise.race([
122
+ this.#request(packageName, controller),
123
+ timeoutFailure
124
+ ]);
125
+ }
126
+ catch (error) {
127
+ if (timedOut) {
128
+ throw new AgentOpsError("REGISTRY_TIMEOUT", "The npm registry request timed out.");
129
+ }
130
+ if (error instanceof AgentOpsError) {
131
+ throw error;
132
+ }
133
+ throw new AgentOpsError("REGISTRY_REQUEST_FAILED", "The npm registry request failed.");
134
+ }
135
+ finally {
136
+ if (timeout !== undefined) {
137
+ clearTimeout(timeout);
138
+ }
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,4 @@
1
+ import { runIndependentReview } from "./runner.js";
2
+ export function runClaudeReview(options) {
3
+ return runIndependentReview(options);
4
+ }
@@ -0,0 +1,4 @@
1
+ import { runIndependentReview } from "./runner.js";
2
+ export function runCodexReview(options) {
3
+ return runIndependentReview(options);
4
+ }
@@ -0,0 +1,10 @@
1
+ export function buildReviewPacket(input) {
2
+ return {
3
+ request: input.request,
4
+ criteria: input.criteria.map((criterion) => ({ ...criterion })),
5
+ artifactRefs: [...input.artifactRefs],
6
+ evidenceRequirements: input.evidenceRequirements.map((requirement) => ({
7
+ ...requirement
8
+ }))
9
+ };
10
+ }
@@ -0,0 +1,24 @@
1
+ export function aggregateReviewResults(requestedCriterionIds, results) {
2
+ const expected = new Set(requestedCriterionIds);
3
+ const seen = new Set();
4
+ let valid = requestedCriterionIds.length === expected.size;
5
+ for (const result of results) {
6
+ if (!expected.has(result.criterionId) ||
7
+ seen.has(result.criterionId) ||
8
+ result.evidence.length === 0 ||
9
+ result.evidence.some((reference) => reference.trim().length === 0)) {
10
+ valid = false;
11
+ }
12
+ seen.add(result.criterionId);
13
+ }
14
+ if (seen.size !== expected.size) {
15
+ valid = false;
16
+ }
17
+ const status = valid && results.every((result) => result.status === "PASS")
18
+ ? "PASS"
19
+ : "FAIL";
20
+ return { status, results: [...results] };
21
+ }
22
+ export function summarizeReview(request) {
23
+ return aggregateReviewResults(request.packet.criteria.map((criterion) => criterion.id), request.criterionResults);
24
+ }
@@ -0,0 +1,3 @@
1
+ export function resolveReviewRole(role, configured) {
2
+ return configured.find((item) => item.role === role);
3
+ }