@open-agent-toolkit/cli 0.2.30 → 0.2.31

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 (72) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +41 -0
  3. package/assets/docs/contributing/code.md +19 -7
  4. package/assets/docs/contributing/explainer-kit-verification.md +9 -1
  5. package/assets/docs/contributing/skills.md +9 -0
  6. package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
  7. package/assets/docs/workflows/skills/explainer-kit.md +98 -38
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +33 -8
  10. package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
  11. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
  12. package/assets/skills/explainer-kit/references/contracts.md +49 -17
  13. package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
  14. package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
  15. package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
  16. package/assets/skills/explainer-kit/references/visual-review.md +19 -5
  17. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
  18. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
  19. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
  20. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
  21. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
  22. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
  23. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
  24. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
  25. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
  26. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
  27. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
  29. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
  30. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
  31. package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
  32. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
  33. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
  34. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
  35. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
  36. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
  37. package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
  38. package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
  39. package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
  40. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
  41. package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
  42. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
  43. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
  44. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
  45. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
  46. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
  47. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
  48. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
  49. package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
  50. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  51. package/assets/skills/oat-project-complete/SKILL.md +19 -3
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  53. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
  54. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  55. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  56. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  57. package/dist/commands/config/index.d.ts.map +1 -1
  58. package/dist/commands/config/index.js +18 -0
  59. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  60. package/dist/commands/project/archive/archive-utils.js +57 -7
  61. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
  62. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
  63. package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
  64. package/dist/config/oat-config.d.ts +2 -0
  65. package/dist/config/oat-config.d.ts.map +1 -1
  66. package/dist/config/oat-config.js +4 -0
  67. package/dist/config/resolve.d.ts.map +1 -1
  68. package/dist/config/resolve.js +1 -0
  69. package/package.json +2 -2
  70. /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
  71. /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
  72. /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
@@ -0,0 +1,353 @@
1
+ const BUCKET_PATTERN = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
2
+ const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
3
+ const RESERVED_BUCKET_PREFIXES = ['xn--', 'sthree-', 'amzn_s3_demo_'];
4
+ const RESERVED_BUCKET_SUFFIXES = [
5
+ '-s3alias',
6
+ '--ol-s3',
7
+ '.mrap',
8
+ '--x-s3',
9
+ '--table-s3',
10
+ ];
11
+ const ENCODED_DOT_OR_SEPARATOR_PATTERN = /%(?:2e|2f|5c)/i;
12
+
13
+ export function normalizePublishRoots(s3Uri, publicBaseUrl) {
14
+ const s3 = parseS3Root(s3Uri);
15
+ const publicRoot = parsePublicRoot(publicBaseUrl);
16
+ const keyPrefix = s3.segments.join('/');
17
+ const pathname = publicRoot.segments.join('/');
18
+ // Deliberately NO relational rule between the two roots. The mapping from an
19
+ // S3 key to a public URL is underdetermined by these two strings: it lives in
20
+ // CDN configuration this tool cannot read. Both of these are legitimate:
21
+ //
22
+ // A s3://bucket/repositories/alpha + https://host/repositories/alpha
23
+ // B s3://bucket/explainers + https://host (CloudFront Origin Path)
24
+ //
25
+ // B is a confirmed real deployment shape, recorded in oat-explainer-kit's
26
+ // migration reference. Suffix-containment does not rescue the rule either --
27
+ // an empty public path is a suffix of everything, so B would pass vacuously
28
+ // while path-rewriting deployments still false-reject. Divergence is surfaced
29
+ // as a non-blocking warning by the connector instead; correctness of the
30
+ // advertised URL is established by verification, not by string shape.
31
+ return {
32
+ bucket: s3.bucket,
33
+ keyPrefix,
34
+ s3Uri: `s3://${s3.bucket}${keyPrefix ? `/${keyPrefix}` : ''}`,
35
+ publicBaseUrl: `https://${publicRoot.authority}${pathname ? `/${pathname}` : ''}`,
36
+ };
37
+ }
38
+
39
+ export const ROOT_DIVERGENCE_WARNING_ENV =
40
+ 'EXPLAINER_KIT_SUPPRESS_ROOT_DIVERGENCE_WARNING';
41
+
42
+ export function rootDivergenceWarningSuppressed(env = process.env) {
43
+ return ['on', 'true', '1'].includes(
44
+ String(env[ROOT_DIVERGENCE_WARNING_ENV] ?? '').toLowerCase(),
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Advisory only, never a gate. A mismatched key prefix and public path is
50
+ * usually a typo, but it is also exactly what a CloudFront Origin Path
51
+ * deployment looks like, so this can only ever be a warning. Returns null when
52
+ * the two roots address the same path.
53
+ */
54
+ export function describeRootDivergence(roots) {
55
+ const keyPrefix = roots?.keyPrefix ?? '';
56
+ let pathname;
57
+ try {
58
+ pathname = new URL(roots?.publicBaseUrl).pathname.replace(/^\/|\/$/g, '');
59
+ } catch {
60
+ return null;
61
+ }
62
+ if (keyPrefix === pathname) return null;
63
+ return (
64
+ `Publication roots address different paths: S3 key prefix "${keyPrefix}" ` +
65
+ `vs public path "${pathname}". This is expected for a CloudFront Origin ` +
66
+ `Path deployment; verify it is not a typo.`
67
+ );
68
+ }
69
+
70
+ export function composePublicationTarget(relativePath, roots) {
71
+ const normalized = normalizePublishRoots(roots?.s3Uri, roots?.publicBaseUrl);
72
+ const segments = parseRelativePath(relativePath);
73
+ const rawSuffix = segments.join('/');
74
+ const publicSuffix = segments
75
+ .map((part) => encodeURIComponent(part))
76
+ .join('/');
77
+ return {
78
+ s3Uri: `${normalized.s3Uri}/${rawSuffix}`,
79
+ publicUrl: `${normalized.publicBaseUrl}/${publicSuffix}`,
80
+ };
81
+ }
82
+
83
+ function parseS3Root(value) {
84
+ if (
85
+ typeof value !== 'string' ||
86
+ /\s|[\\?#]/.test(value) ||
87
+ hasUnsafeRootChar(value) ||
88
+ !value.startsWith('s3://')
89
+ ) {
90
+ throw rootError('S3 root must be a credential-free s3 URI.');
91
+ }
92
+ const match = /^s3:\/\/([^/]+)(?:\/(.*))?$/.exec(value);
93
+ if (!match) {
94
+ throw rootError('S3 root must be a credential-free s3 URI.');
95
+ }
96
+ const [, bucket, rawPath = ''] = match;
97
+ if (!validBucket(bucket)) {
98
+ throw rootError('S3 root has an invalid bucket.');
99
+ }
100
+ return { bucket, segments: parseRootPath(rawPath) };
101
+ }
102
+
103
+ function parsePublicRoot(value) {
104
+ if (
105
+ typeof value !== 'string' ||
106
+ /\s|[?#]/.test(value) ||
107
+ hasUnsafeRootChar(value) ||
108
+ !value.startsWith('https://')
109
+ ) {
110
+ throw rootError('Public root must be credential-free HTTPS.');
111
+ }
112
+ const match = /^https:\/\/([^/]+)(?:\/(.*))?$/.exec(value);
113
+ if (!match || match[1].includes('%') || match[1].includes('@')) {
114
+ throw rootError('Public root must be credential-free HTTPS.');
115
+ }
116
+
117
+ let parsed;
118
+ try {
119
+ parsed = new URL(value);
120
+ } catch {
121
+ throw rootError('Public root must be credential-free HTTPS.');
122
+ }
123
+ if (
124
+ parsed.protocol !== 'https:' ||
125
+ parsed.username ||
126
+ parsed.password ||
127
+ parsed.search ||
128
+ parsed.hash ||
129
+ !parsed.hostname
130
+ ) {
131
+ throw rootError('Public root must be credential-free HTTPS.');
132
+ }
133
+ if (isPrivatePublicHost(parsed.hostname) && !privatePublicRootAllowed()) {
134
+ throw rootError(
135
+ 'Public root must not address a loopback, link-local, or private network.',
136
+ );
137
+ }
138
+ return {
139
+ authority: parsed.host,
140
+ segments: parseRootPath(match[2] ?? ''),
141
+ };
142
+ }
143
+
144
+ function parseRootPath(rawPath) {
145
+ if (rawPath === '') return [];
146
+ const withoutTrailingSlash = rawPath.endsWith('/')
147
+ ? rawPath.slice(0, -1)
148
+ : rawPath;
149
+ if (withoutTrailingSlash === '' || withoutTrailingSlash.includes('//')) {
150
+ throw rootError('Publication root contains an unsafe path.');
151
+ }
152
+ const segments = withoutTrailingSlash.split('/');
153
+ if (segments.some((segment) => !validRootSegment(segment))) {
154
+ throw rootError('Publication root contains an unsafe path.');
155
+ }
156
+ return segments;
157
+ }
158
+
159
+ function parseRelativePath(value) {
160
+ if (
161
+ typeof value !== 'string' ||
162
+ value === '' ||
163
+ value.startsWith('/') ||
164
+ value.endsWith('/') ||
165
+ value.includes('//') ||
166
+ /[\\?#]/.test(value) ||
167
+ ENCODED_DOT_OR_SEPARATOR_PATTERN.test(value)
168
+ ) {
169
+ throw rootError('Publication target contains an unsafe path.');
170
+ }
171
+ const segments = value.split('/');
172
+ if (segments.some((segment) => segment === '.' || segment === '..')) {
173
+ throw rootError('Publication target contains an unsafe path.');
174
+ }
175
+ return segments;
176
+ }
177
+
178
+ function validRootSegment(segment) {
179
+ if (!segment || segment === '.' || segment === '..') return false;
180
+ let decoded;
181
+ try {
182
+ decoded = decodeURIComponent(segment);
183
+ } catch {
184
+ return false;
185
+ }
186
+ return (
187
+ decoded !== '.' &&
188
+ decoded !== '..' &&
189
+ !decoded.includes('/') &&
190
+ !decoded.includes('\\') &&
191
+ !ENCODED_DOT_OR_SEPARATOR_PATTERN.test(decoded)
192
+ );
193
+ }
194
+
195
+ function validBucket(bucket) {
196
+ return (
197
+ BUCKET_PATTERN.test(bucket) &&
198
+ !bucket.includes('..') &&
199
+ !bucket.includes('.-') &&
200
+ !bucket.includes('-.') &&
201
+ !IPV4_PATTERN.test(bucket) &&
202
+ !RESERVED_BUCKET_PREFIXES.some((prefix) => bucket.startsWith(prefix)) &&
203
+ !RESERVED_BUCKET_SUFFIXES.some((suffix) => bucket.endsWith(suffix))
204
+ );
205
+ }
206
+
207
+ // `\s` matches only space, tab, newline, CR, FF and VT, leaving the rest of the
208
+ // C0 range and DEL free to reach S3 object keys, composed public URLs, the
209
+ // catalog, receipts and terminal output. NUL additionally crashed `execFile`
210
+ // with an uncoded `ERR_INVALID_ARG_VALUE` rather than a clean `E_PUBLISH_ROOTS`.
211
+ // Backslash is screened here so both parsers stay symmetric: `parseS3Root`
212
+ // already rejected it, but `parsePublicRoot` silently dropped the segment.
213
+ // The C1 range `0x80`-`0x9f` is screened for the same reason as C0: it was
214
+ // retained verbatim in both normalized roots and flowed into the S3 key, the
215
+ // composed public URL, the catalog, the receipt and `aws` argv. `0x9b` is the
216
+ // 8-bit CSI, a functional terminal control introducer, so leaving it accepted
217
+ // contradicted this project's no-uncontrolled-bytes-in-output posture. No
218
+ // legitimate root contains a raw C1 byte; percent-encoded forms are unaffected
219
+ // because the screen runs on the raw string before any decoding.
220
+ //
221
+ // Expressed as a codepoint scan rather than a regex so the source carries no
222
+ // literal control bytes.
223
+ function hasUnsafeRootChar(value) {
224
+ for (const char of value) {
225
+ const code = char.codePointAt(0);
226
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || char === '\\') {
227
+ return true;
228
+ }
229
+ }
230
+ return false;
231
+ }
232
+
233
+ /**
234
+ * Operators publishing to a genuinely internal mirror may opt back in. The
235
+ * default is deny: public verification issues an outbound GET against whatever
236
+ * this root names, so an unconstrained root is an attacker-influenced request
237
+ * primitive aimed at internal addresses (including the `169.254.169.254` IMDS
238
+ * endpoint), and pass/fail timing discloses internal reachability.
239
+ */
240
+ export const PRIVATE_PUBLIC_ROOT_ENV =
241
+ 'EXPLAINER_KIT_ALLOW_PRIVATE_PUBLIC_ROOT';
242
+
243
+ export function privatePublicRootAllowed(env = process.env) {
244
+ return ['on', 'true', '1'].includes(
245
+ String(env[PRIVATE_PUBLIC_ROOT_ENV] ?? '').toLowerCase(),
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Literal-address policy only. Resolving names would be both TOCTOU-prone and
251
+ * dependent on the resolver of whoever runs the publish, so a name that happens
252
+ * to resolve inward is deliberately out of scope here.
253
+ */
254
+ export function isPrivatePublicHost(hostname) {
255
+ const host = String(hostname).toLowerCase();
256
+ if (host === 'localhost' || host.endsWith('.localhost')) return true;
257
+
258
+ if (host.startsWith('[') && host.endsWith(']')) {
259
+ return isPrivateIpv6(host.slice(1, -1));
260
+ }
261
+ if (host.includes(':')) return isPrivateIpv6(host);
262
+ if (IPV4_PATTERN.test(host)) return isPrivateIpv4(host);
263
+ return false;
264
+ }
265
+
266
+ function isPrivateIpv4(host) {
267
+ const octets = host.split('.').map(Number);
268
+ if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
269
+ // Not a valid dotted quad; leave it to the surrounding host checks.
270
+ return false;
271
+ }
272
+ const [a, b] = octets;
273
+ return (
274
+ a === 0 || // 0.0.0.0/8 "this network"
275
+ a === 127 || // loopback
276
+ a === 10 || // RFC 1918
277
+ (a === 172 && b >= 16 && b <= 31) || // RFC 1918
278
+ (a === 192 && b === 168) || // RFC 1918
279
+ (a === 169 && b === 254) // link-local, incl. the IMDS address
280
+ );
281
+ }
282
+
283
+ function isPrivateIpv6(address) {
284
+ const groups = expandIpv6(address.split('%')[0].toLowerCase());
285
+ if (!groups) return false;
286
+
287
+ // IPv4-mapped (`::ffff:a.b.c.d`) must not launder a private v4 address. The
288
+ // WHATWG URL parser rewrites the dotted form to hex, so `[::ffff:127.0.0.1]`
289
+ // arrives here as `::ffff:7f00:1` and has to be recognised in that shape.
290
+ if (groups.slice(0, 5).every((group) => group === 0)) {
291
+ if (groups[5] === 0xffff || groups[5] === 0) {
292
+ const [, , , , , , seven, eight] = groups;
293
+ const asV4 = [seven >> 8, seven & 0xff, eight >> 8, eight & 0xff].join(
294
+ '.',
295
+ );
296
+ if (groups[5] === 0xffff) return isPrivateIpv4(asV4);
297
+ // `::` unspecified and `::1` loopback.
298
+ if (seven === 0 && (eight === 0 || eight === 1)) return true;
299
+ }
300
+ }
301
+
302
+ return (
303
+ (groups[0] >= 0xfe80 && groups[0] <= 0xfebf) || // fe80::/10 link-local
304
+ (groups[0] >= 0xfc00 && groups[0] <= 0xfdff) // fc00::/7 unique-local
305
+ );
306
+ }
307
+
308
+ /** Expand an IPv6 literal to exactly eight numeric groups, or null. */
309
+ function expandIpv6(address) {
310
+ if (!address.includes(':')) return null;
311
+ const halves = address.split('::');
312
+ if (halves.length > 2) return null;
313
+
314
+ const parse = (part) =>
315
+ part === ''
316
+ ? []
317
+ : part.split(':').map((group) => Number.parseInt(group, 16));
318
+
319
+ // A trailing dotted quad (`::ffff:1.2.3.4`) contributes two groups.
320
+ const dotted = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(address);
321
+ let tail = [];
322
+ let working = address;
323
+ if (dotted) {
324
+ const octets = dotted[1].split('.').map(Number);
325
+ if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
326
+ return null;
327
+ }
328
+ tail = [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
329
+ working = address.slice(0, address.length - dotted[1].length);
330
+ }
331
+
332
+ const [left, right] = working.split('::');
333
+ const head = parse(left.replace(/:$/, ''));
334
+ const rest =
335
+ right === undefined ? [] : parse(right.replace(/:$/, '').replace(/^:/, ''));
336
+ const explicit = [...head, ...rest, ...tail];
337
+ if (explicit.some((group) => Number.isNaN(group) || group > 0xffff)) {
338
+ return null;
339
+ }
340
+
341
+ if (right === undefined) return explicit.length === 8 ? explicit : null;
342
+ if (explicit.length > 8) return null;
343
+ return [
344
+ ...head,
345
+ ...Array.from({ length: 8 - explicit.length }, () => 0),
346
+ ...rest,
347
+ ...tail,
348
+ ];
349
+ }
350
+
351
+ function rootError(message) {
352
+ return Object.assign(new Error(message), { code: 'E_PUBLISH_ROOTS' });
353
+ }