@aexhq/cli 0.25.1 → 0.26.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 +12 -0
- package/dist/index.d.ts +2 -7
- package/dist/index.js +233 -7
- package/package.json +26 -29
- package/LICENSE +0 -201
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -51
- package/dist/cli.js.map +0 -1
- package/dist/cli.mjs +0 -4657
- package/dist/cli.mjs.sha256 +0 -1
- package/dist/host/cancel.d.ts +0 -3
- package/dist/host/cancel.js +0 -30
- package/dist/host/cancel.js.map +0 -1
- package/dist/host/common.d.ts +0 -135
- package/dist/host/common.js +0 -281
- package/dist/host/common.js.map +0 -1
- package/dist/host/delete-asset.d.ts +0 -3
- package/dist/host/delete-asset.js +0 -34
- package/dist/host/delete-asset.js.map +0 -1
- package/dist/host/delete.d.ts +0 -3
- package/dist/host/delete.js +0 -30
- package/dist/host/delete.js.map +0 -1
- package/dist/host/download.d.ts +0 -3
- package/dist/host/download.js +0 -80
- package/dist/host/download.js.map +0 -1
- package/dist/host/events.d.ts +0 -3
- package/dist/host/events.js +0 -97
- package/dist/host/events.js.map +0 -1
- package/dist/host/index.d.ts +0 -17
- package/dist/host/index.js +0 -18
- package/dist/host/index.js.map +0 -1
- package/dist/host/outputs.d.ts +0 -3
- package/dist/host/outputs.js +0 -36
- package/dist/host/outputs.js.map +0 -1
- package/dist/host/run-cmd.d.ts +0 -3
- package/dist/host/run-cmd.js +0 -549
- package/dist/host/run-cmd.js.map +0 -1
- package/dist/host/skills-cmd.d.ts +0 -3
- package/dist/host/skills-cmd.js +0 -304
- package/dist/host/skills-cmd.js.map +0 -1
- package/dist/host/status.d.ts +0 -3
- package/dist/host/status.js +0 -31
- package/dist/host/status.js.map +0 -1
- package/dist/host/wait.d.ts +0 -3
- package/dist/host/wait.js +0 -88
- package/dist/host/wait.js.map +0 -1
- package/dist/host/whoami.d.ts +0 -3
- package/dist/host/whoami.js +0 -33
- package/dist/host/whoami.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts +0 -53
- package/dist/internal.js +0 -10
- package/dist/internal.js.map +0 -1
- package/dist/outputs-sync.d.ts +0 -24
- package/dist/outputs-sync.js +0 -70
- package/dist/outputs-sync.js.map +0 -1
- package/dist/proxy.d.ts +0 -16
- package/dist/proxy.js +0 -316
- package/dist/proxy.js.map +0 -1
- package/dist/run.d.ts +0 -10
- package/dist/run.js +0 -166
- package/dist/run.js.map +0 -1
- package/dist/runtime.d.ts +0 -5
- package/dist/runtime.js +0 -4
- package/dist/runtime.js.map +0 -1
- package/scripts/finalize-bundle.mjs +0 -72
- package/src/cli.ts +0 -51
- package/src/host/cancel.ts +0 -39
- package/src/host/common.ts +0 -329
- package/src/host/delete-asset.ts +0 -43
- package/src/host/delete.ts +0 -39
- package/src/host/download.ts +0 -97
- package/src/host/events.ts +0 -113
- package/src/host/index.ts +0 -25
- package/src/host/outputs.ts +0 -45
- package/src/host/run-cmd.ts +0 -578
- package/src/host/skills-cmd.ts +0 -317
- package/src/host/status.ts +0 -40
- package/src/host/wait.ts +0 -110
- package/src/host/whoami.ts +0 -42
- package/src/index.ts +0 -7
- package/src/internal.ts +0 -63
- package/src/outputs-sync.ts +0 -76
- package/src/proxy.ts +0 -359
- package/src/run.ts +0 -187
- package/src/runtime.ts +0 -15
- package/test/cli.test.ts +0 -519
- package/test/host.test.ts +0 -1154
- package/test/no-env-vars.test.ts +0 -59
- package/test/outputs-sync.test.ts +0 -107
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -14
package/src/host/run-cmd.ts
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex run` — submit a flat run via the dashboard BFF using the
|
|
3
|
-
* same operations module the SDK uses.
|
|
4
|
-
*
|
|
5
|
-
* Two input modes (mutually exclusive):
|
|
6
|
-
*
|
|
7
|
-
* 1. `--config <path>` — plain run-request JSON:
|
|
8
|
-
* `{ model, system?, prompt, skills?, mcpServers?, environment?,
|
|
9
|
-
* runtimeSize?, timeout?, postHook?, proxyEndpoints?, metadata? }`. Skill
|
|
10
|
-
* entries use storage-neutral asset refs (`{ kind: "asset", ... }`).
|
|
11
|
-
* MCP entries may include `headers` — the CLI splits them into the
|
|
12
|
-
* `secrets.mcpServers` bag before posting.
|
|
13
|
-
*
|
|
14
|
-
* 2. Flat flags — for ad-hoc / scriptable invocations:
|
|
15
|
-
* --model <id> REQUIRED in flag mode
|
|
16
|
-
* --system @file | --system "literal" optional system message
|
|
17
|
-
* --prompt @file | --prompt "literal" REQUIRED in flag mode (repeatable)
|
|
18
|
-
* --mcp name=url MCP server (repeatable)
|
|
19
|
-
* --mcp-auth name=Header:Value adds a header on the matching --mcp (repeatable)
|
|
20
|
-
* --metadata key=value string metadata entry (repeatable)
|
|
21
|
-
*
|
|
22
|
-
* Always required (both modes):
|
|
23
|
-
* --anthropic-api-key <key> provider key (never stored)
|
|
24
|
-
* --api-token <token> see `parseCommonHostFlags`
|
|
25
|
-
*
|
|
26
|
-
* Optional (both modes):
|
|
27
|
-
* --runtime-size <size> managed runtime preset (e.g. shared-2x-2gb); default shared-1x-512mb
|
|
28
|
-
* --run-timeout <dur> server-side run deadline (e.g. 1h); bounded [1m, 6h], default 1h
|
|
29
|
-
* --idempotency-key <key> defaults to a fresh UUID
|
|
30
|
-
* --proxy-endpoint '<json>' PlatformProxyEndpoint JSON (repeatable)
|
|
31
|
-
* --proxy-auth name=<spec> bearer:tok | basic:u:p | header:v | query:v (repeatable)
|
|
32
|
-
* --follow poll events to stdout until terminal status
|
|
33
|
-
* --timeout <dur> with --follow: give up after this long (e.g. 8m); exit code 3
|
|
34
|
-
*/
|
|
35
|
-
import {
|
|
36
|
-
AEX_DEFAULT_BASE_URL,
|
|
37
|
-
DEFAULT_RUN_PROVIDER,
|
|
38
|
-
operations,
|
|
39
|
-
parseRunRequestConfig,
|
|
40
|
-
RUN_MODELS,
|
|
41
|
-
RUNTIME_SIZES,
|
|
42
|
-
RUN_PROVIDERS,
|
|
43
|
-
RUNTIME_KINDS,
|
|
44
|
-
TERMINAL_RUN_STATUSES,
|
|
45
|
-
validateProxyAuth,
|
|
46
|
-
type RunRequestConfig,
|
|
47
|
-
type McpServerRef,
|
|
48
|
-
type PlatformRunSubmissionInput,
|
|
49
|
-
type PlatformSubmission,
|
|
50
|
-
type PlatformInlineSecrets,
|
|
51
|
-
type PlatformMcpServerSecret,
|
|
52
|
-
type PlatformProxyAuthValue,
|
|
53
|
-
type PlatformProxyEndpoint,
|
|
54
|
-
type PlatformProxyEndpointAuth,
|
|
55
|
-
type RunModel,
|
|
56
|
-
type RunProvider,
|
|
57
|
-
type RuntimeSize,
|
|
58
|
-
type RuntimeKind,
|
|
59
|
-
type SkillRef
|
|
60
|
-
} from "@aexhq/contracts";
|
|
61
|
-
import { resolve as resolvePath } from "node:path";
|
|
62
|
-
import type { CliIO } from "../internal.js";
|
|
63
|
-
import {
|
|
64
|
-
type CliExitCode,
|
|
65
|
-
RUNTIME_ERR,
|
|
66
|
-
SUCCESS,
|
|
67
|
-
TIMEOUT_ERR,
|
|
68
|
-
USAGE_ERR,
|
|
69
|
-
collectRepeated,
|
|
70
|
-
collectRepeatedKv,
|
|
71
|
-
collectRepeatedKvList,
|
|
72
|
-
emitJsonError,
|
|
73
|
-
makeHttpClient,
|
|
74
|
-
parseCommonHostFlags,
|
|
75
|
-
parseDuration,
|
|
76
|
-
refuseInsideManagedRun,
|
|
77
|
-
takeBooleanFlag,
|
|
78
|
-
takeFlagValue,
|
|
79
|
-
takeOptionFlag
|
|
80
|
-
} from "./common.js";
|
|
81
|
-
|
|
82
|
-
// Membership-tested against the loose `string` run status from the BFF, so we
|
|
83
|
-
// back it with the canonical terminal set rather than a drift-prone local list.
|
|
84
|
-
const TERMINAL_STATUSES = new Set<string>(TERMINAL_RUN_STATUSES);
|
|
85
|
-
|
|
86
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */ // AEX_DEFAULT_BASE_URL is re-exported only for assertion clarity.
|
|
87
|
-
void AEX_DEFAULT_BASE_URL;
|
|
88
|
-
|
|
89
|
-
export async function runRunCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
90
|
-
if (await refuseInsideManagedRun(io, "run")) return USAGE_ERR;
|
|
91
|
-
|
|
92
|
-
const common = parseCommonHostFlags(argv);
|
|
93
|
-
if (!common.ok) {
|
|
94
|
-
io.stderr(`${common.reason}\n`);
|
|
95
|
-
return USAGE_ERR;
|
|
96
|
-
}
|
|
97
|
-
let rest = common.rest;
|
|
98
|
-
|
|
99
|
-
const providerFlag = takeFlagValue(rest, "--provider");
|
|
100
|
-
if (providerFlag.error) { io.stderr(`${providerFlag.error}\n`); return USAGE_ERR; }
|
|
101
|
-
rest = providerFlag.remaining;
|
|
102
|
-
let provider: RunProvider = DEFAULT_RUN_PROVIDER;
|
|
103
|
-
if (providerFlag.value !== null) {
|
|
104
|
-
if (!(RUN_PROVIDERS as readonly string[]).includes(providerFlag.value)) {
|
|
105
|
-
io.stderr(`--provider must be one of: ${RUN_PROVIDERS.join(", ")} (got: ${providerFlag.value})\n`);
|
|
106
|
-
return USAGE_ERR;
|
|
107
|
-
}
|
|
108
|
-
provider = providerFlag.value as RunProvider;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Provider key flag handling: each provider in RUN_PROVIDERS has its
|
|
112
|
-
// own `--<provider>-api-key` flag. Exactly the flag matching the
|
|
113
|
-
// selected provider must be supplied; every other provider's flag
|
|
114
|
-
// must be absent (mirrors the secrets-coupling rule the parser
|
|
115
|
-
// re-enforces server-side).
|
|
116
|
-
const providerKeyValues: Partial<Record<RunProvider, string>> = {};
|
|
117
|
-
for (const p of RUN_PROVIDERS) {
|
|
118
|
-
const flag = takeFlagValue(rest, `--${p}-api-key`);
|
|
119
|
-
if (flag.error) { io.stderr(`${flag.error}\n`); return USAGE_ERR; }
|
|
120
|
-
rest = flag.remaining;
|
|
121
|
-
if (flag.value !== null) providerKeyValues[p] = flag.value;
|
|
122
|
-
}
|
|
123
|
-
if (!providerKeyValues[provider]) {
|
|
124
|
-
io.stderr(`--${provider}-api-key is required when --provider is ${provider} (the platform does not store provider keys on your behalf)\n`);
|
|
125
|
-
return USAGE_ERR;
|
|
126
|
-
}
|
|
127
|
-
for (const p of RUN_PROVIDERS) {
|
|
128
|
-
if (p === provider) continue;
|
|
129
|
-
if (providerKeyValues[p] !== undefined) {
|
|
130
|
-
io.stderr(`--${p}-api-key is not allowed when --provider is ${provider}\n`);
|
|
131
|
-
return USAGE_ERR;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Optional runtime selector. Validate the value against the wire enum
|
|
136
|
-
// here so the user gets an early error from the CLI; the shared parser
|
|
137
|
-
// re-runs the check when the request lands at the API plane.
|
|
138
|
-
const runtimeFlag = takeFlagValue(rest, "--runtime");
|
|
139
|
-
if (runtimeFlag.error) { io.stderr(`${runtimeFlag.error}\n`); return USAGE_ERR; }
|
|
140
|
-
rest = runtimeFlag.remaining;
|
|
141
|
-
let runtime: RuntimeKind | undefined;
|
|
142
|
-
if (runtimeFlag.value !== null) {
|
|
143
|
-
if (!(RUNTIME_KINDS as readonly string[]).includes(runtimeFlag.value)) {
|
|
144
|
-
io.stderr(`--runtime must be one of: ${RUNTIME_KINDS.join(", ")} (got: ${runtimeFlag.value})\n`);
|
|
145
|
-
return USAGE_ERR;
|
|
146
|
-
}
|
|
147
|
-
runtime = runtimeFlag.value as RuntimeKind;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const idempotency = takeFlagValue(rest, "--idempotency-key");
|
|
151
|
-
if (idempotency.error) { io.stderr(`${idempotency.error}\n`); return USAGE_ERR; }
|
|
152
|
-
rest = idempotency.remaining;
|
|
153
|
-
|
|
154
|
-
// `--runtime-size` selects a managed runtime size from the closed preset set.
|
|
155
|
-
const runtimeSizeFlag = takeFlagValue(rest, "--runtime-size");
|
|
156
|
-
if (runtimeSizeFlag.error) { io.stderr(`${runtimeSizeFlag.error}\n`); return USAGE_ERR; }
|
|
157
|
-
rest = runtimeSizeFlag.remaining;
|
|
158
|
-
if (runtimeSizeFlag.value && !(RUNTIME_SIZES as readonly string[]).includes(runtimeSizeFlag.value)) {
|
|
159
|
-
io.stderr(`--runtime-size must be one of: ${RUNTIME_SIZES.join(", ")}\n`);
|
|
160
|
-
return USAGE_ERR;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// `--run-timeout` is the SERVER-side run deadline (distinct from `--timeout`,
|
|
164
|
-
// which bounds the client-side --follow loop). Format-checked locally for
|
|
165
|
-
// fast feedback; the server applies the [1m, 6h] bounds + default.
|
|
166
|
-
const runTimeoutFlag = takeFlagValue(rest, "--run-timeout");
|
|
167
|
-
if (runTimeoutFlag.error) { io.stderr(`${runTimeoutFlag.error}\n`); return USAGE_ERR; }
|
|
168
|
-
rest = runTimeoutFlag.remaining;
|
|
169
|
-
if (runTimeoutFlag.value) {
|
|
170
|
-
const parsed = parseDuration(runTimeoutFlag.value);
|
|
171
|
-
if (parsed.error) {
|
|
172
|
-
io.stderr(`--run-timeout: ${parsed.error}\n`);
|
|
173
|
-
return USAGE_ERR;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const follow = takeBooleanFlag(rest, "--follow");
|
|
178
|
-
rest = follow.remaining;
|
|
179
|
-
const timeoutFlag = takeOptionFlag(rest, "--timeout");
|
|
180
|
-
rest = timeoutFlag.remaining;
|
|
181
|
-
let followTimeoutMs: number | null = null;
|
|
182
|
-
if (timeoutFlag.value !== undefined) {
|
|
183
|
-
const parsed = parseDuration(timeoutFlag.value);
|
|
184
|
-
if (parsed.error) {
|
|
185
|
-
io.stderr(`--timeout: ${parsed.error}\n`);
|
|
186
|
-
return USAGE_ERR;
|
|
187
|
-
}
|
|
188
|
-
followTimeoutMs = parsed.ms;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const config = takeFlagValue(rest, "--config");
|
|
192
|
-
if (config.error) { io.stderr(`${config.error}\n`); return USAGE_ERR; }
|
|
193
|
-
rest = config.remaining;
|
|
194
|
-
|
|
195
|
-
// Flat flags
|
|
196
|
-
const modelFlag = takeFlagValue(rest, "--model");
|
|
197
|
-
if (modelFlag.error) { io.stderr(`${modelFlag.error}\n`); return USAGE_ERR; }
|
|
198
|
-
rest = modelFlag.remaining;
|
|
199
|
-
|
|
200
|
-
const systemFlag = takeFlagValue(rest, "--system");
|
|
201
|
-
if (systemFlag.error) { io.stderr(`${systemFlag.error}\n`); return USAGE_ERR; }
|
|
202
|
-
rest = systemFlag.remaining;
|
|
203
|
-
|
|
204
|
-
const promptFlags = collectRepeated(rest, "--prompt");
|
|
205
|
-
if (promptFlags.error) { io.stderr(`${promptFlags.error}\n`); return USAGE_ERR; }
|
|
206
|
-
rest = promptFlags.remaining;
|
|
207
|
-
|
|
208
|
-
const mcpFlags = collectRepeatedKv(rest, "--mcp");
|
|
209
|
-
if (mcpFlags.error) { io.stderr(`${mcpFlags.error}\n`); return USAGE_ERR; }
|
|
210
|
-
rest = mcpFlags.remaining;
|
|
211
|
-
|
|
212
|
-
const mcpAuthFlags = collectRepeatedKvList(rest, "--mcp-auth");
|
|
213
|
-
if (mcpAuthFlags.error) { io.stderr(`${mcpAuthFlags.error}\n`); return USAGE_ERR; }
|
|
214
|
-
rest = mcpAuthFlags.remaining;
|
|
215
|
-
|
|
216
|
-
const metadataFlags = collectRepeatedKv(rest, "--metadata");
|
|
217
|
-
if (metadataFlags.error) { io.stderr(`${metadataFlags.error}\n`); return USAGE_ERR; }
|
|
218
|
-
rest = metadataFlags.remaining;
|
|
219
|
-
|
|
220
|
-
const proxyEndpointFlags = collectRepeated(rest, "--proxy-endpoint");
|
|
221
|
-
if (proxyEndpointFlags.error) { io.stderr(`${proxyEndpointFlags.error}\n`); return USAGE_ERR; }
|
|
222
|
-
rest = proxyEndpointFlags.remaining;
|
|
223
|
-
|
|
224
|
-
const proxyAuthFlags = collectRepeatedKv(rest, "--proxy-auth");
|
|
225
|
-
if (proxyAuthFlags.error) { io.stderr(`${proxyAuthFlags.error}\n`); return USAGE_ERR; }
|
|
226
|
-
rest = proxyAuthFlags.remaining;
|
|
227
|
-
|
|
228
|
-
const positional = rest.filter((a) => !a.startsWith("--"));
|
|
229
|
-
const unknownFlags = rest.filter((a) => a.startsWith("--"));
|
|
230
|
-
if (unknownFlags.length > 0) {
|
|
231
|
-
io.stderr(`unknown flag: ${unknownFlags[0]}\n`);
|
|
232
|
-
return USAGE_ERR;
|
|
233
|
-
}
|
|
234
|
-
if (positional.length > 0) {
|
|
235
|
-
io.stderr(`aex run takes no positional arguments (got: ${positional.join(" ")})\n`);
|
|
236
|
-
return USAGE_ERR;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ---------------- Resolve run config (config XOR flat flags) ----------------
|
|
240
|
-
let runConfig: RunRequestConfig;
|
|
241
|
-
let mcpHeadersFromConfig: Map<string, Record<string, string>> = new Map();
|
|
242
|
-
if (config.value) {
|
|
243
|
-
if (modelFlag.value || systemFlag.value || promptFlags.values.length || Object.keys(mcpFlags.entries).length || Object.keys(metadataFlags.entries).length) {
|
|
244
|
-
io.stderr("--config cannot be combined with --model/--system/--prompt/--mcp/--metadata\n");
|
|
245
|
-
return USAGE_ERR;
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
const absPath = resolvePath(io.cwd(), config.value);
|
|
249
|
-
const text = await io.readFile(absPath);
|
|
250
|
-
const raw = JSON.parse(text) as unknown;
|
|
251
|
-
const { mcpHeaders, normalised } = stripMcpHeadersForParsing(raw);
|
|
252
|
-
mcpHeadersFromConfig = mcpHeaders;
|
|
253
|
-
runConfig = parseRunRequestConfig(normalised);
|
|
254
|
-
} catch (err) {
|
|
255
|
-
io.stderr(`failed to load --config: ${(err as Error).message}\n`);
|
|
256
|
-
return USAGE_ERR;
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
if (!modelFlag.value) {
|
|
260
|
-
io.stderr("--model is required when --config is not provided\n");
|
|
261
|
-
return USAGE_ERR;
|
|
262
|
-
}
|
|
263
|
-
if (!(RUN_MODELS as readonly string[]).includes(modelFlag.value)) {
|
|
264
|
-
io.stderr(`--model must be one of: ${RUN_MODELS.join(", ")} (got: ${modelFlag.value})\n`);
|
|
265
|
-
return USAGE_ERR;
|
|
266
|
-
}
|
|
267
|
-
if (promptFlags.values.length === 0) {
|
|
268
|
-
io.stderr("--prompt is required (repeatable)\n");
|
|
269
|
-
return USAGE_ERR;
|
|
270
|
-
}
|
|
271
|
-
let resolvedPrompt: string[];
|
|
272
|
-
try {
|
|
273
|
-
resolvedPrompt = await Promise.all(promptFlags.values.map((v) => readMaybeFile(io, v)));
|
|
274
|
-
} catch (err) {
|
|
275
|
-
io.stderr(`failed to read --prompt file: ${(err as Error).message}\n`);
|
|
276
|
-
return USAGE_ERR;
|
|
277
|
-
}
|
|
278
|
-
let resolvedSystem: string | undefined;
|
|
279
|
-
if (systemFlag.value !== null) {
|
|
280
|
-
try {
|
|
281
|
-
resolvedSystem = await readMaybeFile(io, systemFlag.value);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
io.stderr(`failed to read --system file: ${(err as Error).message}\n`);
|
|
284
|
-
return USAGE_ERR;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const mcpRefs: McpServerRef[] = [];
|
|
289
|
-
for (const [name, url] of Object.entries(mcpFlags.entries)) {
|
|
290
|
-
mcpRefs.push({ name, url });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
runConfig = {
|
|
294
|
-
model: modelFlag.value as RunModel,
|
|
295
|
-
...(resolvedSystem ? { system: resolvedSystem } : {}),
|
|
296
|
-
prompt: resolvedPrompt,
|
|
297
|
-
...(mcpRefs.length > 0 ? { mcpServers: mcpRefs } : {}),
|
|
298
|
-
...(Object.keys(metadataFlags.entries).length > 0
|
|
299
|
-
? { metadata: { ...metadataFlags.entries } }
|
|
300
|
-
: {})
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ---------------- Resolve MCP secrets (--mcp-auth + config headers) ----------------
|
|
305
|
-
const mcpServersForSubmission: McpServerRef[] = [...(runConfig.mcpServers ?? [])];
|
|
306
|
-
const mcpServerSecrets: PlatformMcpServerSecret[] = [];
|
|
307
|
-
const mcpHeaderBag = new Map<string, Record<string, string>>(mcpHeadersFromConfig);
|
|
308
|
-
for (const [name, headerSpec] of mcpAuthFlags.entries) {
|
|
309
|
-
const colon = headerSpec.indexOf(":");
|
|
310
|
-
if (colon <= 0 || colon >= headerSpec.length - 1) {
|
|
311
|
-
io.stderr(`--mcp-auth ${name}: expected 'HeaderName:Value' (got: ${headerSpec})\n`);
|
|
312
|
-
return USAGE_ERR;
|
|
313
|
-
}
|
|
314
|
-
const headerName = headerSpec.slice(0, colon).trim();
|
|
315
|
-
const headerValue = headerSpec.slice(colon + 1).trim();
|
|
316
|
-
if (!headerName || !headerValue) {
|
|
317
|
-
io.stderr(`--mcp-auth ${name}: header name and value must be non-empty\n`);
|
|
318
|
-
return USAGE_ERR;
|
|
319
|
-
}
|
|
320
|
-
const existing = mcpHeaderBag.get(name) ?? {};
|
|
321
|
-
if (Object.prototype.hasOwnProperty.call(existing, headerName)) {
|
|
322
|
-
io.stderr(
|
|
323
|
-
`--mcp-auth ${name}: duplicate header "${headerName}" — ` +
|
|
324
|
-
`each header may be set only once per server\n`
|
|
325
|
-
);
|
|
326
|
-
return USAGE_ERR;
|
|
327
|
-
}
|
|
328
|
-
existing[headerName] = headerValue;
|
|
329
|
-
mcpHeaderBag.set(name, existing);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
for (const ref of mcpServersForSubmission) {
|
|
333
|
-
const headers = mcpHeaderBag.get(ref.name);
|
|
334
|
-
if (headers && Object.keys(headers).length > 0) {
|
|
335
|
-
mcpServerSecrets.push({ name: ref.name, url: ref.url, headers });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Reject auth flags that reference an unknown MCP server.
|
|
339
|
-
for (const name of mcpHeaderBag.keys()) {
|
|
340
|
-
if (!mcpServersForSubmission.some((m) => m.name === name)) {
|
|
341
|
-
io.stderr(`--mcp-auth ${name}: no matching --mcp / mcpServers entry declared\n`);
|
|
342
|
-
return USAGE_ERR;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ---------------- Proxy endpoints + auth ----------------
|
|
347
|
-
let proxyEndpoints: readonly PlatformProxyEndpoint[] = runConfig.proxyEndpoints ?? [];
|
|
348
|
-
if (proxyEndpointFlags.values.length > 0) {
|
|
349
|
-
try {
|
|
350
|
-
proxyEndpoints = proxyEndpointFlags.values.map((raw, i) =>
|
|
351
|
-
parseJsonOrThrow<PlatformProxyEndpoint>(raw, `--proxy-endpoint[${i}]`)
|
|
352
|
-
);
|
|
353
|
-
} catch (err) {
|
|
354
|
-
io.stderr(`${(err as Error).message}\n`);
|
|
355
|
-
return USAGE_ERR;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
const proxyAuth: PlatformProxyEndpointAuth[] = [];
|
|
359
|
-
for (const [name, spec] of Object.entries(proxyAuthFlags.entries)) {
|
|
360
|
-
const parsed = parseProxyAuth(spec);
|
|
361
|
-
if (!parsed.ok) {
|
|
362
|
-
io.stderr(`--proxy-auth ${name}: ${parsed.reason}\n`);
|
|
363
|
-
return USAGE_ERR;
|
|
364
|
-
}
|
|
365
|
-
proxyAuth.push({ name, value: parsed.value });
|
|
366
|
-
}
|
|
367
|
-
if (proxyEndpoints.length > 0) {
|
|
368
|
-
try {
|
|
369
|
-
validateProxyAuth(proxyEndpoints, proxyAuth);
|
|
370
|
-
} catch (err) {
|
|
371
|
-
io.stderr(`proxy auth validation failed: ${(err as Error).message}\n`);
|
|
372
|
-
return USAGE_ERR;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// ---------------- Build submission ----------------
|
|
377
|
-
const promptArray = Array.isArray(runConfig.prompt) ? [...runConfig.prompt] : [runConfig.prompt];
|
|
378
|
-
const skills: SkillRef[] = runConfig.skills ? [...runConfig.skills] : [];
|
|
379
|
-
const submission: PlatformSubmission = {
|
|
380
|
-
model: runConfig.model,
|
|
381
|
-
...(runConfig.system ? { system: runConfig.system } : {}),
|
|
382
|
-
prompt: promptArray,
|
|
383
|
-
skills,
|
|
384
|
-
agentsMd: [],
|
|
385
|
-
files: [],
|
|
386
|
-
mcpServers: mcpServersForSubmission,
|
|
387
|
-
tools: [],
|
|
388
|
-
...(runConfig.environment ? { environment: runConfig.environment } : {}),
|
|
389
|
-
...(runConfig.metadata ? { metadata: runConfig.metadata } : {})
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const secrets: PlatformInlineSecrets = {
|
|
393
|
-
apiKey: providerKeyValues[provider] as string,
|
|
394
|
-
...(mcpServerSecrets.length > 0 ? { mcpServers: mcpServerSecrets } : {}),
|
|
395
|
-
...(proxyAuth.length > 0 ? { proxyEndpointAuth: proxyAuth } : {})
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const request: PlatformRunSubmissionInput = {
|
|
399
|
-
idempotencyKey: idempotency.value ?? generateIdempotencyKey(),
|
|
400
|
-
provider,
|
|
401
|
-
...(runtime ? { runtime } : {}),
|
|
402
|
-
submission,
|
|
403
|
-
secrets,
|
|
404
|
-
...(runtimeSizeFlag.value
|
|
405
|
-
? { runtimeSize: runtimeSizeFlag.value as RuntimeSize }
|
|
406
|
-
: runConfig.runtimeSize
|
|
407
|
-
? { runtimeSize: runConfig.runtimeSize }
|
|
408
|
-
: {}),
|
|
409
|
-
...(runTimeoutFlag.value
|
|
410
|
-
? { timeout: runTimeoutFlag.value }
|
|
411
|
-
: runConfig.timeout
|
|
412
|
-
? { timeout: runConfig.timeout }
|
|
413
|
-
: {}),
|
|
414
|
-
...(runConfig.postHook ? { postHook: runConfig.postHook } : {}),
|
|
415
|
-
...(proxyEndpoints.length > 0 ? { proxyEndpoints } : {})
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const http = makeHttpClient(io, common.flags);
|
|
419
|
-
let run;
|
|
420
|
-
try {
|
|
421
|
-
run = await operations.submitRun(http, request);
|
|
422
|
-
} catch (err) {
|
|
423
|
-
return emitJsonError(io, "submit_failed", (err as Error).message ?? "submission failed");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
io.stdout(JSON.stringify(run) + "\n");
|
|
427
|
-
if (!follow.present) return SUCCESS;
|
|
428
|
-
|
|
429
|
-
let emittedEventCount = 0;
|
|
430
|
-
let currentStatus = run.status;
|
|
431
|
-
const deadline = followTimeoutMs === null ? Number.POSITIVE_INFINITY : Date.now() + followTimeoutMs;
|
|
432
|
-
while (!TERMINAL_STATUSES.has(currentStatus)) {
|
|
433
|
-
await sleep(2000);
|
|
434
|
-
try {
|
|
435
|
-
const events = await operations.listRunEvents(http, run.id);
|
|
436
|
-
for (let i = emittedEventCount; i < events.length; i++) {
|
|
437
|
-
io.stdout(JSON.stringify(events[i]) + "\n");
|
|
438
|
-
}
|
|
439
|
-
emittedEventCount = events.length;
|
|
440
|
-
} catch (err) {
|
|
441
|
-
io.stderr(`(transient) event poll failed: ${(err as Error).message}\n`);
|
|
442
|
-
}
|
|
443
|
-
try {
|
|
444
|
-
const updated = await operations.getRun(http, run.id);
|
|
445
|
-
currentStatus = updated.status;
|
|
446
|
-
} catch (err) {
|
|
447
|
-
io.stderr(`(transient) status poll failed: ${(err as Error).message}\n`);
|
|
448
|
-
}
|
|
449
|
-
if (!TERMINAL_STATUSES.has(currentStatus) && Date.now() >= deadline) {
|
|
450
|
-
emitJsonError(io, "run_follow_timeout", `timed out after ${followTimeoutMs}ms following run`, { runId: run.id });
|
|
451
|
-
return TIMEOUT_ERR;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
try {
|
|
455
|
-
const final = await operations.getRun(http, run.id);
|
|
456
|
-
io.stdout(JSON.stringify(final) + "\n");
|
|
457
|
-
return final.status === "succeeded" ? SUCCESS : RUNTIME_ERR;
|
|
458
|
-
} catch (err) {
|
|
459
|
-
io.stderr(`final status fetch failed: ${(err as Error).message}\n`);
|
|
460
|
-
return RUNTIME_ERR;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/* ---------- helpers ---------- */
|
|
465
|
-
|
|
466
|
-
interface ProxyAuthOk { ok: true; value: PlatformProxyAuthValue; }
|
|
467
|
-
interface ProxyAuthErr { ok: false; reason: string; }
|
|
468
|
-
|
|
469
|
-
function parseProxyAuth(spec: string): ProxyAuthOk | ProxyAuthErr {
|
|
470
|
-
const idx = spec.indexOf(":");
|
|
471
|
-
if (idx <= 0) {
|
|
472
|
-
return { ok: false, reason: `expected '<type>:<value>' (got: ${spec})` };
|
|
473
|
-
}
|
|
474
|
-
const type = spec.slice(0, idx);
|
|
475
|
-
const restValue = spec.slice(idx + 1);
|
|
476
|
-
switch (type) {
|
|
477
|
-
case "bearer":
|
|
478
|
-
if (!restValue) return { ok: false, reason: "bearer requires a token value" };
|
|
479
|
-
return { ok: true, value: { type: "bearer", token: restValue } };
|
|
480
|
-
case "header":
|
|
481
|
-
if (!restValue) return { ok: false, reason: "header requires a value" };
|
|
482
|
-
return { ok: true, value: { type: "header", value: restValue } };
|
|
483
|
-
case "query":
|
|
484
|
-
if (!restValue) return { ok: false, reason: "query requires a value" };
|
|
485
|
-
return { ok: true, value: { type: "query", value: restValue } };
|
|
486
|
-
case "basic": {
|
|
487
|
-
const sep = restValue.indexOf(":");
|
|
488
|
-
if (sep <= 0 || sep >= restValue.length - 1) {
|
|
489
|
-
return { ok: false, reason: "basic requires <username>:<password>" };
|
|
490
|
-
}
|
|
491
|
-
return {
|
|
492
|
-
ok: true,
|
|
493
|
-
value: { type: "basic", username: restValue.slice(0, sep), password: restValue.slice(sep + 1) }
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
default:
|
|
497
|
-
return { ok: false, reason: `unknown auth type '${type}' (expected bearer|basic|header|query)` };
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function parseJsonOrThrow<T>(raw: string, label: string): T {
|
|
502
|
-
try {
|
|
503
|
-
return JSON.parse(raw) as T;
|
|
504
|
-
} catch (err) {
|
|
505
|
-
throw new Error(`${label} is not valid JSON: ${(err as Error).message}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Strip `headers` from each `mcpServers[i]` so the value can pass
|
|
511
|
-
* `parseRunRequestConfig` (which forbids headers on the non-secret submission
|
|
512
|
-
* shape). Returns the stripped object plus a name->headers map the
|
|
513
|
-
* caller can fold into `secrets.mcpServers`.
|
|
514
|
-
*/
|
|
515
|
-
function stripMcpHeadersForParsing(input: unknown): {
|
|
516
|
-
readonly normalised: unknown;
|
|
517
|
-
readonly mcpHeaders: Map<string, Record<string, string>>;
|
|
518
|
-
} {
|
|
519
|
-
const mcpHeaders = new Map<string, Record<string, string>>();
|
|
520
|
-
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
521
|
-
return { normalised: input, mcpHeaders };
|
|
522
|
-
}
|
|
523
|
-
const record = input as Record<string, unknown>;
|
|
524
|
-
if (!Array.isArray(record.mcpServers)) {
|
|
525
|
-
return { normalised: input, mcpHeaders };
|
|
526
|
-
}
|
|
527
|
-
const stripped = record.mcpServers.map((entry, i) => {
|
|
528
|
-
if (!entry || typeof entry !== "object") return entry;
|
|
529
|
-
const r = entry as Record<string, unknown>;
|
|
530
|
-
if (r.headers && typeof r.headers === "object" && !Array.isArray(r.headers)) {
|
|
531
|
-
const name = typeof r.name === "string" ? r.name : `__index_${i}__`;
|
|
532
|
-
const headerObj: Record<string, string> = {};
|
|
533
|
-
for (const [k, v] of Object.entries(r.headers as Record<string, unknown>)) {
|
|
534
|
-
if (typeof v !== "string") {
|
|
535
|
-
throw new Error(`mcpServers[${i}].headers["${k}"] must be a string`);
|
|
536
|
-
}
|
|
537
|
-
headerObj[k] = v;
|
|
538
|
-
}
|
|
539
|
-
mcpHeaders.set(name, headerObj);
|
|
540
|
-
const { headers: _omit, ...rest } = r;
|
|
541
|
-
void _omit;
|
|
542
|
-
return rest;
|
|
543
|
-
}
|
|
544
|
-
return r;
|
|
545
|
-
});
|
|
546
|
-
return { normalised: { ...record, mcpServers: stripped }, mcpHeaders };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Resolve a `--system` / `--prompt` argument that may be either a
|
|
551
|
-
* literal string or a file reference. Conventions:
|
|
552
|
-
*
|
|
553
|
-
* plain text -> the text itself
|
|
554
|
-
* "@path/to/file" -> contents of the file (relative to cwd)
|
|
555
|
-
* "@@literal" -> the literal "@literal" (escape for content
|
|
556
|
-
* that genuinely starts with `@`)
|
|
557
|
-
*/
|
|
558
|
-
async function readMaybeFile(io: CliIO, value: string): Promise<string> {
|
|
559
|
-
if (value.startsWith("@@")) {
|
|
560
|
-
return value.slice(1);
|
|
561
|
-
}
|
|
562
|
-
if (value.startsWith("@")) {
|
|
563
|
-
const path = resolvePath(io.cwd(), value.slice(1));
|
|
564
|
-
return io.readFile(path);
|
|
565
|
-
}
|
|
566
|
-
return value;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function generateIdempotencyKey(): string {
|
|
570
|
-
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
571
|
-
if (c?.randomUUID) return c.randomUUID();
|
|
572
|
-
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function sleep(ms: number): Promise<void> {
|
|
576
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
577
|
-
}
|
|
578
|
-
|