antpath 0.10.14 → 0.11.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/LICENSE +201 -0
- package/README.md +16 -8
- package/dist/_shared/blueprint.d.ts +93 -108
- package/dist/_shared/blueprint.js +144 -78
- package/dist/_shared/cleanup-policy.d.ts +2 -2
- package/dist/_shared/cleanup-policy.js +2 -5
- package/dist/_shared/http.d.ts +2 -2
- package/dist/_shared/index.d.ts +7 -1
- package/dist/_shared/index.js +6 -1
- package/dist/_shared/mcp-proxy-url.d.ts +55 -0
- package/dist/_shared/mcp-proxy-url.js +65 -0
- package/dist/_shared/operations.d.ts +55 -8
- package/dist/_shared/operations.js +163 -20
- package/dist/_shared/provider-proxy-url.d.ts +64 -0
- package/dist/_shared/provider-proxy-url.js +73 -0
- package/dist/_shared/proxy-validation.d.ts +1 -1
- package/dist/_shared/proxy-validation.js +2 -2
- package/dist/_shared/run-unit.d.ts +23 -36
- package/dist/_shared/run-unit.js +30 -46
- package/dist/_shared/runner-event.d.ts +120 -0
- package/dist/_shared/runner-event.js +193 -0
- package/dist/_shared/runner-job.d.ts +159 -0
- package/dist/_shared/runner-job.js +54 -0
- package/dist/_shared/runtime-manifest.d.ts +191 -0
- package/dist/_shared/runtime-manifest.js +221 -0
- package/dist/_shared/runtime-types.d.ts +7 -16
- package/dist/_shared/sse.d.ts +74 -0
- package/dist/_shared/sse.js +0 -0
- package/dist/_shared/stable.d.ts +15 -10
- package/dist/_shared/stable.js +15 -10
- package/dist/_shared/submission.d.ts +199 -73
- package/dist/_shared/submission.js +409 -210
- package/dist/_shared/telemetry.d.ts +2 -2
- package/dist/_shared/telemetry.js +2 -2
- package/dist/_shared/template/index.d.ts +0 -1
- package/dist/_shared/template/index.js +0 -1
- package/dist/agents-md.d.ts +25 -67
- package/dist/agents-md.js +35 -121
- package/dist/agents-md.js.map +1 -1
- package/dist/asset-upload.d.ts +34 -0
- package/dist/asset-upload.js +34 -0
- package/dist/asset-upload.js.map +1 -1
- package/dist/blueprint.d.ts +3 -3
- package/dist/bundle.d.ts +2 -2
- package/dist/bundle.js +1 -1
- package/dist/cli.mjs +559 -105
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +53 -22
- package/dist/client.js +196 -130
- package/dist/client.js.map +1 -1
- package/dist/file.d.ts +28 -94
- package/dist/file.js +35 -175
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +10 -2
- package/dist/mcp-server.js +17 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/skill.d.ts +44 -214
- package/dist/skill.js +50 -284
- package/dist/skill.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/cleanup.md +1 -1
- package/docs/credentials.md +2 -2
- package/docs/events.md +8 -8
- package/docs/outputs.md +2 -0
- package/docs/quickstart.md +18 -2
- package/docs/skills.md +1 -3
- package/docs/templates.md +6 -5
- package/package.json +2 -1
- package/dist/_shared/secrets.d.ts +0 -7
- package/dist/_shared/secrets.js +0 -20
- package/dist/_shared/template/mapper.d.ts +0 -11
- package/dist/_shared/template/mapper.js +0 -70
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_RESPONSE_MODES } from "./proxy-protocol.js";
|
|
2
|
-
import { parseMcpServerRef, parseSkillRef } from "./blueprint.js";
|
|
3
|
-
|
|
2
|
+
import { parseMcpServerRef, parseR2RefFields, parseSkillRef } from "./blueprint.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reserved prefix for antpath-set runtime env vars (`ANTPATH_OUTPUTS`,
|
|
5
|
+
* `ANTPATH_CLI`, …). Customer `environment.envVars` keys carrying this
|
|
6
|
+
* prefix are rejected at submission parse time so platform-set values
|
|
7
|
+
* cannot be silently overwritten.
|
|
8
|
+
*/
|
|
9
|
+
export const ANTPATH_RESERVED_ENV_PREFIX = "ANTPATH_";
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of `environment.envVars` entries accepted per
|
|
12
|
+
* submission. Picked to be generous for real customer config bags
|
|
13
|
+
* (the broll case ships a handful — `BROLL_STORE`, `BROLL_OUTPUTS`,
|
|
14
|
+
* `BROLL_MODE`, …) while still bounding the size of every RUNTIME
|
|
15
|
+
* file we mount into the container.
|
|
16
|
+
*/
|
|
17
|
+
export const ENV_VARS_MAX_ENTRIES = 64;
|
|
18
|
+
/** Maximum byte length of a single `environment.envVars` value. */
|
|
19
|
+
export const ENV_VARS_MAX_VALUE_BYTES = 4096;
|
|
20
|
+
/** Maximum total byte length of all `environment.envVars` keys+values combined. */
|
|
21
|
+
export const ENV_VARS_MAX_TOTAL_BYTES = 65536;
|
|
22
|
+
/**
|
|
23
|
+
* POSIX-shell-portable env var key: starts with `A-Z` or `_`, body is
|
|
24
|
+
* `A-Z`, `0-9`, `_`. We deliberately reject lowercase to keep
|
|
25
|
+
* `RUNTIME.env` readable and consistent with platform conventions; if
|
|
26
|
+
* a customer has lowercase keys today, they uppercase them at the
|
|
27
|
+
* call site.
|
|
28
|
+
*/
|
|
29
|
+
const ENV_VAR_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
30
|
+
/**
|
|
31
|
+
* Run-time provider selector. Antpath exposes one customer interface
|
|
32
|
+
* for every provider; the runtime that backs each provider is decided
|
|
33
|
+
* by {@link selectRuntime}.
|
|
34
|
+
*
|
|
35
|
+
* - `anthropic` — defaults to the Anthropic Native runtime (Anthropic
|
|
36
|
+
* Managed Agents API). Customers can opt into the
|
|
37
|
+
* Goose Managed runtime via `runtime: "managed"`.
|
|
38
|
+
* - `deepseek` | `openai` | `gemini` | `mistral` — only the Goose
|
|
39
|
+
* Managed runtime is supported (LiteLLM-gateway'd
|
|
40
|
+
* call into the upstream).
|
|
41
|
+
*
|
|
42
|
+
* See `references/platform-rebuild-2026.md`.
|
|
43
|
+
*/
|
|
44
|
+
export const RUN_PROVIDERS = [
|
|
45
|
+
"anthropic",
|
|
46
|
+
"deepseek",
|
|
47
|
+
"openai",
|
|
48
|
+
"gemini",
|
|
49
|
+
"mistral"
|
|
50
|
+
];
|
|
51
|
+
export const DEFAULT_RUN_PROVIDER = "anthropic";
|
|
52
|
+
/**
|
|
53
|
+
* Customer-facing runtime selector. Optional on the wire — absent means
|
|
54
|
+
* "let the dispatcher route based on provider" ({@link selectRuntime}).
|
|
55
|
+
*
|
|
56
|
+
* - `native` — Anthropic Native runtime. Only valid for
|
|
57
|
+
* `provider: "anthropic"`. Routes through Anthropic's
|
|
58
|
+
* Managed Agents API.
|
|
59
|
+
* - `managed` — Goose Managed runtime. The only option for
|
|
60
|
+
* non-Anthropic providers; also available as an opt-out
|
|
61
|
+
* for `provider: "anthropic"` when the customer wants
|
|
62
|
+
* cross-provider parity.
|
|
63
|
+
*
|
|
64
|
+
* Stored verbatim in `runs.runtime`. See
|
|
65
|
+
* `references/platform-rebuild-2026.md` (Runtime dispatcher logic).
|
|
66
|
+
*/
|
|
67
|
+
export const RUNTIME_KINDS = ["native", "managed"];
|
|
68
|
+
/**
|
|
69
|
+
* Providers whose runtime is implemented by Anthropic Native. Every
|
|
70
|
+
* other provider in {@link RUN_PROVIDERS} routes through Goose Managed.
|
|
71
|
+
* Kept as a constant so the dispatcher logic and the validation tests
|
|
72
|
+
* share one source of truth.
|
|
73
|
+
*/
|
|
74
|
+
export const NATIVE_RUNTIME_PROVIDERS = ["anthropic"];
|
|
4
75
|
const SECRETS_KEY = "secrets";
|
|
5
76
|
/**
|
|
6
77
|
* Default caps for a proxy endpoint when the submission doesn't specify
|
|
@@ -49,86 +120,105 @@ const deniedSecretFields = new Set([
|
|
|
49
120
|
"mcpCredentials",
|
|
50
121
|
"credentials"
|
|
51
122
|
]);
|
|
52
|
-
|
|
53
|
-
const value = requireRecord(input, "submission");
|
|
54
|
-
// The `secrets` block at the top level is the single allowlisted carrier
|
|
55
|
-
// for credential material. Every other top-level field is recursively
|
|
56
|
-
// scanned for secret-bearing keys; the `secrets` block has its own strict
|
|
57
|
-
// schema (no unknown keys, no nested credential leakage).
|
|
58
|
-
for (const [key, fieldValue] of Object.entries(value)) {
|
|
59
|
-
if (key === SECRETS_KEY) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (deniedSecretFields.has(key)) {
|
|
63
|
-
throw new Error(`Secret-bearing field is not allowed in platform submission: ${key}`);
|
|
64
|
-
}
|
|
65
|
-
assertNoSecretBearingFields(fieldValue, [key]);
|
|
66
|
-
}
|
|
67
|
-
const variables = optionalJsonRecord(value.variables, "variables");
|
|
68
|
-
const cleanup = parseCleanupPolicy(value.cleanup);
|
|
69
|
-
const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
|
|
70
|
-
const secrets = parseInlineSecrets(value.secrets);
|
|
71
|
-
crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
|
|
72
|
-
return {
|
|
73
|
-
workspaceId: requireString(value.workspaceId, "workspaceId"),
|
|
74
|
-
idempotencyKey: requireString(value.idempotencyKey, "idempotencyKey"),
|
|
75
|
-
template: parseTemplate(value.template),
|
|
76
|
-
...(variables ? { variables } : {}),
|
|
77
|
-
...(cleanup ? { cleanup } : {}),
|
|
78
|
-
...(proxyEndpoints ? { proxyEndpoints } : {}),
|
|
79
|
-
secrets
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function parseTemplate(input) {
|
|
83
|
-
const value = requireRecord(input, "template");
|
|
84
|
-
const system = optionalString(value.system, "template.system");
|
|
85
|
-
const metadata = optionalJsonRecord(value.metadata, "template.metadata");
|
|
86
|
-
const environment = parseTemplateEnvironment(value.environment);
|
|
87
|
-
return {
|
|
88
|
-
name: requireString(value.name, "template.name"),
|
|
89
|
-
model: requireString(value.model, "template.model"),
|
|
90
|
-
templateHash: requireString(value.templateHash, "template.templateHash"),
|
|
91
|
-
messages: requireStringArray(value.messages, "template.messages"),
|
|
92
|
-
...(system ? { system } : {}),
|
|
93
|
-
...(metadata ? { metadata } : {}),
|
|
94
|
-
...(environment ? { environment } : {})
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function parseTemplateEnvironment(input) {
|
|
123
|
+
function parseEnvironment(input) {
|
|
98
124
|
if (input === undefined) {
|
|
99
125
|
return undefined;
|
|
100
126
|
}
|
|
101
|
-
const value = requireRecord(input, "
|
|
102
|
-
const allowed = new Set(["networking", "packages"]);
|
|
127
|
+
const value = requireRecord(input, "submission.environment");
|
|
128
|
+
const allowed = new Set(["networking", "packages", "envVars"]);
|
|
103
129
|
for (const key of Object.keys(value)) {
|
|
104
130
|
if (!allowed.has(key)) {
|
|
105
|
-
throw new Error(`
|
|
131
|
+
throw new Error(`submission.environment.${key} is not an allowed field; permitted: networking, packages, envVars`);
|
|
106
132
|
}
|
|
107
133
|
}
|
|
108
|
-
const networking =
|
|
109
|
-
const packages =
|
|
110
|
-
|
|
134
|
+
const networking = parseNetworking(value.networking);
|
|
135
|
+
const packages = parsePackages(value.packages);
|
|
136
|
+
const envVars = parseEnvVars(value.envVars);
|
|
137
|
+
if (!networking && !packages && !envVars) {
|
|
111
138
|
return undefined;
|
|
112
139
|
}
|
|
113
140
|
return {
|
|
114
141
|
...(networking ? { networking } : {}),
|
|
115
|
-
...(packages ? { packages } : {})
|
|
142
|
+
...(packages ? { packages } : {}),
|
|
143
|
+
...(envVars ? { envVars } : {})
|
|
116
144
|
};
|
|
117
145
|
}
|
|
118
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Validate a customer-supplied `environment.envVars` map. Returns a
|
|
148
|
+
* frozen copy with keys in insertion order, or `undefined` when the
|
|
149
|
+
* input is absent / an empty object (treated as not supplied so the
|
|
150
|
+
* worker can omit the field from the parsed snapshot).
|
|
151
|
+
*
|
|
152
|
+
* Rules:
|
|
153
|
+
* - Must be a JSON object whose values are all strings.
|
|
154
|
+
* - Keys match `[A-Z_][A-Z0-9_]*` (POSIX-shell portable, uppercase
|
|
155
|
+
* only — keeps RUNTIME.env readable, matches platform convention).
|
|
156
|
+
* - Keys MUST NOT start with the reserved `ANTPATH_` prefix; that
|
|
157
|
+
* prefix is owned by platform-set runtime keys and a collision
|
|
158
|
+
* would silently mask `__ANTPATH_OUTPUTS__` etc. substitution
|
|
159
|
+
* targets.
|
|
160
|
+
* - Bounded: max ENV_VARS_MAX_ENTRIES entries, max
|
|
161
|
+
* ENV_VARS_MAX_VALUE_BYTES per value, max ENV_VARS_MAX_TOTAL_BYTES
|
|
162
|
+
* overall. The caps stop a runaway customer from making the
|
|
163
|
+
* mounted RUNTIME files unbounded.
|
|
164
|
+
* - Values are arbitrary UTF-8 strings, EXCEPT NUL bytes are
|
|
165
|
+
* rejected (NUL terminates C-strings and breaks env-var
|
|
166
|
+
* transport even inside the container).
|
|
167
|
+
*/
|
|
168
|
+
function parseEnvVars(input) {
|
|
169
|
+
if (input === undefined) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const value = requireRecord(input, "submission.environment.envVars");
|
|
173
|
+
const keys = Object.keys(value);
|
|
174
|
+
if (keys.length === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
if (keys.length > ENV_VARS_MAX_ENTRIES) {
|
|
178
|
+
throw new Error(`submission.environment.envVars has ${keys.length} entries; maximum is ${ENV_VARS_MAX_ENTRIES}`);
|
|
179
|
+
}
|
|
180
|
+
const out = {};
|
|
181
|
+
let totalBytes = 0;
|
|
182
|
+
for (const key of keys) {
|
|
183
|
+
if (!ENV_VAR_KEY_PATTERN.test(key)) {
|
|
184
|
+
throw new Error(`submission.environment.envVars.${key} key must match /^[A-Z_][A-Z0-9_]*$/`);
|
|
185
|
+
}
|
|
186
|
+
if (key.startsWith(ANTPATH_RESERVED_ENV_PREFIX)) {
|
|
187
|
+
throw new Error(`submission.environment.envVars.${key} uses reserved prefix "${ANTPATH_RESERVED_ENV_PREFIX}" (set by antpath runtime)`);
|
|
188
|
+
}
|
|
189
|
+
const raw = value[key];
|
|
190
|
+
if (typeof raw !== "string") {
|
|
191
|
+
throw new Error(`submission.environment.envVars.${key} must be a string`);
|
|
192
|
+
}
|
|
193
|
+
if (raw.includes("\0")) {
|
|
194
|
+
throw new Error(`submission.environment.envVars.${key} must not contain NUL bytes`);
|
|
195
|
+
}
|
|
196
|
+
const valueBytes = Buffer.byteLength(raw, "utf8");
|
|
197
|
+
if (valueBytes > ENV_VARS_MAX_VALUE_BYTES) {
|
|
198
|
+
throw new Error(`submission.environment.envVars.${key} value is ${valueBytes} bytes; maximum is ${ENV_VARS_MAX_VALUE_BYTES}`);
|
|
199
|
+
}
|
|
200
|
+
totalBytes += Buffer.byteLength(key, "utf8") + valueBytes;
|
|
201
|
+
if (totalBytes > ENV_VARS_MAX_TOTAL_BYTES) {
|
|
202
|
+
throw new Error(`submission.environment.envVars total byte size exceeds maximum ${ENV_VARS_MAX_TOTAL_BYTES}`);
|
|
203
|
+
}
|
|
204
|
+
out[key] = raw;
|
|
205
|
+
}
|
|
206
|
+
return Object.freeze(out);
|
|
207
|
+
}
|
|
208
|
+
function parseNetworking(input) {
|
|
119
209
|
if (input === undefined) {
|
|
120
210
|
return undefined;
|
|
121
211
|
}
|
|
122
|
-
const value = requireRecord(input, "
|
|
212
|
+
const value = requireRecord(input, "submission.environment.networking");
|
|
123
213
|
const allowed = new Set(["mode", "allowedHosts"]);
|
|
124
214
|
for (const key of Object.keys(value)) {
|
|
125
215
|
if (!allowed.has(key)) {
|
|
126
|
-
throw new Error(`
|
|
216
|
+
throw new Error(`submission.environment.networking.${key} is not an allowed field; permitted: mode, allowedHosts`);
|
|
127
217
|
}
|
|
128
218
|
}
|
|
129
|
-
const mode = optionalEnum(value.mode, "
|
|
219
|
+
const mode = optionalEnum(value.mode, "submission.environment.networking.mode", ["limited", "open"]);
|
|
130
220
|
if (!mode) {
|
|
131
|
-
throw new Error("
|
|
221
|
+
throw new Error("submission.environment.networking.mode is required when networking is provided");
|
|
132
222
|
}
|
|
133
223
|
const allowedHosts = parseAllowedHosts(value.allowedHosts);
|
|
134
224
|
return allowedHosts ? { mode, allowedHosts } : { mode };
|
|
@@ -138,38 +228,38 @@ function parseAllowedHosts(input) {
|
|
|
138
228
|
return undefined;
|
|
139
229
|
}
|
|
140
230
|
if (!Array.isArray(input)) {
|
|
141
|
-
throw new Error("
|
|
231
|
+
throw new Error("submission.environment.networking.allowedHosts must be an array of strings");
|
|
142
232
|
}
|
|
143
233
|
const seen = new Set();
|
|
144
234
|
return input.map((entry, index) => {
|
|
145
235
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
146
|
-
throw new Error(`
|
|
236
|
+
throw new Error(`submission.environment.networking.allowedHosts[${index}] must be a non-empty string`);
|
|
147
237
|
}
|
|
148
238
|
const lower = entry.toLowerCase();
|
|
149
239
|
if (seen.has(lower)) {
|
|
150
|
-
throw new Error(`
|
|
240
|
+
throw new Error(`submission.environment.networking.allowedHosts duplicate entry: ${entry}`);
|
|
151
241
|
}
|
|
152
242
|
seen.add(lower);
|
|
153
243
|
return lower;
|
|
154
244
|
});
|
|
155
245
|
}
|
|
156
|
-
function
|
|
246
|
+
function parsePackages(input) {
|
|
157
247
|
if (input === undefined) {
|
|
158
248
|
return undefined;
|
|
159
249
|
}
|
|
160
250
|
if (!Array.isArray(input)) {
|
|
161
|
-
throw new Error("
|
|
251
|
+
throw new Error("submission.environment.packages must be an array");
|
|
162
252
|
}
|
|
163
253
|
return input.map((entry, index) => {
|
|
164
|
-
const value = requireRecord(entry, `
|
|
254
|
+
const value = requireRecord(entry, `submission.environment.packages[${index}]`);
|
|
165
255
|
const allowed = new Set(["name", "version"]);
|
|
166
256
|
for (const key of Object.keys(value)) {
|
|
167
257
|
if (!allowed.has(key)) {
|
|
168
|
-
throw new Error(`
|
|
258
|
+
throw new Error(`submission.environment.packages[${index}].${key} is not an allowed field; permitted: name, version`);
|
|
169
259
|
}
|
|
170
260
|
}
|
|
171
|
-
const name = requireString(value.name, `
|
|
172
|
-
const version = optionalString(value.version, `
|
|
261
|
+
const name = requireString(value.name, `submission.environment.packages[${index}].name`);
|
|
262
|
+
const version = optionalString(value.version, `submission.environment.packages[${index}].version`);
|
|
173
263
|
return version ? { name, version } : { name };
|
|
174
264
|
});
|
|
175
265
|
}
|
|
@@ -415,23 +505,19 @@ function parseCleanupPolicy(input) {
|
|
|
415
505
|
}
|
|
416
506
|
const value = requireRecord(input, "cleanup");
|
|
417
507
|
const session = optionalEnum(value.session, "cleanup.session", ["retain", "delete"]);
|
|
418
|
-
|
|
419
|
-
if (session !== undefined && claudeSession !== undefined && session !== claudeSession) {
|
|
420
|
-
throw new Error("cleanup.session and cleanup.claudeSession must agree; cleanup.claudeSession is deprecated");
|
|
421
|
-
}
|
|
422
|
-
const resolved = session ?? claudeSession;
|
|
423
|
-
if (resolved === undefined) {
|
|
508
|
+
if (session === undefined) {
|
|
424
509
|
return undefined;
|
|
425
510
|
}
|
|
426
|
-
|
|
427
|
-
if (claudeSession !== undefined) {
|
|
428
|
-
return { ...policy, claudeSession: resolved };
|
|
429
|
-
}
|
|
430
|
-
return policy;
|
|
511
|
+
return { session };
|
|
431
512
|
}
|
|
513
|
+
const PROVIDER_SECRET_KEYS = ["anthropic", "deepseek", "openai", "gemini", "mistral"];
|
|
432
514
|
function parseInlineSecrets(input) {
|
|
433
515
|
const value = requireRecord(input, "secrets");
|
|
434
|
-
const allowedTopLevel = new Set([
|
|
516
|
+
const allowedTopLevel = new Set([
|
|
517
|
+
...PROVIDER_SECRET_KEYS,
|
|
518
|
+
"mcpServers",
|
|
519
|
+
"proxyEndpointAuth"
|
|
520
|
+
]);
|
|
435
521
|
for (const key of Object.keys(value)) {
|
|
436
522
|
if (key.startsWith("__antpath_")) {
|
|
437
523
|
// Platform-internal namespace (e.g. __antpath_proxy_token). The BFF
|
|
@@ -441,33 +527,49 @@ function parseInlineSecrets(input) {
|
|
|
441
527
|
throw new Error(`secrets.${key} uses the platform-internal __antpath_ namespace and may not be set by callers`);
|
|
442
528
|
}
|
|
443
529
|
if (!allowedTopLevel.has(key)) {
|
|
444
|
-
throw new Error(`secrets.${key} is not an allowed field; permitted:
|
|
530
|
+
throw new Error(`secrets.${key} is not an allowed field; permitted: ${[...allowedTopLevel].join(", ")}`);
|
|
445
531
|
}
|
|
446
532
|
}
|
|
447
|
-
const anthropic =
|
|
448
|
-
const
|
|
449
|
-
const
|
|
533
|
+
const anthropic = value.anthropic !== undefined ? parseProviderSecret(value.anthropic, "anthropic") : undefined;
|
|
534
|
+
const deepseek = value.deepseek !== undefined ? parseProviderSecret(value.deepseek, "deepseek") : undefined;
|
|
535
|
+
const openai = value.openai !== undefined ? parseProviderSecret(value.openai, "openai") : undefined;
|
|
536
|
+
const gemini = value.gemini !== undefined ? parseProviderSecret(value.gemini, "gemini") : undefined;
|
|
537
|
+
const mistral = value.mistral !== undefined ? parseProviderSecret(value.mistral, "mistral") : undefined;
|
|
538
|
+
const mcpServers = parseMcpServerSecrets(value.mcpServers);
|
|
450
539
|
const proxyEndpointAuth = parseProxyEndpointAuth(value.proxyEndpointAuth);
|
|
451
540
|
return {
|
|
452
|
-
anthropic,
|
|
541
|
+
...(anthropic ? { anthropic } : {}),
|
|
542
|
+
...(deepseek ? { deepseek } : {}),
|
|
543
|
+
...(openai ? { openai } : {}),
|
|
544
|
+
...(gemini ? { gemini } : {}),
|
|
545
|
+
...(mistral ? { mistral } : {}),
|
|
453
546
|
...(mcpServers ? { mcpServers } : {}),
|
|
454
|
-
...(skills ? { skills } : {}),
|
|
455
547
|
...(proxyEndpointAuth ? { proxyEndpointAuth } : {})
|
|
456
548
|
};
|
|
457
549
|
}
|
|
458
|
-
function
|
|
459
|
-
const
|
|
550
|
+
function parseProviderSecret(input, provider) {
|
|
551
|
+
const field = `secrets.${provider}`;
|
|
552
|
+
const value = requireRecord(input, field);
|
|
460
553
|
const allowed = new Set(["apiKey", "baseUrl"]);
|
|
461
554
|
for (const key of Object.keys(value)) {
|
|
462
555
|
if (!allowed.has(key)) {
|
|
463
|
-
throw new Error(
|
|
556
|
+
throw new Error(`${field}.${key} is not an allowed field; permitted: apiKey, baseUrl`);
|
|
464
557
|
}
|
|
465
558
|
}
|
|
466
|
-
const apiKey = requireString(value.apiKey,
|
|
467
|
-
const
|
|
468
|
-
|
|
559
|
+
const apiKey = requireString(value.apiKey, `${field}.apiKey`);
|
|
560
|
+
const rawBaseUrl = optionalString(value.baseUrl, `${field}.baseUrl`);
|
|
561
|
+
if (rawBaseUrl === undefined) {
|
|
562
|
+
return { apiKey };
|
|
563
|
+
}
|
|
564
|
+
// Reuse the proxy-endpoint URL guard so provider baseUrl gets the
|
|
565
|
+
// same protection: https-only, no credentials, no query/fragment.
|
|
566
|
+
// The provider-proxy in the dashboard forwards a customer-controlled
|
|
567
|
+
// baseUrl to the upstream — accepting http:// (or a userinfo-laden
|
|
568
|
+
// URL) here is an SSRF / credential-leak vector.
|
|
569
|
+
const baseUrl = parseProxyBaseUrl(rawBaseUrl, `${field}.baseUrl`);
|
|
570
|
+
return { apiKey, baseUrl };
|
|
469
571
|
}
|
|
470
|
-
function
|
|
572
|
+
function parseMcpServerSecrets(input) {
|
|
471
573
|
if (input === undefined) {
|
|
472
574
|
return undefined;
|
|
473
575
|
}
|
|
@@ -476,7 +578,7 @@ function parseMcpServers(input) {
|
|
|
476
578
|
}
|
|
477
579
|
const seen = new Set();
|
|
478
580
|
return input.map((entry, index) => {
|
|
479
|
-
const parsed =
|
|
581
|
+
const parsed = parseMcpServerSecret(entry, `secrets.mcpServers[${index}]`);
|
|
480
582
|
if (seen.has(parsed.name)) {
|
|
481
583
|
throw new Error(`secrets.mcpServers duplicate name: ${parsed.name}`);
|
|
482
584
|
}
|
|
@@ -484,7 +586,7 @@ function parseMcpServers(input) {
|
|
|
484
586
|
return parsed;
|
|
485
587
|
});
|
|
486
588
|
}
|
|
487
|
-
function
|
|
589
|
+
function parseMcpServerSecret(input, path) {
|
|
488
590
|
const value = requireRecord(input, path);
|
|
489
591
|
const allowed = new Set(["name", "url", "headers"]);
|
|
490
592
|
for (const key of Object.keys(value)) {
|
|
@@ -497,27 +599,6 @@ function parseMcpServer(input, path) {
|
|
|
497
599
|
const headers = optionalStringRecord(value.headers, `${path}.headers`);
|
|
498
600
|
return headers ? { name, url, headers } : { name, url };
|
|
499
601
|
}
|
|
500
|
-
function parseSkills(input) {
|
|
501
|
-
if (input === undefined) {
|
|
502
|
-
return undefined;
|
|
503
|
-
}
|
|
504
|
-
if (!Array.isArray(input)) {
|
|
505
|
-
throw new Error("secrets.skills must be an array");
|
|
506
|
-
}
|
|
507
|
-
return input.map((entry, index) => parseSkill(entry, `secrets.skills[${index}]`));
|
|
508
|
-
}
|
|
509
|
-
function parseSkill(input, path) {
|
|
510
|
-
const value = requireRecord(input, path);
|
|
511
|
-
const allowed = new Set(["skillId", "version"]);
|
|
512
|
-
for (const key of Object.keys(value)) {
|
|
513
|
-
if (!allowed.has(key)) {
|
|
514
|
-
throw new Error(`${path}.${key} is not an allowed field; permitted: skillId, version`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const skillId = requireString(value.skillId, `${path}.skillId`);
|
|
518
|
-
const version = optionalString(value.version, `${path}.version`);
|
|
519
|
-
return version ? { skillId, version } : { skillId };
|
|
520
|
-
}
|
|
521
602
|
function parseProxyEndpointAuth(input) {
|
|
522
603
|
if (input === undefined) {
|
|
523
604
|
return undefined;
|
|
@@ -701,10 +782,10 @@ function isJsonValue(input) {
|
|
|
701
782
|
}
|
|
702
783
|
return false;
|
|
703
784
|
}
|
|
704
|
-
export function
|
|
785
|
+
export function parseRunSubmissionRequest(input) {
|
|
705
786
|
const value = requireRecord(input, "submission");
|
|
706
787
|
// Defence in depth: scan every non-secrets field for credential-named
|
|
707
|
-
// keys
|
|
788
|
+
// keys. The `secrets` key is
|
|
708
789
|
// the only allow-listed home for credential material.
|
|
709
790
|
for (const [key, fieldValue] of Object.entries(value)) {
|
|
710
791
|
if (key === SECRETS_KEY) {
|
|
@@ -715,11 +796,19 @@ export function parseFlatRunSubmissionRequest(input) {
|
|
|
715
796
|
}
|
|
716
797
|
assertNoSecretBearingFields(fieldValue, [key]);
|
|
717
798
|
}
|
|
799
|
+
const provider = parseRunProvider(value.provider);
|
|
800
|
+
const runtime = parseRuntimeKind(value.runtime);
|
|
801
|
+
// Cross-field validation: native is only valid for the
|
|
802
|
+
// Anthropic-native-capable providers.
|
|
803
|
+
if (runtime === "native" && !isNativeRuntimeProvider(provider)) {
|
|
804
|
+
throw new RuntimeValidationError("runtime_native_unsupported", `runtime: "native" is only supported for provider: ${formatProviderList(NATIVE_RUNTIME_PROVIDERS)} (got provider: "${provider}")`);
|
|
805
|
+
}
|
|
718
806
|
const cleanup = parseCleanupPolicy(value.cleanup);
|
|
719
807
|
const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
|
|
720
808
|
const secrets = parseInlineSecrets(value.secrets);
|
|
809
|
+
enforceProviderSecretCoupling(provider, secrets);
|
|
721
810
|
crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
|
|
722
|
-
const submission =
|
|
811
|
+
const submission = parseSubmission(value.submission);
|
|
723
812
|
// mcpServers names must agree across the submission half and the
|
|
724
813
|
// secrets half — every secrets.mcpServers[i].name MUST resolve to a
|
|
725
814
|
// submission.mcpServers entry (no orphan secrets) AND the URL must
|
|
@@ -738,16 +827,89 @@ export function parseFlatRunSubmissionRequest(input) {
|
|
|
738
827
|
}
|
|
739
828
|
}
|
|
740
829
|
}
|
|
830
|
+
// Cross-field feature/runtime validation. When the caller explicitly
|
|
831
|
+
// opted into runtime: "managed", they must not also request any
|
|
832
|
+
// native-only feature (provider built-in skills, etc.). Done HERE in
|
|
833
|
+
// the parser so every entry-point (Worker, dashboard BFF, SDK) gets
|
|
834
|
+
// the same fail-closed behaviour. Previously this lived only in the
|
|
835
|
+
// separately-invokable selectRuntime() — and the dashboard BFF
|
|
836
|
+
// forgot to invoke it, letting incoherent runs land in the DB.
|
|
837
|
+
if (runtime === "managed") {
|
|
838
|
+
const candidate = {
|
|
839
|
+
workspaceId: "",
|
|
840
|
+
idempotencyKey: "",
|
|
841
|
+
provider,
|
|
842
|
+
runtime,
|
|
843
|
+
submission,
|
|
844
|
+
secrets
|
|
845
|
+
};
|
|
846
|
+
const nativeOnly = collectNativeOnlyFeatures(candidate);
|
|
847
|
+
if (nativeOnly.length > 0) {
|
|
848
|
+
throw new RuntimeValidationError("feature_runtime_mismatch", `runtime: "managed" rejects the following features: ${nativeOnly.join(", ")}. ` +
|
|
849
|
+
`Remove them or switch to runtime: "native".`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
741
852
|
return {
|
|
742
853
|
workspaceId: requireString(value.workspaceId, "workspaceId"),
|
|
743
854
|
idempotencyKey: requireString(value.idempotencyKey, "idempotencyKey"),
|
|
855
|
+
provider,
|
|
856
|
+
...(runtime ? { runtime } : {}),
|
|
744
857
|
submission,
|
|
745
858
|
...(cleanup ? { cleanup } : {}),
|
|
746
859
|
...(proxyEndpoints ? { proxyEndpoints } : {}),
|
|
747
860
|
secrets
|
|
748
861
|
};
|
|
749
862
|
}
|
|
750
|
-
function
|
|
863
|
+
function parseRuntimeKind(input) {
|
|
864
|
+
if (input === undefined) {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
if (typeof input !== "string" || !RUNTIME_KINDS.includes(input)) {
|
|
868
|
+
throw new Error(`runtime must be one of: ${RUNTIME_KINDS.join(", ")} (got ${JSON.stringify(input)})`);
|
|
869
|
+
}
|
|
870
|
+
return input;
|
|
871
|
+
}
|
|
872
|
+
function isNativeRuntimeProvider(provider) {
|
|
873
|
+
return NATIVE_RUNTIME_PROVIDERS.includes(provider);
|
|
874
|
+
}
|
|
875
|
+
function formatProviderList(providers) {
|
|
876
|
+
return providers.map((p) => `"${p}"`).join(", ");
|
|
877
|
+
}
|
|
878
|
+
function parseRunProvider(input) {
|
|
879
|
+
if (input === undefined) {
|
|
880
|
+
return DEFAULT_RUN_PROVIDER;
|
|
881
|
+
}
|
|
882
|
+
if (typeof input !== "string" || !RUN_PROVIDERS.includes(input)) {
|
|
883
|
+
throw new Error(`provider must be one of: ${RUN_PROVIDERS.join(", ")} (got ${JSON.stringify(input)})`);
|
|
884
|
+
}
|
|
885
|
+
return input;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Cross-check the chosen provider against the supplied secrets bundle.
|
|
889
|
+
*
|
|
890
|
+
* - The matching provider's apiKey MUST be present.
|
|
891
|
+
* - Every OTHER provider's secret block MUST be absent (cross-provider
|
|
892
|
+
* secrets are explicitly rejected, not silently dropped — they are
|
|
893
|
+
* almost always a copy-paste mistake or a confused caller, and we
|
|
894
|
+
* want to fail loud).
|
|
895
|
+
* - MCP / proxy endpoint auth carry across providers and are not
|
|
896
|
+
* checked here.
|
|
897
|
+
*/
|
|
898
|
+
function enforceProviderSecretCoupling(provider, secrets) {
|
|
899
|
+
const required = secrets[provider];
|
|
900
|
+
if (!required?.apiKey) {
|
|
901
|
+
throw new Error(`secrets.${provider}.apiKey is required when provider is ${provider}`);
|
|
902
|
+
}
|
|
903
|
+
for (const other of PROVIDER_SECRET_KEYS) {
|
|
904
|
+
if (other === provider) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (secrets[other] !== undefined) {
|
|
908
|
+
throw new Error(`secrets.${other} is not allowed when provider is ${provider}; remove it or set provider to ${other}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function parseSubmission(input) {
|
|
751
913
|
const value = requireRecord(input, "submission.submission");
|
|
752
914
|
const allowed = new Set([
|
|
753
915
|
"model",
|
|
@@ -768,12 +930,12 @@ function parseFlatSubmission(input) {
|
|
|
768
930
|
}
|
|
769
931
|
const model = requireString(value.model, "submission.model");
|
|
770
932
|
const system = optionalString(value.system, "submission.system");
|
|
771
|
-
const prompt =
|
|
772
|
-
const skills =
|
|
773
|
-
const agentsMd =
|
|
774
|
-
const files =
|
|
775
|
-
const mcpServers =
|
|
776
|
-
const environment =
|
|
933
|
+
const prompt = parsePrompt(value.prompt);
|
|
934
|
+
const skills = parseSkills(value.skills);
|
|
935
|
+
const agentsMd = parseAgentsMd(value.agentsMd);
|
|
936
|
+
const files = parseFiles(value.files);
|
|
937
|
+
const mcpServers = parseMcpServers(value.mcpServers);
|
|
938
|
+
const environment = parseEnvironment(value.environment);
|
|
777
939
|
const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
|
|
778
940
|
const outputDirs = parseOutputDirs(value.outputDirs);
|
|
779
941
|
return {
|
|
@@ -863,7 +1025,7 @@ function parseOutputDirs(input) {
|
|
|
863
1025
|
}
|
|
864
1026
|
return normalised;
|
|
865
1027
|
}
|
|
866
|
-
function
|
|
1028
|
+
function parsePrompt(input) {
|
|
867
1029
|
if (typeof input === "string") {
|
|
868
1030
|
if (input.length === 0) {
|
|
869
1031
|
throw new Error("submission.prompt must be non-empty");
|
|
@@ -883,138 +1045,87 @@ function parseFlatPrompt(input) {
|
|
|
883
1045
|
return item;
|
|
884
1046
|
});
|
|
885
1047
|
}
|
|
886
|
-
function
|
|
1048
|
+
function parseSkills(input) {
|
|
887
1049
|
if (input === undefined) {
|
|
888
1050
|
return [];
|
|
889
1051
|
}
|
|
890
1052
|
if (!Array.isArray(input)) {
|
|
891
1053
|
throw new Error("submission.skills must be an array of SkillRef objects");
|
|
892
1054
|
}
|
|
893
|
-
const seenWorkspace = new Set();
|
|
894
1055
|
const seenProvider = new Set();
|
|
895
|
-
const
|
|
1056
|
+
const seenR2Path = new Set();
|
|
896
1057
|
return input.map((item, index) => {
|
|
897
1058
|
const ref = parseSkillRef(item, `submission.skills[${index}]`);
|
|
898
|
-
if (ref.kind === "
|
|
899
|
-
if (seenWorkspace.has(ref.id)) {
|
|
900
|
-
throw new Error(`submission.skills duplicate workspace skill id: ${ref.id}`);
|
|
901
|
-
}
|
|
902
|
-
seenWorkspace.add(ref.id);
|
|
903
|
-
}
|
|
904
|
-
else if (ref.kind === "provider") {
|
|
1059
|
+
if (ref.kind === "provider") {
|
|
905
1060
|
const key = `${ref.vendor}:${ref.skillId}:${ref.version ?? ""}`;
|
|
906
1061
|
if (seenProvider.has(key)) {
|
|
907
1062
|
throw new Error(`submission.skills duplicate provider skill: ${ref.vendor}:${ref.skillId}${ref.version ? `:${ref.version}` : ""}`);
|
|
908
1063
|
}
|
|
909
1064
|
seenProvider.add(key);
|
|
910
1065
|
}
|
|
911
|
-
else {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
throw new Error(`submission.skills duplicate transient slot: ${ref.slot}`);
|
|
1066
|
+
else if (ref.kind === "r2") {
|
|
1067
|
+
if (seenR2Path.has(ref.path)) {
|
|
1068
|
+
throw new Error(`submission.skills duplicate r2 path: ${ref.path}`);
|
|
915
1069
|
}
|
|
916
|
-
|
|
1070
|
+
seenR2Path.add(ref.path);
|
|
917
1071
|
}
|
|
918
1072
|
return ref;
|
|
919
1073
|
});
|
|
920
1074
|
}
|
|
921
|
-
|
|
922
|
-
// shape of an inline-supplied ref is identical (slot, name,
|
|
923
|
-
// contentHash). The discriminator (`kind`) differs by call site.
|
|
924
|
-
const WORKSPACE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
|
|
925
|
-
function parseTransientWireFields(raw, path) {
|
|
926
|
-
const slot = raw.slot;
|
|
927
|
-
if (typeof slot !== "string" || !TRANSIENT_SLOT_PATTERN.test(slot)) {
|
|
928
|
-
throw new Error(`${path}.slot must match ${TRANSIENT_SLOT_PATTERN.source}`);
|
|
929
|
-
}
|
|
930
|
-
const name = raw.name;
|
|
931
|
-
if (typeof name !== "string" || !WORKSPACE_NAME_PATTERN.test(name)) {
|
|
932
|
-
throw new Error(`${path}.name must match ${WORKSPACE_NAME_PATTERN.source}`);
|
|
933
|
-
}
|
|
934
|
-
const contentHash = raw.contentHash;
|
|
935
|
-
if (typeof contentHash !== "string" || !TRANSIENT_CONTENT_HASH_PATTERN.test(contentHash)) {
|
|
936
|
-
throw new Error(`${path}.contentHash must match ${TRANSIENT_CONTENT_HASH_PATTERN.source}`);
|
|
937
|
-
}
|
|
938
|
-
return { slot, name, contentHash };
|
|
939
|
-
}
|
|
940
|
-
function parseFlatAgentsMd(input) {
|
|
1075
|
+
function parseAgentsMd(input) {
|
|
941
1076
|
if (input === undefined)
|
|
942
1077
|
return [];
|
|
943
1078
|
if (!Array.isArray(input)) {
|
|
944
1079
|
throw new Error("submission.agentsMd must be an array of AgentsMdRef objects");
|
|
945
1080
|
}
|
|
946
|
-
const
|
|
947
|
-
const seenTransientSlot = new Set();
|
|
1081
|
+
const seenR2Path = new Set();
|
|
948
1082
|
return input.map((item, index) => {
|
|
949
1083
|
const path = `submission.agentsMd[${index}]`;
|
|
950
1084
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
951
1085
|
throw new Error(`${path} must be an AgentsMdRef object`);
|
|
952
1086
|
}
|
|
953
1087
|
const raw = item;
|
|
954
|
-
if (raw.kind
|
|
955
|
-
|
|
956
|
-
throw new Error(`${path}.id must be a non-empty string`);
|
|
957
|
-
}
|
|
958
|
-
if (seenWorkspace.has(raw.id)) {
|
|
959
|
-
throw new Error(`submission.agentsMd duplicate workspace id: ${raw.id}`);
|
|
960
|
-
}
|
|
961
|
-
seenWorkspace.add(raw.id);
|
|
962
|
-
return { kind: "workspace_agentsmd", id: raw.id };
|
|
1088
|
+
if (raw.kind !== "r2") {
|
|
1089
|
+
throw new Error(`${path}.kind must be 'r2' (got ${JSON.stringify(raw.kind)})`);
|
|
963
1090
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
throw new Error(`submission.agentsMd duplicate transient slot: ${fields.slot}`);
|
|
968
|
-
}
|
|
969
|
-
seenTransientSlot.add(fields.slot);
|
|
970
|
-
return { kind: "transient_agentsmd", ...fields };
|
|
1091
|
+
const fields = parseR2RefFields(raw, path);
|
|
1092
|
+
if (seenR2Path.has(fields.path)) {
|
|
1093
|
+
throw new Error(`submission.agentsMd duplicate r2 path: ${fields.path}`);
|
|
971
1094
|
}
|
|
972
|
-
|
|
1095
|
+
seenR2Path.add(fields.path);
|
|
1096
|
+
return { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name };
|
|
973
1097
|
});
|
|
974
1098
|
}
|
|
975
|
-
function
|
|
1099
|
+
function parseFiles(input) {
|
|
976
1100
|
if (input === undefined)
|
|
977
1101
|
return [];
|
|
978
1102
|
if (!Array.isArray(input)) {
|
|
979
1103
|
throw new Error("submission.files must be an array of FileRef objects");
|
|
980
1104
|
}
|
|
981
|
-
const
|
|
982
|
-
const seenTransientSlot = new Set();
|
|
1105
|
+
const seenR2Path = new Set();
|
|
983
1106
|
return input.map((item, index) => {
|
|
984
1107
|
const path = `submission.files[${index}]`;
|
|
985
1108
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
986
1109
|
throw new Error(`${path} must be a FileRef object`);
|
|
987
1110
|
}
|
|
988
1111
|
const raw = item;
|
|
989
|
-
if (raw.kind
|
|
990
|
-
|
|
991
|
-
throw new Error(`${path}.id must be a non-empty string`);
|
|
992
|
-
}
|
|
993
|
-
if (seenWorkspace.has(raw.id)) {
|
|
994
|
-
throw new Error(`submission.files duplicate workspace id: ${raw.id}`);
|
|
995
|
-
}
|
|
996
|
-
seenWorkspace.add(raw.id);
|
|
997
|
-
return { kind: "workspace_file", id: raw.id };
|
|
1112
|
+
if (raw.kind !== "r2") {
|
|
1113
|
+
throw new Error(`${path}.kind must be 'r2' (got ${JSON.stringify(raw.kind)})`);
|
|
998
1114
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
throw new Error(`submission.files duplicate transient slot: ${fields.slot}`);
|
|
1003
|
-
}
|
|
1004
|
-
seenTransientSlot.add(fields.slot);
|
|
1005
|
-
const mountPath = raw.mountPath;
|
|
1006
|
-
if (mountPath !== undefined) {
|
|
1007
|
-
if (typeof mountPath !== "string" || !mountPath.startsWith("/") || mountPath.length === 0) {
|
|
1008
|
-
throw new Error(`${path}.mountPath must be a non-empty absolute path starting with '/'`);
|
|
1009
|
-
}
|
|
1010
|
-
return { kind: "transient_file", ...fields, mountPath };
|
|
1011
|
-
}
|
|
1012
|
-
return { kind: "transient_file", ...fields };
|
|
1115
|
+
const fields = parseR2RefFields(raw, path);
|
|
1116
|
+
if (seenR2Path.has(fields.path)) {
|
|
1117
|
+
throw new Error(`submission.files duplicate r2 path: ${fields.path}`);
|
|
1013
1118
|
}
|
|
1014
|
-
|
|
1119
|
+
seenR2Path.add(fields.path);
|
|
1120
|
+
if (fields.mountPath !== undefined && !fields.mountPath.startsWith("/")) {
|
|
1121
|
+
throw new Error(`${path}.mountPath must start with '/' if provided`);
|
|
1122
|
+
}
|
|
1123
|
+
return fields.mountPath !== undefined
|
|
1124
|
+
? { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name, mountPath: fields.mountPath }
|
|
1125
|
+
: { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name };
|
|
1015
1126
|
});
|
|
1016
1127
|
}
|
|
1017
|
-
function
|
|
1128
|
+
function parseMcpServers(input) {
|
|
1018
1129
|
if (input === undefined) {
|
|
1019
1130
|
return [];
|
|
1020
1131
|
}
|
|
@@ -1031,4 +1142,92 @@ function parseFlatMcpServers(input) {
|
|
|
1031
1142
|
return ref;
|
|
1032
1143
|
});
|
|
1033
1144
|
}
|
|
1145
|
+
// ===========================================================================
|
|
1146
|
+
// Runtime dispatcher
|
|
1147
|
+
// ===========================================================================
|
|
1148
|
+
/**
|
|
1149
|
+
* Codes the dispatcher emits when a submission can't be served by the
|
|
1150
|
+
* selected runtime. Surfaced both at parse time (cross-field shape
|
|
1151
|
+
* mismatch) and at dispatch time (feature mismatch). Code values are
|
|
1152
|
+
* stable so dashboard / SDK error rendering can branch on them.
|
|
1153
|
+
*/
|
|
1154
|
+
export const RUNTIME_VALIDATION_CODES = [
|
|
1155
|
+
"runtime_native_unsupported",
|
|
1156
|
+
"feature_runtime_mismatch"
|
|
1157
|
+
];
|
|
1158
|
+
/**
|
|
1159
|
+
* Thrown by `parseRunSubmissionRequest` (wire-shape mismatch) and by
|
|
1160
|
+
* `selectRuntime` (feature mismatch) when the submitted run cannot be
|
|
1161
|
+
* served by the chosen runtime. The `code` field is part of the public
|
|
1162
|
+
* contract — keep it stable when phrasings change.
|
|
1163
|
+
*/
|
|
1164
|
+
export class RuntimeValidationError extends Error {
|
|
1165
|
+
code;
|
|
1166
|
+
constructor(code, message) {
|
|
1167
|
+
super(message);
|
|
1168
|
+
this.name = "RuntimeValidationError";
|
|
1169
|
+
this.code = code;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Walk the parsed submission and collect every feature that **only**
|
|
1174
|
+
* works on the Anthropic Native runtime. Today the only such feature
|
|
1175
|
+
* is a provider built-in skill ref (`Skill.provider(...)` — the
|
|
1176
|
+
* Anthropic Skills API is the resolver). Adding a new native-only
|
|
1177
|
+
* feature means extending this function AND the matching error string
|
|
1178
|
+
* in {@link selectRuntime}.
|
|
1179
|
+
*
|
|
1180
|
+
* Exported because Phase 5's runtime dispatcher route handler reuses
|
|
1181
|
+
* it to format the API error body and the dashboard reuses it to point
|
|
1182
|
+
* the user at the offending submission field.
|
|
1183
|
+
*/
|
|
1184
|
+
export function collectNativeOnlyFeatures(req) {
|
|
1185
|
+
const features = [];
|
|
1186
|
+
for (const skill of req.submission.skills) {
|
|
1187
|
+
if (skill.kind === "provider") {
|
|
1188
|
+
const versionSuffix = skill.version ? `, "${skill.version}"` : "";
|
|
1189
|
+
features.push(`Skill.provider("${skill.vendor}", "${skill.skillId}"${versionSuffix})`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return features;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* The runtime dispatcher. Pure function with no I/O — call it after
|
|
1196
|
+
* `parseRunSubmissionRequest` to decide which Workflow class consumes
|
|
1197
|
+
* the run.
|
|
1198
|
+
*
|
|
1199
|
+
* 1. If the customer set `runtime` explicitly, validate that choice
|
|
1200
|
+
* against the submission's provider and feature set. Reject with
|
|
1201
|
+
* a typed error on mismatch — no silent re-routing.
|
|
1202
|
+
* 2. Otherwise auto-route: `provider: "anthropic"` → `"native"` (the
|
|
1203
|
+
* cheapest + fastest path for Anthropic runs); everything else →
|
|
1204
|
+
* `"managed"`.
|
|
1205
|
+
*
|
|
1206
|
+
* Returns the resolved {@link RuntimeKind}; the Workflow dispatcher
|
|
1207
|
+
* (Phase 5) maps that 1:1 to the matching `WorkflowEntrypoint` class.
|
|
1208
|
+
* See `references/platform-rebuild-2026.md` (Runtime dispatcher logic).
|
|
1209
|
+
*/
|
|
1210
|
+
export function selectRuntime(req) {
|
|
1211
|
+
if (req.runtime === "native") {
|
|
1212
|
+
if (!isNativeRuntimeProvider(req.provider)) {
|
|
1213
|
+
throw new RuntimeValidationError("runtime_native_unsupported", `runtime: "native" is only supported for provider: ${formatProviderList(NATIVE_RUNTIME_PROVIDERS)} (got provider: "${req.provider}")`);
|
|
1214
|
+
}
|
|
1215
|
+
return "native";
|
|
1216
|
+
}
|
|
1217
|
+
if (req.runtime === "managed") {
|
|
1218
|
+
const features = collectNativeOnlyFeatures(req);
|
|
1219
|
+
if (features.length > 0) {
|
|
1220
|
+
throw new RuntimeValidationError("feature_runtime_mismatch", `runtime: "managed" rejects the following features: ${features.join(", ")}. ` +
|
|
1221
|
+
`Remove them or switch to runtime: "native".`);
|
|
1222
|
+
}
|
|
1223
|
+
return "managed";
|
|
1224
|
+
}
|
|
1225
|
+
// Auto-routing — no explicit `runtime` on the submission. Anthropic
|
|
1226
|
+
// gets the Native runtime by default (cheaper + faster for Anthropic
|
|
1227
|
+
// runs); everything else routes to the Goose Managed runtime.
|
|
1228
|
+
if (isNativeRuntimeProvider(req.provider)) {
|
|
1229
|
+
return "native";
|
|
1230
|
+
}
|
|
1231
|
+
return "managed";
|
|
1232
|
+
}
|
|
1034
1233
|
//# sourceMappingURL=submission.js.map
|