@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,196 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { sha256 } from "../fs/hash.js";
4
+ import { parseInstallManifest, PROJECT_MANIFEST_PATH } from "../fs/manifest.js";
5
+ import { resolveContainedPath } from "../fs/paths.js";
6
+ import { validateConfig } from "../schema/validate.js";
7
+ import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
8
+ const CONFIG_PATH = ".agent-ops/config.json";
9
+ const MINIMUM_NODE_VERSION = [22, 14, 0];
10
+ const MAX_DOCTOR_FILE_BYTES = 1024 * 1024;
11
+ function check(id, status, message) {
12
+ return { id, status, message };
13
+ }
14
+ function parseNodeVersion(version) {
15
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
16
+ if (match === null) {
17
+ return null;
18
+ }
19
+ const parsed = [Number(match[1]), Number(match[2]), Number(match[3])];
20
+ return parsed.every(Number.isSafeInteger) ? parsed : null;
21
+ }
22
+ function meetsMinimumNodeVersion(version) {
23
+ for (let index = 0; index < MINIMUM_NODE_VERSION.length; index += 1) {
24
+ const actual = version[index];
25
+ const minimum = MINIMUM_NODE_VERSION[index];
26
+ if (actual !== minimum) {
27
+ return actual > minimum;
28
+ }
29
+ }
30
+ return true;
31
+ }
32
+ function checkNodeVersion(version) {
33
+ const parsed = parseNodeVersion(version);
34
+ if (parsed === null || !meetsMinimumNodeVersion(parsed)) {
35
+ return check("node-version", "FAIL", `Node ${version} does not meet the minimum version 22.14.0.`);
36
+ }
37
+ return check("node-version", "PASS", `Node ${version} meets the minimum version 22.14.0.`);
38
+ }
39
+ async function readContained(root, path) {
40
+ const resolvedPath = await resolveContainedPath(root, path);
41
+ const before = await lstat(resolvedPath, { bigint: true });
42
+ if (!before.isFile() ||
43
+ before.size > BigInt(MAX_DOCTOR_FILE_BYTES)) {
44
+ throw new Error("Doctor targets must be bounded regular files.");
45
+ }
46
+ const handle = await open(resolvedPath, constants.O_RDONLY |
47
+ constants.O_NOFOLLOW |
48
+ constants.O_NONBLOCK);
49
+ try {
50
+ const opened = await handle.stat({ bigint: true });
51
+ const resolvedAgain = await resolveContainedPath(root, path);
52
+ const after = await lstat(resolvedAgain, { bigint: true });
53
+ if (!opened.isFile() ||
54
+ opened.size > BigInt(MAX_DOCTOR_FILE_BYTES) ||
55
+ after.dev !== before.dev ||
56
+ after.ino !== before.ino) {
57
+ throw new Error("Doctor target identity changed during inspection.");
58
+ }
59
+ const chunks = [];
60
+ let totalBytes = 0;
61
+ while (totalBytes <= MAX_DOCTOR_FILE_BYTES) {
62
+ const remaining = MAX_DOCTOR_FILE_BYTES + 1 - totalBytes;
63
+ const chunk = Buffer.alloc(Math.min(64 * 1024, remaining));
64
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
65
+ if (bytesRead === 0) {
66
+ return Buffer.concat(chunks, totalBytes);
67
+ }
68
+ chunks.push(chunk.subarray(0, bytesRead));
69
+ totalBytes += bytesRead;
70
+ }
71
+ throw new Error("Doctor target exceeded its inspection limit.");
72
+ }
73
+ finally {
74
+ await handle.close();
75
+ }
76
+ }
77
+ async function readContainedText(root, path) {
78
+ return (await readContained(root, path)).toString("utf8");
79
+ }
80
+ async function checkManifest(root) {
81
+ try {
82
+ const manifest = parseInstallManifest(await readContainedText(root, PROJECT_MANIFEST_PATH));
83
+ assertSupportedManifestOwnership(manifest);
84
+ return {
85
+ check: check("manifest", "PASS", "Installation manifest is valid."),
86
+ manifest
87
+ };
88
+ }
89
+ catch {
90
+ return {
91
+ check: check("manifest", "FAIL", "Installation manifest is missing, unsafe, or invalid.")
92
+ };
93
+ }
94
+ }
95
+ async function checkConfig(root) {
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(await readContainedText(root, CONFIG_PATH));
99
+ }
100
+ catch {
101
+ return {
102
+ check: check("config", "FAIL", "Configuration is missing, unsafe, or invalid JSON.")
103
+ };
104
+ }
105
+ const result = validateConfig(parsed);
106
+ if (!result.ok) {
107
+ const error = result.errors[0];
108
+ return {
109
+ check: check("config", "FAIL", error === undefined
110
+ ? "Configuration failed validation."
111
+ : `Configuration failed validation: ${error.code} at ${error.path}.`)
112
+ };
113
+ }
114
+ return {
115
+ check: check("config", "PASS", "Configuration is valid."),
116
+ config: result.value
117
+ };
118
+ }
119
+ async function checkArtifacts(root, manifest) {
120
+ if (manifest === undefined) {
121
+ return check("artifacts", "FAIL", "Artifacts cannot be verified without a valid manifest.");
122
+ }
123
+ const failures = [];
124
+ for (const artifact of manifest.artifacts) {
125
+ try {
126
+ const content = await readContained(root, artifact.path);
127
+ if (sha256(content) !== artifact.hash) {
128
+ failures.push(artifact.path);
129
+ }
130
+ }
131
+ catch {
132
+ failures.push(artifact.path);
133
+ }
134
+ }
135
+ return failures.length === 0
136
+ ? check("artifacts", "PASS", "All managed artifacts match their hashes.")
137
+ : check("artifacts", "FAIL", `Managed artifacts failed verification: ${failures.join(", ")}.`);
138
+ }
139
+ async function checkMarkers(root, manifest) {
140
+ if (manifest === undefined) {
141
+ return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.");
142
+ }
143
+ const failures = [];
144
+ const expectedMarkers = assertSupportedManifestOwnership(manifest);
145
+ for (const marker of manifest.markers) {
146
+ try {
147
+ const source = await readContainedText(root, marker.path);
148
+ const expected = expectedMarkers.get(marker.id);
149
+ if (expected === undefined) {
150
+ failures.push(marker.path);
151
+ continue;
152
+ }
153
+ assertExpectedManagedBlock(source, marker, expected);
154
+ }
155
+ catch {
156
+ failures.push(marker.path);
157
+ }
158
+ }
159
+ return failures.length === 0
160
+ ? check("markers", "PASS", "All managed block markers are intact.")
161
+ : check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
162
+ }
163
+ async function checkProbe(id, probe) {
164
+ if (probe === undefined) {
165
+ return check(id, "UNKNOWN", "No probe was provided.");
166
+ }
167
+ try {
168
+ return (await probe())
169
+ ? check(id, "PASS", "Probe passed.")
170
+ : check(id, "FAIL", "Probe failed.");
171
+ }
172
+ catch {
173
+ return check(id, "FAIL", "Probe failed.");
174
+ }
175
+ }
176
+ export async function doctorInstallation(options) {
177
+ const manifest = await checkManifest(options.root);
178
+ const config = await checkConfig(options.root);
179
+ const checks = [
180
+ checkNodeVersion(options.nodeVersion ?? process.versions.node),
181
+ manifest.check,
182
+ config.check,
183
+ await checkArtifacts(options.root, manifest.manifest),
184
+ await checkMarkers(options.root, manifest.manifest),
185
+ await checkProbe("hook-registration", options.probes?.hookRegistration),
186
+ await checkProbe("repository-trust", options.probes?.repositoryTrust),
187
+ await checkProbe("smoke-availability", options.probes?.smokeAvailability)
188
+ ];
189
+ return {
190
+ checks,
191
+ ...(manifest.manifest === undefined
192
+ ? {}
193
+ : { manifest: manifest.manifest }),
194
+ ...(config.config === undefined ? {} : { config: config.config })
195
+ };
196
+ }
@@ -0,0 +1,87 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ export const COMMON_AGENTS_BLOCK = "## Loop Engineering\n\nUse `.agent-ops/AGENTS.md` as the canonical Loop Engineering specification for this project.\n";
3
+ export const COMMON_CLAUDE_BLOCK = "## Loop Engineering\n\nUse `.agent-ops/CLAUDE.md` as the canonical Loop Engineering specification for this project.\n";
4
+ function managedRules(id, context) {
5
+ const instructionFile = id === "codex" ? "AGENTS.md" : "CLAUDE.md";
6
+ const lines = [
7
+ "# Loop Engineering",
8
+ "",
9
+ "This routing specification is managed by `agent-ops`.",
10
+ "",
11
+ `Active profiles: ${context.profiles.join(", ")}`,
12
+ `Active capabilities: ${context.capabilities.join(", ")}`,
13
+ ...(context.toolkitVersion === undefined
14
+ ? []
15
+ : [`Toolkit version: ${context.toolkitVersion}`]),
16
+ ""
17
+ ];
18
+ if (context.capabilities.includes("rules")) {
19
+ lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "explicit matching trust record; installation approval never grants trust.", "");
20
+ }
21
+ if (context.capabilities.includes("lifecycle-summary")) {
22
+ lines.push("Advisory lifecycle summaries and local logs are informational. Advisory", "failures must remain fail-open and cannot become verification evidence.", "");
23
+ }
24
+ if (context.capabilities.includes("command-policy")) {
25
+ lines.push("Command policy guards high-confidence unsafe actions. Optional Stop", "verification never marks a task complete by itself.", "");
26
+ }
27
+ lines.push(`This file is routed from the active ${instructionFile}.`, "");
28
+ return lines.join("\n");
29
+ }
30
+ export function commonHarnessAdapters() {
31
+ return ["codex", "claude"].map((id) => {
32
+ const instructionFile = id === "codex" ? "AGENTS.md" : "CLAUDE.md";
33
+ const blockContent = id === "codex" ? COMMON_AGENTS_BLOCK : COMMON_CLAUDE_BLOCK;
34
+ return {
35
+ id,
36
+ async plan(context) {
37
+ return {
38
+ artifacts: [
39
+ {
40
+ id: `${id}-rules`,
41
+ path: `.agent-ops/${instructionFile}`,
42
+ content: managedRules(id, context)
43
+ }
44
+ ],
45
+ blocks: [
46
+ {
47
+ id: `${id}-routing`,
48
+ path: context.scope === "project"
49
+ ? instructionFile
50
+ : `.${id}/${instructionFile}`,
51
+ version: 1,
52
+ content: blockContent
53
+ }
54
+ ]
55
+ };
56
+ }
57
+ };
58
+ });
59
+ }
60
+ function requestedHarnessIds(harness) {
61
+ return harness === "both" ? ["codex", "claude"] : [harness];
62
+ }
63
+ function selectAdapter(id, adapters) {
64
+ const matches = adapters.filter((adapter) => adapter.id === id);
65
+ if (matches.length === 0) {
66
+ throw new AgentOpsError("HARNESS_ADAPTER_MISSING", `Missing harness adapter: ${id}`);
67
+ }
68
+ if (matches.length > 1) {
69
+ throw new AgentOpsError("HARNESS_ADAPTER_DUPLICATE", `Duplicate harness adapter: ${id}`);
70
+ }
71
+ const adapter = matches[0];
72
+ if (adapter === undefined) {
73
+ throw new AgentOpsError("HARNESS_ADAPTER_MISSING", `Missing harness adapter: ${id}`);
74
+ }
75
+ return adapter;
76
+ }
77
+ export async function planHarnessContributions(harness, context, adapters) {
78
+ const selectedAdapters = requestedHarnessIds(harness).map((id) => selectAdapter(id, adapters));
79
+ const artifacts = [];
80
+ const blocks = [];
81
+ for (const adapter of selectedAdapters) {
82
+ const contribution = await adapter.plan(context);
83
+ artifacts.push(...contribution.artifacts);
84
+ blocks.push(...contribution.blocks);
85
+ }
86
+ return { artifacts, blocks };
87
+ }
@@ -0,0 +1,84 @@
1
+ import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ import { COMMON_AGENTS_BLOCK, COMMON_CLAUDE_BLOCK } from "./harness.js";
4
+ function selectedHarnesses(manifest) {
5
+ return manifest.harness === "both"
6
+ ? ["codex", "claude"]
7
+ : [manifest.harness];
8
+ }
9
+ function expectedMarker(manifest, id) {
10
+ const isCodex = id === "codex";
11
+ const markerId = `${id}-routing`;
12
+ const markers = managedBlockMarkers(markerId, 1);
13
+ const instructionFile = isCodex ? "AGENTS.md" : "CLAUDE.md";
14
+ return {
15
+ id: markerId,
16
+ path: manifest.scope === "project"
17
+ ? instructionFile
18
+ : `.${id}/${instructionFile}`,
19
+ startMarker: markers.start,
20
+ endMarker: markers.end,
21
+ content: isCodex ? COMMON_AGENTS_BLOCK : COMMON_CLAUDE_BLOCK
22
+ };
23
+ }
24
+ function manifestOwnershipError() {
25
+ return new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest does not match a supported managed installation shape.");
26
+ }
27
+ export function assertSupportedManifestOwnership(manifest) {
28
+ const harnesses = selectedHarnesses(manifest);
29
+ const expectedArtifacts = new Map([
30
+ ["config", ".agent-ops/config.json"]
31
+ ]);
32
+ const expectedMarkers = new Map();
33
+ for (const id of harnesses) {
34
+ expectedArtifacts.set(`${id}-rules`, `.agent-ops/${id === "codex" ? "AGENTS.md" : "CLAUDE.md"}`);
35
+ const marker = expectedMarker(manifest, id);
36
+ expectedMarkers.set(marker.id, marker);
37
+ }
38
+ if (manifest.artifacts.length !== expectedArtifacts.size ||
39
+ manifest.markers.length !== expectedMarkers.size) {
40
+ throw manifestOwnershipError();
41
+ }
42
+ for (const artifact of manifest.artifacts) {
43
+ if (expectedArtifacts.get(artifact.id) !== artifact.path) {
44
+ throw manifestOwnershipError();
45
+ }
46
+ }
47
+ for (const marker of manifest.markers) {
48
+ const expected = expectedMarkers.get(marker.id);
49
+ if (expected === undefined ||
50
+ marker.path !== expected.path ||
51
+ marker.startMarker !== expected.startMarker ||
52
+ marker.endMarker !== expected.endMarker) {
53
+ throw manifestOwnershipError();
54
+ }
55
+ }
56
+ return expectedMarkers;
57
+ }
58
+ function exactMarkerCount(source, marker) {
59
+ let count = 0;
60
+ let offset = 0;
61
+ while ((offset = source.indexOf(marker, offset)) !== -1) {
62
+ count += 1;
63
+ offset += marker.length;
64
+ }
65
+ return count;
66
+ }
67
+ export function assertExpectedManagedBlock(source, marker, expected) {
68
+ const startIndex = source.indexOf(marker.startMarker);
69
+ const endIndex = source.indexOf(marker.endMarker);
70
+ if (exactMarkerCount(source, marker.startMarker) !== 1 ||
71
+ exactMarkerCount(source, marker.endMarker) !== 1 ||
72
+ startIndex >= endIndex) {
73
+ throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block boundaries changed after installation: ${marker.path}`);
74
+ }
75
+ const expectedBlock = applyManagedBlock("", {
76
+ id: expected.id,
77
+ version: 1,
78
+ content: expected.content
79
+ }).replace(/\n$/u, "");
80
+ const currentBlock = source.slice(startIndex, endIndex + marker.endMarker.length);
81
+ if (currentBlock !== expectedBlock) {
82
+ throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block content changed after installation: ${marker.path}`);
83
+ }
84
+ }
@@ -0,0 +1,257 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import { SCHEMA_VERSION } from "../contracts.js";
3
+ import { sha256 } from "../fs/hash.js";
4
+ import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
5
+ import { formatInstallManifest, parseInstallManifest } from "../fs/manifest.js";
6
+ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
7
+ import { validateConfig } from "../schema/validate.js";
8
+ import { planHarnessContributions } from "./harness.js";
9
+ import { resolveProfiles } from "./profiles.js";
10
+ const CONFIG_PATH = ".agent-ops/config.json";
11
+ const MANIFEST_PATH = ".agent-ops/manifest.json";
12
+ const MAX_PLANNED_FILE_BYTES = 1024 * 1024;
13
+ function isMissing(error) {
14
+ return (typeof error === "object" &&
15
+ error !== null &&
16
+ "code" in error &&
17
+ error.code === "ENOENT");
18
+ }
19
+ async function readCurrentFile(root, path) {
20
+ const resolvedPath = await resolveContainedPath(root, path);
21
+ try {
22
+ const status = await lstat(resolvedPath);
23
+ if (!status.isFile()) {
24
+ throw new AgentOpsError("INSTALL_TARGET_INVALID", `Install target must be a regular file: ${path}`);
25
+ }
26
+ if (status.size > MAX_PLANNED_FILE_BYTES) {
27
+ throw new AgentOpsError("INSTALL_TARGET_TOO_LARGE", `Install target exceeds the planning limit: ${path}`);
28
+ }
29
+ const content = await readFile(resolvedPath, "utf8");
30
+ return { content, hash: sha256(content) };
31
+ }
32
+ catch (error) {
33
+ if (isMissing(error)) {
34
+ return null;
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ function formatConfig(profiles, existing) {
40
+ return `${JSON.stringify({
41
+ schemaVersion: SCHEMA_VERSION,
42
+ profiles,
43
+ verification: existing?.verification ?? { commands: [] },
44
+ pathMappings: existing?.pathMappings ?? [],
45
+ securityExceptions: existing?.securityExceptions ?? []
46
+ }, null, 2)}\n`;
47
+ }
48
+ async function planConfig(root, profiles, existingManifest, suppliedConfig) {
49
+ const current = await readCurrentFile(root, CONFIG_PATH);
50
+ const owned = findOwnedArtifact(existingManifest, CONFIG_PATH);
51
+ if (current !== null && owned === undefined) {
52
+ throw new AgentOpsError("UNMANAGED_INSTALL_PATH", `Refusing to replace an unmanaged install artifact: ${CONFIG_PATH}`);
53
+ }
54
+ let existingConfig;
55
+ if (suppliedConfig !== undefined) {
56
+ if (current === null ||
57
+ current.hash !== suppliedConfig.sourceHash) {
58
+ throw new AgentOpsError("PRECONDITION_CHANGED", "Managed configuration changed during plan creation.");
59
+ }
60
+ const result = validateConfig(suppliedConfig.value);
61
+ if (!result.ok) {
62
+ throw new AgentOpsError("CONFIG_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid managed configuration."}`);
63
+ }
64
+ existingConfig = result.value;
65
+ }
66
+ else if (current !== null) {
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(current.content);
70
+ }
71
+ catch (error) {
72
+ throw new AgentOpsError("CONFIG_INVALID_JSON", "Managed configuration is not valid JSON.", { cause: error });
73
+ }
74
+ const result = validateConfig(parsed);
75
+ if (!result.ok) {
76
+ throw new AgentOpsError("CONFIG_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid managed configuration."}`);
77
+ }
78
+ existingConfig = result.value;
79
+ }
80
+ const content = formatConfig(profiles, existingConfig);
81
+ return {
82
+ operation: {
83
+ kind: "write",
84
+ path: CONFIG_PATH,
85
+ content,
86
+ expectedHash: current?.hash ?? null
87
+ },
88
+ record: {
89
+ id: "config",
90
+ path: CONFIG_PATH,
91
+ hash: sha256(content),
92
+ owner: "agent-ops"
93
+ }
94
+ };
95
+ }
96
+ function pathKey(path) {
97
+ return path.toLowerCase();
98
+ }
99
+ function assertUniqueContributions(artifacts, blocks) {
100
+ const ids = new Set(["config"]);
101
+ const artifactPaths = new Set([pathKey(CONFIG_PATH)]);
102
+ for (const artifact of artifacts) {
103
+ if (ids.has(artifact.id) || artifactPaths.has(pathKey(artifact.path))) {
104
+ throw new AgentOpsError("INSTALL_PATH_CONFLICT", `Harness artifact conflicts with another managed entry: ${artifact.path}`);
105
+ }
106
+ ids.add(artifact.id);
107
+ artifactPaths.add(pathKey(artifact.path));
108
+ }
109
+ const markerBoundaries = new Set();
110
+ for (const block of blocks) {
111
+ const markers = managedBlockMarkers(block.id, block.version);
112
+ const key = pathKey(block.path);
113
+ if (ids.has(block.id) ||
114
+ artifactPaths.has(key) ||
115
+ markerBoundaries.has(`${key}\0${markers.start}`) ||
116
+ markerBoundaries.has(`${key}\0${markers.end}`)) {
117
+ throw new AgentOpsError("INSTALL_PATH_CONFLICT", `Harness block conflicts with another managed entry: ${block.path}`);
118
+ }
119
+ ids.add(block.id);
120
+ markerBoundaries.add(`${key}\0${markers.start}`);
121
+ markerBoundaries.add(`${key}\0${markers.end}`);
122
+ }
123
+ }
124
+ async function readExistingManifest(root) {
125
+ const current = await readCurrentFile(root, MANIFEST_PATH);
126
+ return current === null
127
+ ? null
128
+ : {
129
+ manifest: parseInstallManifest(current.content),
130
+ hash: current.hash
131
+ };
132
+ }
133
+ function assertCompatibleManifest(existing, scope, harness) {
134
+ if (existing !== null &&
135
+ (existing.scope !== scope || existing.harness !== harness)) {
136
+ throw new AgentOpsError("INSTALL_ALREADY_CONFIGURED", "Use update to change the scope or harness of an existing installation.");
137
+ }
138
+ }
139
+ function findOwnedArtifact(manifest, path) {
140
+ const key = pathKey(path);
141
+ return manifest?.artifacts.find((artifact) => pathKey(artifact.path) === key);
142
+ }
143
+ async function planArtifact(root, artifact, existingManifest) {
144
+ const current = await readCurrentFile(root, artifact.path);
145
+ const owned = findOwnedArtifact(existingManifest, artifact.path);
146
+ if (current !== null && owned === undefined) {
147
+ throw new AgentOpsError("UNMANAGED_INSTALL_PATH", `Refusing to replace an unmanaged install artifact: ${artifact.path}`);
148
+ }
149
+ if (current !== null &&
150
+ owned !== undefined &&
151
+ owned.hash !== current.hash) {
152
+ throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
153
+ }
154
+ const hash = sha256(artifact.content);
155
+ return {
156
+ operation: {
157
+ kind: "write",
158
+ path: artifact.path,
159
+ content: artifact.content,
160
+ expectedHash: current?.hash ?? null
161
+ },
162
+ record: {
163
+ id: artifact.id,
164
+ path: artifact.path,
165
+ hash,
166
+ owner: "agent-ops"
167
+ }
168
+ };
169
+ }
170
+ async function planBlocks(root, blocks) {
171
+ const grouped = new Map();
172
+ for (const block of blocks) {
173
+ const existing = grouped.get(block.path) ?? [];
174
+ existing.push(block);
175
+ grouped.set(block.path, existing);
176
+ }
177
+ const operations = [];
178
+ const records = [];
179
+ for (const [path, pathBlocks] of grouped) {
180
+ const current = await readCurrentFile(root, path);
181
+ let content = current?.content ?? "";
182
+ for (const block of pathBlocks) {
183
+ content = applyManagedBlock(content, block);
184
+ }
185
+ const hash = sha256(content);
186
+ operations.push({
187
+ kind: "write",
188
+ path,
189
+ content,
190
+ expectedHash: current?.hash ?? null
191
+ });
192
+ for (const block of pathBlocks) {
193
+ const markers = managedBlockMarkers(block.id, block.version);
194
+ records.push({
195
+ id: block.id,
196
+ path,
197
+ hash,
198
+ owner: "agent-ops",
199
+ startMarker: markers.start,
200
+ endMarker: markers.end
201
+ });
202
+ }
203
+ }
204
+ return { operations, records };
205
+ }
206
+ export async function createInstallPlan(options) {
207
+ if (options.toolkitVersion !== undefined &&
208
+ !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u
209
+ .test(options.toolkitVersion)) {
210
+ throw new AgentOpsError("INVALID_TOOLKIT_VERSION", "Toolkit version must be a valid semantic version.");
211
+ }
212
+ const resolved = resolveProfiles(options.profiles);
213
+ const contribution = await planHarnessContributions(options.harness, {
214
+ scope: options.scope,
215
+ profiles: resolved.profiles,
216
+ capabilities: resolved.capabilities,
217
+ ...(options.toolkitVersion === undefined
218
+ ? {}
219
+ : { toolkitVersion: options.toolkitVersion })
220
+ }, options.adapters);
221
+ assertUniqueContributions(contribution.artifacts, contribution.blocks);
222
+ const existing = await readExistingManifest(options.root);
223
+ assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness);
224
+ const operations = [];
225
+ const artifacts = [];
226
+ const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig);
227
+ operations.push(config.operation);
228
+ artifacts.push(config.record);
229
+ for (const artifact of contribution.artifacts) {
230
+ const planned = await planArtifact(options.root, artifact, existing?.manifest ?? null);
231
+ operations.push(planned.operation);
232
+ artifacts.push(planned.record);
233
+ }
234
+ const plannedBlocks = await planBlocks(options.root, contribution.blocks);
235
+ operations.push(...plannedBlocks.operations);
236
+ const manifest = {
237
+ schemaVersion: SCHEMA_VERSION,
238
+ scope: options.scope,
239
+ harness: options.harness,
240
+ artifacts,
241
+ markers: plannedBlocks.records
242
+ };
243
+ operations.push({
244
+ kind: "write",
245
+ path: MANIFEST_PATH,
246
+ content: formatInstallManifest(manifest),
247
+ expectedHash: existing?.hash ?? null
248
+ });
249
+ return {
250
+ scope: options.scope,
251
+ harness: options.harness,
252
+ profiles: resolved.profiles,
253
+ capabilities: resolved.capabilities,
254
+ manifest,
255
+ operations
256
+ };
257
+ }
@@ -0,0 +1,28 @@
1
+ import { AgentOpsError } from "../fs/paths.js";
2
+ const PROFILE_ORDER = ["core", "advisory", "guardrails"];
3
+ export const PROFILE_CAPABILITIES = {
4
+ core: ["rules", "task", "verify", "review"],
5
+ advisory: ["lifecycle-summary", "local-log"],
6
+ guardrails: ["command-policy", "optional-stop-verify"]
7
+ };
8
+ export function resolveProfiles(inputProfiles) {
9
+ if (inputProfiles.length === 0) {
10
+ throw new AgentOpsError("PROFILE_REQUIRED", "At least one installation profile is required.");
11
+ }
12
+ const selectedProfiles = new Set(inputProfiles);
13
+ if (selectedProfiles.has("guardrails")) {
14
+ selectedProfiles.add("core");
15
+ }
16
+ const profiles = PROFILE_ORDER.filter((profile) => selectedProfiles.has(profile));
17
+ const capabilities = [];
18
+ const seenCapabilities = new Set();
19
+ for (const profile of profiles) {
20
+ for (const capability of PROFILE_CAPABILITIES[profile]) {
21
+ if (!seenCapabilities.has(capability)) {
22
+ seenCapabilities.add(capability);
23
+ capabilities.push(capability);
24
+ }
25
+ }
26
+ }
27
+ return { profiles: [...profiles], capabilities };
28
+ }
@@ -0,0 +1 @@
1
+ export {};