@open-agent-toolkit/cli 0.2.72 → 0.2.74

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 (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,759 +0,0 @@
1
- import { execFile as execFileCallback } from 'node:child_process';
2
- import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
3
- import {
4
- mkdtemp,
5
- readFile,
6
- realpath,
7
- rename,
8
- rm,
9
- writeFile,
10
- } from 'node:fs/promises';
11
- import { tmpdir } from 'node:os';
12
- import { basename, dirname, join, resolve, sep } from 'node:path';
13
- import { promisify } from 'node:util';
14
-
15
- import {
16
- catalogFromManifest,
17
- initiativeCatalogPath,
18
- resolvePublicVerificationPolicy,
19
- serializeInitiativeCatalog,
20
- validateInitiativeCatalog,
21
- } from './catalog.mjs';
22
- import { validateContract } from './contracts.mjs';
23
- import { assertManifestPublishable } from './publication-policy.mjs';
24
- import {
25
- composePublicationTarget,
26
- describeRootDivergence,
27
- isPrivatePublicHost,
28
- normalizePublishRoots,
29
- privatePublicRootAllowed,
30
- rootDivergenceWarningSuppressed,
31
- } from './s3-roots.mjs';
32
-
33
- export {
34
- composePublicationTarget,
35
- normalizePublishRoots,
36
- } from './s3-roots.mjs';
37
-
38
- const execFile = promisify(execFileCallback);
39
- const SENTINEL_BODY = 'explainer-kit sentinel\n';
40
- const TRANSIENT_PATTERN =
41
- /(?:timeout|timed out|connection reset|temporar|throttl|slowdown|service unavailable|\b5\d\d\b)/i;
42
- const AUTH_PATTERN =
43
- /(?:credential|expired|sso session|access key|invalidclienttokenid|unrecognizedclient|token)/i;
44
- const NOT_FOUND_PATTERN = /(?:\b404\b|not found|nosuchkey)/i;
45
-
46
- export function createSentinelRelativePath(
47
- runId,
48
- randomBytes = nodeRandomBytes,
49
- ) {
50
- const safeRunId =
51
- String(runId)
52
- .toLowerCase()
53
- .replace(/[^a-z0-9]+/g, '-')
54
- .replace(/^-|-$/g, '') || 'run';
55
- const suffix = randomBytes(16).toString('hex');
56
- if (!/^[a-f0-9]{32}$/.test(suffix)) {
57
- throw publishError(
58
- 'E_PUBLISH_INPUT',
59
- 'Sentinel randomness must provide 128 bits.',
60
- );
61
- }
62
- return `.explainer-kit-sentinel/${safeRunId}-${suffix}.txt`;
63
- }
64
-
65
- export async function publishS3Static(request, dependencies = {}) {
66
- if (dependencies.approved !== true) {
67
- throw publishError(
68
- 'E_PUBLISH_APPROVAL',
69
- 'Publishing requires explicit human approval.',
70
- );
71
- }
72
-
73
- const requestValidation = validateContract('publish-request', request);
74
- if (!requestValidation.valid) {
75
- throw publishError(
76
- 'E_PUBLISH_INPUT',
77
- `Invalid publish request: ${requestValidation.errors[0].message}`,
78
- );
79
- }
80
- const roots = normalizePublishRoots(request.s3Uri, request.publicBaseUrl);
81
- const cwd = dependencies.cwd ?? process.cwd();
82
- const siteRoot = resolve(cwd, request.siteRoot);
83
- const manifestPath = resolve(cwd, request.manifestPath);
84
- const manifest = await readJson(manifestPath, 'manifest');
85
- const buildRecord =
86
- manifest.outcome === 'incomplete'
87
- ? await readJson(
88
- resolve(dirname(manifestPath), manifest.buildRecord?.path ?? ''),
89
- 'build record',
90
- )
91
- : undefined;
92
- assertManifestPublishable(manifest, { buildRecord });
93
- const manifestValidation = validateContract('manifest', manifest);
94
- if (!manifestValidation.valid) {
95
- throw publishError(
96
- 'E_PUBLISH_INPUT',
97
- `Invalid manifest: ${manifestValidation.errors[0].message}`,
98
- );
99
- }
100
- const artifacts = await collectArtifacts(manifest, siteRoot);
101
- const command = dependencies.command ?? defaultCommand;
102
- const httpGet = dependencies.httpGet ?? defaultHttpGet;
103
- const randomBytes = dependencies.randomBytes ?? nodeRandomBytes;
104
- const now = dependencies.now ?? (() => new Date().toISOString());
105
- const sleep = dependencies.sleep ?? defaultSleep;
106
- const awsOptions = {
107
- region: request.awsRegion,
108
- profile: request.awsProfile,
109
- };
110
- const sentinelRelativePath = createSentinelRelativePath(
111
- manifest.runId,
112
- randomBytes,
113
- );
114
- const sentinelTargetPath = composePublicationTarget(
115
- sentinelRelativePath,
116
- roots,
117
- );
118
- // Resolved before the catalog is built: the catalog is hashed and uploaded
119
- // before any public verification runs, so it must carry the policy up front.
120
- const publicAccess =
121
- request.schemaVersion === 'explainer-kit.publish-request/v1'
122
- ? 'public'
123
- : request.publicAccess;
124
- const verificationPolicy = resolvePublicVerificationPolicy(publicAccess);
125
-
126
- const divergence = describeRootDivergence(roots);
127
- if (divergence && !rootDivergenceWarningSuppressed()) {
128
- (dependencies.warn ?? ((message) => console.warn(message)))(
129
- `explainer-kit: ${divergence}`,
130
- );
131
- }
132
-
133
- const catalog = catalogFromManifest(manifest, roots.publicBaseUrl, {
134
- publicAccess,
135
- });
136
- const catalogValidation = validateInitiativeCatalog(
137
- catalog,
138
- manifest,
139
- roots.publicBaseUrl,
140
- { publicAccess },
141
- );
142
- if (!catalogValidation.valid) {
143
- throw publishError(
144
- 'E_PUBLISH_INPUT',
145
- `Invalid initiative catalog: ${catalogValidation.errors[0].message}`,
146
- );
147
- }
148
- const catalogBody = Buffer.from(serializeInitiativeCatalog(catalog));
149
- const catalogManifestPath = initiativeCatalogPath(manifest.slug);
150
- const catalogPublishPath = catalogManifestPath.slice('site/'.length);
151
- if (artifacts.some(({ publishPath }) => publishPath === catalogPublishPath)) {
152
- throw publishError(
153
- 'E_PUBLISH_INPUT',
154
- `Generated catalog path collides with a manifest artifact: ${catalogManifestPath}`,
155
- );
156
- }
157
- const sentinelDirectory = await mkdtemp(
158
- join(tmpdir(), 'explainer-sentinel-'),
159
- );
160
- const sentinelBodyPath = join(sentinelDirectory, 'sentinel.txt');
161
- const catalogBodyPath = join(sentinelDirectory, 'catalog.json');
162
- const catalogArtifact = {
163
- source: { kind: 'auxiliary', name: 'catalog' },
164
- manifestPath: catalogManifestPath,
165
- publishPath: catalogPublishPath,
166
- filePath: catalogBodyPath,
167
- hash: fileHash(catalogBody),
168
- contentType: 'application/json',
169
- cacheControl: 'public, max-age=300',
170
- };
171
- const publicationArtifacts = [...artifacts, catalogArtifact];
172
- let sentinelUploaded = false;
173
- let sentinelDeleted = false;
174
-
175
- try {
176
- await writeFile(sentinelBodyPath, SENTINEL_BODY, 'utf8');
177
- await writeFile(catalogBodyPath, catalogBody);
178
- await runAws(
179
- command,
180
- putObjectArgs({
181
- roots,
182
- relativePath: sentinelRelativePath,
183
- bodyPath: sentinelBodyPath,
184
- contentType: 'text/plain; charset=utf-8',
185
- cacheControl: 'no-store',
186
- hash: fileHash(Buffer.from(SENTINEL_BODY)),
187
- awsOptions,
188
- }),
189
- { sleep },
190
- );
191
- sentinelUploaded = true;
192
- const sentinelMetadata = await readObjectMetadata(
193
- command,
194
- headObjectArgs(roots, sentinelRelativePath, awsOptions),
195
- { sleep },
196
- );
197
- const sentinelObjectVerification = await verifyObjectBytes({
198
- command,
199
- metadata: sentinelMetadata,
200
- roots,
201
- relativePath: sentinelRelativePath,
202
- expectedHash: fileHash(Buffer.from(SENTINEL_BODY)),
203
- downloadPath: join(sentinelDirectory, 'sentinel-download.txt'),
204
- awsOptions,
205
- sleep,
206
- });
207
- let sentinelPublicVerification = {
208
- status: verificationPolicy.receiptSkipStatus,
209
- };
210
- if (verificationPolicy.verifyPublicly) {
211
- const sentinelResponse = await httpGet(sentinelTargetPath.publicUrl);
212
- if (
213
- sentinelResponse.status < 200 ||
214
- sentinelResponse.status > 299 ||
215
- !responseBytes(sentinelResponse.body).equals(Buffer.from(SENTINEL_BODY))
216
- ) {
217
- throw publishError(
218
- 'E_PUBLISH_ROOTS',
219
- 'Public root did not serve the uploaded sentinel.',
220
- );
221
- }
222
- sentinelPublicVerification = {
223
- status: 'verified',
224
- httpStatus: sentinelResponse.status,
225
- hash: fileHash(responseBytes(sentinelResponse.body)),
226
- };
227
- }
228
- await runAws(
229
- command,
230
- deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
231
- { sleep },
232
- );
233
- sentinelDeleted = true;
234
-
235
- const receiptArtifacts = [];
236
- for (const artifact of publicationArtifacts) {
237
- const target = composePublicationTarget(artifact.publishPath, roots);
238
- const metadata = await readExistingMetadata(
239
- command,
240
- headObjectArgs(roots, artifact.publishPath, awsOptions),
241
- { sleep },
242
- );
243
- if (!matchesPublishedArtifact(metadata, artifact)) {
244
- await runAws(
245
- command,
246
- putObjectArgs({
247
- roots,
248
- relativePath: artifact.publishPath,
249
- bodyPath: artifact.filePath,
250
- contentType: artifact.contentType,
251
- cacheControl: artifact.cacheControl,
252
- hash: artifact.hash,
253
- awsOptions,
254
- }),
255
- { sleep },
256
- );
257
- }
258
- const uploadedMetadata = await readObjectMetadata(
259
- command,
260
- headObjectArgs(roots, artifact.publishPath, awsOptions),
261
- { sleep },
262
- );
263
- assertMetadata(uploadedMetadata, artifact);
264
- const objectVerification = await verifyObjectBytes({
265
- command,
266
- metadata: uploadedMetadata,
267
- roots,
268
- relativePath: artifact.publishPath,
269
- expectedHash: artifact.hash,
270
- downloadPath: join(
271
- sentinelDirectory,
272
- `object-${receiptArtifacts.length}.download`,
273
- ),
274
- awsOptions,
275
- sleep,
276
- });
277
- let publicVerification = {
278
- status: verificationPolicy.receiptSkipStatus,
279
- };
280
- if (verificationPolicy.verifyPublicly) {
281
- const response = await httpGet(target.publicUrl);
282
- if (response.status < 200 || response.status > 299) {
283
- throw publishError(
284
- 'E_PUBLISH_VERIFY',
285
- `Public verification failed for ${artifact.manifestPath}.`,
286
- );
287
- }
288
- const servedType = headerValue(response.headers, 'content-type');
289
- if (!contentTypesMatch(servedType, artifact.contentType)) {
290
- throw publishError(
291
- 'E_PUBLISH_VERIFY',
292
- `Public content type mismatch for ${artifact.manifestPath}.`,
293
- );
294
- }
295
- const publicHash = fileHash(responseBytes(response.body));
296
- if (publicHash !== artifact.hash) {
297
- throw publishError(
298
- 'E_PUBLISH_VERIFY',
299
- `Public bytes do not match the manifest for ${artifact.manifestPath}.`,
300
- );
301
- }
302
- publicVerification = {
303
- status: 'verified',
304
- httpStatus: response.status,
305
- hash: publicHash,
306
- };
307
- }
308
- receiptArtifacts.push({
309
- source: artifact.source,
310
- relativePath: artifact.manifestPath,
311
- hash: artifact.hash,
312
- s3Uri: target.s3Uri,
313
- publicUrl: target.publicUrl,
314
- contentType: artifact.contentType,
315
- objectVerification,
316
- publicVerification,
317
- });
318
- }
319
-
320
- const receipt = {
321
- schemaVersion: 'explainer-kit.publish-receipt/v2',
322
- provider: 's3-static',
323
- publishedAt: now(),
324
- publicAccess,
325
- // Durable trace for the anti-SSRF opt-in. Without it, a run that
326
- // published to an internal address with the control disabled is
327
- // indistinguishable in retained evidence from one that did not, because
328
- // the variable is read straight from the environment and enters no other
329
- // record. Emitted only in that exceptional case, so ordinary receipts are
330
- // byte-identical to before.
331
- ...(privatePublicRootAllowed() &&
332
- isPrivatePublicHost(new URL(roots.publicBaseUrl).hostname) && {
333
- publicRootPolicy: 'private-allowed',
334
- }),
335
- roots: { s3Uri: roots.s3Uri, publicBaseUrl: roots.publicBaseUrl },
336
- sentinel: {
337
- relativePath: sentinelRelativePath,
338
- objectVerification: sentinelObjectVerification,
339
- publicVerification: sentinelPublicVerification,
340
- deleted: sentinelDeleted,
341
- },
342
- artifacts: receiptArtifacts,
343
- };
344
- const receiptValidation = validateContract('publish-receipt', receipt, {
345
- manifest,
346
- catalogArtifact: {
347
- relativePath: catalogArtifact.manifestPath,
348
- hash: catalogArtifact.hash,
349
- },
350
- });
351
- if (!receiptValidation.valid) {
352
- throw publishError(
353
- 'E_PUBLISH_VERIFY',
354
- `Invalid publish receipt: ${receiptValidation.errors[0].message}`,
355
- );
356
- }
357
- const receiptPath =
358
- dependencies.receiptPath ??
359
- join(dirname(manifestPath), 'publish-receipt.json');
360
- await writeJsonAtomic(receiptPath, receipt);
361
- return receipt;
362
- } finally {
363
- if (sentinelUploaded && !sentinelDeleted) {
364
- try {
365
- await runAws(
366
- command,
367
- deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
368
- { sleep },
369
- );
370
- } catch {
371
- // Preserve the original failure. The missing cleanup remains observable
372
- // because no successful receipt is emitted.
373
- }
374
- }
375
- await rm(sentinelDirectory, { recursive: true, force: true });
376
- }
377
- }
378
-
379
- async function collectArtifacts(manifest, siteRoot) {
380
- const canonicalSiteRoot = await realpath(siteRoot);
381
- const sitePrefix = `${basename(canonicalSiteRoot)}/`;
382
- const seen = new Set();
383
- const artifacts = [];
384
- for (const entry of manifest.artifacts) {
385
- if (entry.status !== 'built' || typeof entry.renderedPath !== 'string') {
386
- continue;
387
- }
388
- if (!entry.renderedPath.startsWith(sitePrefix)) {
389
- throw publishError(
390
- 'E_PUBLISH_INPUT',
391
- `Rendered path is outside the site root: ${entry.renderedPath}`,
392
- );
393
- }
394
- const publishPath = entry.renderedPath.slice(sitePrefix.length);
395
- if (
396
- !publishPath ||
397
- publishPath.startsWith('/') ||
398
- publishPath.includes('..')
399
- ) {
400
- throw publishError(
401
- 'E_PUBLISH_INPUT',
402
- `Unsafe publish path: ${entry.renderedPath}`,
403
- );
404
- }
405
- if (seen.has(publishPath)) {
406
- throw publishError(
407
- 'E_PUBLISH_INPUT',
408
- `Duplicate publish path: ${publishPath}`,
409
- );
410
- }
411
- seen.add(publishPath);
412
- const filePath = await realpath(join(canonicalSiteRoot, publishPath));
413
- if (
414
- filePath !== canonicalSiteRoot &&
415
- !filePath.startsWith(`${canonicalSiteRoot}${sep}`)
416
- ) {
417
- throw publishError(
418
- 'E_PUBLISH_INPUT',
419
- `Artifact escapes the site root: ${entry.renderedPath}`,
420
- );
421
- }
422
- const bytes = await readFile(filePath);
423
- const actualHash = fileHash(bytes);
424
- if (actualHash !== entry.hash) {
425
- throw publishError(
426
- 'E_PUBLISH_INPUT',
427
- `Artifact hash mismatch: ${entry.renderedPath}`,
428
- );
429
- }
430
- const contentType = contentTypeFor(publishPath, entry.mediaType);
431
- artifacts.push({
432
- source: { kind: 'manifest', artifactId: entry.id },
433
- manifestPath: entry.renderedPath,
434
- publishPath,
435
- filePath,
436
- hash: actualHash,
437
- contentType,
438
- cacheControl: 'public, max-age=300',
439
- });
440
- }
441
- return artifacts;
442
- }
443
-
444
- function putObjectArgs({
445
- roots,
446
- relativePath,
447
- bodyPath,
448
- contentType,
449
- cacheControl,
450
- hash,
451
- awsOptions,
452
- }) {
453
- return withAwsOptions(
454
- [
455
- 's3api',
456
- 'put-object',
457
- '--bucket',
458
- roots.bucket,
459
- '--key',
460
- objectKey(roots, relativePath),
461
- '--body',
462
- bodyPath,
463
- '--content-type',
464
- contentType,
465
- '--cache-control',
466
- cacheControl,
467
- '--metadata',
468
- `explainer-sha256=${hash.slice('sha256:'.length)}`,
469
- '--checksum-sha256',
470
- Buffer.from(hash.slice('sha256:'.length), 'hex').toString('base64'),
471
- '--no-cli-pager',
472
- ],
473
- awsOptions,
474
- );
475
- }
476
-
477
- function headObjectArgs(roots, relativePath, awsOptions) {
478
- return withAwsOptions(
479
- [
480
- 's3api',
481
- 'head-object',
482
- '--bucket',
483
- roots.bucket,
484
- '--key',
485
- objectKey(roots, relativePath),
486
- '--checksum-mode',
487
- 'ENABLED',
488
- '--output',
489
- 'json',
490
- '--no-cli-pager',
491
- ],
492
- awsOptions,
493
- );
494
- }
495
-
496
- function getObjectArgs(roots, relativePath, downloadPath, awsOptions) {
497
- return withAwsOptions(
498
- [
499
- 's3api',
500
- 'get-object',
501
- '--bucket',
502
- roots.bucket,
503
- '--key',
504
- objectKey(roots, relativePath),
505
- '--checksum-mode',
506
- 'ENABLED',
507
- downloadPath,
508
- '--no-cli-pager',
509
- ],
510
- awsOptions,
511
- );
512
- }
513
-
514
- function deleteObjectArgs(roots, relativePath, awsOptions) {
515
- return withAwsOptions(
516
- [
517
- 's3api',
518
- 'delete-object',
519
- '--bucket',
520
- roots.bucket,
521
- '--key',
522
- objectKey(roots, relativePath),
523
- '--no-cli-pager',
524
- ],
525
- awsOptions,
526
- );
527
- }
528
-
529
- function withAwsOptions(args, { region, profile }) {
530
- const result = [...args, '--region', region];
531
- if (profile) {
532
- result.push('--profile', profile);
533
- }
534
- return result;
535
- }
536
-
537
- async function runAws(
538
- command,
539
- args,
540
- { sleep = defaultSleep, attempts = 3, allowNotFound = false } = {},
541
- ) {
542
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
543
- try {
544
- return await command('aws', args);
545
- } catch (caught) {
546
- const diagnostic = `${caught?.message ?? ''}\n${caught?.stderr ?? ''}`;
547
- if (AUTH_PATTERN.test(diagnostic)) {
548
- throw publishError(
549
- 'E_PUBLISH_AUTH',
550
- 'AWS authentication failed; refresh the configured credential chain and retry.',
551
- );
552
- }
553
- if (allowNotFound && NOT_FOUND_PATTERN.test(diagnostic)) {
554
- return null;
555
- }
556
- if (!TRANSIENT_PATTERN.test(diagnostic) || attempt === attempts) {
557
- throw publishError('E_PUBLISH_AWS', 'AWS object operation failed.');
558
- }
559
- await sleep(50 * 2 ** (attempt - 1));
560
- }
561
- }
562
- }
563
-
564
- async function readExistingMetadata(command, args, { sleep }) {
565
- const result = await runAws(command, args, {
566
- sleep,
567
- allowNotFound: true,
568
- });
569
- if (result === null) return null;
570
- try {
571
- return JSON.parse(result.stdout || '{}');
572
- } catch {
573
- throw publishError(
574
- 'E_PUBLISH_VERIFY',
575
- 'AWS returned invalid object metadata.',
576
- );
577
- }
578
- }
579
-
580
- async function readObjectMetadata(command, args, { sleep }) {
581
- const result = await runAws(command, args, { sleep });
582
- return parseAwsJson(result.stdout);
583
- }
584
-
585
- function matchesPublishedArtifact(metadata, artifact) {
586
- return (
587
- metadata?.Metadata?.['explainer-sha256'] ===
588
- artifact.hash.slice('sha256:'.length) &&
589
- metadata.ContentType === artifact.contentType &&
590
- metadata.CacheControl === artifact.cacheControl
591
- );
592
- }
593
-
594
- function assertMetadata(metadata, artifact) {
595
- if (!matchesPublishedArtifact(metadata, artifact)) {
596
- throw publishError(
597
- 'E_PUBLISH_VERIFY',
598
- `Uploaded metadata mismatch for ${artifact.manifestPath}.`,
599
- );
600
- }
601
- }
602
-
603
- async function verifyObjectBytes({
604
- command,
605
- metadata,
606
- roots,
607
- relativePath,
608
- expectedHash,
609
- downloadPath,
610
- awsOptions,
611
- sleep,
612
- }) {
613
- const expectedBase64 = Buffer.from(
614
- expectedHash.slice('sha256:'.length),
615
- 'hex',
616
- ).toString('base64');
617
- if (typeof metadata.ChecksumSHA256 === 'string') {
618
- if (metadata.ChecksumSHA256 !== expectedBase64) {
619
- throw publishError(
620
- 'E_PUBLISH_VERIFY',
621
- `Authenticated object checksum mismatch for ${relativePath}.`,
622
- );
623
- }
624
- return {
625
- status: 'verified',
626
- method: 'service-checksum',
627
- hash: expectedHash,
628
- };
629
- }
630
-
631
- try {
632
- await runAws(
633
- command,
634
- getObjectArgs(roots, relativePath, downloadPath, awsOptions),
635
- { sleep },
636
- );
637
- } catch {
638
- throw publishError(
639
- 'E_PUBLISH_VERIFY',
640
- `Authenticated object-byte verification is unavailable for ${relativePath}.`,
641
- );
642
- }
643
- if (fileHash(await readFile(downloadPath)) !== expectedHash) {
644
- throw publishError(
645
- 'E_PUBLISH_VERIFY',
646
- `Authenticated object bytes do not match ${relativePath}.`,
647
- );
648
- }
649
- return {
650
- status: 'verified',
651
- method: 'authenticated-download',
652
- hash: expectedHash,
653
- };
654
- }
655
-
656
- function parseAwsJson(stdout) {
657
- try {
658
- return JSON.parse(stdout || '{}');
659
- } catch {
660
- throw publishError(
661
- 'E_PUBLISH_VERIFY',
662
- 'AWS returned invalid object metadata.',
663
- );
664
- }
665
- }
666
-
667
- function contentTypeFor(path, declared) {
668
- const extension = path.slice(path.lastIndexOf('.')).toLowerCase();
669
- const known = {
670
- '.css': 'text/css; charset=utf-8',
671
- '.html': 'text/html; charset=utf-8',
672
- '.js': 'text/javascript; charset=utf-8',
673
- '.json': 'application/json',
674
- '.svg': 'image/svg+xml',
675
- '.txt': 'text/plain; charset=utf-8',
676
- };
677
- return known[extension] ?? declared ?? 'application/octet-stream';
678
- }
679
-
680
- function contentTypesMatch(served, expected) {
681
- if (!served) {
682
- return false;
683
- }
684
- return (
685
- served.toLowerCase().split(';')[0].trim() ===
686
- expected.toLowerCase().split(';')[0].trim()
687
- );
688
- }
689
-
690
- function objectKey(roots, relativePath) {
691
- return roots.keyPrefix ? `${roots.keyPrefix}/${relativePath}` : relativePath;
692
- }
693
-
694
- function fileHash(bytes) {
695
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
696
- }
697
-
698
- function headerValue(headers, name) {
699
- if (typeof headers?.get === 'function') {
700
- return headers.get(name);
701
- }
702
- return headers?.[name] ?? headers?.[name.toLowerCase()];
703
- }
704
-
705
- async function readJson(path, label) {
706
- try {
707
- return JSON.parse(await readFile(path, 'utf8'));
708
- } catch {
709
- throw publishError('E_PUBLISH_INPUT', `Could not read ${label} JSON.`);
710
- }
711
- }
712
-
713
- async function writeJsonAtomic(path, value) {
714
- const temporary = `${path}.tmp-${process.pid}-${nodeRandomBytes(6).toString('hex')}`;
715
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
716
- flag: 'wx',
717
- });
718
- await rename(temporary, path);
719
- }
720
-
721
- async function defaultCommand(file, args) {
722
- return execFile(file, args, { maxBuffer: 1024 * 1024 });
723
- }
724
-
725
- export async function defaultHttpGet(url, { fetchImpl = fetch } = {}) {
726
- // A canonical public artifact URL is uploaded to a known key and should never
727
- // legitimately redirect. Following redirects turned public verification into
728
- // an outbound GET primitive that a third party could bounce inward, so a
729
- // redirect is a verification failure rather than something to chase.
730
- const response = await fetchImpl(url, {
731
- redirect: 'error',
732
- signal: AbortSignal.timeout(15_000),
733
- });
734
- return {
735
- status: response.status,
736
- headers: response.headers,
737
- body: Buffer.from(await response.arrayBuffer()),
738
- };
739
- }
740
-
741
- function responseBytes(body) {
742
- if (Buffer.isBuffer(body)) return body;
743
- if (body instanceof Uint8Array) {
744
- return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
745
- }
746
- if (typeof body === 'string') return Buffer.from(body);
747
- throw publishError(
748
- 'E_PUBLISH_VERIFY',
749
- 'Public verification returned an unreadable response body.',
750
- );
751
- }
752
-
753
- function defaultSleep(milliseconds) {
754
- return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
755
- }
756
-
757
- function publishError(code, message) {
758
- return Object.assign(new Error(message), { code });
759
- }