@aexhq/sdk 0.34.0 → 0.36.0

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 (63) hide show
  1. package/README.md +16 -15
  2. package/dist/_contracts/index.d.ts +3 -4
  3. package/dist/_contracts/index.js +1 -4
  4. package/dist/_contracts/operations.d.ts +2 -1
  5. package/dist/_contracts/operations.js +10 -0
  6. package/dist/_contracts/run-config.d.ts +1 -3
  7. package/dist/_contracts/run-config.js +2 -7
  8. package/dist/_contracts/run-trace.d.ts +0 -86
  9. package/dist/_contracts/run-trace.js +1 -184
  10. package/dist/_contracts/run-unit.d.ts +2 -25
  11. package/dist/_contracts/run-unit.js +1 -2
  12. package/dist/_contracts/runtime-manifest.d.ts +1 -1
  13. package/dist/_contracts/runtime-security-profile.d.ts +0 -2
  14. package/dist/_contracts/runtime-security-profile.js +0 -9
  15. package/dist/_contracts/runtime-types.d.ts +25 -4
  16. package/dist/_contracts/stable.d.ts +1 -1
  17. package/dist/_contracts/stable.js +1 -1
  18. package/dist/_contracts/submission.d.ts +62 -95
  19. package/dist/_contracts/submission.js +59 -482
  20. package/dist/cli.mjs +99 -442
  21. package/dist/cli.mjs.sha256 +1 -1
  22. package/dist/client.d.ts +49 -25
  23. package/dist/client.js +341 -70
  24. package/dist/client.js.map +1 -1
  25. package/dist/index.d.ts +9 -15
  26. package/dist/index.js +11 -17
  27. package/dist/index.js.map +1 -1
  28. package/dist/retry.d.ts +162 -0
  29. package/dist/retry.js +320 -0
  30. package/dist/retry.js.map +1 -0
  31. package/dist/secret.d.ts +2 -2
  32. package/dist/secret.js +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/docs/concepts/composition.md +8 -14
  36. package/docs/credentials.md +59 -101
  37. package/docs/defaults.md +0 -8
  38. package/docs/events.md +8 -9
  39. package/docs/limits-and-quotas.md +1 -4
  40. package/docs/limits.md +2 -6
  41. package/docs/mcp.md +4 -5
  42. package/docs/networking.md +6 -16
  43. package/docs/outputs.md +0 -4
  44. package/docs/public-surface.json +3 -3
  45. package/docs/quickstart.md +3 -7
  46. package/docs/retries.md +129 -0
  47. package/docs/run-config.md +6 -3
  48. package/docs/secrets.md +1 -1
  49. package/docs/skills.md +3 -3
  50. package/docs/vision-skills.md +52 -101
  51. package/examples/feature-tour.ts +284 -0
  52. package/package.json +1 -1
  53. package/dist/_contracts/proxy-protocol.d.ts +0 -305
  54. package/dist/_contracts/proxy-protocol.js +0 -297
  55. package/dist/_contracts/proxy-validation.d.ts +0 -19
  56. package/dist/_contracts/proxy-validation.js +0 -51
  57. package/dist/data-tools.d.ts +0 -82
  58. package/dist/data-tools.js +0 -251
  59. package/dist/data-tools.js.map +0 -1
  60. package/dist/proxy-endpoint.d.ts +0 -131
  61. package/dist/proxy-endpoint.js +0 -144
  62. package/dist/proxy-endpoint.js.map +0 -1
  63. package/examples/chat-corpus.ts +0 -84
@@ -1,9 +1,3 @@
1
- import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_ENDPOINT_DEFAULTS, PROXY_RETRY_JITTERS, PROXY_RETRY_POLICY_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
1
  import { TOOL_NAME_PATTERN, normaliseSkillBundlePath, parseAssetRefFields, parseMcpServerRef } from "./run-config.js";
8
2
  import { parseRunTimeout, parseRuntimeSize } from "./runtime-sizes.js";
9
3
  import { assertRunModelMatchesProvider, parseRunModel } from "./models.js";
@@ -118,29 +112,6 @@ export const SECRETS_KEY = "secrets";
118
112
  export const SECRET_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
119
113
  /** Workspace secret handle a `secretEnv` ref points at (and the name `secret.upload` persists to). */
120
114
  export const SECRET_HANDLE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
121
- export const PROXY_ENDPOINT_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
122
- export const RESERVED_PROXY_ENDPOINT_NAMES = new Set(["proxy", "aex", "internal", "admin"]);
123
- /**
124
- * Headers the proxy never lets through, regardless of policy. Lowercase.
125
- * Anything that could re-introduce credentials, cookies, or routing
126
- * primitives. Kept in lockstep with the proxy route's reject list.
127
- */
128
- const PROXY_DENY_HEADER_LIST = new Set([
129
- "authorization",
130
- "cookie",
131
- "set-cookie",
132
- "proxy-authorization",
133
- "host",
134
- "content-length",
135
- "transfer-encoding",
136
- "connection",
137
- "upgrade",
138
- "expect",
139
- "x-forwarded-for",
140
- "x-forwarded-host",
141
- "x-forwarded-proto",
142
- "x-real-ip"
143
- ]);
144
115
  export const deniedSecretFields = new Set([
145
116
  "providerApiKey",
146
117
  "anthropicApiKey",
@@ -313,322 +284,9 @@ function parsePackages(input) {
313
284
  return version ? { name, version, ecosystem } : { name, ecosystem };
314
285
  });
315
286
  }
316
- function parseProxyEndpoints(input) {
317
- if (input === undefined) {
318
- return undefined;
319
- }
320
- if (!Array.isArray(input)) {
321
- throw new Error("proxyEndpoints must be an array");
322
- }
323
- if (input.length === 0) {
324
- return undefined;
325
- }
326
- const seen = new Set();
327
- return input.map((entry, index) => {
328
- const endpoint = parseProxyEndpoint(entry, `proxyEndpoints[${index}]`);
329
- if (seen.has(endpoint.name)) {
330
- throw new Error(`proxyEndpoints duplicate name: ${endpoint.name}`);
331
- }
332
- seen.add(endpoint.name);
333
- return endpoint;
334
- });
335
- }
336
- function parseProxyEndpoint(input, path) {
337
- const value = requireRecord(input, path);
338
- const allowed = new Set([
339
- "name",
340
- "baseUrl",
341
- "authShape",
342
- "allowMethods",
343
- "allowPathPrefixes",
344
- "allowHeaders",
345
- "responseMode",
346
- "maxRequestBytes",
347
- "maxResponseBytes",
348
- "timeoutMs",
349
- "retry"
350
- ]);
351
- for (const key of Object.keys(value)) {
352
- if (!allowed.has(key)) {
353
- throw new Error(`${path}.${key} is not an allowed field`);
354
- }
355
- }
356
- const name = requireString(value.name, `${path}.name`);
357
- if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
358
- throw new Error(`${path}.name must match ${PROXY_ENDPOINT_NAME_PATTERN} (lowercase letters, digits, '_' and '-'; <=63 chars)`);
359
- }
360
- if (RESERVED_PROXY_ENDPOINT_NAMES.has(name)) {
361
- throw new Error(`${path}.name is reserved: ${name}`);
362
- }
363
- const baseUrl = parseProxyBaseUrl(value.baseUrl, `${path}.baseUrl`);
364
- const authShape = parseProxyAuthShape(value.authShape, `${path}.authShape`);
365
- const allowMethods = parseProxyMethods(value.allowMethods, `${path}.allowMethods`);
366
- const allowPathPrefixes = parseProxyPathPrefixes(value.allowPathPrefixes, `${path}.allowPathPrefixes`);
367
- const allowHeaders = parseProxyAllowedHeaders(value.allowHeaders, `${path}.allowHeaders`, authShape);
368
- const responseMode = optionalEnum(value.responseMode, `${path}.responseMode`, PROXY_RESPONSE_MODES);
369
- const maxRequestBytes = optionalPositiveInt(value.maxRequestBytes, `${path}.maxRequestBytes`);
370
- const maxResponseBytes = optionalPositiveInt(value.maxResponseBytes, `${path}.maxResponseBytes`);
371
- const timeoutMs = optionalPositiveInt(value.timeoutMs, `${path}.timeoutMs`);
372
- const retry = parseProxyRetryPolicy(value.retry, `${path}.retry`);
373
- return {
374
- name,
375
- baseUrl,
376
- authShape,
377
- allowMethods,
378
- allowPathPrefixes,
379
- ...(allowHeaders ? { allowHeaders } : {}),
380
- ...(responseMode ? { responseMode } : {}),
381
- ...(maxRequestBytes !== undefined ? { maxRequestBytes } : {}),
382
- ...(maxResponseBytes !== undefined ? { maxResponseBytes } : {}),
383
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
384
- ...(retry !== undefined ? { retry } : {})
385
- };
386
- }
387
- export function parseProxyRetryPolicy(input, field) {
388
- if (input === undefined) {
389
- return undefined;
390
- }
391
- const value = requireRecord(input, field);
392
- const allowed = new Set([
393
- "maxAttempts",
394
- "initialDelayMs",
395
- "maxDelayMs",
396
- "jitter",
397
- "retryOnStatuses",
398
- "retryOnMethods",
399
- "respectRetryAfter"
400
- ]);
401
- for (const key of Object.keys(value)) {
402
- if (!allowed.has(key)) {
403
- throw new Error(`${field}.${key} is not an allowed field`);
404
- }
405
- }
406
- const maxAttempts = parseOptionalBoundedInt(value.maxAttempts, `${field}.maxAttempts`, 1, 5);
407
- const initialDelayMs = optionalPositiveInt(value.initialDelayMs, `${field}.initialDelayMs`);
408
- const maxDelayMs = optionalPositiveInt(value.maxDelayMs, `${field}.maxDelayMs`);
409
- const effectiveInitialDelayMs = initialDelayMs ?? PROXY_RETRY_POLICY_DEFAULTS.initialDelayMs;
410
- const effectiveMaxDelayMs = maxDelayMs ?? PROXY_RETRY_POLICY_DEFAULTS.maxDelayMs;
411
- if (effectiveMaxDelayMs < effectiveInitialDelayMs) {
412
- throw new Error(`${field}.maxDelayMs must be greater than or equal to ${field}.initialDelayMs`);
413
- }
414
- const jitter = optionalEnum(value.jitter, `${field}.jitter`, PROXY_RETRY_JITTERS);
415
- const retryOnStatuses = parseProxyRetryStatuses(value.retryOnStatuses, `${field}.retryOnStatuses`);
416
- const retryOnMethods = parseProxyRetryMethods(value.retryOnMethods, `${field}.retryOnMethods`);
417
- const respectRetryAfter = parseOptionalBoolean(value.respectRetryAfter, `${field}.respectRetryAfter`);
418
- return {
419
- ...(maxAttempts !== undefined ? { maxAttempts } : {}),
420
- ...(initialDelayMs !== undefined ? { initialDelayMs } : {}),
421
- ...(maxDelayMs !== undefined ? { maxDelayMs } : {}),
422
- ...(jitter !== undefined ? { jitter } : {}),
423
- ...(retryOnStatuses !== undefined ? { retryOnStatuses } : {}),
424
- ...(retryOnMethods !== undefined ? { retryOnMethods } : {}),
425
- ...(respectRetryAfter !== undefined ? { respectRetryAfter } : {})
426
- };
427
- }
428
- function parseProxyRetryStatuses(input, field) {
429
- if (input === undefined) {
430
- return undefined;
431
- }
432
- if (!Array.isArray(input)) {
433
- throw new Error(`${field} must be an array of HTTP status codes`);
434
- }
435
- const seen = new Set();
436
- for (const entry of input) {
437
- if (typeof entry !== "number" ||
438
- !Number.isSafeInteger(entry) ||
439
- entry < 100 ||
440
- entry > 599) {
441
- throw new Error(`${field} entries must be HTTP status codes between 100 and 599`);
442
- }
443
- seen.add(entry);
444
- }
445
- return Array.from(seen);
446
- }
447
- function parseProxyRetryMethods(input, field) {
448
- if (input === undefined) {
449
- return undefined;
450
- }
451
- if (!Array.isArray(input)) {
452
- throw new Error(`${field} must be an array of HTTP methods`);
453
- }
454
- const seen = new Set();
455
- for (const entry of input) {
456
- if (typeof entry !== "string") {
457
- throw new Error(`${field} entries must be strings`);
458
- }
459
- const upper = entry.toUpperCase();
460
- if (!PROXY_ALLOWED_METHODS.includes(upper)) {
461
- throw new Error(`${field} contains unsupported method: ${entry}`);
462
- }
463
- seen.add(upper);
464
- }
465
- return Array.from(seen);
466
- }
467
- function parseProxyBaseUrl(input, field) {
468
- const raw = requireString(input, field);
469
- let parsed;
470
- try {
471
- parsed = new URL(raw);
472
- }
473
- catch {
474
- throw new Error(`${field} must be a valid absolute URL`);
475
- }
476
- if (parsed.protocol !== "https:") {
477
- throw new Error(`${field} must use https://`);
478
- }
479
- if (parsed.username || parsed.password) {
480
- throw new Error(`${field} must not embed credentials`);
481
- }
482
- if (parsed.search || parsed.hash) {
483
- throw new Error(`${field} must not include a query string or fragment`);
484
- }
485
- // Normalize: strip trailing slash so prefix matching is predictable.
486
- const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
487
- return normalized;
488
- }
489
- export function parseProxyAuthShape(input, field) {
490
- const value = requireRecord(input, field);
491
- const type = requireString(value.type, `${field}.type`);
492
- switch (type) {
493
- case "none":
494
- assertOnlyKeys(value, field, ["type"]);
495
- return { type: "none" };
496
- case "bearer":
497
- assertOnlyKeys(value, field, ["type"]);
498
- return { type: "bearer" };
499
- case "basic":
500
- assertOnlyKeys(value, field, ["type"]);
501
- return { type: "basic" };
502
- case "header": {
503
- assertOnlyKeys(value, field, ["type", "name"]);
504
- const name = requireString(value.name, `${field}.name`);
505
- assertHeaderName(name, `${field}.name`);
506
- return { type: "header", name };
507
- }
508
- case "query": {
509
- assertOnlyKeys(value, field, ["type", "name"]);
510
- const name = requireString(value.name, `${field}.name`);
511
- if (!/^[a-zA-Z0-9_\-.]{1,64}$/.test(name)) {
512
- throw new Error(`${field}.name must be a URL-safe identifier (<=64 chars)`);
513
- }
514
- return { type: "query", name };
515
- }
516
- default:
517
- throw new Error(`${field}.type must be one of: none, bearer, basic, header, query`);
518
- }
519
- }
520
- export function parseProxyMethods(input, field) {
521
- if (!Array.isArray(input) || input.length === 0) {
522
- throw new Error(`${field} must be a non-empty array of HTTP methods`);
523
- }
524
- const seen = new Set();
525
- for (const entry of input) {
526
- if (typeof entry !== "string") {
527
- throw new Error(`${field} entries must be strings`);
528
- }
529
- const upper = entry.toUpperCase();
530
- if (!PROXY_ALLOWED_METHODS.includes(upper)) {
531
- throw new Error(`${field} contains unsupported method: ${entry}`);
532
- }
533
- seen.add(upper);
534
- }
535
- return Array.from(seen);
536
- }
537
- export function parseProxyPathPrefixes(input, field) {
538
- if (!Array.isArray(input) || input.length === 0) {
539
- throw new Error(`${field} must be a non-empty array of path prefixes`);
540
- }
541
- const seen = new Set();
542
- for (const entry of input) {
543
- if (typeof entry !== "string" || !entry.startsWith("/")) {
544
- throw new Error(`${field} entries must be non-empty strings starting with '/'`);
545
- }
546
- // Reject traversal / encoded traversal at config time so we never
547
- // need to second-guess at request time.
548
- if (entry.includes("..") || entry.toLowerCase().includes("%2e%2e")) {
549
- throw new Error(`${field} entry must not contain path traversal: ${entry}`);
550
- }
551
- seen.add(entry);
552
- }
553
- return Array.from(seen);
554
- }
555
- export function parseProxyAllowedHeaders(input, field, authShape) {
556
- if (input === undefined) {
557
- return undefined;
558
- }
559
- if (!Array.isArray(input)) {
560
- throw new Error(`${field} must be an array of header names`);
561
- }
562
- const seen = new Set();
563
- const result = [];
564
- for (const entry of input) {
565
- if (typeof entry !== "string" || entry.length === 0) {
566
- throw new Error(`${field} entries must be non-empty strings`);
567
- }
568
- const lower = entry.toLowerCase();
569
- assertHeaderName(entry, field);
570
- if (PROXY_DENY_HEADER_LIST.has(lower)) {
571
- throw new Error(`${field} contains a forbidden header: ${entry}`);
572
- }
573
- const authHeader = authShapeHeaderName(authShape);
574
- if (authHeader && lower === authHeader) {
575
- throw new Error(`${field} must not contain the auth header for this endpoint (${authHeader}); the proxy injects it from secrets.proxyEndpointAuth`);
576
- }
577
- if (seen.has(lower)) {
578
- continue;
579
- }
580
- seen.add(lower);
581
- result.push(lower);
582
- }
583
- return result;
584
- }
585
- function assertHeaderName(value, field) {
586
- // RFC 7230 token chars, conservative.
587
- if (!/^[A-Za-z0-9!#$%&'*+\-.^_`|~]{1,64}$/.test(value)) {
588
- throw new Error(`${field} must be a valid header token (<=64 chars): ${value}`);
589
- }
590
- }
591
- function assertOnlyKeys(value, field, allowed) {
592
- const permitted = new Set(allowed);
593
- for (const key of Object.keys(value)) {
594
- if (!permitted.has(key)) {
595
- throw new Error(`${field}.${key} is not an allowed field; permitted: ${allowed.join(", ")}`);
596
- }
597
- }
598
- }
599
- export function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
600
- const endpointsList = endpoints ?? [];
601
- const authList = auth ?? [];
602
- const endpointsByName = new Map(endpointsList.map((e) => [e.name, e]));
603
- const authByName = new Map(authList.map((a) => [a.name, a]));
604
- for (const endpoint of endpointsList) {
605
- const authEntry = authByName.get(endpoint.name);
606
- if (endpoint.authShape.type === "none") {
607
- // Keyless endpoints carry no auth value. Reject any matching
608
- // auth entry so callers don't accidentally ship a secret bound
609
- // to a "none" endpoint (which would be silently ignored at
610
- // request time — confusing and a leak risk).
611
- if (authEntry) {
612
- throw new Error(`proxyEndpoints[${endpoint.name}] has authShape "none" but a matching secrets.proxyEndpointAuth entry was supplied; remove the auth entry`);
613
- }
614
- continue;
615
- }
616
- if (!authEntry) {
617
- throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
618
- }
619
- if (authEntry.value.type !== endpoint.authShape.type) {
620
- throw new Error(`secrets.proxyEndpointAuth[${endpoint.name}].value.type must equal proxyEndpoints[${endpoint.name}].authShape.type (expected ${endpoint.authShape.type}, got ${authEntry.value.type})`);
621
- }
622
- }
623
- for (const authEntry of authList) {
624
- if (!endpointsByName.has(authEntry.name)) {
625
- throw new Error(`secrets.proxyEndpointAuth[${authEntry.name}] has no matching proxyEndpoints entry`);
626
- }
627
- }
628
- }
629
287
  /**
630
288
  * Cross-check `submission.secretEnv` declarations against `secrets.envSecrets`
631
- * values. Mirrors {@link crossValidateProxyEndpointsAndAuth}:
289
+ * values:
632
290
  *
633
291
  * - `{ ephemeral: true }` MUST have a matching `secrets.envSecrets` value.
634
292
  * - `{ ref }` MUST NOT supply a value (the value lives in the workspace store).
@@ -658,18 +316,18 @@ export function crossValidateSecretEnvAndValues(secretEnv, envSecrets) {
658
316
  }
659
317
  }
660
318
  export function parseInlineSecrets(input) {
661
- // A child run (parentRunId set) inherits its provider keys server-side from
662
- // the parent's vault, so it may omit `secrets` entirely.
319
+ // Absent/null secrets collapse to an empty bundle; the credential-policy gate
320
+ // (enforceCredentialSecretPolicy) decides whether that is admissible for the
321
+ // run's mode (a run inheriting keys server-side may legitimately omit them).
663
322
  if (input === undefined || input === null)
664
323
  return {};
665
324
  const value = requireRecord(input, "secrets");
666
- const allowedTopLevel = new Set(["apiKeys", "mcpServers", "proxyEndpointAuth", "envSecrets"]);
325
+ const allowedTopLevel = new Set(["apiKeys", "mcpServers", "envSecrets"]);
667
326
  for (const key of Object.keys(value)) {
668
327
  if (key.startsWith("__aex_")) {
669
- // Platform-internal namespace (e.g. __aex_proxy_token). The BFF
670
- // mutates the vaulted bundle to inject these; inbound submissions
671
- // are never allowed to set them, to prevent a malicious caller
672
- // from forging the bearer.
328
+ // Platform-internal namespace. The BFF may mutate the vaulted bundle
329
+ // to inject reserved values; inbound submissions are never allowed to
330
+ // set them.
673
331
  throw new Error(`secrets.${key} uses the platform-internal __aex_ namespace and may not be set by callers`);
674
332
  }
675
333
  if (!allowedTopLevel.has(key)) {
@@ -678,12 +336,10 @@ export function parseInlineSecrets(input) {
678
336
  }
679
337
  const apiKeys = parseApiKeys(value.apiKeys);
680
338
  const mcpServers = parseMcpServerSecrets(value.mcpServers);
681
- const proxyEndpointAuth = parseProxyEndpointAuth(value.proxyEndpointAuth);
682
339
  const envSecrets = parseEnvSecrets(value.envSecrets);
683
340
  return {
684
341
  ...(apiKeys ? { apiKeys } : {}),
685
342
  ...(mcpServers ? { mcpServers } : {}),
686
- ...(proxyEndpointAuth ? { proxyEndpointAuth } : {}),
687
343
  ...(envSecrets ? { envSecrets } : {})
688
344
  };
689
345
  }
@@ -754,103 +410,6 @@ function parseMcpServerSecret(input, path) {
754
410
  const headers = optionalStringRecord(value.headers, `${path}.headers`);
755
411
  return headers ? { name, url, headers } : { name, url };
756
412
  }
757
- function parseProxyEndpointAuth(input) {
758
- if (input === undefined) {
759
- return undefined;
760
- }
761
- if (!Array.isArray(input)) {
762
- throw new Error("secrets.proxyEndpointAuth must be an array");
763
- }
764
- if (input.length === 0) {
765
- return undefined;
766
- }
767
- const seen = new Set();
768
- return input.map((entry, index) => {
769
- const auth = parseProxyEndpointAuthEntry(entry, `secrets.proxyEndpointAuth[${index}]`);
770
- if (seen.has(auth.name)) {
771
- throw new Error(`secrets.proxyEndpointAuth duplicate name: ${auth.name}`);
772
- }
773
- seen.add(auth.name);
774
- return auth;
775
- });
776
- }
777
- function parseProxyEndpointAuthEntry(input, path) {
778
- const value = requireRecord(input, path);
779
- const allowed = new Set(["name", "value"]);
780
- for (const key of Object.keys(value)) {
781
- if (!allowed.has(key)) {
782
- throw new Error(`${path}.${key} is not an allowed field; permitted: name, value`);
783
- }
784
- }
785
- const name = requireString(value.name, `${path}.name`);
786
- if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
787
- throw new Error(`${path}.name must match the same pattern as proxyEndpoints[].name (lowercase letters, digits, '_' and '-'; <=63 chars)`);
788
- }
789
- const valueField = parseProxyAuthValue(value.value, `${path}.value`);
790
- return { name, value: valueField };
791
- }
792
- function parseProxyAuthValue(input, path) {
793
- const value = requireRecord(input, path);
794
- const type = requireString(value.type, `${path}.type`);
795
- switch (type) {
796
- case "bearer": {
797
- assertOnlyKeys(value, path, ["type", "token"]);
798
- const token = requireSecretValue(value.token, `${path}.token`);
799
- return { type: "bearer", token };
800
- }
801
- case "basic": {
802
- assertOnlyKeys(value, path, ["type", "username", "password"]);
803
- // Usernames are not redactable in the strict sense (often public
804
- // identifiers like an email), so we only enforce non-emptiness.
805
- // The password is the secret-bearing half.
806
- const username = requireString(value.username, `${path}.username`);
807
- const password = requireSecretValue(value.password, `${path}.password`);
808
- return { type: "basic", username, password };
809
- }
810
- case "header": {
811
- assertOnlyKeys(value, path, ["type", "value"]);
812
- const headerValue = requireSecretValue(value.value, `${path}.value`);
813
- return { type: "header", value: headerValue };
814
- }
815
- case "query": {
816
- assertOnlyKeys(value, path, ["type", "value"]);
817
- const queryValue = requireSecretValue(value.value, `${path}.value`);
818
- return { type: "query", value: queryValue };
819
- }
820
- default:
821
- throw new Error(`${path}.type must be one of: bearer, basic, header, query`);
822
- }
823
- }
824
- /**
825
- * The proxy body-redactor refuses to mask any derived target string shorter
826
- * than this many bytes — masking a 1-byte literal would corrupt the response
827
- * body. This is the floor for the *derived* redaction targets (e.g.
828
- * `Bearer <token>`, base64 fragments), used by
829
- * the hosted proxy redactor, which imports this constant so the two sides can
830
- * never silently diverge.
831
- */
832
- export const MIN_REDACTION_TARGET_BYTES = 4;
833
- /**
834
- * Minimum byte length for an accepted proxy secret *value*. Strictly greater
835
- * than {@link MIN_REDACTION_TARGET_BYTES}: a secret short enough to fall under
836
- * the redactor's floor could slip through unmasked, so the submission parser
837
- * rejects it up front. The `satisfies` check below pins that invariant at
838
- * compile time.
839
- */
840
- const MIN_PROXY_SECRET_BYTES = 8;
841
- // Invariant: an accepted secret must always be long enough for the redactor to
842
- // mask it. If someone lowers MIN_PROXY_SECRET_BYTES below the redaction floor,
843
- // this errors at compile time.
844
- const _MIN_PROXY_SECRET_BYTES_OK = (MIN_PROXY_SECRET_BYTES >= MIN_REDACTION_TARGET_BYTES);
845
- void _MIN_PROXY_SECRET_BYTES_OK;
846
- function requireSecretValue(input, field) {
847
- const value = requireString(input, field);
848
- const byteLen = Buffer.byteLength(value, "utf8");
849
- if (byteLen < MIN_PROXY_SECRET_BYTES) {
850
- throw new Error(`${field} must be at least ${MIN_PROXY_SECRET_BYTES} bytes; shorter values cannot be reliably redacted from upstream responses`);
851
- }
852
- return value;
853
- }
854
413
  export function assertNoSecretBearingFields(input, path) {
855
414
  if (Array.isArray(input)) {
856
415
  input.forEach((item, index) => assertNoSecretBearingFields(item, [...path, String(index)]));
@@ -948,27 +507,6 @@ export function optionalPositiveNumber(input, field) {
948
507
  }
949
508
  return input;
950
509
  }
951
- function parseOptionalBoundedInt(input, field, min, max) {
952
- if (input === undefined) {
953
- return undefined;
954
- }
955
- if (typeof input !== "number" ||
956
- !Number.isSafeInteger(input) ||
957
- input < min ||
958
- input > max) {
959
- throw new Error(`${field} must be a safe integer between ${min} and ${max}`);
960
- }
961
- return input;
962
- }
963
- function parseOptionalBoolean(input, field) {
964
- if (input === undefined) {
965
- return undefined;
966
- }
967
- if (typeof input !== "boolean") {
968
- throw new Error(`${field} must be a boolean`);
969
- }
970
- return input;
971
- }
972
510
  function isJsonValue(input) {
973
511
  if (typeof input === "number") {
974
512
  return Number.isFinite(input);
@@ -993,10 +531,9 @@ export function parseRunSubmissionRequest(input, options = {}) {
993
531
  "submission",
994
532
  "runtimeSize",
995
533
  "timeout",
996
- "proxyEndpoints",
997
- "parentRunId",
998
534
  "webhook",
999
535
  "limits",
536
+ "machine",
1000
537
  SECRETS_KEY
1001
538
  ]);
1002
539
  for (const key of Object.keys(value)) {
@@ -1020,17 +557,11 @@ export function parseRunSubmissionRequest(input, options = {}) {
1020
557
  void options;
1021
558
  const runtimeSize = parseRuntimeSize(value.runtimeSize);
1022
559
  const timeoutMs = parseRunTimeout(value.timeout);
1023
- // Lineage parent only. `depth` is NEVER accepted from the wire — the server
1024
- // derives it from the parent row (a forged depth must not bypass the cap).
1025
- const parentRunId = optionalString(value.parentRunId, "submission.parentRunId");
1026
560
  const webhook = parseRunWebhook(value.webhook);
1027
561
  const limits = parseRunLimits(value.limits);
1028
- const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
562
+ const machine = parseRunMachine(value.machine);
1029
563
  const secrets = parseInlineSecrets(value.secrets);
1030
- enforceCredentialSecretPolicy(secrets, provider, {
1031
- inheritsFromParent: parentRunId !== undefined
1032
- });
1033
- crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
564
+ enforceCredentialSecretPolicy(secrets, provider);
1034
565
  const submission = parseSubmission(value.submission);
1035
566
  assertRunModelMatchesProvider(provider, submission.model);
1036
567
  crossValidateSecretEnvAndValues(submission.secretEnv, secrets.envSecrets);
@@ -1059,10 +590,9 @@ export function parseRunSubmissionRequest(input, options = {}) {
1059
590
  submission,
1060
591
  ...(runtimeSize ? { runtimeSize } : {}),
1061
592
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
1062
- ...(proxyEndpoints ? { proxyEndpoints } : {}),
1063
- ...(parentRunId !== undefined ? { parentRunId } : {}),
1064
593
  ...(webhook !== undefined ? { webhook } : {}),
1065
594
  ...(limits !== undefined ? { limits } : {}),
595
+ ...(machine !== undefined ? { machine } : {}),
1066
596
  secrets
1067
597
  };
1068
598
  }
@@ -1141,6 +671,53 @@ export function parseRunLimits(input) {
1141
671
  ...(maxSpendUsd !== undefined ? { maxSpendUsd } : {})
1142
672
  };
1143
673
  }
674
+ /**
675
+ * Boot-session budget fragment. The public submit surface names a run's spend
676
+ * cap `limits.maxSpendUsd`; the frozen boot session config the managed runtime
677
+ * folds the loop against names the SAME USD value `budgetUsd` — the field the
678
+ * session planner reads to enforce/terminate a run that would out-spend its cap.
679
+ * This is the single source of truth for that wire→boot name mapping so the two
680
+ * layers can never drift.
681
+ *
682
+ * Returns a fragment safe to spread into `sessionConfig.limits`: `{ budgetUsd }`
683
+ * when a cap is set, `{}` when none is (an absent cap stays absent — the run is
684
+ * unbounded per-run, subject only to the run timeout + the per-workspace cap).
685
+ * Pure: same input ⇒ same output.
686
+ */
687
+ export function sessionBudgetLimits(limits) {
688
+ if (limits?.maxSpendUsd === undefined) {
689
+ return {};
690
+ }
691
+ return { budgetUsd: limits.maxSpendUsd };
692
+ }
693
+ /**
694
+ * Parse the optional per-run `machine` capacity intent. Mirrors
695
+ * {@link parseRunWebhook}: absent ⇒ `undefined`; a non-object or any unknown
696
+ * subfield is rejected so the strict top-level allow-list extends to the nested
697
+ * object. `spot` must be a boolean when present. A no-signal object (e.g.
698
+ * `machine: {}`) collapses to `undefined` so it never lands an empty object on
699
+ * the request. An explicit `spot` (true or false) is preserved verbatim. Only
700
+ * shape is validated here — capacity selection is a runtime concern.
701
+ */
702
+ export function parseRunMachine(input) {
703
+ if (input === undefined) {
704
+ return undefined;
705
+ }
706
+ const value = requireRecord(input, "machine");
707
+ const allowed = new Set(["spot"]);
708
+ for (const key of Object.keys(value)) {
709
+ if (!allowed.has(key)) {
710
+ throw new Error(`machine.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
711
+ }
712
+ }
713
+ if (value.spot !== undefined && typeof value.spot !== "boolean") {
714
+ throw new Error("machine.spot must be a boolean");
715
+ }
716
+ if (value.spot === undefined) {
717
+ return undefined;
718
+ }
719
+ return { spot: value.spot };
720
+ }
1144
721
  export function parseRunProvider(input) {
1145
722
  if (input === undefined) {
1146
723
  return DEFAULT_RUN_PROVIDER;