@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,1380 @@
1
+ import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_ENDPOINT_DEFAULTS, PROXY_RESPONSE_MODES } from "./proxy-protocol.js";
2
+ // Re-exported from the protocol module (its canonical home, alongside the
3
+ // index-file shape the builder fills). Kept on the submission surface so
4
+ // existing `@aexhq/contracts` consumers of `PROXY_ENDPOINT_DEFAULTS` are
5
+ // unaffected by the move.
6
+ export { PROXY_ENDPOINT_DEFAULTS };
7
+ import { parseAssetRefFields, parseMcpServerRef, parseSkillRef } from "./run-config.js";
8
+ import { parseRunTimeout, parseRuntimeSize } from "./runtime-sizes.js";
9
+ import { parseRuntimeSecurityProfile } from "./runtime-security-profile.js";
10
+ import { assertManagedKeyAdmissionAllowed, parseCredentialMode } from "./managed-key.js";
11
+ /**
12
+ * Reserved prefix for aex-set runtime env vars (`AEX_CLI`,
13
+ * `AEX_RUNTIME_JSON`, …). Customer `environment.envVars` keys carrying this
14
+ * prefix are rejected at submission parse time so platform-set values
15
+ * cannot be silently overwritten.
16
+ */
17
+ export const AEX_RESERVED_ENV_PREFIX = "AEX_";
18
+ /**
19
+ * Maximum number of `environment.envVars` entries accepted per
20
+ * submission. Picked to be generous for real customer config bags
21
+ * (the broll case ships a handful — `BROLL_STORE`, `BROLL_OUTPUTS`,
22
+ * `BROLL_MODE`, …) while still bounding the size of every RUNTIME
23
+ * file we mount into the container.
24
+ */
25
+ export const ENV_VARS_MAX_ENTRIES = 64;
26
+ /** Maximum byte length of a single `environment.envVars` value. */
27
+ export const ENV_VARS_MAX_VALUE_BYTES = 4096;
28
+ /** Maximum total byte length of all `environment.envVars` keys+values combined. */
29
+ export const ENV_VARS_MAX_TOTAL_BYTES = 65536;
30
+ /**
31
+ * POSIX-shell-portable env var key: starts with `A-Z` or `_`, body is
32
+ * `A-Z`, `0-9`, `_`. We deliberately reject lowercase to keep
33
+ * `RUNTIME.env` readable and consistent with platform conventions; if
34
+ * a customer has lowercase keys today, they uppercase them at the
35
+ * call site.
36
+ */
37
+ const ENV_VAR_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
38
+ /**
39
+ * Package-manager ecosystems accepted by the public submission schema.
40
+ * The customer encodes the target manager as a `name` prefix
41
+ * `"<eco>:<pkg>"` (e.g. "pip:pandas", "npm:express", "apt:ffmpeg"); an
42
+ * UNPREFIXED name defaults to `apt`. After parsing, `PlatformPackage.name`
43
+ * is the bare package and `PlatformPackage.ecosystem` is the resolved
44
+ * manager.
45
+ */
46
+ export const PLATFORM_PACKAGE_ECOSYSTEMS = ["apt", "npm", "pip"];
47
+ /**
48
+ * Render a parsed {@link PlatformPackage} as the version-embedded install
49
+ * string used by runtime materialization. The join differs per manager:
50
+ * - pip → `name==version`
51
+ * - npm → `name@version`
52
+ * - apt → `name=version`
53
+ * With no `version`, just the bare `name`. Pure; used by the managed runner
54
+ * package installer.
55
+ */
56
+ export function packageInstallString(pkg) {
57
+ if (pkg.version === undefined) {
58
+ return pkg.name;
59
+ }
60
+ switch (pkg.ecosystem) {
61
+ case "pip":
62
+ return `${pkg.name}==${pkg.version}`;
63
+ case "npm":
64
+ return `${pkg.name}@${pkg.version}`;
65
+ case "apt":
66
+ return `${pkg.name}=${pkg.version}`;
67
+ }
68
+ }
69
+ /**
70
+ * Run-time provider selector. Aex exposes one customer interface
71
+ * for every provider. All new submissions execute through the managed
72
+ * runtime; provider selection only decides which upstream model route
73
+ * the managed provider-proxy uses.
74
+ */
75
+ export const RUN_PROVIDERS = [
76
+ "anthropic",
77
+ "deepseek",
78
+ "openai",
79
+ "gemini",
80
+ "mistral"
81
+ ];
82
+ export const DEFAULT_RUN_PROVIDER = "anthropic";
83
+ /**
84
+ * Customer-facing runtime selector. Optional on the wire; absent resolves
85
+ * to the same managed runtime as `"managed"`. `"native"` is no longer an
86
+ * accepted submission value and fails schema validation.
87
+ */
88
+ export const RUNTIME_KINDS = ["managed"];
89
+ /**
90
+ * Centralized runtime-support validator. Native is removed from the public
91
+ * runtime enum, so an absent runtime and `"managed"` are the only supported
92
+ * inputs. Schema parsing rejects other runtime strings before this helper is
93
+ * reached, but the result type remains for SDK preflight checks.
94
+ */
95
+ export function checkRuntimeSupported(provider, runtime) {
96
+ void provider;
97
+ return { ok: true };
98
+ }
99
+ const SECRETS_KEY = "secrets";
100
+ const PROXY_ENDPOINT_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
101
+ const RESERVED_PROXY_ENDPOINT_NAMES = new Set(["proxy", "aex", "internal", "admin"]);
102
+ /**
103
+ * Headers the proxy never lets through, regardless of policy. Lowercase.
104
+ * Anything that could re-introduce credentials, cookies, or routing
105
+ * primitives. Kept in lockstep with the proxy route's reject list.
106
+ */
107
+ const PROXY_DENY_HEADER_LIST = new Set([
108
+ "authorization",
109
+ "cookie",
110
+ "set-cookie",
111
+ "proxy-authorization",
112
+ "host",
113
+ "content-length",
114
+ "transfer-encoding",
115
+ "connection",
116
+ "upgrade",
117
+ "expect",
118
+ "x-forwarded-for",
119
+ "x-forwarded-host",
120
+ "x-forwarded-proto",
121
+ "x-real-ip"
122
+ ]);
123
+ const deniedSecretFields = new Set([
124
+ "providerApiKey",
125
+ "anthropicApiKey",
126
+ "apiKey",
127
+ "accessToken",
128
+ "refreshToken",
129
+ "password",
130
+ "mcpCredentials",
131
+ "credentials"
132
+ ]);
133
+ function parseEnvironment(input) {
134
+ if (input === undefined) {
135
+ return undefined;
136
+ }
137
+ const value = requireRecord(input, "submission.environment");
138
+ const allowed = new Set(["networking", "packages", "envVars"]);
139
+ for (const key of Object.keys(value)) {
140
+ if (!allowed.has(key)) {
141
+ throw new Error(`submission.environment.${key} is not an allowed field; permitted: networking, packages, envVars`);
142
+ }
143
+ }
144
+ const networking = parseNetworking(value.networking);
145
+ const packages = parsePackages(value.packages);
146
+ const envVars = parseEnvVars(value.envVars);
147
+ if (!networking && !packages && !envVars) {
148
+ return undefined;
149
+ }
150
+ return {
151
+ ...(networking ? { networking } : {}),
152
+ ...(packages ? { packages } : {}),
153
+ ...(envVars ? { envVars } : {})
154
+ };
155
+ }
156
+ /**
157
+ * Validate a customer-supplied `environment.envVars` map. Returns a
158
+ * frozen copy with keys in insertion order, or `undefined` when the
159
+ * input is absent / an empty object (treated as not supplied so the
160
+ * worker can omit the field from the parsed snapshot).
161
+ *
162
+ * Rules:
163
+ * - Must be a JSON object whose values are all strings.
164
+ * - Keys match `[A-Z_][A-Z0-9_]*` (POSIX-shell portable, uppercase
165
+ * only — keeps RUNTIME.env readable, matches platform convention).
166
+ * - Keys MUST NOT start with the reserved `AEX_` prefix; that
167
+ * prefix is owned by platform-set runtime keys and a collision
168
+ * would silently mask `__AEX_CLI__` etc. substitution
169
+ * targets.
170
+ * - Bounded: max ENV_VARS_MAX_ENTRIES entries, max
171
+ * ENV_VARS_MAX_VALUE_BYTES per value, max ENV_VARS_MAX_TOTAL_BYTES
172
+ * overall. The caps stop a runaway customer from making the
173
+ * mounted RUNTIME files unbounded.
174
+ * - Values are arbitrary UTF-8 strings, EXCEPT NUL bytes are
175
+ * rejected (NUL terminates C-strings and breaks env-var
176
+ * transport even inside the container).
177
+ */
178
+ function parseEnvVars(input) {
179
+ if (input === undefined) {
180
+ return undefined;
181
+ }
182
+ const value = requireRecord(input, "submission.environment.envVars");
183
+ const keys = Object.keys(value);
184
+ if (keys.length === 0) {
185
+ return undefined;
186
+ }
187
+ if (keys.length > ENV_VARS_MAX_ENTRIES) {
188
+ throw new Error(`submission.environment.envVars has ${keys.length} entries; maximum is ${ENV_VARS_MAX_ENTRIES}`);
189
+ }
190
+ const out = {};
191
+ let totalBytes = 0;
192
+ for (const key of keys) {
193
+ if (!ENV_VAR_KEY_PATTERN.test(key)) {
194
+ throw new Error(`submission.environment.envVars.${key} key must match /^[A-Z_][A-Z0-9_]*$/`);
195
+ }
196
+ if (key.startsWith(AEX_RESERVED_ENV_PREFIX)) {
197
+ throw new Error(`submission.environment.envVars.${key} uses reserved prefix "${AEX_RESERVED_ENV_PREFIX}" (set by aex runtime)`);
198
+ }
199
+ const raw = value[key];
200
+ if (typeof raw !== "string") {
201
+ throw new Error(`submission.environment.envVars.${key} must be a string`);
202
+ }
203
+ if (raw.includes("\0")) {
204
+ throw new Error(`submission.environment.envVars.${key} must not contain NUL bytes`);
205
+ }
206
+ const valueBytes = Buffer.byteLength(raw, "utf8");
207
+ if (valueBytes > ENV_VARS_MAX_VALUE_BYTES) {
208
+ throw new Error(`submission.environment.envVars.${key} value is ${valueBytes} bytes; maximum is ${ENV_VARS_MAX_VALUE_BYTES}`);
209
+ }
210
+ totalBytes += Buffer.byteLength(key, "utf8") + valueBytes;
211
+ if (totalBytes > ENV_VARS_MAX_TOTAL_BYTES) {
212
+ throw new Error(`submission.environment.envVars total byte size exceeds maximum ${ENV_VARS_MAX_TOTAL_BYTES}`);
213
+ }
214
+ out[key] = raw;
215
+ }
216
+ return Object.freeze(out);
217
+ }
218
+ function parseNetworking(input) {
219
+ if (input === undefined) {
220
+ return undefined;
221
+ }
222
+ const value = requireRecord(input, "submission.environment.networking");
223
+ const allowed = new Set(["mode", "allowedHosts"]);
224
+ for (const key of Object.keys(value)) {
225
+ if (!allowed.has(key)) {
226
+ throw new Error(`submission.environment.networking.${key} is not an allowed field; permitted: mode, allowedHosts`);
227
+ }
228
+ }
229
+ const mode = optionalEnum(value.mode, "submission.environment.networking.mode", ["limited", "open"]);
230
+ if (!mode) {
231
+ throw new Error("submission.environment.networking.mode is required when networking is provided");
232
+ }
233
+ const allowedHosts = parseAllowedHosts(value.allowedHosts);
234
+ return allowedHosts ? { mode, allowedHosts } : { mode };
235
+ }
236
+ function parseAllowedHosts(input) {
237
+ if (input === undefined) {
238
+ return undefined;
239
+ }
240
+ if (!Array.isArray(input)) {
241
+ throw new Error("submission.environment.networking.allowedHosts must be an array of strings");
242
+ }
243
+ const seen = new Set();
244
+ return input.map((entry, index) => {
245
+ if (typeof entry !== "string" || entry.length === 0) {
246
+ throw new Error(`submission.environment.networking.allowedHosts[${index}] must be a non-empty string`);
247
+ }
248
+ const lower = entry.toLowerCase();
249
+ if (seen.has(lower)) {
250
+ throw new Error(`submission.environment.networking.allowedHosts duplicate entry: ${entry}`);
251
+ }
252
+ seen.add(lower);
253
+ return lower;
254
+ });
255
+ }
256
+ function parsePackages(input) {
257
+ if (input === undefined) {
258
+ return undefined;
259
+ }
260
+ if (!Array.isArray(input)) {
261
+ throw new Error("submission.environment.packages must be an array");
262
+ }
263
+ return input.map((entry, index) => {
264
+ const value = requireRecord(entry, `submission.environment.packages[${index}]`);
265
+ const allowed = new Set(["name", "version"]);
266
+ for (const key of Object.keys(value)) {
267
+ if (!allowed.has(key)) {
268
+ throw new Error(`submission.environment.packages[${index}].${key} is not an allowed field; permitted: name, version`);
269
+ }
270
+ }
271
+ const rawName = requireString(value.name, `submission.environment.packages[${index}].name`);
272
+ const version = optionalString(value.version, `submission.environment.packages[${index}].version`);
273
+ // The ecosystem is encoded as a `name` prefix `"<eco>:<pkg>"`; an
274
+ // unprefixed name defaults to `apt`. A colon-delimited prefix that is
275
+ // NOT a known ecosystem is rejected (rather than silently folded into
276
+ // the package name) so a typo'd manager fails closed.
277
+ let ecosystem = "apt";
278
+ let name = rawName;
279
+ const colon = rawName.indexOf(":");
280
+ if (colon > 0) {
281
+ const prefix = rawName.slice(0, colon);
282
+ if (!PLATFORM_PACKAGE_ECOSYSTEMS.includes(prefix)) {
283
+ throw new Error(`submission.environment.packages[${index}].name has unknown ecosystem prefix "${prefix}:"; permitted: ${PLATFORM_PACKAGE_ECOSYSTEMS.join(", ")}`);
284
+ }
285
+ ecosystem = prefix;
286
+ name = rawName.slice(colon + 1);
287
+ }
288
+ if (name.length === 0) {
289
+ throw new Error(`submission.environment.packages[${index}].name resolves to an empty package after stripping the "${ecosystem}:" ecosystem prefix`);
290
+ }
291
+ return version ? { name, version, ecosystem } : { name, ecosystem };
292
+ });
293
+ }
294
+ function parseProxyEndpoints(input) {
295
+ if (input === undefined) {
296
+ return undefined;
297
+ }
298
+ if (!Array.isArray(input)) {
299
+ throw new Error("proxyEndpoints must be an array");
300
+ }
301
+ if (input.length === 0) {
302
+ return undefined;
303
+ }
304
+ const seen = new Set();
305
+ return input.map((entry, index) => {
306
+ const endpoint = parseProxyEndpoint(entry, `proxyEndpoints[${index}]`);
307
+ if (seen.has(endpoint.name)) {
308
+ throw new Error(`proxyEndpoints duplicate name: ${endpoint.name}`);
309
+ }
310
+ seen.add(endpoint.name);
311
+ return endpoint;
312
+ });
313
+ }
314
+ function parseProxyEndpoint(input, path) {
315
+ const value = requireRecord(input, path);
316
+ const allowed = new Set([
317
+ "name",
318
+ "baseUrl",
319
+ "authShape",
320
+ "allowMethods",
321
+ "allowPathPrefixes",
322
+ "allowHeaders",
323
+ "responseMode",
324
+ "maxRequestBytes",
325
+ "maxResponseBytes",
326
+ "timeoutMs",
327
+ "perCallBudget",
328
+ "responseByteBudget"
329
+ ]);
330
+ for (const key of Object.keys(value)) {
331
+ if (!allowed.has(key)) {
332
+ throw new Error(`${path}.${key} is not an allowed field`);
333
+ }
334
+ }
335
+ const name = requireString(value.name, `${path}.name`);
336
+ if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
337
+ throw new Error(`${path}.name must match ${PROXY_ENDPOINT_NAME_PATTERN} (lowercase letters, digits, '_' and '-'; <=63 chars)`);
338
+ }
339
+ if (RESERVED_PROXY_ENDPOINT_NAMES.has(name)) {
340
+ throw new Error(`${path}.name is reserved: ${name}`);
341
+ }
342
+ const baseUrl = parseProxyBaseUrl(value.baseUrl, `${path}.baseUrl`);
343
+ const authShape = parseProxyAuthShape(value.authShape, `${path}.authShape`);
344
+ const allowMethods = parseProxyMethods(value.allowMethods, `${path}.allowMethods`);
345
+ const allowPathPrefixes = parseProxyPathPrefixes(value.allowPathPrefixes, `${path}.allowPathPrefixes`);
346
+ const allowHeaders = parseProxyAllowedHeaders(value.allowHeaders, `${path}.allowHeaders`, authShape);
347
+ const responseMode = optionalEnum(value.responseMode, `${path}.responseMode`, PROXY_RESPONSE_MODES);
348
+ const maxRequestBytes = optionalPositiveInt(value.maxRequestBytes, `${path}.maxRequestBytes`);
349
+ const maxResponseBytes = optionalPositiveInt(value.maxResponseBytes, `${path}.maxResponseBytes`);
350
+ const timeoutMs = optionalPositiveInt(value.timeoutMs, `${path}.timeoutMs`);
351
+ const perCallBudget = optionalPositiveInt(value.perCallBudget, `${path}.perCallBudget`);
352
+ const responseByteBudget = optionalPositiveInt(value.responseByteBudget, `${path}.responseByteBudget`);
353
+ return {
354
+ name,
355
+ baseUrl,
356
+ authShape,
357
+ allowMethods,
358
+ allowPathPrefixes,
359
+ ...(allowHeaders ? { allowHeaders } : {}),
360
+ ...(responseMode ? { responseMode } : {}),
361
+ ...(maxRequestBytes !== undefined ? { maxRequestBytes } : {}),
362
+ ...(maxResponseBytes !== undefined ? { maxResponseBytes } : {}),
363
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
364
+ ...(perCallBudget !== undefined ? { perCallBudget } : {}),
365
+ ...(responseByteBudget !== undefined ? { responseByteBudget } : {})
366
+ };
367
+ }
368
+ function parseProxyBaseUrl(input, field) {
369
+ const raw = requireString(input, field);
370
+ let parsed;
371
+ try {
372
+ parsed = new URL(raw);
373
+ }
374
+ catch {
375
+ throw new Error(`${field} must be a valid absolute URL`);
376
+ }
377
+ if (parsed.protocol !== "https:") {
378
+ throw new Error(`${field} must use https://`);
379
+ }
380
+ if (parsed.username || parsed.password) {
381
+ throw new Error(`${field} must not embed credentials`);
382
+ }
383
+ if (parsed.search || parsed.hash) {
384
+ throw new Error(`${field} must not include a query string or fragment`);
385
+ }
386
+ // Normalize: strip trailing slash so prefix matching is predictable.
387
+ const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
388
+ return normalized;
389
+ }
390
+ function parseProxyAuthShape(input, field) {
391
+ const value = requireRecord(input, field);
392
+ const type = requireString(value.type, `${field}.type`);
393
+ switch (type) {
394
+ case "none":
395
+ assertOnlyKeys(value, field, ["type"]);
396
+ return { type: "none" };
397
+ case "bearer":
398
+ assertOnlyKeys(value, field, ["type"]);
399
+ return { type: "bearer" };
400
+ case "basic":
401
+ assertOnlyKeys(value, field, ["type"]);
402
+ return { type: "basic" };
403
+ case "header": {
404
+ assertOnlyKeys(value, field, ["type", "name"]);
405
+ const name = requireString(value.name, `${field}.name`);
406
+ assertHeaderName(name, `${field}.name`);
407
+ return { type: "header", name };
408
+ }
409
+ case "query": {
410
+ assertOnlyKeys(value, field, ["type", "name"]);
411
+ const name = requireString(value.name, `${field}.name`);
412
+ if (!/^[a-zA-Z0-9_\-.]{1,64}$/.test(name)) {
413
+ throw new Error(`${field}.name must be a URL-safe identifier (<=64 chars)`);
414
+ }
415
+ return { type: "query", name };
416
+ }
417
+ default:
418
+ throw new Error(`${field}.type must be one of: none, bearer, basic, header, query`);
419
+ }
420
+ }
421
+ function parseProxyMethods(input, field) {
422
+ if (!Array.isArray(input) || input.length === 0) {
423
+ throw new Error(`${field} must be a non-empty array of HTTP methods`);
424
+ }
425
+ const seen = new Set();
426
+ for (const entry of input) {
427
+ if (typeof entry !== "string") {
428
+ throw new Error(`${field} entries must be strings`);
429
+ }
430
+ const upper = entry.toUpperCase();
431
+ if (!PROXY_ALLOWED_METHODS.includes(upper)) {
432
+ throw new Error(`${field} contains unsupported method: ${entry}`);
433
+ }
434
+ seen.add(upper);
435
+ }
436
+ return Array.from(seen);
437
+ }
438
+ function parseProxyPathPrefixes(input, field) {
439
+ if (!Array.isArray(input) || input.length === 0) {
440
+ throw new Error(`${field} must be a non-empty array of path prefixes`);
441
+ }
442
+ const seen = new Set();
443
+ for (const entry of input) {
444
+ if (typeof entry !== "string" || !entry.startsWith("/")) {
445
+ throw new Error(`${field} entries must be non-empty strings starting with '/'`);
446
+ }
447
+ // Reject traversal / encoded traversal at config time so we never
448
+ // need to second-guess at request time.
449
+ if (entry.includes("..") || entry.toLowerCase().includes("%2e%2e")) {
450
+ throw new Error(`${field} entry must not contain path traversal: ${entry}`);
451
+ }
452
+ seen.add(entry);
453
+ }
454
+ return Array.from(seen);
455
+ }
456
+ function parseProxyAllowedHeaders(input, field, authShape) {
457
+ if (input === undefined) {
458
+ return undefined;
459
+ }
460
+ if (!Array.isArray(input)) {
461
+ throw new Error(`${field} must be an array of header names`);
462
+ }
463
+ const seen = new Set();
464
+ const result = [];
465
+ for (const entry of input) {
466
+ if (typeof entry !== "string" || entry.length === 0) {
467
+ throw new Error(`${field} entries must be non-empty strings`);
468
+ }
469
+ const lower = entry.toLowerCase();
470
+ assertHeaderName(entry, field);
471
+ if (PROXY_DENY_HEADER_LIST.has(lower)) {
472
+ throw new Error(`${field} contains a forbidden header: ${entry}`);
473
+ }
474
+ const authHeader = authShapeHeaderName(authShape);
475
+ if (authHeader && lower === authHeader) {
476
+ throw new Error(`${field} must not contain the auth header for this endpoint (${authHeader}); the proxy injects it from secrets.proxyEndpointAuth`);
477
+ }
478
+ if (seen.has(lower)) {
479
+ continue;
480
+ }
481
+ seen.add(lower);
482
+ result.push(lower);
483
+ }
484
+ return result;
485
+ }
486
+ function assertHeaderName(value, field) {
487
+ // RFC 7230 token chars, conservative.
488
+ if (!/^[A-Za-z0-9!#$%&'*+\-.^_`|~]{1,64}$/.test(value)) {
489
+ throw new Error(`${field} must be a valid header token (<=64 chars): ${value}`);
490
+ }
491
+ }
492
+ function assertOnlyKeys(value, field, allowed) {
493
+ const permitted = new Set(allowed);
494
+ for (const key of Object.keys(value)) {
495
+ if (!permitted.has(key)) {
496
+ throw new Error(`${field}.${key} is not an allowed field; permitted: ${allowed.join(", ")}`);
497
+ }
498
+ }
499
+ }
500
+ function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
501
+ const endpointsList = endpoints ?? [];
502
+ const authList = auth ?? [];
503
+ const endpointsByName = new Map(endpointsList.map((e) => [e.name, e]));
504
+ const authByName = new Map(authList.map((a) => [a.name, a]));
505
+ for (const endpoint of endpointsList) {
506
+ const authEntry = authByName.get(endpoint.name);
507
+ if (endpoint.authShape.type === "none") {
508
+ // Keyless endpoints carry no auth value. Reject any matching
509
+ // auth entry so callers don't accidentally ship a secret bound
510
+ // to a "none" endpoint (which would be silently ignored at
511
+ // request time — confusing and a leak risk).
512
+ if (authEntry) {
513
+ throw new Error(`proxyEndpoints[${endpoint.name}] has authShape "none" but a matching secrets.proxyEndpointAuth entry was supplied; remove the auth entry`);
514
+ }
515
+ continue;
516
+ }
517
+ if (!authEntry) {
518
+ throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
519
+ }
520
+ if (authEntry.value.type !== endpoint.authShape.type) {
521
+ throw new Error(`secrets.proxyEndpointAuth[${endpoint.name}].value.type must equal proxyEndpoints[${endpoint.name}].authShape.type (expected ${endpoint.authShape.type}, got ${authEntry.value.type})`);
522
+ }
523
+ }
524
+ for (const authEntry of authList) {
525
+ if (!endpointsByName.has(authEntry.name)) {
526
+ throw new Error(`secrets.proxyEndpointAuth[${authEntry.name}] has no matching proxyEndpoints entry`);
527
+ }
528
+ }
529
+ }
530
+ const PROVIDER_SECRET_KEYS = ["anthropic", "deepseek", "openai", "gemini", "mistral"];
531
+ function parseInlineSecrets(input) {
532
+ const value = requireRecord(input, "secrets");
533
+ const allowedTopLevel = new Set([
534
+ ...PROVIDER_SECRET_KEYS,
535
+ "mcpServers",
536
+ "proxyEndpointAuth"
537
+ ]);
538
+ for (const key of Object.keys(value)) {
539
+ if (key.startsWith("__aex_")) {
540
+ // Platform-internal namespace (e.g. __aex_proxy_token). The BFF
541
+ // mutates the vaulted bundle to inject these; inbound submissions
542
+ // are never allowed to set them, to prevent a malicious caller
543
+ // from forging the bearer.
544
+ throw new Error(`secrets.${key} uses the platform-internal __aex_ namespace and may not be set by callers`);
545
+ }
546
+ if (!allowedTopLevel.has(key)) {
547
+ throw new Error(`secrets.${key} is not an allowed field; permitted: ${[...allowedTopLevel].join(", ")}`);
548
+ }
549
+ }
550
+ const anthropic = value.anthropic !== undefined ? parseProviderSecret(value.anthropic, "anthropic") : undefined;
551
+ const deepseek = value.deepseek !== undefined ? parseProviderSecret(value.deepseek, "deepseek") : undefined;
552
+ const openai = value.openai !== undefined ? parseProviderSecret(value.openai, "openai") : undefined;
553
+ const gemini = value.gemini !== undefined ? parseProviderSecret(value.gemini, "gemini") : undefined;
554
+ const mistral = value.mistral !== undefined ? parseProviderSecret(value.mistral, "mistral") : undefined;
555
+ const mcpServers = parseMcpServerSecrets(value.mcpServers);
556
+ const proxyEndpointAuth = parseProxyEndpointAuth(value.proxyEndpointAuth);
557
+ return {
558
+ ...(anthropic ? { anthropic } : {}),
559
+ ...(deepseek ? { deepseek } : {}),
560
+ ...(openai ? { openai } : {}),
561
+ ...(gemini ? { gemini } : {}),
562
+ ...(mistral ? { mistral } : {}),
563
+ ...(mcpServers ? { mcpServers } : {}),
564
+ ...(proxyEndpointAuth ? { proxyEndpointAuth } : {})
565
+ };
566
+ }
567
+ function parseProviderSecret(input, provider) {
568
+ const field = `secrets.${provider}`;
569
+ const value = requireRecord(input, field);
570
+ const allowed = new Set(["apiKey", "baseUrl"]);
571
+ for (const key of Object.keys(value)) {
572
+ if (!allowed.has(key)) {
573
+ throw new Error(`${field}.${key} is not an allowed field; permitted: apiKey, baseUrl`);
574
+ }
575
+ }
576
+ const apiKey = requireString(value.apiKey, `${field}.apiKey`);
577
+ const rawBaseUrl = optionalString(value.baseUrl, `${field}.baseUrl`);
578
+ if (rawBaseUrl === undefined) {
579
+ return { apiKey };
580
+ }
581
+ // Reuse the proxy-endpoint URL guard so provider baseUrl gets the
582
+ // same protection: https-only, no credentials, no query/fragment.
583
+ // The provider-proxy in the dashboard forwards a customer-controlled
584
+ // baseUrl to the upstream — accepting http:// (or a userinfo-laden
585
+ // URL) here is an SSRF / credential-leak vector.
586
+ const baseUrl = parseProxyBaseUrl(rawBaseUrl, `${field}.baseUrl`);
587
+ return { apiKey, baseUrl };
588
+ }
589
+ function parseMcpServerSecrets(input) {
590
+ if (input === undefined) {
591
+ return undefined;
592
+ }
593
+ if (!Array.isArray(input)) {
594
+ throw new Error("secrets.mcpServers must be an array");
595
+ }
596
+ const seen = new Set();
597
+ return input.map((entry, index) => {
598
+ const parsed = parseMcpServerSecret(entry, `secrets.mcpServers[${index}]`);
599
+ if (seen.has(parsed.name)) {
600
+ throw new Error(`secrets.mcpServers duplicate name: ${parsed.name}`);
601
+ }
602
+ seen.add(parsed.name);
603
+ return parsed;
604
+ });
605
+ }
606
+ function parseMcpServerSecret(input, path) {
607
+ const value = requireRecord(input, path);
608
+ const allowed = new Set(["name", "url", "headers"]);
609
+ for (const key of Object.keys(value)) {
610
+ if (!allowed.has(key)) {
611
+ throw new Error(`${path}.${key} is not an allowed field; permitted: name, url, headers`);
612
+ }
613
+ }
614
+ const name = requireString(value.name, `${path}.name`);
615
+ const url = requireString(value.url, `${path}.url`);
616
+ const headers = optionalStringRecord(value.headers, `${path}.headers`);
617
+ return headers ? { name, url, headers } : { name, url };
618
+ }
619
+ function parseProxyEndpointAuth(input) {
620
+ if (input === undefined) {
621
+ return undefined;
622
+ }
623
+ if (!Array.isArray(input)) {
624
+ throw new Error("secrets.proxyEndpointAuth must be an array");
625
+ }
626
+ if (input.length === 0) {
627
+ return undefined;
628
+ }
629
+ const seen = new Set();
630
+ return input.map((entry, index) => {
631
+ const auth = parseProxyEndpointAuthEntry(entry, `secrets.proxyEndpointAuth[${index}]`);
632
+ if (seen.has(auth.name)) {
633
+ throw new Error(`secrets.proxyEndpointAuth duplicate name: ${auth.name}`);
634
+ }
635
+ seen.add(auth.name);
636
+ return auth;
637
+ });
638
+ }
639
+ function parseProxyEndpointAuthEntry(input, path) {
640
+ const value = requireRecord(input, path);
641
+ const allowed = new Set(["name", "value"]);
642
+ for (const key of Object.keys(value)) {
643
+ if (!allowed.has(key)) {
644
+ throw new Error(`${path}.${key} is not an allowed field; permitted: name, value`);
645
+ }
646
+ }
647
+ const name = requireString(value.name, `${path}.name`);
648
+ if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
649
+ throw new Error(`${path}.name must match the same pattern as proxyEndpoints[].name (lowercase letters, digits, '_' and '-'; <=63 chars)`);
650
+ }
651
+ const valueField = parseProxyAuthValue(value.value, `${path}.value`);
652
+ return { name, value: valueField };
653
+ }
654
+ function parseProxyAuthValue(input, path) {
655
+ const value = requireRecord(input, path);
656
+ const type = requireString(value.type, `${path}.type`);
657
+ switch (type) {
658
+ case "bearer": {
659
+ assertOnlyKeys(value, path, ["type", "token"]);
660
+ const token = requireSecretValue(value.token, `${path}.token`);
661
+ return { type: "bearer", token };
662
+ }
663
+ case "basic": {
664
+ assertOnlyKeys(value, path, ["type", "username", "password"]);
665
+ // Usernames are not redactable in the strict sense (often public
666
+ // identifiers like an email), so we only enforce non-emptiness.
667
+ // The password is the secret-bearing half.
668
+ const username = requireString(value.username, `${path}.username`);
669
+ const password = requireSecretValue(value.password, `${path}.password`);
670
+ return { type: "basic", username, password };
671
+ }
672
+ case "header": {
673
+ assertOnlyKeys(value, path, ["type", "value"]);
674
+ const headerValue = requireSecretValue(value.value, `${path}.value`);
675
+ return { type: "header", value: headerValue };
676
+ }
677
+ case "query": {
678
+ assertOnlyKeys(value, path, ["type", "value"]);
679
+ const queryValue = requireSecretValue(value.value, `${path}.value`);
680
+ return { type: "query", value: queryValue };
681
+ }
682
+ default:
683
+ throw new Error(`${path}.type must be one of: bearer, basic, header, query`);
684
+ }
685
+ }
686
+ /**
687
+ * The proxy body-redactor refuses to mask any derived target string shorter
688
+ * than this many bytes — masking a 1-byte literal would corrupt the response
689
+ * body. This is the floor for the *derived* redaction targets (e.g.
690
+ * `Bearer <token>`, base64 fragments), used by
691
+ * the hosted proxy redactor, which imports this constant so the two sides can
692
+ * never silently diverge.
693
+ */
694
+ export const MIN_REDACTION_TARGET_BYTES = 4;
695
+ /**
696
+ * Minimum byte length for an accepted proxy secret *value*. Strictly greater
697
+ * than {@link MIN_REDACTION_TARGET_BYTES}: a secret short enough to fall under
698
+ * the redactor's floor could slip through unmasked, so the submission parser
699
+ * rejects it up front. The `satisfies` check below pins that invariant at
700
+ * compile time.
701
+ */
702
+ const MIN_PROXY_SECRET_BYTES = 8;
703
+ // Invariant: an accepted secret must always be long enough for the redactor to
704
+ // mask it. If someone lowers MIN_PROXY_SECRET_BYTES below the redaction floor,
705
+ // this errors at compile time.
706
+ const _MIN_PROXY_SECRET_BYTES_OK = (MIN_PROXY_SECRET_BYTES >= MIN_REDACTION_TARGET_BYTES);
707
+ void _MIN_PROXY_SECRET_BYTES_OK;
708
+ function requireSecretValue(input, field) {
709
+ const value = requireString(input, field);
710
+ const byteLen = Buffer.byteLength(value, "utf8");
711
+ if (byteLen < MIN_PROXY_SECRET_BYTES) {
712
+ throw new Error(`${field} must be at least ${MIN_PROXY_SECRET_BYTES} bytes; shorter values cannot be reliably redacted from upstream responses`);
713
+ }
714
+ return value;
715
+ }
716
+ function assertNoSecretBearingFields(input, path) {
717
+ if (Array.isArray(input)) {
718
+ input.forEach((item, index) => assertNoSecretBearingFields(item, [...path, String(index)]));
719
+ return;
720
+ }
721
+ if (!isRecord(input)) {
722
+ return;
723
+ }
724
+ for (const [key, value] of Object.entries(input)) {
725
+ if (deniedSecretFields.has(key)) {
726
+ throw new Error(`Secret-bearing field is not allowed in platform submission: ${[...path, key].join(".")}`);
727
+ }
728
+ assertNoSecretBearingFields(value, [...path, key]);
729
+ }
730
+ }
731
+ function requireRecord(input, field) {
732
+ if (!isRecord(input)) {
733
+ throw new Error(`${field} must be an object`);
734
+ }
735
+ return input;
736
+ }
737
+ function isRecord(input) {
738
+ return typeof input === "object" && input !== null && !Array.isArray(input);
739
+ }
740
+ function requireString(input, field) {
741
+ if (typeof input !== "string" || input.length === 0) {
742
+ throw new Error(`${field} must be a non-empty string`);
743
+ }
744
+ return input;
745
+ }
746
+ function optionalString(input, field) {
747
+ if (input === undefined) {
748
+ return undefined;
749
+ }
750
+ return requireString(input, field);
751
+ }
752
+ function optionalEnum(input, field, allowed) {
753
+ if (input === undefined) {
754
+ return undefined;
755
+ }
756
+ if (typeof input !== "string" || !allowed.includes(input)) {
757
+ throw new Error(`${field} must be one of: ${allowed.join(", ")}`);
758
+ }
759
+ return input;
760
+ }
761
+ function requireStringArray(input, field) {
762
+ if (!Array.isArray(input) || input.length === 0 || input.some((item) => typeof item !== "string" || item.length === 0)) {
763
+ throw new Error(`${field} must be a non-empty string array`);
764
+ }
765
+ return input;
766
+ }
767
+ function optionalStringRecord(input, field) {
768
+ if (input === undefined) {
769
+ return undefined;
770
+ }
771
+ const value = requireRecord(input, field);
772
+ for (const [key, entry] of Object.entries(value)) {
773
+ if (typeof entry !== "string" || entry.length === 0) {
774
+ throw new Error(`${field}.${key} must be a non-empty string`);
775
+ }
776
+ }
777
+ return value;
778
+ }
779
+ function optionalJsonRecord(input, field) {
780
+ if (input === undefined) {
781
+ return undefined;
782
+ }
783
+ const value = requireRecord(input, field);
784
+ for (const [key, entry] of Object.entries(value)) {
785
+ if (!isJsonValue(entry)) {
786
+ throw new Error(`${field}.${key} must be JSON-serializable`);
787
+ }
788
+ }
789
+ return value;
790
+ }
791
+ function optionalPositiveInt(input, field) {
792
+ if (input === undefined) {
793
+ return undefined;
794
+ }
795
+ if (typeof input !== "number" || !Number.isSafeInteger(input) || input <= 0) {
796
+ throw new Error(`${field} must be a positive safe integer`);
797
+ }
798
+ return input;
799
+ }
800
+ function isJsonValue(input) {
801
+ if (typeof input === "number") {
802
+ return Number.isFinite(input);
803
+ }
804
+ if (input === null || typeof input === "string" || typeof input === "boolean") {
805
+ return true;
806
+ }
807
+ if (Array.isArray(input)) {
808
+ return input.every(isJsonValue);
809
+ }
810
+ if (isRecord(input)) {
811
+ return Object.values(input).every(isJsonValue);
812
+ }
813
+ return false;
814
+ }
815
+ export function parseRunSubmissionRequest(input, options = {}) {
816
+ const value = requireRecord(input, "submission");
817
+ const allowedTopLevelFields = new Set([
818
+ "workspaceId",
819
+ "idempotencyKey",
820
+ "credentialMode",
821
+ "provider",
822
+ "runtime",
823
+ "submission",
824
+ "runtimeSize",
825
+ "timeout",
826
+ "proxyEndpoints",
827
+ SECRETS_KEY
828
+ ]);
829
+ for (const key of Object.keys(value)) {
830
+ if (!allowedTopLevelFields.has(key)) {
831
+ throw new Error(`submission.${key} is not an allowed field; permitted: ${[...allowedTopLevelFields].join(", ")}`);
832
+ }
833
+ }
834
+ // Defence in depth: scan every non-secrets field for credential-named
835
+ // keys. The `secrets` key is
836
+ // the only allow-listed home for credential material.
837
+ for (const [key, fieldValue] of Object.entries(value)) {
838
+ if (key === SECRETS_KEY) {
839
+ continue;
840
+ }
841
+ if (deniedSecretFields.has(key)) {
842
+ throw new Error(`Secret-bearing field is not allowed in platform submission: ${key}`);
843
+ }
844
+ assertNoSecretBearingFields(fieldValue, [key]);
845
+ }
846
+ const provider = parseRunProvider(value.provider);
847
+ const runtime = parseRuntimeKind(value.runtime);
848
+ const credentialMode = parseCredentialMode(value.credentialMode);
849
+ if (credentialMode === "managed") {
850
+ assertManagedKeyAdmissionAllowed(options.managedKeyPolicy);
851
+ }
852
+ // Cross-field validation via the centralized runtime-support validator.
853
+ const runtimeSupport = checkRuntimeSupported(provider, runtime);
854
+ if (!runtimeSupport.ok) {
855
+ throw new Error(runtimeSupport.message ?? "unsupported runtime");
856
+ }
857
+ const runtimeSize = parseRuntimeSize(value.runtimeSize);
858
+ const timeoutMs = parseRunTimeout(value.timeout);
859
+ const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
860
+ const secrets = parseInlineSecrets(value.secrets);
861
+ enforceCredentialSecretPolicy(provider, credentialMode, secrets);
862
+ crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
863
+ const submission = parseSubmission(value.submission);
864
+ // mcpServers names must agree across the submission half and the
865
+ // secrets half — every secrets.mcpServers[i].name MUST resolve to a
866
+ // submission.mcpServers entry (no orphan secrets) AND the URL must
867
+ // match exactly. The reverse is allowed (an MCP server with no auth
868
+ // headers is a valid public-MCP mode).
869
+ if (secrets.mcpServers !== undefined) {
870
+ const declared = new Map(submission.mcpServers.map((m) => [m.name, m.url]));
871
+ for (const secret of secrets.mcpServers) {
872
+ const declaredUrl = declared.get(secret.name);
873
+ if (declaredUrl === undefined) {
874
+ throw new Error(`secrets.mcpServers[name=${secret.name}] has no matching submission.mcpServers entry`);
875
+ }
876
+ if (declaredUrl !== secret.url) {
877
+ throw new Error(`secrets.mcpServers[name=${secret.name}].url must equal submission.mcpServers[name=${secret.name}].url ` +
878
+ `(got submission=${declaredUrl}, secrets=${secret.url})`);
879
+ }
880
+ }
881
+ }
882
+ const candidate = {
883
+ workspaceId: "",
884
+ idempotencyKey: "",
885
+ credentialMode,
886
+ provider,
887
+ ...(runtime ? { runtime } : {}),
888
+ submission,
889
+ secrets
890
+ };
891
+ const unsupportedManagedFeatures = collectManagedUnsupportedFeatures(candidate);
892
+ if (unsupportedManagedFeatures.length > 0) {
893
+ throw new RuntimeValidationError("feature_runtime_mismatch", `The managed runtime does not support these submission features: ` +
894
+ `${unsupportedManagedFeatures.join(", ")}. Remove them or use inline aex skills.`);
895
+ }
896
+ return {
897
+ workspaceId: requireString(value.workspaceId, "workspaceId"),
898
+ idempotencyKey: requireString(value.idempotencyKey, "idempotencyKey"),
899
+ credentialMode,
900
+ provider,
901
+ ...(runtime ? { runtime } : {}),
902
+ submission,
903
+ ...(runtimeSize ? { runtimeSize } : {}),
904
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
905
+ ...(proxyEndpoints ? { proxyEndpoints } : {}),
906
+ secrets
907
+ };
908
+ }
909
+ function parseRuntimeKind(input) {
910
+ if (input === undefined) {
911
+ return undefined;
912
+ }
913
+ if (typeof input !== "string" || !RUNTIME_KINDS.includes(input)) {
914
+ throw new Error(`runtime must be one of: ${RUNTIME_KINDS.join(", ")} (got ${JSON.stringify(input)})`);
915
+ }
916
+ return input;
917
+ }
918
+ function parseRunProvider(input) {
919
+ if (input === undefined) {
920
+ return DEFAULT_RUN_PROVIDER;
921
+ }
922
+ if (typeof input !== "string" || !RUN_PROVIDERS.includes(input)) {
923
+ throw new Error(`provider must be one of: ${RUN_PROVIDERS.join(", ")} (got ${JSON.stringify(input)})`);
924
+ }
925
+ return input;
926
+ }
927
+ /**
928
+ * Cross-check the chosen provider against the supplied secrets bundle.
929
+ *
930
+ * - The matching provider's apiKey MUST be present.
931
+ * - Every OTHER provider's secret block MUST be absent (cross-provider
932
+ * secrets are explicitly rejected, not silently dropped — they are
933
+ * almost always a copy-paste mistake or a confused caller, and we
934
+ * want to fail loud).
935
+ * - MCP / proxy endpoint auth carry across providers and are not
936
+ * checked here.
937
+ */
938
+ function enforceCredentialSecretPolicy(provider, credentialMode, secrets) {
939
+ if (credentialMode === "managed") {
940
+ for (const providerKey of PROVIDER_SECRET_KEYS) {
941
+ if (secrets[providerKey] !== undefined) {
942
+ throw new Error(`secrets.${providerKey} is not allowed when credentialMode is managed; provider access is resolved by the managed-key policy`);
943
+ }
944
+ }
945
+ return;
946
+ }
947
+ const required = secrets[provider];
948
+ if (!required?.apiKey) {
949
+ throw new Error(`secrets.${provider}.apiKey is required when provider is ${provider}`);
950
+ }
951
+ for (const other of PROVIDER_SECRET_KEYS) {
952
+ if (other === provider) {
953
+ continue;
954
+ }
955
+ if (secrets[other] !== undefined) {
956
+ throw new Error(`secrets.${other} is not allowed when provider is ${provider}; remove it or set provider to ${other}`);
957
+ }
958
+ }
959
+ }
960
+ function parseSubmission(input) {
961
+ const value = requireRecord(input, "submission.submission");
962
+ const allowed = new Set([
963
+ "model",
964
+ "system",
965
+ "prompt",
966
+ "skills",
967
+ "agentsMd",
968
+ "files",
969
+ "mcpServers",
970
+ "environment",
971
+ "securityProfile",
972
+ "metadata",
973
+ "outputs",
974
+ "builtins",
975
+ "platform"
976
+ ]);
977
+ for (const key of Object.keys(value)) {
978
+ if (!allowed.has(key)) {
979
+ throw new Error(`submission.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
980
+ }
981
+ }
982
+ const model = requireString(value.model, "submission.model");
983
+ const system = optionalString(value.system, "submission.system");
984
+ const prompt = parsePrompt(value.prompt);
985
+ const skills = parseSkills(value.skills);
986
+ const agentsMd = parseAgentsMd(value.agentsMd);
987
+ const files = parseFiles(value.files);
988
+ const mcpServers = parseMcpServers(value.mcpServers);
989
+ const environment = parseEnvironment(value.environment);
990
+ const securityProfile = parseRuntimeSecurityProfile(value.securityProfile);
991
+ const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
992
+ const outputs = parseOutputs(value.outputs);
993
+ const builtins = parseBuiltins(value.builtins);
994
+ const platform = parsePlatformConfig(value.platform);
995
+ return {
996
+ model,
997
+ ...(system ? { system } : {}),
998
+ prompt,
999
+ skills,
1000
+ agentsMd,
1001
+ files,
1002
+ mcpServers,
1003
+ ...(environment ? { environment } : {}),
1004
+ ...(securityProfile ? { securityProfile } : {}),
1005
+ ...(metadata ? { metadata } : {}),
1006
+ ...(outputs ? { outputs } : {}),
1007
+ ...(builtins !== undefined ? { builtins } : {}),
1008
+ ...(platform ? { platform } : {})
1009
+ };
1010
+ }
1011
+ function parsePlatformConfig(input) {
1012
+ if (input === undefined || input === null)
1013
+ return undefined;
1014
+ const value = requireRecord(input, "submission.platform");
1015
+ for (const key of Object.keys(value)) {
1016
+ if (key !== "systemPrompt") {
1017
+ throw new Error(`submission.platform.${key} is not an allowed field; permitted: systemPrompt`);
1018
+ }
1019
+ }
1020
+ if (value.systemPrompt === undefined)
1021
+ return undefined;
1022
+ if (value.systemPrompt !== "default" && value.systemPrompt !== "off") {
1023
+ throw new Error(`submission.platform.systemPrompt must be "default" or "off"`);
1024
+ }
1025
+ return { systemPrompt: value.systemPrompt };
1026
+ }
1027
+ const BUILTIN_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
1028
+ const MAX_BUILTINS = 16;
1029
+ function parseBuiltins(input) {
1030
+ if (input === undefined || input === null)
1031
+ return undefined;
1032
+ if (!Array.isArray(input)) {
1033
+ throw new Error("submission.builtins must be an array of strings");
1034
+ }
1035
+ if (input.length > MAX_BUILTINS) {
1036
+ throw new Error(`submission.builtins exceeds the max of ${MAX_BUILTINS} entries`);
1037
+ }
1038
+ const seen = new Set();
1039
+ const out = [];
1040
+ for (let i = 0; i < input.length; i++) {
1041
+ const v = input[i];
1042
+ if (typeof v !== "string") {
1043
+ throw new Error(`submission.builtins[${i}] must be a string`);
1044
+ }
1045
+ if (!BUILTIN_NAME_PATTERN.test(v)) {
1046
+ throw new Error(`submission.builtins[${i}] (${JSON.stringify(v)}) is not a valid Goose builtin name; expected /^[a-z][a-z0-9_-]{0,63}$/`);
1047
+ }
1048
+ if (seen.has(v))
1049
+ continue; // dedupe silently
1050
+ seen.add(v);
1051
+ out.push(v);
1052
+ }
1053
+ return out;
1054
+ }
1055
+ /**
1056
+ * Maximum number of output capture entries accepted per list.
1057
+ *
1058
+ * 32 is enough room for the typical "one or two capture roots" pattern
1059
+ * plus a generous margin for legitimate multi-root use cases (per-tool
1060
+ * output directory + scratch state + logs, repeated across a few
1061
+ * subdirectories), without inviting abuse of the synthetic-turn path
1062
+ * the worker drives at session terminal.
1063
+ */
1064
+ const MAX_OUTPUT_DIRS = 32;
1065
+ /**
1066
+ * Maximum byte length of a single output capture entry (after UTF-8
1067
+ * encoding). 512 bytes comfortably covers `/very/long/nested/path`
1068
+ * style entries without letting a misuse smuggle large blobs through
1069
+ * the field.
1070
+ */
1071
+ const MAX_OUTPUT_DIR_BYTES = 512;
1072
+ function parseOutputs(input) {
1073
+ if (input === undefined || input === null) {
1074
+ return undefined;
1075
+ }
1076
+ const value = requireRecord(input, "submission.outputs");
1077
+ const allowed = new Set(["allowedDirs", "deniedDirs"]);
1078
+ for (const key of Object.keys(value)) {
1079
+ if (!allowed.has(key)) {
1080
+ throw new Error(`submission.outputs.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
1081
+ }
1082
+ }
1083
+ const allowedDirs = parseOutputAllowedDirs(value.allowedDirs);
1084
+ const deniedDirs = parseOutputDeniedDirs(value.deniedDirs);
1085
+ if (!allowedDirs && !deniedDirs) {
1086
+ return undefined;
1087
+ }
1088
+ return {
1089
+ ...(allowedDirs ? { allowedDirs } : {}),
1090
+ ...(deniedDirs ? { deniedDirs } : {})
1091
+ };
1092
+ }
1093
+ function parseOutputAllowedDirs(input) {
1094
+ if (input === undefined) {
1095
+ return undefined;
1096
+ }
1097
+ if (!Array.isArray(input)) {
1098
+ throw new Error("submission.outputs.allowedDirs must be an array of absolute UNIX paths");
1099
+ }
1100
+ if (input.length === 0) {
1101
+ // Treat an empty array as omission so the idempotency hash matches
1102
+ // the "no allowedDirs" case.
1103
+ return undefined;
1104
+ }
1105
+ if (input.length > MAX_OUTPUT_DIRS) {
1106
+ throw new Error(`submission.outputs.allowedDirs has ${input.length} entries; max is ${MAX_OUTPUT_DIRS}`);
1107
+ }
1108
+ const seen = new Set();
1109
+ const normalised = [];
1110
+ for (let i = 0; i < input.length; i++) {
1111
+ const item = input[i];
1112
+ if (typeof item !== "string") {
1113
+ throw new Error(`submission.outputs.allowedDirs[${i}] must be a string`);
1114
+ }
1115
+ if (item.length === 0) {
1116
+ throw new Error(`submission.outputs.allowedDirs[${i}] must be a non-empty absolute UNIX path`);
1117
+ }
1118
+ const bytes = new TextEncoder().encode(item).length;
1119
+ if (bytes > MAX_OUTPUT_DIR_BYTES) {
1120
+ throw new Error(`submission.outputs.allowedDirs[${i}] exceeds ${MAX_OUTPUT_DIR_BYTES} bytes (got ${bytes})`);
1121
+ }
1122
+ if (!item.startsWith("/")) {
1123
+ throw new Error(`submission.outputs.allowedDirs[${i}] must be an absolute UNIX path (start with '/')`);
1124
+ }
1125
+ if (item.includes("\0")) {
1126
+ throw new Error(`submission.outputs.allowedDirs[${i}] must not contain NUL bytes`);
1127
+ }
1128
+ if (item.includes("\n") || item.includes("\r")) {
1129
+ throw new Error(`submission.outputs.allowedDirs[${i}] must not contain newline characters`);
1130
+ }
1131
+ const segments = item.split("/");
1132
+ if (segments.includes("..")) {
1133
+ throw new Error(`submission.outputs.allowedDirs[${i}] must not contain '..' segments`);
1134
+ }
1135
+ const collapsed = segments
1136
+ .filter((seg, idx) => seg.length > 0 || idx === 0)
1137
+ .join("/");
1138
+ const stripped = collapsed.length > 1 && collapsed.endsWith("/")
1139
+ ? collapsed.slice(0, -1)
1140
+ : collapsed;
1141
+ const canonical = stripped.length === 0 ? "/" : stripped;
1142
+ if (seen.has(canonical)) {
1143
+ continue;
1144
+ }
1145
+ seen.add(canonical);
1146
+ normalised.push(canonical);
1147
+ }
1148
+ return normalised;
1149
+ }
1150
+ function parseOutputDeniedDirs(input) {
1151
+ if (input === undefined) {
1152
+ return undefined;
1153
+ }
1154
+ if (!Array.isArray(input)) {
1155
+ throw new Error("submission.outputs.deniedDirs must be an array of strings");
1156
+ }
1157
+ if (input.length === 0) {
1158
+ return undefined;
1159
+ }
1160
+ if (input.length > MAX_OUTPUT_DIRS) {
1161
+ throw new Error(`submission.outputs.deniedDirs has ${input.length} entries; max is ${MAX_OUTPUT_DIRS}`);
1162
+ }
1163
+ const seen = new Set();
1164
+ const normalised = [];
1165
+ for (let i = 0; i < input.length; i++) {
1166
+ const item = input[i];
1167
+ if (typeof item !== "string") {
1168
+ throw new Error(`submission.outputs.deniedDirs[${i}] must be a string`);
1169
+ }
1170
+ if (item.length === 0) {
1171
+ throw new Error(`submission.outputs.deniedDirs[${i}] must be a non-empty pattern`);
1172
+ }
1173
+ const bytes = new TextEncoder().encode(item).length;
1174
+ if (bytes > MAX_OUTPUT_DIR_BYTES) {
1175
+ throw new Error(`submission.outputs.deniedDirs[${i}] exceeds ${MAX_OUTPUT_DIR_BYTES} bytes (got ${bytes})`);
1176
+ }
1177
+ if (item.includes("\0")) {
1178
+ throw new Error(`submission.outputs.deniedDirs[${i}] must not contain NUL bytes`);
1179
+ }
1180
+ if (item.includes("\n") || item.includes("\r")) {
1181
+ throw new Error(`submission.outputs.deniedDirs[${i}] must not contain newline characters`);
1182
+ }
1183
+ if (item.split("/").includes("..")) {
1184
+ throw new Error(`submission.outputs.deniedDirs[${i}] must not contain '..' segments`);
1185
+ }
1186
+ let canonical = item;
1187
+ if (item.startsWith("/")) {
1188
+ const collapsed = item
1189
+ .split("/")
1190
+ .filter((seg, idx) => seg.length > 0 || idx === 0)
1191
+ .join("/");
1192
+ canonical =
1193
+ collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
1194
+ }
1195
+ if (seen.has(canonical)) {
1196
+ continue;
1197
+ }
1198
+ seen.add(canonical);
1199
+ normalised.push(canonical);
1200
+ }
1201
+ return normalised;
1202
+ }
1203
+ function parsePrompt(input) {
1204
+ if (typeof input === "string") {
1205
+ if (input.length === 0) {
1206
+ throw new Error("submission.prompt must be non-empty");
1207
+ }
1208
+ if (input.trim().length === 0) {
1209
+ throw new Error("submission.prompt must contain non-whitespace text");
1210
+ }
1211
+ return [input];
1212
+ }
1213
+ if (!Array.isArray(input)) {
1214
+ throw new Error("submission.prompt must be a string or an array of strings");
1215
+ }
1216
+ if (input.length === 0) {
1217
+ throw new Error("submission.prompt array must be non-empty");
1218
+ }
1219
+ const parts = input.map((item, index) => {
1220
+ if (typeof item !== "string" || item.length === 0) {
1221
+ throw new Error(`submission.prompt[${index}] must be a non-empty string`);
1222
+ }
1223
+ return item;
1224
+ });
1225
+ if (parts.every((part) => part.trim().length === 0)) {
1226
+ throw new Error("submission.prompt must contain non-whitespace text");
1227
+ }
1228
+ return parts;
1229
+ }
1230
+ function parseSkills(input) {
1231
+ if (input === undefined) {
1232
+ return [];
1233
+ }
1234
+ if (!Array.isArray(input)) {
1235
+ throw new Error("submission.skills must be an array of SkillRef objects");
1236
+ }
1237
+ const seenProvider = new Set();
1238
+ const seenAssetId = new Set();
1239
+ return input.map((item, index) => {
1240
+ const ref = parseSkillRef(item, `submission.skills[${index}]`);
1241
+ if (ref.kind === "provider") {
1242
+ const key = `${ref.vendor}:${ref.skillId}:${ref.version ?? ""}`;
1243
+ if (seenProvider.has(key)) {
1244
+ throw new Error(`submission.skills duplicate provider skill: ${ref.vendor}:${ref.skillId}${ref.version ? `:${ref.version}` : ""}`);
1245
+ }
1246
+ seenProvider.add(key);
1247
+ }
1248
+ else if (ref.kind === "asset") {
1249
+ if (seenAssetId.has(ref.assetId)) {
1250
+ throw new Error(`submission.skills duplicate assetId: ${ref.assetId}`);
1251
+ }
1252
+ seenAssetId.add(ref.assetId);
1253
+ }
1254
+ return ref;
1255
+ });
1256
+ }
1257
+ function parseAgentsMd(input) {
1258
+ if (input === undefined)
1259
+ return [];
1260
+ if (!Array.isArray(input)) {
1261
+ throw new Error("submission.agentsMd must be an array of AgentsMdRef objects");
1262
+ }
1263
+ const seenAssetId = new Set();
1264
+ return input.map((item, index) => {
1265
+ const path = `submission.agentsMd[${index}]`;
1266
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1267
+ throw new Error(`${path} must be an AgentsMdRef object`);
1268
+ }
1269
+ const raw = item;
1270
+ if (raw.kind !== "asset") {
1271
+ throw new Error(`${path}.kind must be 'asset' (got ${JSON.stringify(raw.kind)})`);
1272
+ }
1273
+ const fields = parseAssetRefFields(raw, path);
1274
+ if (seenAssetId.has(fields.assetId)) {
1275
+ throw new Error(`submission.agentsMd duplicate assetId: ${fields.assetId}`);
1276
+ }
1277
+ seenAssetId.add(fields.assetId);
1278
+ return { kind: "asset", assetId: fields.assetId, name: fields.name };
1279
+ });
1280
+ }
1281
+ function parseFiles(input) {
1282
+ if (input === undefined)
1283
+ return [];
1284
+ if (!Array.isArray(input)) {
1285
+ throw new Error("submission.files must be an array of FileRef objects");
1286
+ }
1287
+ const seenAssetId = new Set();
1288
+ return input.map((item, index) => {
1289
+ const path = `submission.files[${index}]`;
1290
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1291
+ throw new Error(`${path} must be a FileRef object`);
1292
+ }
1293
+ const raw = item;
1294
+ if (raw.kind !== "asset") {
1295
+ throw new Error(`${path}.kind must be 'asset' (got ${JSON.stringify(raw.kind)})`);
1296
+ }
1297
+ const fields = parseAssetRefFields(raw, path);
1298
+ if (seenAssetId.has(fields.assetId)) {
1299
+ throw new Error(`submission.files duplicate assetId: ${fields.assetId}`);
1300
+ }
1301
+ seenAssetId.add(fields.assetId);
1302
+ if (fields.mountPath !== undefined && !fields.mountPath.startsWith("/")) {
1303
+ throw new Error(`${path}.mountPath must start with '/' if provided`);
1304
+ }
1305
+ return fields.mountPath !== undefined
1306
+ ? { kind: "asset", assetId: fields.assetId, name: fields.name, mountPath: fields.mountPath }
1307
+ : { kind: "asset", assetId: fields.assetId, name: fields.name };
1308
+ });
1309
+ }
1310
+ function parseMcpServers(input) {
1311
+ if (input === undefined) {
1312
+ return [];
1313
+ }
1314
+ if (!Array.isArray(input)) {
1315
+ throw new Error("submission.mcpServers must be an array of {name, url} objects");
1316
+ }
1317
+ const seen = new Set();
1318
+ return input.map((item, index) => {
1319
+ const ref = parseMcpServerRef(item, `submission.mcpServers[${index}]`);
1320
+ if (seen.has(ref.name)) {
1321
+ throw new Error(`submission.mcpServers duplicate name: ${ref.name}`);
1322
+ }
1323
+ seen.add(ref.name);
1324
+ return ref;
1325
+ });
1326
+ }
1327
+ // ===========================================================================
1328
+ // Runtime dispatcher
1329
+ // ===========================================================================
1330
+ /**
1331
+ * Codes emitted when a submission contains features the active runtime cannot
1332
+ * serve. Code values are stable so dashboard / SDK error rendering can branch
1333
+ * on them.
1334
+ */
1335
+ export const RUNTIME_VALIDATION_CODES = [
1336
+ "feature_runtime_mismatch"
1337
+ ];
1338
+ /**
1339
+ * Thrown by `parseRunSubmissionRequest` and `selectRuntime` when the submitted
1340
+ * run cannot be served by the active managed runtime. The `code` field is part
1341
+ * of the public contract; keep it stable when phrasing changes.
1342
+ */
1343
+ export class RuntimeValidationError extends Error {
1344
+ code;
1345
+ constructor(code, message) {
1346
+ super(message);
1347
+ this.name = "RuntimeValidationError";
1348
+ this.code = code;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Walk the parsed submission and collect features that the active managed
1353
+ * runtime cannot serve. Provider-hosted skill refs (`Skill.provider(...)`) are
1354
+ * rejected now that new submissions only dispatch through managed runs.
1355
+ */
1356
+ export function collectManagedUnsupportedFeatures(req) {
1357
+ const features = [];
1358
+ for (const skill of req.submission.skills) {
1359
+ if (skill.kind === "provider") {
1360
+ const versionSuffix = skill.version ? `, "${skill.version}"` : "";
1361
+ features.push(`Skill.provider("${skill.vendor}", "${skill.skillId}"${versionSuffix})`);
1362
+ }
1363
+ }
1364
+ return features;
1365
+ }
1366
+ /**
1367
+ * Backward-incompatible replacement for the old dual-runtime dispatcher. It is
1368
+ * kept as a pure helper so SDK, CLI, and tests can resolve the runtime without
1369
+ * I/O.
1370
+ */
1371
+ export function selectRuntime(req) {
1372
+ const unsupported = collectManagedUnsupportedFeatures(req);
1373
+ if (unsupported.length > 0) {
1374
+ throw new RuntimeValidationError("feature_runtime_mismatch", `The managed runtime does not support these submission features: ` +
1375
+ `${unsupported.join(", ")}. Remove them or use inline aex skills.`);
1376
+ }
1377
+ void req;
1378
+ return "managed";
1379
+ }
1380
+ //# sourceMappingURL=submission.js.map