promigence 0.1.0-beta.16 → 0.1.0-beta.17
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/dist/account.js +67 -13
- package/dist/billing.js +14 -5
- package/dist/bin.js +5 -1
- package/dist/commands.d.ts +9 -1
- package/dist/commands.js +99 -21
- package/dist/errfix.js +100 -6
- package/dist/generated.js +3 -0
- package/dist/guide.js +5 -5
- package/dist/io.js +1 -1
- package/dist/live.js +36 -4
- package/dist/mcp.js +11 -2
- package/dist/orgs.js +41 -6
- package/dist/report.js +1 -1
- package/dist/routes.js +2 -0
- package/dist/sdk/client.d.ts +123 -10
- package/dist/sdk/client.js +375 -83
- package/dist/sdk/context.d.ts +2 -1
- package/dist/sdk/context.js +5 -0
- package/dist/sdk/core.d.ts +25 -2
- package/dist/sdk/core.js +23 -3
- package/dist/sdk/handles.js +2 -1
- package/dist/sdk/http2.js +5 -0
- package/dist/sdk/models.gen.d.ts +205 -23
- package/dist/sdk/proxy.js +33 -4
- package/dist/sdk/sandbox.d.ts +12 -5
- package/dist/sdk/sandbox.js +1 -1
- package/dist/surface.js +22 -8
- package/dist/uploadguard.js +26 -5
- package/llms.txt +4 -4
- package/package.json +1 -1
package/dist/account.js
CHANGED
|
@@ -69,6 +69,21 @@ export function planDisplay(me, b) {
|
|
|
69
69
|
const name = b?.plan_status?.name;
|
|
70
70
|
return typeof name === "string" && name ? name : planName(me, b);
|
|
71
71
|
}
|
|
72
|
+
function gbText(bytes) {
|
|
73
|
+
const r = Math.round((bytes / 2 ** 30) * 10) / 10;
|
|
74
|
+
return Number.isInteger(r) ? String(r) : r.toFixed(1);
|
|
75
|
+
}
|
|
76
|
+
export function storageLine(me) {
|
|
77
|
+
const s = me?.storage;
|
|
78
|
+
if (!s || typeof s.used_bytes !== "number")
|
|
79
|
+
return undefined;
|
|
80
|
+
const lim = me?.limits ?? {};
|
|
81
|
+
const of = (used, max) => `${gbText(used)} GB${typeof max === "number" ? ` of ${max} GB` : ""}`;
|
|
82
|
+
const capMax = lim.max_capture_storage_gb;
|
|
83
|
+
const captures = typeof s.captures_bytes === "number" ? s.captures_bytes : 0;
|
|
84
|
+
return `storage: ${of(s.used_bytes, lim.max_storage_gb)} used · saved failures ${of(captures, capMax)}` +
|
|
85
|
+
(typeof capMax === "number" ? ", the oldest removed at that limit" : "");
|
|
86
|
+
}
|
|
72
87
|
export function sessionLimit(seconds) {
|
|
73
88
|
return seconds % 3600 === 0 ? `${seconds / 3600} h per sandbox` : `${span(seconds)} per sandbox`;
|
|
74
89
|
}
|
|
@@ -184,7 +199,7 @@ const me = async (ctx) => {
|
|
|
184
199
|
lines.push(` plan: ${shown}${trial?.on_trial ? " (trial)" : ""}`);
|
|
185
200
|
if (conc)
|
|
186
201
|
lines.push(` concurrency: ${concurrencyText(conc)}`);
|
|
187
|
-
const billCredits = creditsLine(bill
|
|
202
|
+
const billCredits = creditsLine(bill);
|
|
188
203
|
if (billCredits)
|
|
189
204
|
lines.push(` ${billCredits}`);
|
|
190
205
|
else if (trial) {
|
|
@@ -210,6 +225,9 @@ const me = async (ctx) => {
|
|
|
210
225
|
].filter(Boolean);
|
|
211
226
|
if (shape.length)
|
|
212
227
|
lines.push(` limits: ${shape.join(" · ")}`);
|
|
228
|
+
const stored = storageLine(info);
|
|
229
|
+
if (stored)
|
|
230
|
+
lines.push(` ${stored}`);
|
|
213
231
|
if (outcomes && outcomes.sandboxes > 0) {
|
|
214
232
|
const [head, ...rest] = outcomeLines(outcomes);
|
|
215
233
|
lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
|
|
@@ -350,7 +368,7 @@ const billingUsage = async (ctx) => {
|
|
|
350
368
|
lines.push(` paid from credits: ${usd(usage["credits_applied"])}`);
|
|
351
369
|
}
|
|
352
370
|
const trial = b.trial;
|
|
353
|
-
const credits = creditsLine(b
|
|
371
|
+
const credits = creditsLine(b) ?? (trial?.credits_remaining === undefined ? undefined : `credits: ${dollars(trial.credits_remaining)} available`);
|
|
354
372
|
if (credits)
|
|
355
373
|
lines.push(` ${credits}`);
|
|
356
374
|
if (b.auto_reload)
|
|
@@ -551,20 +569,54 @@ function stateFlag(ctx, states) {
|
|
|
551
569
|
}
|
|
552
570
|
const BUILD_STATES = ["queued", "running", "verified", "built", "rejected", "failed"];
|
|
553
571
|
const RUN_STATES = ["running", "completed", "cap_killed", "aborted"];
|
|
572
|
+
export const FILTER_PAGES = 10;
|
|
573
|
+
export async function filteredPage(page, keep, limit, cursor) {
|
|
574
|
+
const rows = [];
|
|
575
|
+
const from = (at) => (at === undefined ? {} : { cursor: at });
|
|
576
|
+
let at = cursor;
|
|
577
|
+
let read = 0;
|
|
578
|
+
for (let n = 0; n < FILTER_PAGES; n++) {
|
|
579
|
+
const p = await page({ limit: 500, ...from(at) });
|
|
580
|
+
for (let i = 0; i < p.items.length; i++) {
|
|
581
|
+
const row = p.items[i];
|
|
582
|
+
if (!keep(row))
|
|
583
|
+
continue;
|
|
584
|
+
if (rows.length < limit) {
|
|
585
|
+
rows.push(row);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
read += i;
|
|
589
|
+
const nextCursor = i === 0 ? at ?? null : (await page({ limit: i, ...from(at) })).nextCursor ?? at ?? null;
|
|
590
|
+
return { rows, nextCursor, read, bounded: false };
|
|
591
|
+
}
|
|
592
|
+
read += p.items.length;
|
|
593
|
+
if (!p.nextCursor)
|
|
594
|
+
return { rows, nextCursor: null, read, bounded: false };
|
|
595
|
+
at = p.nextCursor;
|
|
596
|
+
}
|
|
597
|
+
return { rows, nextCursor: at ?? null, read, bounded: true };
|
|
598
|
+
}
|
|
599
|
+
function moreLines(more, filtered, rows, limit, read, what, bounded = false) {
|
|
600
|
+
if (!more)
|
|
601
|
+
return [];
|
|
602
|
+
return [filtered && (bounded || rows < limit) ? ` the newest ${read} ${what} were read; older ones: --cursor ${more}` : ` more: --cursor ${more}`];
|
|
603
|
+
}
|
|
554
604
|
const snapshotBuilds = async (ctx) => {
|
|
555
605
|
const wanted = stateFlag(ctx, BUILD_STATES);
|
|
556
606
|
const limit = limitOf(ctx, 20, 500);
|
|
557
607
|
const cursor = str(ctx, "--cursor");
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
608
|
+
const c = client(ctx);
|
|
609
|
+
const found = wanted === undefined
|
|
610
|
+
? await c.snapshots.buildsPage({ limit, ...(cursor === undefined ? {} : { cursor }) }).then((p) => ({ rows: p.items, nextCursor: p.nextCursor, read: p.items.length, bounded: false }))
|
|
611
|
+
: await filteredPage((o) => c.snapshots.buildsPage(o), (b) => b.state === wanted, limit, cursor);
|
|
612
|
+
const builds = found.rows;
|
|
613
|
+
const more = found.nextCursor;
|
|
562
614
|
const next = nexts(...(more ? [[`${sh `promigence snapshot builds --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(builds.length ? [[sh `promigence snapshot status ${builds[0].build_id} --log 40`, "the newest build, with the tail of its log"]] : []));
|
|
563
615
|
const lines = [
|
|
564
616
|
`promigence snapshot builds: ${builds.length}${wanted ? ` in state ${wanted}` : ""}`,
|
|
565
617
|
...builds.map((b) => ` ${b.build_id} ${b.state} ${b.snapshot_id ?? "—"}${buildAliasNote(b)}${b.error?.code ? ` ${b.error.code}` : ""}`),
|
|
566
618
|
...(builds.length ? [] : [" (no builds)"]),
|
|
567
|
-
...(more
|
|
619
|
+
...moreLines(more, wanted !== undefined, builds.length, limit, found.read, "builds", found.bounded),
|
|
568
620
|
];
|
|
569
621
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { builds, count: builds.length, next_cursor: more }, next), lines, 0, ctx.pointer);
|
|
570
622
|
};
|
|
@@ -598,12 +650,14 @@ const runList = async (ctx) => {
|
|
|
598
650
|
const limit = limitOf(ctx, 20, 500);
|
|
599
651
|
const cursor = str(ctx, "--cursor");
|
|
600
652
|
const filtering = wanted !== undefined || labels.length > 0;
|
|
601
|
-
const page = await client(ctx).runs.listPage({ limit: filtering ? 500 : limit, ...(cursor === undefined ? {} : { cursor }) });
|
|
602
653
|
const labelsOfRun = (r) => (r.labels ?? {});
|
|
603
654
|
const labelled = (r) => labels.every(([k, v]) => labelsOfRun(r)[k] === v);
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
|
|
655
|
+
const c = client(ctx);
|
|
656
|
+
const found = filtering
|
|
657
|
+
? await filteredPage((o) => c.runs.listPage(o), (r) => (wanted === undefined || r.state === wanted) && labelled(r), limit, cursor)
|
|
658
|
+
: await c.runs.listPage({ limit, ...(cursor === undefined ? {} : { cursor }) }).then((p) => ({ rows: p.items, nextCursor: p.nextCursor, read: p.items.length, bounded: false }));
|
|
659
|
+
const runs = found.rows;
|
|
660
|
+
const more = found.nextCursor;
|
|
607
661
|
const labelArgs = labels.map(([k, v]) => sh ` --label ${`${k}=${v}`}`).join("");
|
|
608
662
|
const next = nexts(...(more ? [[`${sh `promigence run list --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${labelArgs}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(runs.length ? [[sh `promigence run report ${runs[0].run_id}`, "the newest run's validity report"]] : []));
|
|
609
663
|
const labelText = (r) => {
|
|
@@ -614,7 +668,7 @@ const runList = async (ctx) => {
|
|
|
614
668
|
`promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}${labels.length ? ` with ${labels.map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`,
|
|
615
669
|
...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}${labelText(r)}`),
|
|
616
670
|
...(runs.length ? [] : [" (no runs)"]),
|
|
617
|
-
...(more
|
|
671
|
+
...moreLines(more, filtering, runs.length, limit, found.read, "runs", found.bounded),
|
|
618
672
|
];
|
|
619
673
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { runs, count: runs.length, next_cursor: more }, next), lines, 0, ctx.pointer);
|
|
620
674
|
};
|
|
@@ -771,7 +825,7 @@ export function pausedLines(sid, s) {
|
|
|
771
825
|
const wakes = Number(s.auto_pause_s ?? 0) > 0;
|
|
772
826
|
return [
|
|
773
827
|
wakes
|
|
774
|
-
? " it
|
|
828
|
+
? " it has an idle rule, so the next request (exec, files, ssh) wakes it, whether it paused itself when idle or was paused by hand"
|
|
775
829
|
: ` it stays paused until \`${sh `promigence sandbox resume ${sid}`}\` (it has no idle rule, so a request does not wake it)`,
|
|
776
830
|
s.pause_expires_at
|
|
777
831
|
? ` its files and state are kept until ${s.pause_expires_at} unless it is resumed before then`
|
package/dist/billing.js
CHANGED
|
@@ -70,14 +70,23 @@ export function autoReloadShort(a) {
|
|
|
70
70
|
const base = `below ${dollars(a.threshold_usd)}, add ${dollars(a.amount_usd)}`;
|
|
71
71
|
return a.paused_reason ? `${base} (paused: ${a.paused_reason.replaceAll("_", " ")})` : base;
|
|
72
72
|
}
|
|
73
|
-
export function creditsLine(
|
|
74
|
-
|
|
73
|
+
export function creditsLine(b, now = Date.now()) {
|
|
74
|
+
const c = b?.credits;
|
|
75
|
+
if (!b || !c)
|
|
75
76
|
return undefined;
|
|
76
|
-
const
|
|
77
|
+
const onTrial = b.trial?.on_trial === true;
|
|
78
|
+
const gated = b.model !== "postpaid" || onTrial;
|
|
79
|
+
const stopped = b.suspended === true ? "the organization is suspended" : b.hold ? "a billing hold" : "";
|
|
80
|
+
const exp = c.next_expiry && dollars(c.next_expiry.amount_usd) !== "$0" && dollars(c.balance_usd) !== "$0" ? c.next_expiry : undefined;
|
|
77
81
|
const days = exp ? Math.ceil((Date.parse(exp.at) - now) / 86_400_000) : NaN;
|
|
82
|
+
const forNewWork = stopped
|
|
83
|
+
? `${gated && c.exhausted ? "used up; " : ""}new work cannot start: ${stopped}`
|
|
84
|
+
: gated
|
|
85
|
+
? (c.exhausted ? "used up: new work cannot start" : c.low ? "running low" : "")
|
|
86
|
+
: `postpaid: new work is billed after use, on the next invoice${b.card_on_file ? ", charged to the card on file" : ""}`;
|
|
78
87
|
const notes = [
|
|
79
88
|
c.held_usd > 0 ? `${dollars(c.held_usd)} held by running work` : "",
|
|
80
|
-
|
|
89
|
+
forNewWork,
|
|
81
90
|
exp ? `${dollars(exp.amount_usd)} expires ${dateWords(exp.at)}${Number.isFinite(days) && days >= 0 ? `, in ${days} day${days === 1 ? "" : "s"}` : ""}` : "",
|
|
82
91
|
].filter(Boolean);
|
|
83
92
|
return `credits: ${dollars(c.available_usd)} available${notes.length ? ` (${notes.join("; ")})` : ""}`;
|
|
@@ -96,7 +105,7 @@ const billing = async (ctx) => {
|
|
|
96
105
|
: `Your ${plan.name} renewal did not go through.${plan.renewal.next_try_at ? ` We try again on ${dateWords(plan.renewal.next_try_at)}.` : ""}`;
|
|
97
106
|
lines.push(` renewal: ${said}${plan.renewal.pay_url ? ` ${plan.renewal.pay_url}` : ""}`);
|
|
98
107
|
}
|
|
99
|
-
const credits = creditsLine(b
|
|
108
|
+
const credits = creditsLine(b);
|
|
100
109
|
if (credits)
|
|
101
110
|
lines.push(` ${credits}`);
|
|
102
111
|
if (b.model === "postpaid" && b.switch) {
|
package/dist/bin.js
CHANGED
|
@@ -362,6 +362,7 @@ async function main(argv) {
|
|
|
362
362
|
keySource: () => keySourceOf(ctx),
|
|
363
363
|
flags: p.flags,
|
|
364
364
|
...(flagValue(p, "--sandbox") === undefined ? {} : { sandbox: flagValue(p, "--sandbox") }),
|
|
365
|
+
argv: p.cmdTail,
|
|
365
366
|
ownAliases: () => client(ctx).snapshots.list({ limit: 200 })
|
|
366
367
|
.then((xs) => xs.map((s) => s.alias).filter((a) => typeof a === "string" && a !== "")),
|
|
367
368
|
billingOff: () => (billingOff ??= client(ctx).billing.usage()
|
|
@@ -393,10 +394,13 @@ async function main(argv) {
|
|
|
393
394
|
Object.assign(env.error, refined.fields);
|
|
394
395
|
if (runId !== undefined)
|
|
395
396
|
env["run_id"] = runId;
|
|
397
|
+
const lostIn = e.code === "exec_lost" ? e.sandboxId : undefined;
|
|
398
|
+
if (typeof lostIn === "string")
|
|
399
|
+
env["sandbox_id"] = lostIn;
|
|
396
400
|
let exit = e.cliExit;
|
|
397
401
|
if (cmd.passthroughExit && !p.flags.has("--no-passthrough") && !(cmd.name === "exec" && p.flags.has("--run"))) {
|
|
398
402
|
env.promigence_exit = exit;
|
|
399
|
-
exit = e.code === "enoent" ? 127 : cmd.passthroughExit;
|
|
403
|
+
exit = e.code === "enoent" ? 127 : exit === 2 ? 2 : cmd.passthroughExit;
|
|
400
404
|
}
|
|
401
405
|
if (e.code === "credit_unclaimed" && mode === "text" && canAsk()) {
|
|
402
406
|
const lines = await claimOnRefusal(ctx).catch(() => undefined);
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PromigenceClient, PromigenceError, type ExecResponse, type KeyOrigin, type OptimizeFor, type RunReport, type SandboxRef, type ScoreMode, type SecretRecord, type SnapshotBuild } from "./sdk/index.js";
|
|
1
|
+
import { PromigenceClient, PromigenceError, type CaptureMode, type ExecResponse, type KeyOrigin, type OptimizeFor, type RunReport, type SandboxRef, type ScoreMode, type SecretRecord, type SnapshotBuild } from "./sdk/index.js";
|
|
2
2
|
import { type Mode } from "./io.js";
|
|
3
3
|
import { type BuildPrice } from "./pricing.js";
|
|
4
4
|
import { type Command, type NextHint } from "./surface.js";
|
|
@@ -47,6 +47,7 @@ export declare function buildReplayNote(started: {
|
|
|
47
47
|
idempotent_replay_age_s?: number;
|
|
48
48
|
}, callersKey: boolean): string | undefined;
|
|
49
49
|
export declare function optimizeForFlag(ctx: Ctx): OptimizeFor | undefined;
|
|
50
|
+
export declare function captureFlag(ctx: Ctx): CaptureMode | undefined;
|
|
50
51
|
export declare function envFlags(ctx: Ctx): Record<string, string> | undefined;
|
|
51
52
|
export declare function emptyAllowList(flag: "--allow" | "allow_hosts"): PromigenceError;
|
|
52
53
|
export declare function gradeOpts(ctx: Ctx): {
|
|
@@ -114,6 +115,13 @@ export declare function runHeadline(report: RunReport, count: number): string;
|
|
|
114
115
|
export declare const FAILURES_SHOWN = 5;
|
|
115
116
|
export declare function limitPauseLines(report: RunReport): string[];
|
|
116
117
|
export declare function failureLines(report: RunReport, outputs: ReadonlyMap<string, OutputTail>, stderrTail: boolean): string[];
|
|
118
|
+
export declare function failedSandboxNext(runId: string, s: {
|
|
119
|
+
sandbox_id: string;
|
|
120
|
+
failure?: unknown;
|
|
121
|
+
execs?: ReadonlyArray<{
|
|
122
|
+
exec_id?: string;
|
|
123
|
+
}> | null;
|
|
124
|
+
}, gradeOutput?: boolean): [string, string] | undefined;
|
|
117
125
|
export interface OutputTail {
|
|
118
126
|
stdout: string;
|
|
119
127
|
stderr: string;
|
package/dist/commands.js
CHANGED
|
@@ -177,6 +177,16 @@ export function optimizeForFlag(ctx) {
|
|
|
177
177
|
}
|
|
178
178
|
return v;
|
|
179
179
|
}
|
|
180
|
+
const CAPTURE_MODES = ["on_failure", "off", "always"];
|
|
181
|
+
export function captureFlag(ctx) {
|
|
182
|
+
const v = str(ctx, "--capture");
|
|
183
|
+
if (v === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (!CAPTURE_MODES.includes(v)) {
|
|
186
|
+
throw usageError(`--capture: '${v}' is not a choice`, `Pass --capture ${CAPTURE_MODES.join(" | ")}.`);
|
|
187
|
+
}
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
180
190
|
export function envFlags(ctx) {
|
|
181
191
|
const env = {};
|
|
182
192
|
for (const kv of list(ctx, "--env")) {
|
|
@@ -210,6 +220,9 @@ async function runStartOpts(ctx, c, resolved) {
|
|
|
210
220
|
const optimizeFor = optimizeForFlag(ctx);
|
|
211
221
|
if (optimizeFor !== undefined)
|
|
212
222
|
opts.optimizeFor = optimizeFor;
|
|
223
|
+
const capture = captureFlag(ctx);
|
|
224
|
+
if (capture !== undefined)
|
|
225
|
+
opts.capture = capture;
|
|
213
226
|
const network = str(ctx, "--network");
|
|
214
227
|
const allow = list(ctx, "--allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean);
|
|
215
228
|
if (ctx.flags.has("--allow") && !allow.length)
|
|
@@ -417,7 +430,9 @@ const secretsSet = async (ctx) => {
|
|
|
417
430
|
...(noRaw ? { raw: "refused" } : {}),
|
|
418
431
|
};
|
|
419
432
|
const res = await c.secrets.set(name, value, opts);
|
|
420
|
-
const
|
|
433
|
+
const warnings = (res.warnings ?? []).filter((w) => typeof w === "string");
|
|
434
|
+
const isKey = warnings.some((w) => w.startsWith("api_key_value:"));
|
|
435
|
+
const next = nexts([`${sh `promigence run --count 1 --cap 1 --secret ${name}`} -- CMD`, "give it to every sandbox of a run"], ...(isKey ? [[sh `promigence secrets set ${name} --delete`, "delete it: a sandbox given it can use the key"]] : []), ["promigence secrets set --list", "what this org has stored"]);
|
|
421
436
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
422
437
|
name: res.name, bytes: res.bytes, created: res.created ?? true, source,
|
|
423
438
|
...(res.hosts?.length
|
|
@@ -427,6 +442,7 @@ const secretsSet = async (ctx) => {
|
|
|
427
442
|
...(res.env && Object.keys(res.env).length ? { env: res.env } : {}),
|
|
428
443
|
}
|
|
429
444
|
: {}),
|
|
445
|
+
...(warnings.length ? { warnings } : {}),
|
|
430
446
|
}, next), [
|
|
431
447
|
`${sh `promigence secrets set ${name}`}: stored ${res.bytes} bytes from ${source}`,
|
|
432
448
|
...(res.hosts?.length
|
|
@@ -437,6 +453,7 @@ const secretsSet = async (ctx) => {
|
|
|
437
453
|
]
|
|
438
454
|
: [" the value is encrypted at rest and redacted wherever it appears verbatim in logs, reports and command output",
|
|
439
455
|
" redaction is best effort (an encoded or altered value prints in clear): never print a secret"]),
|
|
456
|
+
...warnings.map((w) => ` warning: ${w}`),
|
|
440
457
|
` next: ${next[0].cmd}`,
|
|
441
458
|
], 0, false);
|
|
442
459
|
};
|
|
@@ -1410,6 +1427,7 @@ function wantsStream(ctx) {
|
|
|
1410
1427
|
return Boolean(process.stdout.isTTY);
|
|
1411
1428
|
}
|
|
1412
1429
|
let followKill;
|
|
1430
|
+
const EXEC_STOP_WAIT_MS = 4_000;
|
|
1413
1431
|
const FOLLOW_MAX_OUTPUT = 8 * 1024 * 1024;
|
|
1414
1432
|
const FOLLOW_RECONNECT_MS = 10 * 60_000;
|
|
1415
1433
|
async function followExec(c, sandboxId, execOpts, cut) {
|
|
@@ -1674,14 +1692,25 @@ const exec = async (ctx) => {
|
|
|
1674
1692
|
if (ctx.mode === "text" && !process.stdout.isTTY && execOpts.maxOutputBytes === undefined) {
|
|
1675
1693
|
execOpts.maxOutputBytes = MAX_OUTPUT_BYTES_PIPED;
|
|
1676
1694
|
}
|
|
1695
|
+
const stop = new AbortController();
|
|
1696
|
+
let foreground;
|
|
1697
|
+
const stopForeground = () => {
|
|
1698
|
+
stop.abort();
|
|
1699
|
+
if (foreground === undefined)
|
|
1700
|
+
return Promise.resolve();
|
|
1701
|
+
return Promise.race([
|
|
1702
|
+
foreground.then(() => undefined, () => undefined),
|
|
1703
|
+
new Promise((r) => setTimeout(r, EXEC_STOP_WAIT_MS)),
|
|
1704
|
+
]);
|
|
1705
|
+
};
|
|
1677
1706
|
const interrupted = (sig) => {
|
|
1678
|
-
const ending = followKill?.() ??
|
|
1707
|
+
const ending = followKill?.() ?? stopForeground();
|
|
1679
1708
|
void ending.finally(() => interruptedEnvelope(sig));
|
|
1680
1709
|
};
|
|
1681
1710
|
const interruptedEnvelope = (sig) => {
|
|
1682
1711
|
const err = new PromigenceError({
|
|
1683
1712
|
code: "aborted_by_client",
|
|
1684
|
-
message: `interrupted by ${sig}; the command in the sandbox
|
|
1713
|
+
message: `interrupted by ${sig}; the command in the sandbox is stopped`,
|
|
1685
1714
|
fix: "Nothing to fix; billed for the seconds used. The sandbox itself is still running.",
|
|
1686
1715
|
exitCode: 130,
|
|
1687
1716
|
});
|
|
@@ -1696,9 +1725,22 @@ const exec = async (ctx) => {
|
|
|
1696
1725
|
const followed = streaming && ctx.mode === "text" && process.env["PROMIGENCE_EXEC_FOLLOW"] !== "0"
|
|
1697
1726
|
? await followExec(c, sandboxId, execOpts, maxOutput === undefined ? undefined : execOpts.maxOutputBytes)
|
|
1698
1727
|
: undefined;
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1728
|
+
let res;
|
|
1729
|
+
if (followed !== undefined)
|
|
1730
|
+
res = followed;
|
|
1731
|
+
else {
|
|
1732
|
+
foreground = streaming
|
|
1733
|
+
? streamExec(ctx, c, sandboxId, { ...execOpts, signal: stop.signal })
|
|
1734
|
+
: c.sandboxes.exec(sandboxId, { ...execOpts, encoding: "base64", signal: stop.signal }).then(asText);
|
|
1735
|
+
try {
|
|
1736
|
+
res = await foreground;
|
|
1737
|
+
}
|
|
1738
|
+
catch (err) {
|
|
1739
|
+
if (stop.signal.aborted)
|
|
1740
|
+
return new Promise(() => { });
|
|
1741
|
+
throw err;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1702
1744
|
const execTotalMs = performance.now() - execStarted;
|
|
1703
1745
|
const outcome = execOutcome(res, sandboxId, execOpts.timeoutMs);
|
|
1704
1746
|
const passthrough = !bool(ctx, "--no-passthrough");
|
|
@@ -1766,9 +1808,22 @@ const exec = async (ctx) => {
|
|
|
1766
1808
|
? "still waiting for capacity being added (pass --wait)"
|
|
1767
1809
|
: s.state === "queued" ? "not ready (pass --wait)" : "ended",
|
|
1768
1810
|
}));
|
|
1811
|
+
const stop = new AbortController();
|
|
1812
|
+
const running = [];
|
|
1813
|
+
const onSignal = (sig) => {
|
|
1814
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
1815
|
+
process.removeListener(s, onSignal);
|
|
1816
|
+
stop.abort();
|
|
1817
|
+
void Promise.race([Promise.allSettled(running), new Promise((r) => setTimeout(r, EXEC_STOP_WAIT_MS))])
|
|
1818
|
+
.finally(() => process.kill(process.pid, sig));
|
|
1819
|
+
};
|
|
1820
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
1821
|
+
process.once(sig, onSignal);
|
|
1769
1822
|
const rows = await Promise.all(targets.map(async (s) => {
|
|
1770
1823
|
try {
|
|
1771
|
-
const
|
|
1824
|
+
const call = c.sandboxes.exec(s.sandbox_id, { ...execOpts, signal: stop.signal });
|
|
1825
|
+
running.push(call);
|
|
1826
|
+
const res = await call;
|
|
1772
1827
|
const o = execOutcome(res, s.sandbox_id, execOpts.timeoutMs);
|
|
1773
1828
|
return {
|
|
1774
1829
|
sandbox_id: s.sandbox_id, exec_id: res.exec_id, ...o.fields, exit_code: o.exitCode,
|
|
@@ -1781,6 +1836,10 @@ const exec = async (ctx) => {
|
|
|
1781
1836
|
return { sandbox_id: s.sandbox_id, error: e.toJSON(), exit: e.cliExit };
|
|
1782
1837
|
}
|
|
1783
1838
|
}));
|
|
1839
|
+
if (stop.signal.aborted)
|
|
1840
|
+
return new Promise(() => { });
|
|
1841
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
1842
|
+
process.removeListener(sig, onSignal);
|
|
1784
1843
|
const codes = [];
|
|
1785
1844
|
for (const r of rows) {
|
|
1786
1845
|
if (!("exec_id" in r))
|
|
@@ -2113,17 +2172,14 @@ const runComposite = async (ctx) => {
|
|
|
2113
2172
|
: []));
|
|
2114
2173
|
const outputs = streaming ? new Map() : await outputTails(c, report.sandboxes ?? []);
|
|
2115
2174
|
const culprit = exit === 0 ? undefined : (report.sandboxes ?? []).find((s) => s.state === "ended" && OUTCOME_EXIT[s.outcome ?? ""] === exit && s.execs?.length);
|
|
2116
|
-
const
|
|
2117
|
-
const firstExec =
|
|
2118
|
-
if (
|
|
2119
|
-
next.push(...nexts(
|
|
2120
|
-
report.requested?.grade_output ? "its grade, and what the grader printed" : "its grade (what the grader prints is kept only with --grade-output)"]));
|
|
2175
|
+
const culpritNext = culprit === undefined ? undefined : failedSandboxNext(runId, culprit, Boolean(report.requested?.grade_output));
|
|
2176
|
+
const firstExec = (report.sandboxes ?? []).find((s) => s.execs?.length);
|
|
2177
|
+
if (culpritNext) {
|
|
2178
|
+
next.push(...nexts(culpritNext));
|
|
2121
2179
|
}
|
|
2122
2180
|
else if (firstExec) {
|
|
2123
2181
|
const eid = firstExec.execs[firstExec.execs.length - 1].exec_id;
|
|
2124
|
-
next.push(...nexts(
|
|
2125
|
-
? [sh `promigence exec output ${eid} --sandbox ${firstExec.sandbox_id} --stderr`, "the failed sandbox's command output (stderr; drop --stderr for stdout)"]
|
|
2126
|
-
: [sh `promigence exec output ${eid} --sandbox ${firstExec.sandbox_id}`, "one sandbox's full command output"]));
|
|
2182
|
+
next.push(...nexts([sh `promigence exec output ${eid} --sandbox ${firstExec.sandbox_id}`, "one sandbox's full command output"]));
|
|
2127
2183
|
}
|
|
2128
2184
|
const common = commonWarnings(report.sandboxes ?? []);
|
|
2129
2185
|
const ownWarnings = (w) => (w ?? []).filter((x) => !common.includes(x));
|
|
@@ -2370,8 +2426,9 @@ export function failureLines(report, outputs, stderrTail) {
|
|
|
2370
2426
|
lines.push(` stderr: ${l}`);
|
|
2371
2427
|
}
|
|
2372
2428
|
lines.push(` fix: ${failureFix(report.run_id, s)}`);
|
|
2373
|
-
|
|
2374
|
-
|
|
2429
|
+
const next = failedSandboxNext(report.run_id, s);
|
|
2430
|
+
if (next)
|
|
2431
|
+
lines.push(` next: ${next[0]}`);
|
|
2375
2432
|
}
|
|
2376
2433
|
if (failed.length > FAILURES_SHOWN) {
|
|
2377
2434
|
lines.push(` … ${failed.length - FAILURES_SHOWN} more: ${sh `promigence run report ${report.run_id} --sandboxes failed`}`);
|
|
@@ -2381,6 +2438,14 @@ export function failureLines(report, outputs, stderrTail) {
|
|
|
2381
2438
|
lines.push(` note: ${net}`);
|
|
2382
2439
|
return lines;
|
|
2383
2440
|
}
|
|
2441
|
+
export function failedSandboxNext(runId, s, gradeOutput = false) {
|
|
2442
|
+
if (s.failure?.code === "grade_not_passed") {
|
|
2443
|
+
return [sh `promigence run report ${runId} --sandbox ${s.sandbox_id}`,
|
|
2444
|
+
gradeOutput ? "its grade, and what the grader printed" : "its grade (what the grader prints is kept only with --grade-output)"];
|
|
2445
|
+
}
|
|
2446
|
+
const eid = s.execs?.at(-1)?.exec_id;
|
|
2447
|
+
return eid ? [sh `promigence exec output ${eid} --sandbox ${s.sandbox_id} --stderr`, "the failed sandbox's command output (stderr; drop --stderr for stdout)"] : undefined;
|
|
2448
|
+
}
|
|
2384
2449
|
export const OUTPUT_TAIL_SANDBOXES = 20;
|
|
2385
2450
|
export const OUTPUT_TAIL_CHARS = 4096;
|
|
2386
2451
|
export async function outputTails(c, sandboxes) {
|
|
@@ -2609,7 +2674,9 @@ const sandboxNetwork = async (ctx) => {
|
|
|
2609
2674
|
const v = await client(ctx).sandboxes.network(sid);
|
|
2610
2675
|
const mode = typeof v.network === "string" ? v.network : `only ${v.network.allow.join(", ")}`;
|
|
2611
2676
|
const lines = [`sandbox ${sid}: ${mode}`, ` ${networkMeaning(v.network)}`];
|
|
2612
|
-
const refused = v.refused_hosts
|
|
2677
|
+
const refused = Array.isArray(v.refused_hosts) ? v.refused_hosts : [];
|
|
2678
|
+
const counted = (k) => typeof v[k] === "number";
|
|
2679
|
+
const known = counted("refused_lookups") || counted("refused_connections");
|
|
2613
2680
|
for (const r of refused) {
|
|
2614
2681
|
lines.push(` refused ${r.host}${r.count ? ` (looked up ${r.count} time${r.count === 1 ? "" : "s"})` : ""}`);
|
|
2615
2682
|
}
|
|
@@ -2617,11 +2684,18 @@ const sandboxNetwork = async (ctx) => {
|
|
|
2617
2684
|
if (toAddresses > 0) {
|
|
2618
2685
|
lines.push(` refused ${toAddresses} connection${toAddresses === 1 ? "" : "s"} to an address with no name (counted; addresses are not recorded)`);
|
|
2619
2686
|
}
|
|
2687
|
+
if (refused.length && !known)
|
|
2688
|
+
lines.push(" (as recorded while it ran: it may not list every refusal)");
|
|
2620
2689
|
if (refused.length) {
|
|
2621
2690
|
lines.push(`add one with --allow HOST when you start the sandbox`);
|
|
2622
2691
|
}
|
|
2623
2692
|
else if (typeof v.network !== "string" && toAddresses === 0) {
|
|
2624
|
-
|
|
2693
|
+
const why = v.state === "ended"
|
|
2694
|
+
? "it has ended, and refusals are listed while a sandbox runs"
|
|
2695
|
+
: v.state === "ready" || v.state === "running"
|
|
2696
|
+
? "they could not be read just now; run this again"
|
|
2697
|
+
: `it is ${v.state || "not running"}, and refusals are listed while a sandbox runs`;
|
|
2698
|
+
lines.push(known ? " nothing has been refused" : ` what it was refused is not known: ${why}`);
|
|
2625
2699
|
}
|
|
2626
2700
|
for (const b of v.via_secret ?? []) {
|
|
2627
2701
|
lines.push(` via secret ${b.secret}: ${b.hosts.join(", ") || "(no hosts)"}`);
|
|
@@ -2847,10 +2921,12 @@ const filesPut = async (ctx) => {
|
|
|
2847
2921
|
export function transferProgress(label, total, mode, out = process.stderr, now = Date.now, env = process.env) {
|
|
2848
2922
|
if (mode !== "text" || !out.isTTY || env["PROMIGENCE_QUIET"] === "1")
|
|
2849
2923
|
return { add: () => { }, done: () => { } };
|
|
2924
|
+
const plain = "NO_COLOR" in env || env["TERM"] === "dumb";
|
|
2850
2925
|
const t0 = now();
|
|
2851
2926
|
let moved = 0;
|
|
2852
2927
|
let drawn = 0;
|
|
2853
2928
|
let shown = false;
|
|
2929
|
+
let width = 0;
|
|
2854
2930
|
return {
|
|
2855
2931
|
add: (n) => {
|
|
2856
2932
|
moved += n;
|
|
@@ -2860,11 +2936,13 @@ export function transferProgress(label, total, mode, out = process.stderr, now =
|
|
|
2860
2936
|
drawn = t;
|
|
2861
2937
|
shown = true;
|
|
2862
2938
|
const of = total ? ` of ${bytes(total)} (${Math.min(100, Math.floor((moved * 100) / total))}%)` : "";
|
|
2863
|
-
|
|
2939
|
+
const line = `${label}: ${bytes(moved)}${of}`;
|
|
2940
|
+
out.write(plain ? `\r${line}${" ".repeat(Math.max(0, width - line.length))}` : `\r${line}\x1b[K`);
|
|
2941
|
+
width = line.length;
|
|
2864
2942
|
},
|
|
2865
2943
|
done: () => {
|
|
2866
2944
|
if (shown)
|
|
2867
|
-
out.write("\r\x1b[K");
|
|
2945
|
+
out.write(plain ? `\r${" ".repeat(width)}\r` : "\r\x1b[K");
|
|
2868
2946
|
},
|
|
2869
2947
|
};
|
|
2870
2948
|
}
|