@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.
- package/README.md +16 -15
- package/dist/_contracts/index.d.ts +3 -4
- package/dist/_contracts/index.js +1 -4
- package/dist/_contracts/operations.d.ts +2 -1
- package/dist/_contracts/operations.js +10 -0
- package/dist/_contracts/run-config.d.ts +1 -3
- package/dist/_contracts/run-config.js +2 -7
- package/dist/_contracts/run-trace.d.ts +0 -86
- package/dist/_contracts/run-trace.js +1 -184
- package/dist/_contracts/run-unit.d.ts +2 -25
- package/dist/_contracts/run-unit.js +1 -2
- package/dist/_contracts/runtime-manifest.d.ts +1 -1
- package/dist/_contracts/runtime-security-profile.d.ts +0 -2
- package/dist/_contracts/runtime-security-profile.js +0 -9
- package/dist/_contracts/runtime-types.d.ts +25 -4
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +62 -95
- package/dist/_contracts/submission.js +59 -482
- package/dist/cli.mjs +99 -442
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +49 -25
- package/dist/client.js +341 -70
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -15
- package/dist/index.js +11 -17
- package/dist/index.js.map +1 -1
- package/dist/retry.d.ts +162 -0
- package/dist/retry.js +320 -0
- package/dist/retry.js.map +1 -0
- package/dist/secret.d.ts +2 -2
- package/dist/secret.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/concepts/composition.md +8 -14
- package/docs/credentials.md +59 -101
- package/docs/defaults.md +0 -8
- package/docs/events.md +8 -9
- package/docs/limits-and-quotas.md +1 -4
- package/docs/limits.md +2 -6
- package/docs/mcp.md +4 -5
- package/docs/networking.md +6 -16
- package/docs/outputs.md +0 -4
- package/docs/public-surface.json +3 -3
- package/docs/quickstart.md +3 -7
- package/docs/retries.md +129 -0
- package/docs/run-config.md +6 -3
- package/docs/secrets.md +1 -1
- package/docs/skills.md +3 -3
- package/docs/vision-skills.md +52 -101
- package/examples/feature-tour.ts +284 -0
- package/package.json +1 -1
- package/dist/_contracts/proxy-protocol.d.ts +0 -305
- package/dist/_contracts/proxy-protocol.js +0 -297
- package/dist/_contracts/proxy-validation.d.ts +0 -19
- package/dist/_contracts/proxy-validation.js +0 -51
- package/dist/data-tools.d.ts +0 -82
- package/dist/data-tools.js +0 -251
- package/dist/data-tools.js.map +0 -1
- package/dist/proxy-endpoint.d.ts +0 -131
- package/dist/proxy-endpoint.js +0 -144
- package/dist/proxy-endpoint.js.map +0 -1
- 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
|
|
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
|
-
//
|
|
662
|
-
//
|
|
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", "
|
|
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
|
|
670
|
-
//
|
|
671
|
-
//
|
|
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
|
|
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;
|