@aexhq/sdk 0.13.6

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 (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +160 -0
  3. package/dist/_contracts/connection-ticket.d.ts +21 -0
  4. package/dist/_contracts/connection-ticket.js +49 -0
  5. package/dist/_contracts/event-envelope.d.ts +276 -0
  6. package/dist/_contracts/event-envelope.js +324 -0
  7. package/dist/_contracts/event-stream-client.d.ts +47 -0
  8. package/dist/_contracts/event-stream-client.js +141 -0
  9. package/dist/_contracts/http.d.ts +35 -0
  10. package/dist/_contracts/http.js +114 -0
  11. package/dist/_contracts/index.d.ts +28 -0
  12. package/dist/_contracts/index.js +29 -0
  13. package/dist/_contracts/managed-key.d.ts +74 -0
  14. package/dist/_contracts/managed-key.js +110 -0
  15. package/dist/_contracts/operations.d.ts +237 -0
  16. package/dist/_contracts/operations.js +632 -0
  17. package/dist/_contracts/provider-support.d.ts +220 -0
  18. package/dist/_contracts/provider-support.js +90 -0
  19. package/dist/_contracts/proxy-protocol.d.ts +257 -0
  20. package/dist/_contracts/proxy-protocol.js +234 -0
  21. package/dist/_contracts/proxy-validation.d.ts +19 -0
  22. package/dist/_contracts/proxy-validation.js +51 -0
  23. package/dist/_contracts/run-artifacts.d.ts +47 -0
  24. package/dist/_contracts/run-artifacts.js +101 -0
  25. package/dist/_contracts/run-config.d.ts +304 -0
  26. package/dist/_contracts/run-config.js +659 -0
  27. package/dist/_contracts/run-cost.d.ts +125 -0
  28. package/dist/_contracts/run-cost.js +616 -0
  29. package/dist/_contracts/run-custody.d.ts +226 -0
  30. package/dist/_contracts/run-custody.js +465 -0
  31. package/dist/_contracts/run-record.d.ts +127 -0
  32. package/dist/_contracts/run-record.js +177 -0
  33. package/dist/_contracts/run-retention.d.ts +213 -0
  34. package/dist/_contracts/run-retention.js +484 -0
  35. package/dist/_contracts/run-unit.d.ts +194 -0
  36. package/dist/_contracts/run-unit.js +215 -0
  37. package/dist/_contracts/runner-event.d.ts +114 -0
  38. package/dist/_contracts/runner-event.js +187 -0
  39. package/dist/_contracts/runtime-manifest.d.ts +106 -0
  40. package/dist/_contracts/runtime-manifest.js +98 -0
  41. package/dist/_contracts/runtime-security-profile.d.ts +27 -0
  42. package/dist/_contracts/runtime-security-profile.js +82 -0
  43. package/dist/_contracts/runtime-sizes.d.ts +144 -0
  44. package/dist/_contracts/runtime-sizes.js +136 -0
  45. package/dist/_contracts/runtime-types.d.ts +212 -0
  46. package/dist/_contracts/runtime-types.js +2 -0
  47. package/dist/_contracts/sdk-errors.d.ts +34 -0
  48. package/dist/_contracts/sdk-errors.js +52 -0
  49. package/dist/_contracts/sdk-secrets.d.ts +31 -0
  50. package/dist/_contracts/sdk-secrets.js +220 -0
  51. package/dist/_contracts/side-effect-audit.d.ts +129 -0
  52. package/dist/_contracts/side-effect-audit.js +494 -0
  53. package/dist/_contracts/sse.d.ts +74 -0
  54. package/dist/_contracts/sse.js +0 -0
  55. package/dist/_contracts/stable.d.ts +26 -0
  56. package/dist/_contracts/stable.js +44 -0
  57. package/dist/_contracts/status.d.ts +19 -0
  58. package/dist/_contracts/status.js +61 -0
  59. package/dist/_contracts/submission.d.ts +383 -0
  60. package/dist/_contracts/submission.js +1380 -0
  61. package/dist/agents-md.d.ts +46 -0
  62. package/dist/agents-md.js +83 -0
  63. package/dist/agents-md.js.map +1 -0
  64. package/dist/asset-upload.d.ts +66 -0
  65. package/dist/asset-upload.js +168 -0
  66. package/dist/asset-upload.js.map +1 -0
  67. package/dist/bundle.d.ts +33 -0
  68. package/dist/bundle.js +89 -0
  69. package/dist/bundle.js.map +1 -0
  70. package/dist/cli.mjs +4140 -0
  71. package/dist/cli.mjs.sha256 +1 -0
  72. package/dist/client.d.ts +460 -0
  73. package/dist/client.js +857 -0
  74. package/dist/client.js.map +1 -0
  75. package/dist/fetch-archive.d.ts +16 -0
  76. package/dist/fetch-archive.js +170 -0
  77. package/dist/fetch-archive.js.map +1 -0
  78. package/dist/file.d.ts +57 -0
  79. package/dist/file.js +153 -0
  80. package/dist/file.js.map +1 -0
  81. package/dist/index.d.ts +30 -0
  82. package/dist/index.js +34 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/mcp-server.d.ts +84 -0
  85. package/dist/mcp-server.js +114 -0
  86. package/dist/mcp-server.js.map +1 -0
  87. package/dist/node-fs.d.ts +12 -0
  88. package/dist/node-fs.js +44 -0
  89. package/dist/node-fs.js.map +1 -0
  90. package/dist/proxy-endpoint.d.ts +131 -0
  91. package/dist/proxy-endpoint.js +147 -0
  92. package/dist/proxy-endpoint.js.map +1 -0
  93. package/dist/skill.d.ts +117 -0
  94. package/dist/skill.js +169 -0
  95. package/dist/skill.js.map +1 -0
  96. package/dist/version.d.ts +9 -0
  97. package/dist/version.js +10 -0
  98. package/dist/version.js.map +1 -0
  99. package/docs/cleanup.md +38 -0
  100. package/docs/credentials.md +153 -0
  101. package/docs/events.md +76 -0
  102. package/docs/mcp.md +47 -0
  103. package/docs/outputs.md +157 -0
  104. package/docs/product-boundaries.md +57 -0
  105. package/docs/provider-runtime-capabilities.md +103 -0
  106. package/docs/quickstart.md +110 -0
  107. package/docs/release.md +99 -0
  108. package/docs/run-config.md +53 -0
  109. package/docs/run-record.md +39 -0
  110. package/docs/skills.md +139 -0
  111. package/docs/testing.md +29 -0
  112. package/package.json +47 -0
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Run-request config and composition refs for the public SDK/CLI surface.
3
+ *
4
+ * Public composition concepts:
5
+ *
6
+ * - `SkillRef` is the wire-level reference to a skill — either an
7
+ * `skl_*` id pointing at a workspace-uploaded bundle, a
8
+ * `{vendor, skillId, version}` reference to a provider built-in, or
9
+ * a `{slot, name, contentHash}` reference to per-run bytes attached
10
+ * as a multipart part on the submitRun call (and torn down at run
11
+ * terminal). The three shapes are discriminated by `kind` so
12
+ * consumers branch mechanically and providers can never accidentally
13
+ * be looked up in `skill_bundles`. Transient refs do NOT round-trip
14
+ * through JSON (bytes can't be serialised back); `parseRunRequestConfig`
15
+ * therefore rejects them while the BFF multipart submission parser
16
+ * accepts them.
17
+ *
18
+ * - `McpServerRef` is the non-secret part of an MCP server declaration:
19
+ * `name` and `url`. Bearer / cookie / per-request headers travel in
20
+ * the run's vaulted `secrets.mcpServers` block keyed by the same
21
+ * `name`, and never enter the hashed submission payload or the
22
+ * run snapshot.
23
+ *
24
+ * - `RunRequestConfig` is the credential-free set of run parameters that
25
+ * can be persisted to disk (e.g. `aex run --config run.json`) or
26
+ * returned from ordinary application helper functions. It excludes
27
+ * `secrets`/`idempotencyKey`/`signal`; strings are already resolved at
28
+ * the call site before submission.
29
+ *
30
+ * - Skill bundle validation lives here so the SDK (zipping locally),
31
+ * hosted API (server-side unzip + manifest extraction) and runtime
32
+ * mount layer share a single source of truth for
33
+ * the limits, the path normaliser, and the manifest invariants. The
34
+ * DB CHECK constraints on `skill_bundles.manifest` mirror these.
35
+ *
36
+ * Keep this as the public source of truth for the SDK/CLI composition
37
+ * boundary.
38
+ */
39
+ // ---------------------------------------------------------------------------
40
+ // Skill ID + name format
41
+ // ---------------------------------------------------------------------------
42
+ /**
43
+ * Mirrors the server-side CHECK constraint
44
+ * `skill_bundles_id_format_chk = check (id ~ '^skl_[A-Za-z0-9_-]{8,128}$')`
45
+ * on persisted skill bundles. Keep the two in lockstep.
46
+ */
47
+ export const SKILL_ID_PATTERN = /^skl_[A-Za-z0-9_-]{8,128}$/;
48
+ /**
49
+ * Human-readable, workspace-scoped name. Lowercase, kebab-friendly,
50
+ * 1..128 chars. The DB enforces the length bound via
51
+ * `skill_bundles_name_len_chk`; this regex tightens the SDK/CLI input
52
+ * surface so callers fail at the boundary rather than in the BFF.
53
+ */
54
+ export const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,127}$/;
55
+ // ---------------------------------------------------------------------------
56
+ // Skill bundle limits (uploaded bundles)
57
+ // ---------------------------------------------------------------------------
58
+ /**
59
+ * Hard caps applied at upload time. The SDK enforces these before
60
+ * computing the zip hash so a clearly-too-big bundle never wastes
61
+ * bytes-on-the-wire; the BFF re-enforces server-side because the SDK
62
+ * is untrusted. Numbers are deliberately conservative for the MVP and
63
+ * can be tuned later; keep this object as the single tuning point.
64
+ */
65
+ export const SKILL_BUNDLE_LIMITS = {
66
+ /** Compressed (.zip) ceiling. */
67
+ maxCompressedBytes: 10 * 1024 * 1024,
68
+ /** Sum of uncompressed file sizes. */
69
+ maxDecompressedBytes: 50 * 1024 * 1024,
70
+ /** Number of regular file entries (directories don't count). */
71
+ maxFiles: 1000,
72
+ /** Maximum directory nesting depth — `a/b/c/d` has depth 4. */
73
+ maxDepth: 16,
74
+ /** Single-entry path length cap. */
75
+ maxPathLength: 512,
76
+ /** Stored file mode for ordinary files. */
77
+ defaultFileMode: 0o644,
78
+ /** Stored directory mode. */
79
+ defaultDirMode: 0o755
80
+ };
81
+ /** Content-hash format: `sha256:<64 lowercase hex>`. */
82
+ export const INLINE_CONTENT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
83
+ export function isProviderSkillRef(ref) {
84
+ return ref.kind === "provider";
85
+ }
86
+ export function isAssetRef(ref) {
87
+ return ref.kind === "asset";
88
+ }
89
+ /**
90
+ * Asset ids are storage-neutral product ids. Current uploads derive the id from
91
+ * the content digest (`asset_<sha256hex>`), but callers must treat it as opaque.
92
+ */
93
+ export const ASSET_ID_PATTERN = /^asset_[A-Za-z0-9_-]{8,128}$/;
94
+ export function isAgentsMdAssetRef(ref) {
95
+ return ref.kind === "asset";
96
+ }
97
+ export function isFileAssetRef(ref) {
98
+ return ref.kind === "asset";
99
+ }
100
+ /**
101
+ * Parse a `SkillRef` from untrusted input. Used by the BFF run parser
102
+ * and by the operations module when deserialising API responses. Only
103
+ * `kind: "asset"` and `kind: "provider"` are valid; all other historical
104
+ * wire shapes (including storage-specific refs) are rejected.
105
+ */
106
+ export function parseSkillRef(input, path) {
107
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
108
+ throw new Error(`${path} must be a SkillRef object`);
109
+ }
110
+ const record = input;
111
+ const kind = record.kind;
112
+ if (kind === "provider") {
113
+ for (const key of Object.keys(record)) {
114
+ if (key !== "kind" && key !== "vendor" && key !== "skillId" && key !== "version") {
115
+ throw new Error(`${path} contains unexpected field for provider SkillRef: ${key}`);
116
+ }
117
+ }
118
+ const vendor = record.vendor;
119
+ if (vendor !== "anthropic" && vendor !== "custom") {
120
+ throw new Error(`${path}.vendor must be 'anthropic' or 'custom'`);
121
+ }
122
+ const skillId = record.skillId;
123
+ if (typeof skillId !== "string" || skillId.length === 0 || skillId.length > 256) {
124
+ throw new Error(`${path}.skillId must be a non-empty string (<= 256 chars)`);
125
+ }
126
+ const version = record.version;
127
+ if (version !== undefined && (typeof version !== "string" || version.length === 0 || version.length > 64)) {
128
+ throw new Error(`${path}.version, when provided, must be a non-empty string (<= 64 chars)`);
129
+ }
130
+ return {
131
+ kind: "provider",
132
+ vendor,
133
+ skillId,
134
+ ...(version !== undefined ? { version } : {})
135
+ };
136
+ }
137
+ if (kind === "asset") {
138
+ return parseAssetRefFields(record, path);
139
+ }
140
+ throw new Error(`${path}.kind must be 'provider' or 'asset'`);
141
+ }
142
+ /**
143
+ * Common parser for any `kind: "asset"` ref (skill / agentsMd / file).
144
+ */
145
+ export function parseAssetRefFields(record, path) {
146
+ for (const key of Object.keys(record)) {
147
+ if (key !== "kind" &&
148
+ key !== "assetId" &&
149
+ key !== "name" &&
150
+ key !== "mountPath") {
151
+ throw new Error(`${path} contains unexpected field for asset ref: ${key}`);
152
+ }
153
+ }
154
+ const assetId = record.assetId;
155
+ if (typeof assetId !== "string" || !ASSET_ID_PATTERN.test(assetId)) {
156
+ throw new Error(`${path}.assetId must match ${ASSET_ID_PATTERN.source}`);
157
+ }
158
+ const name = record.name;
159
+ if (typeof name !== "string" || name.length === 0 || name.length > 128) {
160
+ throw new Error(`${path}.name must be a non-empty string (<= 128 chars)`);
161
+ }
162
+ const mountPath = record.mountPath;
163
+ if (mountPath !== undefined && (typeof mountPath !== "string" || mountPath.length === 0)) {
164
+ throw new Error(`${path}.mountPath, when provided, must be a non-empty string`);
165
+ }
166
+ return {
167
+ kind: "asset",
168
+ assetId,
169
+ name,
170
+ ...(mountPath !== undefined ? { mountPath } : {})
171
+ };
172
+ }
173
+ export class SkillBundleValidationError extends Error {
174
+ constructor(message) {
175
+ super(message);
176
+ this.name = "SkillBundleValidationError";
177
+ }
178
+ }
179
+ /**
180
+ * Reject input paths that try to escape the bundle root or smuggle
181
+ * platform-specific syntax. Returns the canonical forward-slash
182
+ * relative path; never returns paths starting or ending with `/`.
183
+ *
184
+ * Rejects:
185
+ * - empty strings and pure whitespace
186
+ * - absolute paths (`/foo`, `C:\foo`, `\\server\share`)
187
+ * - backslash separators (Windows)
188
+ * - `..` segments anywhere in the path
189
+ * - `.` segments anywhere except a leading bare `.`
190
+ * - paths whose length exceeds `SKILL_BUNDLE_LIMITS.maxPathLength`
191
+ * - paths whose depth exceeds `SKILL_BUNDLE_LIMITS.maxDepth`
192
+ * - NUL bytes
193
+ */
194
+ export function normaliseSkillBundlePath(input) {
195
+ if (typeof input !== "string") {
196
+ throw new SkillBundleValidationError("bundle entry path must be a string");
197
+ }
198
+ if (input.length === 0 || input.trim().length === 0) {
199
+ throw new SkillBundleValidationError("bundle entry path must be non-empty");
200
+ }
201
+ if (input.length > SKILL_BUNDLE_LIMITS.maxPathLength) {
202
+ throw new SkillBundleValidationError(`bundle entry path exceeds maxPathLength (${SKILL_BUNDLE_LIMITS.maxPathLength}): ${input}`);
203
+ }
204
+ if (input.includes("\0")) {
205
+ throw new SkillBundleValidationError(`bundle entry path contains NUL byte: ${JSON.stringify(input)}`);
206
+ }
207
+ if (input.includes("\\")) {
208
+ throw new SkillBundleValidationError(`bundle entry path uses backslash separator: ${input}`);
209
+ }
210
+ if (/^[A-Za-z]:[\\/]/.test(input)) {
211
+ throw new SkillBundleValidationError(`bundle entry path uses a drive letter: ${input}`);
212
+ }
213
+ if (input.startsWith("/")) {
214
+ throw new SkillBundleValidationError(`bundle entry path must be relative: ${input}`);
215
+ }
216
+ // Reject trailing slash so callers cannot disguise directory entries
217
+ // as files. The manifest is files-only.
218
+ if (input.endsWith("/")) {
219
+ throw new SkillBundleValidationError(`bundle entry path must not end with '/': ${input}`);
220
+ }
221
+ const segments = input.split("/");
222
+ for (const segment of segments) {
223
+ if (segment === "..") {
224
+ throw new SkillBundleValidationError(`bundle entry path contains '..' segment: ${input}`);
225
+ }
226
+ if (segment === "." || segment === "") {
227
+ throw new SkillBundleValidationError(`bundle entry path contains empty or '.' segment: ${input}`);
228
+ }
229
+ }
230
+ if (segments.length > SKILL_BUNDLE_LIMITS.maxDepth) {
231
+ throw new SkillBundleValidationError(`bundle entry path exceeds maxDepth (${SKILL_BUNDLE_LIMITS.maxDepth}): ${input}`);
232
+ }
233
+ return input;
234
+ }
235
+ /**
236
+ * Validate one manifest entry: normalises the path, bounds the size,
237
+ * and sanitises the mode to one of {defaultFileMode, defaultDirMode}.
238
+ * The bundle is files-only, so any non-regular-file entry is rejected
239
+ * upstream by the caller (zip parser must skip symlinks, device files,
240
+ * etc. before reaching this function).
241
+ */
242
+ export function validateSkillBundleEntry(input) {
243
+ const path = normaliseSkillBundlePath(input.path);
244
+ if (!Number.isFinite(input.size) || !Number.isInteger(input.size) || input.size < 0) {
245
+ throw new SkillBundleValidationError(`bundle entry size must be a non-negative integer (${path})`);
246
+ }
247
+ if (input.size > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
248
+ throw new SkillBundleValidationError(`bundle entry size exceeds maxDecompressedBytes (${SKILL_BUNDLE_LIMITS.maxDecompressedBytes}): ${path}`);
249
+ }
250
+ // Sanitise the stored mode. Executable bit is implied by runtime
251
+ // convention; we never persist arbitrary chmod from the user's FS.
252
+ const mode = (input.mode ?? SKILL_BUNDLE_LIMITS.defaultFileMode) & 0o777;
253
+ if (mode !== SKILL_BUNDLE_LIMITS.defaultFileMode && mode !== SKILL_BUNDLE_LIMITS.defaultDirMode) {
254
+ return { path, size: input.size, mode: SKILL_BUNDLE_LIMITS.defaultFileMode };
255
+ }
256
+ return { path, size: input.size, mode };
257
+ }
258
+ /**
259
+ * Validate a full **skill bundle** manifest. Enforces:
260
+ * - entries is a non-empty array
261
+ * - `SKILL.md` exists at the bundle root (this is what makes a
262
+ * bundle a skill rather than a plain workspace file)
263
+ * - file count <= maxFiles
264
+ * - total uncompressed size <= maxDecompressedBytes
265
+ * - per-entry validation (see `validateSkillBundleEntry`)
266
+ * - no duplicate paths
267
+ *
268
+ * In this public surface, **skill** means "Claude Skill" — bundles without
269
+ * `SKILL.md` are not skills and must go
270
+ * through the `AgentsMd` or `File` upload concepts instead.
271
+ *
272
+ * Returns a canonical manifest with totals computed.
273
+ */
274
+ export function validateSkillBundleManifest(input) {
275
+ if (!Array.isArray(input) || input.length === 0) {
276
+ throw new SkillBundleValidationError("bundle manifest must be a non-empty array of entries");
277
+ }
278
+ if (input.length > SKILL_BUNDLE_LIMITS.maxFiles) {
279
+ throw new SkillBundleValidationError(`bundle exceeds maxFiles (${SKILL_BUNDLE_LIMITS.maxFiles}): got ${input.length}`);
280
+ }
281
+ const seen = new Set();
282
+ const entries = [];
283
+ let totalSize = 0;
284
+ let hasSkillMd = false;
285
+ for (const raw of input) {
286
+ const entry = validateSkillBundleEntry(raw);
287
+ if (seen.has(entry.path)) {
288
+ throw new SkillBundleValidationError(`bundle manifest contains duplicate path: ${entry.path}`);
289
+ }
290
+ seen.add(entry.path);
291
+ if (entry.path === "SKILL.md") {
292
+ hasSkillMd = true;
293
+ }
294
+ totalSize += entry.size;
295
+ if (totalSize > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
296
+ throw new SkillBundleValidationError(`bundle total size exceeds maxDecompressedBytes (${SKILL_BUNDLE_LIMITS.maxDecompressedBytes})`);
297
+ }
298
+ entries.push(entry);
299
+ }
300
+ if (!hasSkillMd) {
301
+ throw new SkillBundleValidationError("skill bundle manifest must contain a 'SKILL.md' entry at the bundle root. " +
302
+ "If you want to upload an instructions file or generic agent context, use " +
303
+ "AgentsMd or File instead.");
304
+ }
305
+ return { entries, totalSize, fileCount: entries.length };
306
+ }
307
+ /**
308
+ * Returns true when the manifest carries a `SKILL.md` entry at the
309
+ * bundle root. The presence of this file is Anthropic's
310
+ * skill-auto-discovery signal — bundles that have it are treated as
311
+ * Claude skills and mounted accordingly; bundles that don't are still
312
+ * usable agent context (AGENTS.md, settings files, folders of helper
313
+ * data) but the agent won't pick them up via the skills mechanism.
314
+ *
315
+ * The check is intentionally a separate, callable predicate (rather
316
+ * than baked into `validateSkillBundleManifest`) so the storage and
317
+ * the attach layers can remain independent.
318
+ */
319
+ export function hasSkillMdAtRoot(manifest) {
320
+ return manifest.entries.some((entry) => entry.path === "SKILL.md");
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // McpServerRef (non-secret) + RunConfigMcpServer (with optional headers)
324
+ // ---------------------------------------------------------------------------
325
+ /**
326
+ * Remote MCP transports Aex accepts. Both are over HTTP — `http`
327
+ * is the streamable-HTTP transport, `sse` is the event-stream
328
+ * transport. `stdio` is explicitly NOT a value here: local-process
329
+ * MCP is not implemented.
330
+ */
331
+ export const REMOTE_MCP_TRANSPORTS = ["http", "sse"];
332
+ /**
333
+ * Canonical error string for any attempt to declare a stdio-shaped MCP
334
+ * server (`transport: "stdio"`, or a stdio-only field like `command` /
335
+ * `args` / `env`). Pinned in source so every surface — shared parser,
336
+ * SDK builder, CLI flag parser, dashboard form — surfaces the same
337
+ * message and a user can find it via grep.
338
+ */
339
+ export const REMOTE_MCP_STDIO_REJECTED_MESSAGE = "stdio MCP servers are not supported by Aex. Aex supports remote MCP servers over HTTP/SSE only.";
340
+ /**
341
+ * Stdio-only fields. Used by the parser to detect a stdio shape even
342
+ * when the caller omits `transport: "stdio"` (e.g. `{ url, command }`
343
+ * — the presence of `command` alone is enough to identify a stdio
344
+ * declaration and reject it).
345
+ */
346
+ const STDIO_ONLY_FIELDS = ["command", "args", "env"];
347
+ export const MCP_SERVER_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
348
+ export function parseMcpServerRef(input, path) {
349
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
350
+ throw new Error(`${path} must be an object`);
351
+ }
352
+ const record = input;
353
+ rejectStdioMcpShape(record);
354
+ // Headers belong on `RunConfigMcpServer`, not the non-secret wire ref;
355
+ // and the wire `submission.mcpServers` must NEVER contain headers. So
356
+ // reject any field other than {name,url,transport} explicitly to make a
357
+ // caller accidentally inlining `headers` into the non-secret half fail
358
+ // loudly instead of silently dropping the field.
359
+ // `parseRunConfigMcpServerRef` handles the headers case separately for
360
+ // run-config entries.
361
+ for (const key of Object.keys(record)) {
362
+ if (key !== "name" && key !== "url" && key !== "transport") {
363
+ throw new Error(`${path}.${key} is not an allowed field for McpServerRef; permitted: name, url, transport`);
364
+ }
365
+ }
366
+ const name = record.name;
367
+ if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
368
+ throw new Error(`${path}.name must match ${MCP_SERVER_NAME_PATTERN.source}`);
369
+ }
370
+ const url = record.url;
371
+ if (typeof url !== "string" || url.length === 0) {
372
+ throw new Error(`${path}.url must be a non-empty string`);
373
+ }
374
+ try {
375
+ const parsed = new URL(url);
376
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
377
+ throw new Error(`${path}.url must use http or https (got ${parsed.protocol})`);
378
+ }
379
+ // Auth belongs in `secrets.mcpServers[i].headers`, never in the URL
380
+ // itself. A `https://user:pass@host` style URL would be persisted in
381
+ // the non-secret run snapshot and hashed into the idempotency key —
382
+ // both unacceptable for credential material.
383
+ if (parsed.username !== "" || parsed.password !== "") {
384
+ throw new Error(`${path}.url must not contain userinfo (username/password); use secrets.mcpServers[].headers for auth`);
385
+ }
386
+ // SSRF guard at the parser boundary (C4) — the Worker MCP proxy
387
+ // relies on this validation; CF's outbound fetch refuses RFC1918 but
388
+ // does NOT block loopback names, link-local IPv6, 169.254.x, or
389
+ // arbitrary non-443 ports on https. Each branch below maps to a
390
+ // c4-worker-ssrf.regression test case.
391
+ const ssrfDenial = denyReasonForMcpHost(parsed);
392
+ if (ssrfDenial !== null) {
393
+ throw new Error(`${path}.url ${ssrfDenial}`);
394
+ }
395
+ }
396
+ catch (cause) {
397
+ if (cause instanceof Error && cause.message.startsWith(path)) {
398
+ throw cause;
399
+ }
400
+ throw new Error(`${path}.url is not a valid URL: ${url}`);
401
+ }
402
+ const transport = parseRemoteMcpTransport(record.transport, `${path}.transport`);
403
+ return transport ? { name, url, transport } : { name, url };
404
+ }
405
+ /**
406
+ * Throw the canonical stdio-rejected error if the record carries any
407
+ * stdio-only marker (`transport: "stdio"`, `command`, `args`, `env`).
408
+ * Used by both the shared parser and the SDK `McpServer.remote`
409
+ * builder so every entry point surfaces the same message.
410
+ */
411
+ export function rejectStdioMcpShape(record) {
412
+ if (record.transport === "stdio") {
413
+ throw new Error(REMOTE_MCP_STDIO_REJECTED_MESSAGE);
414
+ }
415
+ for (const field of STDIO_ONLY_FIELDS) {
416
+ if (record[field] !== undefined) {
417
+ throw new Error(REMOTE_MCP_STDIO_REJECTED_MESSAGE);
418
+ }
419
+ }
420
+ }
421
+ /**
422
+ * Reasons an MCP server URL should be refused at parse time. Returns null
423
+ * when the URL is acceptable. Hostnames are lowercased; numeric ranges
424
+ * are checked literally so the catch covers both names ("localhost") and
425
+ * IP literals ("127.0.0.1") symmetrically.
426
+ *
427
+ * Surface tracked by server-side SSRF regression coverage.
428
+ */
429
+ function denyReasonForMcpHost(parsed) {
430
+ // `new URL("https://[fe80::1]/").hostname` returns `[fe80::1]` WITH
431
+ // the brackets on Node 22; strip them so the IPv6 checks match either
432
+ // shape symmetrically.
433
+ const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
434
+ // Loopback name (covers `localhost` + `localhost.localdomain` etc.)
435
+ if (host === "localhost" || host.endsWith(".localhost")) {
436
+ return "must not target a loopback hostname";
437
+ }
438
+ // Loopback IPv4 (127.0.0.0/8)
439
+ if (/^127(?:\.[0-9]+){3}$/.test(host)) {
440
+ return "must not target loopback IPv4 (127.0.0.0/8)";
441
+ }
442
+ // Loopback IPv6 (::1 in any acceptable form)
443
+ if (host === "::1" || host === "0:0:0:0:0:0:0:1") {
444
+ return "must not target loopback IPv6 (::1)";
445
+ }
446
+ // Link-local IPv6 (fe80::/10 — fe80:: through febf::)
447
+ if (/^fe[89ab][0-9a-f]?:/.test(host)) {
448
+ return "must not target link-local IPv6 (fe80::/10)";
449
+ }
450
+ // Link-local / metadata IPv4 (169.254.0.0/16 — includes 169.254.169.254)
451
+ if (/^169\.254\.[0-9]+\.[0-9]+$/.test(host)) {
452
+ return "must not target link-local IPv4 (169.254.0.0/16) — cloud metadata range";
453
+ }
454
+ // RFC1918 private ranges (10/8, 172.16/12, 192.168/16) — defense in depth.
455
+ if (/^10\.[0-9]+\.[0-9]+\.[0-9]+$/.test(host)) {
456
+ return "must not target RFC1918 IPv4 (10.0.0.0/8)";
457
+ }
458
+ if (/^172\.(1[6-9]|2[0-9]|3[01])\.[0-9]+\.[0-9]+$/.test(host)) {
459
+ return "must not target RFC1918 IPv4 (172.16.0.0/12)";
460
+ }
461
+ if (/^192\.168\.[0-9]+\.[0-9]+$/.test(host)) {
462
+ return "must not target RFC1918 IPv4 (192.168.0.0/16)";
463
+ }
464
+ // Port constraint: https must be on 443 (defense in depth — non-standard
465
+ // https ports often indicate internal services). http allowance keeps
466
+ // the existing local-dev pattern (e.g. host.docker.internal:8787) usable.
467
+ if (parsed.protocol === "https:" && parsed.port !== "" && parsed.port !== "443") {
468
+ return `must use port 443 for https (got ${parsed.port})`;
469
+ }
470
+ return null;
471
+ }
472
+ function parseRemoteMcpTransport(input, field) {
473
+ if (input === undefined) {
474
+ return undefined;
475
+ }
476
+ if (typeof input !== "string" || !REMOTE_MCP_TRANSPORTS.includes(input)) {
477
+ throw new Error(`${field} must be one of: ${REMOTE_MCP_TRANSPORTS.join(", ")} (got ${JSON.stringify(input)})`);
478
+ }
479
+ return input;
480
+ }
481
+ /**
482
+ * Strict parser for run-config MCP server entries. Allows only the
483
+ * `{name, url, headers?}` shape so config loaded from `--config run.json`
484
+ * cannot smuggle unrelated fields past the parser.
485
+ */
486
+ function parseRunConfigMcpServerRef(input, path) {
487
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
488
+ throw new Error(`${path} must be an object`);
489
+ }
490
+ const record = input;
491
+ rejectStdioMcpShape(record);
492
+ for (const key of Object.keys(record)) {
493
+ if (key !== "name" && key !== "url" && key !== "headers" && key !== "transport") {
494
+ throw new Error(`${path}.${key} is not an allowed field for RunConfigMcpServer; permitted: name, url, transport, headers`);
495
+ }
496
+ }
497
+ // Reuse the {name,url,transport} validator by passing the stripped object.
498
+ const stripped = { name: record.name, url: record.url };
499
+ if (record.transport !== undefined)
500
+ stripped.transport = record.transport;
501
+ const ref = parseMcpServerRef(stripped, path);
502
+ const rawHeaders = record.headers;
503
+ if (rawHeaders === undefined) {
504
+ return ref;
505
+ }
506
+ if (rawHeaders === null || typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
507
+ throw new Error(`${path}.headers, when provided, must be a string-keyed object`);
508
+ }
509
+ const headers = {};
510
+ for (const [hk, hv] of Object.entries(rawHeaders)) {
511
+ if (typeof hv !== "string") {
512
+ throw new Error(`${path}.headers.${hk} must be a string`);
513
+ }
514
+ headers[hk] = hv;
515
+ }
516
+ return { ...ref, headers };
517
+ }
518
+ // ---------------------------------------------------------------------------
519
+ // Run request config parser (used by CLI to load `run.json`)
520
+ // ---------------------------------------------------------------------------
521
+ /**
522
+ * Parse a run request config from JSON. Defensive — used by the host CLI to
523
+ * load `--config run.json`. Throws with the JSON path that failed so
524
+ * a user can fix their file. Headers are preserved here and split out
525
+ * later by the SDK normalisation step.
526
+ */
527
+ export function parseRunRequestConfig(input) {
528
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
529
+ throw new Error("run request config must be an object");
530
+ }
531
+ const record = input;
532
+ const allowed = new Set([
533
+ "model",
534
+ "system",
535
+ "prompt",
536
+ "skills",
537
+ "mcpServers",
538
+ "environment",
539
+ "runtimeSize",
540
+ "timeout",
541
+ "proxyEndpoints",
542
+ "metadata"
543
+ ]);
544
+ for (const key of Object.keys(record)) {
545
+ if (!allowed.has(key)) {
546
+ throw new Error(`run request config contains unexpected field: ${key}`);
547
+ }
548
+ }
549
+ const model = record.model;
550
+ if (typeof model !== "string" || model.length === 0) {
551
+ throw new Error("run request config model must be a non-empty string");
552
+ }
553
+ const system = record.system;
554
+ if (system !== undefined && typeof system !== "string") {
555
+ throw new Error("run request config system, when provided, must be a string");
556
+ }
557
+ const prompt = parseRunRequestConfigPrompt(record.prompt);
558
+ const skills = parseRunRequestConfigSkills(record.skills);
559
+ const mcpServers = parseRunRequestConfigMcpServers(record.mcpServers);
560
+ return {
561
+ model,
562
+ ...(system !== undefined ? { system } : {}),
563
+ prompt,
564
+ ...(skills !== undefined ? { skills } : {}),
565
+ ...(mcpServers !== undefined ? { mcpServers } : {}),
566
+ // environment / proxyEndpoints / metadata: passed through
567
+ // as-is — the BFF revalidates them via `parseRunSubmissionRequest`,
568
+ // so duplicating the heavyweight parsers here would mean two sources
569
+ // of truth. The CLI surfaces structural errors at submission time.
570
+ ...(record.environment !== undefined
571
+ ? { environment: record.environment }
572
+ : {}),
573
+ ...(record.runtimeSize !== undefined
574
+ ? { runtimeSize: record.runtimeSize }
575
+ : {}),
576
+ ...(record.timeout !== undefined
577
+ ? { timeout: record.timeout }
578
+ : {}),
579
+ ...(record.proxyEndpoints !== undefined
580
+ ? { proxyEndpoints: record.proxyEndpoints }
581
+ : {}),
582
+ ...(record.metadata !== undefined
583
+ ? { metadata: record.metadata }
584
+ : {})
585
+ };
586
+ }
587
+ function parseRunRequestConfigPrompt(value) {
588
+ if (typeof value === "string") {
589
+ if (value.length === 0) {
590
+ throw new Error("run request config prompt must be a non-empty string");
591
+ }
592
+ return value;
593
+ }
594
+ if (Array.isArray(value)) {
595
+ const arr = [];
596
+ for (let i = 0; i < value.length; i++) {
597
+ const item = value[i];
598
+ if (typeof item !== "string" || item.length === 0) {
599
+ throw new Error(`run request config prompt[${i}] must be a non-empty string`);
600
+ }
601
+ arr.push(item);
602
+ }
603
+ if (arr.length === 0) {
604
+ throw new Error("run request config prompt must be a non-empty string or array of strings");
605
+ }
606
+ return arr;
607
+ }
608
+ throw new Error("run request config prompt must be a string or array of strings");
609
+ }
610
+ function parseRunRequestConfigSkills(value) {
611
+ if (value === undefined) {
612
+ return undefined;
613
+ }
614
+ if (!Array.isArray(value)) {
615
+ throw new Error("run request config skills must be an array");
616
+ }
617
+ return value.map((item, index) => parseSkillRef(item, `run request config skills[${index}]`));
618
+ }
619
+ function parseRunRequestConfigMcpServers(value) {
620
+ if (value === undefined) {
621
+ return undefined;
622
+ }
623
+ if (!Array.isArray(value)) {
624
+ throw new Error("run request config mcpServers must be an array");
625
+ }
626
+ const seen = new Set();
627
+ return value.map((item, index) => {
628
+ const entry = parseRunConfigMcpServerRef(item, `run request config mcpServers[${index}]`);
629
+ if (seen.has(entry.name)) {
630
+ throw new Error(`run request config mcpServers duplicate name: ${entry.name}`);
631
+ }
632
+ seen.add(entry.name);
633
+ return entry;
634
+ });
635
+ }
636
+ export function normaliseRunRequestConfig(config) {
637
+ const prompt = typeof config.prompt === "string" ? [config.prompt] : config.prompt;
638
+ const skills = config.skills ?? [];
639
+ const mcpServers = [];
640
+ const mcpServerSecrets = [];
641
+ for (const entry of config.mcpServers ?? []) {
642
+ mcpServers.push({ name: entry.name, url: entry.url });
643
+ if (entry.headers !== undefined) {
644
+ mcpServerSecrets.push({ name: entry.name, url: entry.url, headers: entry.headers });
645
+ }
646
+ }
647
+ return {
648
+ model: config.model,
649
+ ...(config.system !== undefined ? { system: config.system } : {}),
650
+ prompt,
651
+ skills,
652
+ mcpServers,
653
+ ...(config.environment !== undefined ? { environment: config.environment } : {}),
654
+ ...(config.proxyEndpoints !== undefined ? { proxyEndpoints: config.proxyEndpoints } : {}),
655
+ ...(config.metadata !== undefined ? { metadata: config.metadata } : {}),
656
+ mcpServerSecrets
657
+ };
658
+ }
659
+ //# sourceMappingURL=run-config.js.map