antpath 0.10.15 → 0.11.4
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 +5 -1
- package/dist/_shared/index.js +5 -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 +7 -8
- package/dist/_shared/operations.js +14 -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/stable.d.ts +15 -10
- package/dist/_shared/stable.js +15 -10
- package/dist/_shared/submission.d.ts +221 -73
- package/dist/_shared/submission.js +442 -212
- 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 +191 -100
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +56 -19
- package/dist/client.js +147 -125
- 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 +3 -2
- 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) {
|
|
119
169
|
if (input === undefined) {
|
|
120
170
|
return undefined;
|
|
121
171
|
}
|
|
122
|
-
const value = requireRecord(input, "
|
|
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) {
|
|
209
|
+
if (input === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
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",
|
|
@@ -759,7 +921,8 @@ function parseFlatSubmission(input) {
|
|
|
759
921
|
"mcpServers",
|
|
760
922
|
"environment",
|
|
761
923
|
"metadata",
|
|
762
|
-
"outputDirs"
|
|
924
|
+
"outputDirs",
|
|
925
|
+
"builtins"
|
|
763
926
|
]);
|
|
764
927
|
for (const key of Object.keys(value)) {
|
|
765
928
|
if (!allowed.has(key)) {
|
|
@@ -768,14 +931,15 @@ function parseFlatSubmission(input) {
|
|
|
768
931
|
}
|
|
769
932
|
const model = requireString(value.model, "submission.model");
|
|
770
933
|
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 =
|
|
934
|
+
const prompt = parsePrompt(value.prompt);
|
|
935
|
+
const skills = parseSkills(value.skills);
|
|
936
|
+
const agentsMd = parseAgentsMd(value.agentsMd);
|
|
937
|
+
const files = parseFiles(value.files);
|
|
938
|
+
const mcpServers = parseMcpServers(value.mcpServers);
|
|
939
|
+
const environment = parseEnvironment(value.environment);
|
|
777
940
|
const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
|
|
778
941
|
const outputDirs = parseOutputDirs(value.outputDirs);
|
|
942
|
+
const builtins = parseBuiltins(value.builtins);
|
|
779
943
|
return {
|
|
780
944
|
model,
|
|
781
945
|
...(system ? { system } : {}),
|
|
@@ -786,9 +950,38 @@ function parseFlatSubmission(input) {
|
|
|
786
950
|
mcpServers,
|
|
787
951
|
...(environment ? { environment } : {}),
|
|
788
952
|
...(metadata ? { metadata } : {}),
|
|
789
|
-
...(outputDirs ? { outputDirs } : {})
|
|
953
|
+
...(outputDirs ? { outputDirs } : {}),
|
|
954
|
+
...(builtins !== undefined ? { builtins } : {})
|
|
790
955
|
};
|
|
791
956
|
}
|
|
957
|
+
const BUILTIN_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
958
|
+
const MAX_BUILTINS = 16;
|
|
959
|
+
function parseBuiltins(input) {
|
|
960
|
+
if (input === undefined || input === null)
|
|
961
|
+
return undefined;
|
|
962
|
+
if (!Array.isArray(input)) {
|
|
963
|
+
throw new Error("submission.builtins must be an array of strings");
|
|
964
|
+
}
|
|
965
|
+
if (input.length > MAX_BUILTINS) {
|
|
966
|
+
throw new Error(`submission.builtins exceeds the max of ${MAX_BUILTINS} entries`);
|
|
967
|
+
}
|
|
968
|
+
const seen = new Set();
|
|
969
|
+
const out = [];
|
|
970
|
+
for (let i = 0; i < input.length; i++) {
|
|
971
|
+
const v = input[i];
|
|
972
|
+
if (typeof v !== "string") {
|
|
973
|
+
throw new Error(`submission.builtins[${i}] must be a string`);
|
|
974
|
+
}
|
|
975
|
+
if (!BUILTIN_NAME_PATTERN.test(v)) {
|
|
976
|
+
throw new Error(`submission.builtins[${i}] (${JSON.stringify(v)}) is not a valid Goose builtin name; expected /^[a-z][a-z0-9_-]{0,63}$/`);
|
|
977
|
+
}
|
|
978
|
+
if (seen.has(v))
|
|
979
|
+
continue; // dedupe silently
|
|
980
|
+
seen.add(v);
|
|
981
|
+
out.push(v);
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
792
985
|
/**
|
|
793
986
|
* Maximum number of `outputDirs` entries accepted per submission.
|
|
794
987
|
*
|
|
@@ -863,7 +1056,7 @@ function parseOutputDirs(input) {
|
|
|
863
1056
|
}
|
|
864
1057
|
return normalised;
|
|
865
1058
|
}
|
|
866
|
-
function
|
|
1059
|
+
function parsePrompt(input) {
|
|
867
1060
|
if (typeof input === "string") {
|
|
868
1061
|
if (input.length === 0) {
|
|
869
1062
|
throw new Error("submission.prompt must be non-empty");
|
|
@@ -883,138 +1076,87 @@ function parseFlatPrompt(input) {
|
|
|
883
1076
|
return item;
|
|
884
1077
|
});
|
|
885
1078
|
}
|
|
886
|
-
function
|
|
1079
|
+
function parseSkills(input) {
|
|
887
1080
|
if (input === undefined) {
|
|
888
1081
|
return [];
|
|
889
1082
|
}
|
|
890
1083
|
if (!Array.isArray(input)) {
|
|
891
1084
|
throw new Error("submission.skills must be an array of SkillRef objects");
|
|
892
1085
|
}
|
|
893
|
-
const seenWorkspace = new Set();
|
|
894
1086
|
const seenProvider = new Set();
|
|
895
|
-
const
|
|
1087
|
+
const seenR2Path = new Set();
|
|
896
1088
|
return input.map((item, index) => {
|
|
897
1089
|
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") {
|
|
1090
|
+
if (ref.kind === "provider") {
|
|
905
1091
|
const key = `${ref.vendor}:${ref.skillId}:${ref.version ?? ""}`;
|
|
906
1092
|
if (seenProvider.has(key)) {
|
|
907
1093
|
throw new Error(`submission.skills duplicate provider skill: ${ref.vendor}:${ref.skillId}${ref.version ? `:${ref.version}` : ""}`);
|
|
908
1094
|
}
|
|
909
1095
|
seenProvider.add(key);
|
|
910
1096
|
}
|
|
911
|
-
else {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
throw new Error(`submission.skills duplicate transient slot: ${ref.slot}`);
|
|
1097
|
+
else if (ref.kind === "r2") {
|
|
1098
|
+
if (seenR2Path.has(ref.path)) {
|
|
1099
|
+
throw new Error(`submission.skills duplicate r2 path: ${ref.path}`);
|
|
915
1100
|
}
|
|
916
|
-
|
|
1101
|
+
seenR2Path.add(ref.path);
|
|
917
1102
|
}
|
|
918
1103
|
return ref;
|
|
919
1104
|
});
|
|
920
1105
|
}
|
|
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) {
|
|
1106
|
+
function parseAgentsMd(input) {
|
|
941
1107
|
if (input === undefined)
|
|
942
1108
|
return [];
|
|
943
1109
|
if (!Array.isArray(input)) {
|
|
944
1110
|
throw new Error("submission.agentsMd must be an array of AgentsMdRef objects");
|
|
945
1111
|
}
|
|
946
|
-
const
|
|
947
|
-
const seenTransientSlot = new Set();
|
|
1112
|
+
const seenR2Path = new Set();
|
|
948
1113
|
return input.map((item, index) => {
|
|
949
1114
|
const path = `submission.agentsMd[${index}]`;
|
|
950
1115
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
951
1116
|
throw new Error(`${path} must be an AgentsMdRef object`);
|
|
952
1117
|
}
|
|
953
1118
|
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 };
|
|
1119
|
+
if (raw.kind !== "r2") {
|
|
1120
|
+
throw new Error(`${path}.kind must be 'r2' (got ${JSON.stringify(raw.kind)})`);
|
|
963
1121
|
}
|
|
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 };
|
|
1122
|
+
const fields = parseR2RefFields(raw, path);
|
|
1123
|
+
if (seenR2Path.has(fields.path)) {
|
|
1124
|
+
throw new Error(`submission.agentsMd duplicate r2 path: ${fields.path}`);
|
|
971
1125
|
}
|
|
972
|
-
|
|
1126
|
+
seenR2Path.add(fields.path);
|
|
1127
|
+
return { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name };
|
|
973
1128
|
});
|
|
974
1129
|
}
|
|
975
|
-
function
|
|
1130
|
+
function parseFiles(input) {
|
|
976
1131
|
if (input === undefined)
|
|
977
1132
|
return [];
|
|
978
1133
|
if (!Array.isArray(input)) {
|
|
979
1134
|
throw new Error("submission.files must be an array of FileRef objects");
|
|
980
1135
|
}
|
|
981
|
-
const
|
|
982
|
-
const seenTransientSlot = new Set();
|
|
1136
|
+
const seenR2Path = new Set();
|
|
983
1137
|
return input.map((item, index) => {
|
|
984
1138
|
const path = `submission.files[${index}]`;
|
|
985
1139
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
986
1140
|
throw new Error(`${path} must be a FileRef object`);
|
|
987
1141
|
}
|
|
988
1142
|
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 };
|
|
1143
|
+
if (raw.kind !== "r2") {
|
|
1144
|
+
throw new Error(`${path}.kind must be 'r2' (got ${JSON.stringify(raw.kind)})`);
|
|
998
1145
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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 };
|
|
1146
|
+
const fields = parseR2RefFields(raw, path);
|
|
1147
|
+
if (seenR2Path.has(fields.path)) {
|
|
1148
|
+
throw new Error(`submission.files duplicate r2 path: ${fields.path}`);
|
|
1149
|
+
}
|
|
1150
|
+
seenR2Path.add(fields.path);
|
|
1151
|
+
if (fields.mountPath !== undefined && !fields.mountPath.startsWith("/")) {
|
|
1152
|
+
throw new Error(`${path}.mountPath must start with '/' if provided`);
|
|
1013
1153
|
}
|
|
1014
|
-
|
|
1154
|
+
return fields.mountPath !== undefined
|
|
1155
|
+
? { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name, mountPath: fields.mountPath }
|
|
1156
|
+
: { kind: "r2", path: fields.path, hash: fields.hash, sizeBytes: fields.sizeBytes, name: fields.name };
|
|
1015
1157
|
});
|
|
1016
1158
|
}
|
|
1017
|
-
function
|
|
1159
|
+
function parseMcpServers(input) {
|
|
1018
1160
|
if (input === undefined) {
|
|
1019
1161
|
return [];
|
|
1020
1162
|
}
|
|
@@ -1031,4 +1173,92 @@ function parseFlatMcpServers(input) {
|
|
|
1031
1173
|
return ref;
|
|
1032
1174
|
});
|
|
1033
1175
|
}
|
|
1176
|
+
// ===========================================================================
|
|
1177
|
+
// Runtime dispatcher
|
|
1178
|
+
// ===========================================================================
|
|
1179
|
+
/**
|
|
1180
|
+
* Codes the dispatcher emits when a submission can't be served by the
|
|
1181
|
+
* selected runtime. Surfaced both at parse time (cross-field shape
|
|
1182
|
+
* mismatch) and at dispatch time (feature mismatch). Code values are
|
|
1183
|
+
* stable so dashboard / SDK error rendering can branch on them.
|
|
1184
|
+
*/
|
|
1185
|
+
export const RUNTIME_VALIDATION_CODES = [
|
|
1186
|
+
"runtime_native_unsupported",
|
|
1187
|
+
"feature_runtime_mismatch"
|
|
1188
|
+
];
|
|
1189
|
+
/**
|
|
1190
|
+
* Thrown by `parseRunSubmissionRequest` (wire-shape mismatch) and by
|
|
1191
|
+
* `selectRuntime` (feature mismatch) when the submitted run cannot be
|
|
1192
|
+
* served by the chosen runtime. The `code` field is part of the public
|
|
1193
|
+
* contract — keep it stable when phrasings change.
|
|
1194
|
+
*/
|
|
1195
|
+
export class RuntimeValidationError extends Error {
|
|
1196
|
+
code;
|
|
1197
|
+
constructor(code, message) {
|
|
1198
|
+
super(message);
|
|
1199
|
+
this.name = "RuntimeValidationError";
|
|
1200
|
+
this.code = code;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Walk the parsed submission and collect every feature that **only**
|
|
1205
|
+
* works on the Anthropic Native runtime. Today the only such feature
|
|
1206
|
+
* is a provider built-in skill ref (`Skill.provider(...)` — the
|
|
1207
|
+
* Anthropic Skills API is the resolver). Adding a new native-only
|
|
1208
|
+
* feature means extending this function AND the matching error string
|
|
1209
|
+
* in {@link selectRuntime}.
|
|
1210
|
+
*
|
|
1211
|
+
* Exported because Phase 5's runtime dispatcher route handler reuses
|
|
1212
|
+
* it to format the API error body and the dashboard reuses it to point
|
|
1213
|
+
* the user at the offending submission field.
|
|
1214
|
+
*/
|
|
1215
|
+
export function collectNativeOnlyFeatures(req) {
|
|
1216
|
+
const features = [];
|
|
1217
|
+
for (const skill of req.submission.skills) {
|
|
1218
|
+
if (skill.kind === "provider") {
|
|
1219
|
+
const versionSuffix = skill.version ? `, "${skill.version}"` : "";
|
|
1220
|
+
features.push(`Skill.provider("${skill.vendor}", "${skill.skillId}"${versionSuffix})`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return features;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The runtime dispatcher. Pure function with no I/O — call it after
|
|
1227
|
+
* `parseRunSubmissionRequest` to decide which Workflow class consumes
|
|
1228
|
+
* the run.
|
|
1229
|
+
*
|
|
1230
|
+
* 1. If the customer set `runtime` explicitly, validate that choice
|
|
1231
|
+
* against the submission's provider and feature set. Reject with
|
|
1232
|
+
* a typed error on mismatch — no silent re-routing.
|
|
1233
|
+
* 2. Otherwise auto-route: `provider: "anthropic"` → `"native"` (the
|
|
1234
|
+
* cheapest + fastest path for Anthropic runs); everything else →
|
|
1235
|
+
* `"managed"`.
|
|
1236
|
+
*
|
|
1237
|
+
* Returns the resolved {@link RuntimeKind}; the Workflow dispatcher
|
|
1238
|
+
* (Phase 5) maps that 1:1 to the matching `WorkflowEntrypoint` class.
|
|
1239
|
+
* See `references/platform-rebuild-2026.md` (Runtime dispatcher logic).
|
|
1240
|
+
*/
|
|
1241
|
+
export function selectRuntime(req) {
|
|
1242
|
+
if (req.runtime === "native") {
|
|
1243
|
+
if (!isNativeRuntimeProvider(req.provider)) {
|
|
1244
|
+
throw new RuntimeValidationError("runtime_native_unsupported", `runtime: "native" is only supported for provider: ${formatProviderList(NATIVE_RUNTIME_PROVIDERS)} (got provider: "${req.provider}")`);
|
|
1245
|
+
}
|
|
1246
|
+
return "native";
|
|
1247
|
+
}
|
|
1248
|
+
if (req.runtime === "managed") {
|
|
1249
|
+
const features = collectNativeOnlyFeatures(req);
|
|
1250
|
+
if (features.length > 0) {
|
|
1251
|
+
throw new RuntimeValidationError("feature_runtime_mismatch", `runtime: "managed" rejects the following features: ${features.join(", ")}. ` +
|
|
1252
|
+
`Remove them or switch to runtime: "native".`);
|
|
1253
|
+
}
|
|
1254
|
+
return "managed";
|
|
1255
|
+
}
|
|
1256
|
+
// Auto-routing — no explicit `runtime` on the submission. Anthropic
|
|
1257
|
+
// gets the Native runtime by default (cheaper + faster for Anthropic
|
|
1258
|
+
// runs); everything else routes to the Goose Managed runtime.
|
|
1259
|
+
if (isNativeRuntimeProvider(req.provider)) {
|
|
1260
|
+
return "native";
|
|
1261
|
+
}
|
|
1262
|
+
return "managed";
|
|
1263
|
+
}
|
|
1034
1264
|
//# sourceMappingURL=submission.js.map
|