terminalhire 0.42.43 → 0.42.45
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/bin/jpi-claim.js +2084 -1496
- package/dist/bin/jpi-connector-header.js +584 -0
- package/dist/bin/jpi-dispatch.js +7174 -5598
- package/dist/bin/jpi-mcp.js +2095 -1503
- package/dist/bin/jpi-post.js +454 -36
- package/dist/bin/jpi-run.js +2099 -1511
- package/dist/bin/jpi-setup.js +1509 -0
- package/dist/src/founder-connector.js +382 -0
- package/dist/src/founder-oauth.js +253 -0
- package/package.json +1 -1
package/dist/bin/jpi-claim.js
CHANGED
|
@@ -6934,7 +6934,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
6934
6934
|
});
|
|
6935
6935
|
const { prehash } = eddsaOpts;
|
|
6936
6936
|
const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
|
|
6937
|
-
const
|
|
6937
|
+
const randomBytes14 = eddsaOpts.randomBytes || randomBytes;
|
|
6938
6938
|
const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
|
|
6939
6939
|
const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
|
|
6940
6940
|
_abool2(phflag, "phflag");
|
|
@@ -7016,7 +7016,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
7016
7016
|
signature: 2 * _size,
|
|
7017
7017
|
seed: _size
|
|
7018
7018
|
};
|
|
7019
|
-
function randomSecretKey(seed =
|
|
7019
|
+
function randomSecretKey(seed = randomBytes14(lengths.seed)) {
|
|
7020
7020
|
return _abytes2(seed, lengths.seed, "seed");
|
|
7021
7021
|
}
|
|
7022
7022
|
function keygen(seed) {
|
|
@@ -26201,6 +26201,14 @@ var init_progress = __esm({
|
|
|
26201
26201
|
});
|
|
26202
26202
|
|
|
26203
26203
|
// ../../packages/envrun/dist/classify.js
|
|
26204
|
+
function venueExhaustionFrom(minFreeMiB, stdout, stderr) {
|
|
26205
|
+
if (!(minFreeMiB < LAYER_STORAGE_BAND_MIB))
|
|
26206
|
+
return null;
|
|
26207
|
+
return outputNamesNoSpace(stdout, stderr) ? "layer storage full" : null;
|
|
26208
|
+
}
|
|
26209
|
+
function outputNamesNoSpace(stdout, stderr) {
|
|
26210
|
+
return NO_SPACE.test(stdout) || NO_SPACE.test(stderr);
|
|
26211
|
+
}
|
|
26204
26212
|
function reportedCounts(classified, reparsed) {
|
|
26205
26213
|
return classified !== void 0 ? classified : reparsed;
|
|
26206
26214
|
}
|
|
@@ -26289,7 +26297,7 @@ ${facts.stderr}`);
|
|
|
26289
26297
|
function resourceExhaustion(facts, counts) {
|
|
26290
26298
|
if (counts !== null && counts.tests_failed > 0)
|
|
26291
26299
|
return null;
|
|
26292
|
-
if (
|
|
26300
|
+
if (NO_SPACE.test(facts.stderr))
|
|
26293
26301
|
return "no space left on device";
|
|
26294
26302
|
if (SUITE_REPORTED_FAILURE.test(`${facts.stdout}
|
|
26295
26303
|
${facts.stderr}`))
|
|
@@ -26349,6 +26357,13 @@ function classifyVerification(facts) {
|
|
|
26349
26357
|
reason: "the run was killed for exceeding its time budget, so no result is trustworthy"
|
|
26350
26358
|
};
|
|
26351
26359
|
}
|
|
26360
|
+
if (facts.venueExhausted) {
|
|
26361
|
+
return {
|
|
26362
|
+
outcome: "environment-exhausted",
|
|
26363
|
+
counts: null,
|
|
26364
|
+
reason: "our layer storage on the verification venue filled during the test step (it was measured within 1 GiB of full and the output reports no space left on device). This is an environment failure on our side and the repo has NOT been judged."
|
|
26365
|
+
};
|
|
26366
|
+
}
|
|
26352
26367
|
const counts = readCounts(facts.stdout, facts.stderr);
|
|
26353
26368
|
const exhausted = resourceExhaustion(facts, counts);
|
|
26354
26369
|
if (exhausted !== null) {
|
|
@@ -26447,7 +26462,12 @@ function lastStepMarker(text) {
|
|
|
26447
26462
|
last = m[1];
|
|
26448
26463
|
return last !== null && isStepName(last) ? last : null;
|
|
26449
26464
|
}
|
|
26450
|
-
|
|
26465
|
+
function stepNameOf(line) {
|
|
26466
|
+
const m = STEP_MARKER_LINE.exec(line);
|
|
26467
|
+
const name = m?.[1] ?? null;
|
|
26468
|
+
return name !== null && isStepName(name) ? name : null;
|
|
26469
|
+
}
|
|
26470
|
+
var VERIFICATION_OUTCOMES, NO_SPACE, LAYER_STORAGE_BAND_MIB, int, withSuiteFailures, countLines, READERS, SUPPORTED_RUNNERS, COVERAGE_TABLE, EXEC_FAILURE, SUITE_REPORTED_FAILURE, MISSING_SYSTEM_DEPENDENCY, OFFLINE_BUILD_GAP, LIMIT_OWNER, NPM_STEP_MARKER, STOPPED_AT_MAX, SCRIPT_NAME, STEP_MARKER_LINE;
|
|
26451
26471
|
var init_classify2 = __esm({
|
|
26452
26472
|
"../../packages/envrun/dist/classify.js"() {
|
|
26453
26473
|
"use strict";
|
|
@@ -26460,6 +26480,8 @@ var init_classify2 = __esm({
|
|
|
26460
26480
|
"environment-exhausted",
|
|
26461
26481
|
"budget-exceeded"
|
|
26462
26482
|
];
|
|
26483
|
+
NO_SPACE = /no space left on device|ENOSPC/i;
|
|
26484
|
+
LAYER_STORAGE_BAND_MIB = 1024;
|
|
26463
26485
|
int = (m, i = 1) => m ? Number(m[i]) : 0;
|
|
26464
26486
|
withSuiteFailures = (counts, suiteLine) => {
|
|
26465
26487
|
if (counts.tests_failed > 0 || !suiteLine)
|
|
@@ -26873,6 +26895,7 @@ var init_classify2 = __esm({
|
|
|
26873
26895
|
NPM_STEP_MARKER = /^> (?:@[^\s@/]+\/)?[^\s@]+@\S+ (\S+)/gm;
|
|
26874
26896
|
STOPPED_AT_MAX = 200;
|
|
26875
26897
|
SCRIPT_NAME = /^[A-Za-z0-9:_.\-/+@]+$/;
|
|
26898
|
+
STEP_MARKER_LINE = new RegExp(NPM_STEP_MARKER.source);
|
|
26876
26899
|
}
|
|
26877
26900
|
});
|
|
26878
26901
|
|
|
@@ -27466,6 +27489,7 @@ var init_egressProxy = __esm({
|
|
|
27466
27489
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
27467
27490
|
import { dirname as dirname7, join as join20 } from "path";
|
|
27468
27491
|
import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync10, mkdtempSync, rmSync as rmSync6, statSync as statSync4 } from "fs";
|
|
27492
|
+
import { isIPv4 } from "net";
|
|
27469
27493
|
import { tmpdir } from "os";
|
|
27470
27494
|
function scrubEnvPathsFor(containmentKind, host) {
|
|
27471
27495
|
return containmentKind === "container" ? { jailHome: GUEST.jail, tmpDir: GUEST.tmp, jailHomeNamespace: "guest" } : { jailHome: host.jail, tmpDir: host.tmp, jailHomeNamespace: "host" };
|
|
@@ -27625,10 +27649,7 @@ function stageMounts(spec) {
|
|
|
27625
27649
|
const cloneSource = spec.cloneVolume === void 0 ? clone : validateVolumeName(spec.cloneVolume, "the clone volume");
|
|
27626
27650
|
const jail = resolve6(spec.jail, "jail");
|
|
27627
27651
|
assertHoldsNoRealHome(jail, "the jail bind source", "stageMounts");
|
|
27628
|
-
return [
|
|
27629
|
-
`--volume=${cloneSource}:${GUEST.clone}:rw`,
|
|
27630
|
-
`--volume=${jail}:${GUEST.jail}:rw`
|
|
27631
|
-
];
|
|
27652
|
+
return [`--volume=${cloneSource}:${GUEST.clone}:rw`, `--volume=${jail}:${GUEST.jail}:rw`];
|
|
27632
27653
|
}
|
|
27633
27654
|
function guestIdentityMounts(spec) {
|
|
27634
27655
|
if (guestUserFlag(spec).length === 0)
|
|
@@ -27642,6 +27663,17 @@ function guestIdentityMounts(spec) {
|
|
|
27642
27663
|
return [`--volume=${passwd}:/etc/passwd:ro`, `--volume=${group}:/etc/group:ro`];
|
|
27643
27664
|
}
|
|
27644
27665
|
function containerArgs(spec, env, opts) {
|
|
27666
|
+
if (opts.runtime !== void 0 && !RUNTIME_NAME.test(opts.runtime)) {
|
|
27667
|
+
throw new FenceError(`refusing runtime ${JSON.stringify(opts.runtime)}: not a runtime name`);
|
|
27668
|
+
}
|
|
27669
|
+
if (opts.networkOf !== void 0) {
|
|
27670
|
+
if (!NETNS_HOLDER_NAME.test(opts.networkOf)) {
|
|
27671
|
+
throw new FenceError(`refusing to join ${JSON.stringify(opts.networkOf)}: not a netns holder's name`);
|
|
27672
|
+
}
|
|
27673
|
+
if (spec.profile !== "offline" || opts.net) {
|
|
27674
|
+
throw new FenceError("only an offline step joins a netns holder; its network is none");
|
|
27675
|
+
}
|
|
27676
|
+
}
|
|
27645
27677
|
if (spec.profile === "offline" && opts.net) {
|
|
27646
27678
|
throw new FenceError("the offline profile grants no network; passing a sidecar net is a category error");
|
|
27647
27679
|
}
|
|
@@ -27658,10 +27690,11 @@ function containerArgs(spec, env, opts) {
|
|
|
27658
27690
|
// Offline: no interface at all. Install: ONLY the sidecar's internal network
|
|
27659
27691
|
// (no gateway), so the proxy container is the guest's sole route out and the
|
|
27660
27692
|
// allowlist is enforced, not advisory.
|
|
27661
|
-
opts.net ? `--network=${opts.net.network}` : "--network=none",
|
|
27693
|
+
opts.net ? `--network=${opts.net.network}` : opts.networkOf !== void 0 ? `--network=container:${opts.networkOf}` : "--network=none",
|
|
27662
27694
|
"--cap-drop=ALL",
|
|
27663
27695
|
"--security-opt=no-new-privileges",
|
|
27664
27696
|
"--pids-limit=512",
|
|
27697
|
+
...opts.runtime === void 0 ? [] : [`--runtime=${opts.runtime}`],
|
|
27665
27698
|
...labelArgs(opts.labels),
|
|
27666
27699
|
// TERM-198. Run as the HOST owner of the three bind mounts, not as the
|
|
27667
27700
|
// image's default uid 0. On Linux a bind mount is a passthrough: the guest
|
|
@@ -27820,7 +27853,8 @@ function dockerSync(d, args, timeoutMs = DOCKER_TIMEOUT_MS) {
|
|
|
27820
27853
|
return {
|
|
27821
27854
|
ok: !res.error && res.status === 0,
|
|
27822
27855
|
stdout: res.stdout,
|
|
27823
|
-
stderr: (res.error ? res.error.message : "") + res.stderr
|
|
27856
|
+
stderr: (res.error ? res.error.message : "") + res.stderr,
|
|
27857
|
+
timedOut: res.error?.code === "ETIMEDOUT"
|
|
27824
27858
|
};
|
|
27825
27859
|
}
|
|
27826
27860
|
function pickProxyDir(baseDir) {
|
|
@@ -27951,6 +27985,12 @@ async function startProxySidecar(d, allow, idBase, staged, labels) {
|
|
|
27951
27985
|
"-d",
|
|
27952
27986
|
`--name=${proxyName}`,
|
|
27953
27987
|
`--network=${netInt}`,
|
|
27988
|
+
// runc, whatever the daemon's default (TERM-1336, design D2). On the
|
|
27989
|
+
// Confidential Space venue dockerd defaults to gVisor (runsc), and a runsc
|
|
27990
|
+
// container could not reach the internet through a network attached after
|
|
27991
|
+
// it started, which is exactly how netExt joins below. runc is registered
|
|
27992
|
+
// on every dockerd, so on every other tier this names the default.
|
|
27993
|
+
"--runtime=runc",
|
|
27954
27994
|
"--cap-drop=ALL",
|
|
27955
27995
|
"--security-opt=no-new-privileges",
|
|
27956
27996
|
"--pids-limit=128",
|
|
@@ -27963,14 +28003,29 @@ async function startProxySidecar(d, allow, idBase, staged, labels) {
|
|
|
27963
28003
|
"node",
|
|
27964
28004
|
`${SIDECAR_CODE_GUEST}/proxyEntry.js`
|
|
27965
28005
|
], 3e4);
|
|
27966
|
-
if (!run2.ok)
|
|
27967
|
-
|
|
28006
|
+
if (!run2.ok) {
|
|
28007
|
+
const reason = `could not start the proxy sidecar: ${run2.stderr.trim()}`;
|
|
28008
|
+
if (run2.timedOut || /no space left on device/i.test(run2.stderr)) {
|
|
28009
|
+
throw new ContainmentRefusalError(reason);
|
|
28010
|
+
}
|
|
28011
|
+
throw new FenceError(reason);
|
|
28012
|
+
}
|
|
27968
28013
|
if (!dockerSync(d, ["network", "connect", netExt, proxyName]).ok) {
|
|
27969
28014
|
throw new FenceError(`could not attach the proxy to the egress network ${netExt}`);
|
|
27970
28015
|
}
|
|
27971
28016
|
await waitForProxyReady(d, proxyName);
|
|
28017
|
+
const inspected = dockerSync(d, [
|
|
28018
|
+
"inspect",
|
|
28019
|
+
"-f",
|
|
28020
|
+
`{{(index .NetworkSettings.Networks "${netInt}").IPAddress}}`,
|
|
28021
|
+
proxyName
|
|
28022
|
+
]);
|
|
28023
|
+
const proxyIp = inspected.stdout.trim();
|
|
28024
|
+
if (!inspected.ok || !isIPv4(proxyIp)) {
|
|
28025
|
+
throw new ContainmentRefusalError(`could not read the proxy sidecar's address on ${netInt}: ` + (inspected.ok ? `got ${JSON.stringify(proxyIp)}` : inspected.stderr.trim()));
|
|
28026
|
+
}
|
|
27972
28027
|
return {
|
|
27973
|
-
net: { network: netInt, proxyHost:
|
|
28028
|
+
net: { network: netInt, proxyHost: proxyIp, proxyPort: SIDECAR_PROXY_PORT },
|
|
27974
28029
|
teardown,
|
|
27975
28030
|
// A `docker logs` that fails yields no hosts. That loses the hint and
|
|
27976
28031
|
// changes nothing else: the list only ever adds words to a reason.
|
|
@@ -27985,13 +28040,20 @@ ${logs.stderr}`) : [];
|
|
|
27985
28040
|
throw err;
|
|
27986
28041
|
}
|
|
27987
28042
|
}
|
|
27988
|
-
function spawnWorkload(d, name, argv, timeoutMs) {
|
|
28043
|
+
function spawnWorkload(d, name, argv, timeoutMs, onStdout) {
|
|
27989
28044
|
return new Promise((resolvePromise, reject) => {
|
|
27990
28045
|
const child = d.spawn(argv);
|
|
27991
28046
|
let stdout = "";
|
|
27992
28047
|
let stderr = "";
|
|
27993
28048
|
let timedOut = false;
|
|
27994
|
-
child.stdout.on("data", (d2) =>
|
|
28049
|
+
child.stdout.on("data", (d2) => {
|
|
28050
|
+
const chunk = d2.toString();
|
|
28051
|
+
stdout += chunk;
|
|
28052
|
+
try {
|
|
28053
|
+
onStdout?.(chunk);
|
|
28054
|
+
} catch {
|
|
28055
|
+
}
|
|
28056
|
+
});
|
|
27995
28057
|
child.stderr.on("data", (d2) => stderr += d2.toString());
|
|
27996
28058
|
const timer = setTimeout(() => {
|
|
27997
28059
|
timedOut = true;
|
|
@@ -28063,9 +28125,11 @@ async function runContainedOn(d, spec, env, opts = {}) {
|
|
|
28063
28125
|
cwd: opts.cwd,
|
|
28064
28126
|
image: opts.image,
|
|
28065
28127
|
net: sidecar?.net,
|
|
28066
|
-
labels: opts.labels
|
|
28128
|
+
labels: opts.labels,
|
|
28129
|
+
...opts.runtime === void 0 ? {} : { runtime: opts.runtime },
|
|
28130
|
+
...opts.networkOf === void 0 ? {} : { networkOf: opts.networkOf }
|
|
28067
28131
|
});
|
|
28068
|
-
const result = await spawnWorkload(d, idBase, argv, opts.timeoutMs ?? 9e5);
|
|
28132
|
+
const result = await spawnWorkload(d, idBase, argv, opts.timeoutMs ?? 9e5, opts.onStdout);
|
|
28069
28133
|
return sidecar ? { ...result, egressDenied: sidecar.deniedHosts() } : result;
|
|
28070
28134
|
} finally {
|
|
28071
28135
|
sidecar?.teardown();
|
|
@@ -28078,7 +28142,7 @@ function containerContainmentOn(d) {
|
|
|
28078
28142
|
run: (spec, env, opts = {}) => runContainedOn(d, spec, env, opts)
|
|
28079
28143
|
};
|
|
28080
28144
|
}
|
|
28081
|
-
var DEFAULT_CONTAINER_IMAGE, GUEST, TMPFS_SIZE_MB, DOCKER_TIMEOUT_MS, SIDECAR_PROXY_PORT, SIDECAR_CODE_GUEST, PROXY_ENV_KEYS, WINDOWS_DRIVE_ROOT, IMAGE_HOST, IMAGE_NAME, IMAGE_PATH, IMAGE_TAG, IMAGE_DIGEST, IMAGE_REF, LABEL_KEY, VOLUME_NAME, VOLUME_NAME_MAX, probed, PROXY_FILES, leaveToTheLease;
|
|
28145
|
+
var DEFAULT_CONTAINER_IMAGE, GUEST, TMPFS_SIZE_MB, DOCKER_TIMEOUT_MS, SIDECAR_PROXY_PORT, SIDECAR_CODE_GUEST, PROXY_ENV_KEYS, WINDOWS_DRIVE_ROOT, IMAGE_HOST, IMAGE_NAME, IMAGE_PATH, IMAGE_TAG, IMAGE_DIGEST, IMAGE_REF, LABEL_KEY, VOLUME_NAME, VOLUME_NAME_MAX, NETNS_HOLDER_NAME, RUNTIME_NAME, probed, PROXY_FILES, leaveToTheLease;
|
|
28082
28146
|
var init_container = __esm({
|
|
28083
28147
|
"../../packages/containment/dist/container.js"() {
|
|
28084
28148
|
"use strict";
|
|
@@ -28112,6 +28176,8 @@ var init_container = __esm({
|
|
|
28112
28176
|
LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
28113
28177
|
VOLUME_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
28114
28178
|
VOLUME_NAME_MAX = 128;
|
|
28179
|
+
NETNS_HOLDER_NAME = /^th-netns-[A-Za-z0-9][A-Za-z0-9_.-]{0,120}$/;
|
|
28180
|
+
RUNTIME_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
28115
28181
|
probed = /* @__PURE__ */ new WeakMap();
|
|
28116
28182
|
PROXY_FILES = ["proxyEntry.js", "egressProxy.js"];
|
|
28117
28183
|
leaveToTheLease = () => {
|
|
@@ -28186,13 +28252,35 @@ function timeoutWithin(capMs, deadline) {
|
|
|
28186
28252
|
return capMs;
|
|
28187
28253
|
return Math.max(1, Math.min(capMs, deadline - Date.now()));
|
|
28188
28254
|
}
|
|
28255
|
+
function netnsHolderArgs(o) {
|
|
28256
|
+
if (!NETNS_HOLDER_NAME.test(o.name)) {
|
|
28257
|
+
throw new FenceError(`refusing netns holder name ${JSON.stringify(o.name)}`);
|
|
28258
|
+
}
|
|
28259
|
+
return [
|
|
28260
|
+
"run",
|
|
28261
|
+
"-d",
|
|
28262
|
+
"--rm",
|
|
28263
|
+
`--name=${o.name}`,
|
|
28264
|
+
"--network=none",
|
|
28265
|
+
"--runtime=runc",
|
|
28266
|
+
"--user=0:0",
|
|
28267
|
+
"--cap-drop=ALL",
|
|
28268
|
+
"--security-opt=no-new-privileges",
|
|
28269
|
+
"--pids-limit=8",
|
|
28270
|
+
...labelArgs(o.labels),
|
|
28271
|
+
// Bounded, in case every removal path is missed: the phase's own budget is shorter.
|
|
28272
|
+
"--entrypoint=sleep",
|
|
28273
|
+
o.image,
|
|
28274
|
+
"1800"
|
|
28275
|
+
];
|
|
28276
|
+
}
|
|
28189
28277
|
function captureArgs(o) {
|
|
28190
28278
|
let network;
|
|
28191
28279
|
if (o.network === "none") {
|
|
28192
28280
|
network = "--network=none";
|
|
28193
28281
|
} else if (typeof o.network === "object" && o.network !== null) {
|
|
28194
|
-
if (!
|
|
28195
|
-
throw new FenceError(`refusing to join container ${JSON.stringify(o.network.container)}: not a
|
|
28282
|
+
if (!NETNS_HOLDER_NAME.test(o.network.container)) {
|
|
28283
|
+
throw new FenceError(`refusing to join container ${JSON.stringify(o.network.container)}: not a netns holder's name`);
|
|
28196
28284
|
}
|
|
28197
28285
|
network = `--network=container:${o.network.container}`;
|
|
28198
28286
|
} else {
|
|
@@ -28217,6 +28305,12 @@ function captureArgs(o) {
|
|
|
28217
28305
|
"--init",
|
|
28218
28306
|
`--name=${o.name}`,
|
|
28219
28307
|
network,
|
|
28308
|
+
// runc, whatever the daemon's default (TERM-1336, design D2). Under gVisor each
|
|
28309
|
+
// sandbox keeps its own loopback, so a server capture sharing the app's
|
|
28310
|
+
// namespace could not reach `localhost`; the app's serve step is pinned to runc
|
|
28311
|
+
// in envrun for the same reason. The static capture is pinned too, so the whole
|
|
28312
|
+
// screenshot phase runs under one runtime. runc is registered on every dockerd.
|
|
28313
|
+
"--runtime=runc",
|
|
28220
28314
|
"--cap-drop=ALL",
|
|
28221
28315
|
"--security-opt=no-new-privileges",
|
|
28222
28316
|
"--pids-limit=512",
|
|
@@ -28603,1627 +28697,1900 @@ var init_dist = __esm({
|
|
|
28603
28697
|
}
|
|
28604
28698
|
});
|
|
28605
28699
|
|
|
28606
|
-
// ../../packages/
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
"../../packages/attest/dist/types.js"() {
|
|
28610
|
-
"use strict";
|
|
28611
|
-
IN_TOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
28612
|
-
ACCEPTANCE_RUN_PREDICATE_TYPE = "https://terminalhire.com/attestations/acceptance-run/v1";
|
|
28613
|
-
IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json";
|
|
28614
|
-
TEST_COMMAND_SOURCES = ["detected", "founder-declared", "none"];
|
|
28615
|
-
}
|
|
28616
|
-
});
|
|
28617
|
-
|
|
28618
|
-
// ../../packages/attest/dist/pae.js
|
|
28619
|
-
function pae(payloadType, body) {
|
|
28620
|
-
const typeBytes = Buffer.from(payloadType, "utf8");
|
|
28621
|
-
return Buffer.concat([
|
|
28622
|
-
Buffer.from(`DSSEv1 ${typeBytes.length} `, "utf8"),
|
|
28623
|
-
typeBytes,
|
|
28624
|
-
Buffer.from(` ${body.length} `, "utf8"),
|
|
28625
|
-
body
|
|
28626
|
-
]);
|
|
28700
|
+
// ../../packages/envrun/dist/labels.js
|
|
28701
|
+
function runLabels(runId, callerLabels) {
|
|
28702
|
+
return { ...callerLabels ?? {}, [RUN_ID_LABEL_KEY]: runId, [RUN_LABEL_KEY]: "term-350" };
|
|
28627
28703
|
}
|
|
28628
|
-
|
|
28629
|
-
|
|
28630
|
-
"use strict";
|
|
28631
|
-
}
|
|
28632
|
-
});
|
|
28633
|
-
|
|
28634
|
-
// ../../packages/attest/dist/keys.js
|
|
28635
|
-
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign as cryptoSign, verify as cryptoVerify } from "crypto";
|
|
28636
|
-
function signBytes(privateKey, data) {
|
|
28637
|
-
return cryptoSign(null, data, privateKey);
|
|
28704
|
+
function censusTotal(c) {
|
|
28705
|
+
return c.containers.length + c.volumes.length + c.networks.length;
|
|
28638
28706
|
}
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28707
|
+
function query(docker3, args) {
|
|
28708
|
+
const res = docker3.sync([...args], { timeoutMs: 15e3 });
|
|
28709
|
+
if (res.error || res.status !== 0) {
|
|
28710
|
+
const why = res.error?.message ?? (res.stderr.trim() || `exit ${String(res.status)}`);
|
|
28711
|
+
return { ids: [], failure: `docker ${args.slice(0, 2).join(" ")}: ${why}` };
|
|
28643
28712
|
}
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
function
|
|
28713
|
+
const ids2 = res.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
28714
|
+
return { ids: ids2, failure: null };
|
|
28715
|
+
}
|
|
28716
|
+
function ids(docker3, args) {
|
|
28717
|
+
return query(docker3, args).ids;
|
|
28718
|
+
}
|
|
28719
|
+
function census(docker3, label) {
|
|
28720
|
+
const filter = `label=${label}`;
|
|
28648
28721
|
return {
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
predicate
|
|
28722
|
+
containers: ids(docker3, ["ps", "-aq", "--filter", filter]),
|
|
28723
|
+
volumes: ids(docker3, ["volume", "ls", "-q", "--filter", filter]),
|
|
28724
|
+
networks: ids(docker3, ["network", "ls", "-q", "--filter", filter])
|
|
28653
28725
|
};
|
|
28654
28726
|
}
|
|
28655
|
-
function
|
|
28656
|
-
const
|
|
28657
|
-
const
|
|
28658
|
-
|
|
28659
|
-
|
|
28660
|
-
|
|
28661
|
-
|
|
28727
|
+
function censusReport(docker3, label) {
|
|
28728
|
+
const filter = `label=${label}`;
|
|
28729
|
+
const failures = [];
|
|
28730
|
+
const ask2 = (args) => {
|
|
28731
|
+
const q = query(docker3, args);
|
|
28732
|
+
if (q.failure !== null)
|
|
28733
|
+
failures.push(q.failure);
|
|
28734
|
+
return q.ids;
|
|
28735
|
+
};
|
|
28736
|
+
const taken = {
|
|
28737
|
+
containers: ask2(["ps", "-aq", "--filter", filter]),
|
|
28738
|
+
volumes: ask2(["volume", "ls", "-q", "--filter", filter]),
|
|
28739
|
+
networks: ask2(["network", "ls", "-q", "--filter", filter])
|
|
28662
28740
|
};
|
|
28741
|
+
return failures.length === 0 ? { observed: true, census: taken, unobservedReason: null } : { observed: false, census: taken, unobservedReason: failures.join("; ") };
|
|
28663
28742
|
}
|
|
28664
|
-
function
|
|
28665
|
-
return
|
|
28743
|
+
function localCensus(label) {
|
|
28744
|
+
return census(localDockerClient(), label);
|
|
28666
28745
|
}
|
|
28667
|
-
|
|
28668
|
-
|
|
28669
|
-
|
|
28670
|
-
|
|
28671
|
-
|
|
28672
|
-
|
|
28673
|
-
|
|
28674
|
-
|
|
28675
|
-
|
|
28676
|
-
|
|
28677
|
-
|
|
28678
|
-
|
|
28679
|
-
|
|
28680
|
-
"use strict";
|
|
28681
|
-
init_keys();
|
|
28682
|
-
init_pae();
|
|
28683
|
-
init_types4();
|
|
28684
|
-
}
|
|
28685
|
-
});
|
|
28686
|
-
|
|
28687
|
-
// ../../packages/attest/dist/sealedbox.js
|
|
28688
|
-
import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, diffieHellman, generateKeyPairSync as generateKeyPairSync2, hkdfSync as hkdfSync2, randomBytes as randomBytes7 } from "crypto";
|
|
28689
|
-
var init_sealedbox = __esm({
|
|
28690
|
-
"../../packages/attest/dist/sealedbox.js"() {
|
|
28691
|
-
"use strict";
|
|
28692
|
-
init_keys();
|
|
28693
|
-
init_pae();
|
|
28694
|
-
}
|
|
28695
|
-
});
|
|
28696
|
-
|
|
28697
|
-
// ../../packages/attest/dist/aead.js
|
|
28698
|
-
import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes8 } from "crypto";
|
|
28699
|
-
var init_aead = __esm({
|
|
28700
|
-
"../../packages/attest/dist/aead.js"() {
|
|
28701
|
-
"use strict";
|
|
28702
|
-
init_pae();
|
|
28746
|
+
function judgeLeaks(peak, after, observation) {
|
|
28747
|
+
const labelObserved = peak.containers.length > 0;
|
|
28748
|
+
if (!observation.observed) {
|
|
28749
|
+
return {
|
|
28750
|
+
labelObserved,
|
|
28751
|
+
reaped: false,
|
|
28752
|
+
observed: false,
|
|
28753
|
+
clean: false,
|
|
28754
|
+
state: "unobserved",
|
|
28755
|
+
peak,
|
|
28756
|
+
after,
|
|
28757
|
+
note: `UNOBSERVED, not clean: we could not look at what survived teardown (${observation.unobservedReason ?? "no reason given"}), so nothing is known about leaks on this run, in either direction.`
|
|
28758
|
+
};
|
|
28703
28759
|
}
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28707
|
-
|
|
28708
|
-
|
|
28709
|
-
"
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
|
|
28760
|
+
const reaped = censusTotal(after) === 0;
|
|
28761
|
+
let note;
|
|
28762
|
+
let state;
|
|
28763
|
+
if (!labelObserved && reaped) {
|
|
28764
|
+
state = "inconclusive";
|
|
28765
|
+
note = "INCONCLUSIVE, not clean: nothing labelled was ever seen alive, so an empty final census is equally consistent with the label never being applied. The control failed, so the denial proves nothing.";
|
|
28766
|
+
} else if (!labelObserved) {
|
|
28767
|
+
state = "leak";
|
|
28768
|
+
note = "no labelled container was observed alive AND objects remain \u2014 the label wiring is wrong.";
|
|
28769
|
+
} else if (!reaped) {
|
|
28770
|
+
state = "leak";
|
|
28771
|
+
note = `LEAK: ${String(censusTotal(after))} labelled object(s) survived teardown (containers=${String(after.containers.length)} volumes=${String(after.volumes.length)} networks=${String(after.networks.length)}).`;
|
|
28772
|
+
} else {
|
|
28773
|
+
state = "clean";
|
|
28774
|
+
note = `clean: peak ${String(peak.containers.length)} labelled container(s) observed alive, 0 labelled objects remain after teardown.`;
|
|
28713
28775
|
}
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
28776
|
+
return {
|
|
28777
|
+
labelObserved,
|
|
28778
|
+
reaped,
|
|
28779
|
+
observed: true,
|
|
28780
|
+
clean: labelObserved && reaped,
|
|
28781
|
+
state,
|
|
28782
|
+
peak,
|
|
28783
|
+
after,
|
|
28784
|
+
note
|
|
28785
|
+
};
|
|
28786
|
+
}
|
|
28787
|
+
var RUN_LABEL_KEY, RUN_ID_LABEL_KEY, LabelWatch, LEAK_STATES;
|
|
28788
|
+
var init_labels = __esm({
|
|
28789
|
+
"../../packages/envrun/dist/labels.js"() {
|
|
28719
28790
|
"use strict";
|
|
28720
|
-
|
|
28721
|
-
|
|
28791
|
+
init_dist();
|
|
28792
|
+
RUN_LABEL_KEY = "supergoal.run";
|
|
28793
|
+
RUN_ID_LABEL_KEY = "supergoal.run-id";
|
|
28794
|
+
LabelWatch = class {
|
|
28795
|
+
label;
|
|
28796
|
+
docker;
|
|
28797
|
+
intervalMs;
|
|
28798
|
+
#timer = null;
|
|
28799
|
+
#peak = { containers: [], volumes: [], networks: [] };
|
|
28800
|
+
#samples = 0;
|
|
28801
|
+
/**
|
|
28802
|
+
* `docker` is REQUIRED and second, so a sampler cannot be built without
|
|
28803
|
+
* naming the daemon it watches. A watch polling one daemon while the run
|
|
28804
|
+
* executes on another reports a high-water mark of 0 — indistinguishable
|
|
28805
|
+
* from "the label never applied", which is the exact ambiguity this class
|
|
28806
|
+
* exists to remove.
|
|
28807
|
+
*/
|
|
28808
|
+
constructor(label, docker3, intervalMs = 250) {
|
|
28809
|
+
this.label = label;
|
|
28810
|
+
this.docker = docker3;
|
|
28811
|
+
this.intervalMs = intervalMs;
|
|
28812
|
+
}
|
|
28813
|
+
start() {
|
|
28814
|
+
if (this.#timer !== null)
|
|
28815
|
+
return;
|
|
28816
|
+
this.#sample();
|
|
28817
|
+
this.#timer = setInterval(() => this.#sample(), this.intervalMs);
|
|
28818
|
+
this.#timer.unref();
|
|
28819
|
+
}
|
|
28820
|
+
#sample() {
|
|
28821
|
+
this.#samples += 1;
|
|
28822
|
+
const now = census(this.docker, this.label);
|
|
28823
|
+
this.#peak = {
|
|
28824
|
+
containers: now.containers.length > this.#peak.containers.length ? now.containers : this.#peak.containers,
|
|
28825
|
+
volumes: now.volumes.length > this.#peak.volumes.length ? now.volumes : this.#peak.volumes,
|
|
28826
|
+
networks: now.networks.length > this.#peak.networks.length ? now.networks : this.#peak.networks
|
|
28827
|
+
};
|
|
28828
|
+
}
|
|
28829
|
+
stop() {
|
|
28830
|
+
if (this.#timer !== null) {
|
|
28831
|
+
clearInterval(this.#timer);
|
|
28832
|
+
this.#timer = null;
|
|
28833
|
+
}
|
|
28834
|
+
this.#sample();
|
|
28835
|
+
}
|
|
28836
|
+
get peak() {
|
|
28837
|
+
return this.#peak;
|
|
28838
|
+
}
|
|
28839
|
+
get samples() {
|
|
28840
|
+
return this.#samples;
|
|
28841
|
+
}
|
|
28842
|
+
};
|
|
28843
|
+
LEAK_STATES = ["clean", "leak", "inconclusive", "unobserved"];
|
|
28722
28844
|
}
|
|
28723
28845
|
});
|
|
28724
28846
|
|
|
28725
|
-
// ../../packages/
|
|
28726
|
-
|
|
28727
|
-
|
|
28847
|
+
// ../../packages/envrun/dist/previewRegistry.js
|
|
28848
|
+
function createPreviewRegistry() {
|
|
28849
|
+
const live = /* @__PURE__ */ new Map();
|
|
28850
|
+
return {
|
|
28851
|
+
register(client, container) {
|
|
28852
|
+
const names = live.get(client) ?? /* @__PURE__ */ new Set();
|
|
28853
|
+
names.add(container);
|
|
28854
|
+
live.set(client, names);
|
|
28855
|
+
},
|
|
28856
|
+
deregister(client, container) {
|
|
28857
|
+
const names = live.get(client);
|
|
28858
|
+
if (names === void 0)
|
|
28859
|
+
return;
|
|
28860
|
+
names.delete(container);
|
|
28861
|
+
if (names.size === 0)
|
|
28862
|
+
live.delete(client);
|
|
28863
|
+
},
|
|
28864
|
+
pairs() {
|
|
28865
|
+
const out = [];
|
|
28866
|
+
for (const [client, names] of live) {
|
|
28867
|
+
for (const container of names)
|
|
28868
|
+
out.push({ client, container });
|
|
28869
|
+
}
|
|
28870
|
+
return out;
|
|
28871
|
+
},
|
|
28872
|
+
reapAll() {
|
|
28873
|
+
for (const [client, names] of live) {
|
|
28874
|
+
for (const container of names) {
|
|
28875
|
+
client.sync(["rm", "-f", container], { timeoutMs: 15e3 });
|
|
28876
|
+
}
|
|
28877
|
+
}
|
|
28878
|
+
live.clear();
|
|
28879
|
+
}
|
|
28880
|
+
};
|
|
28881
|
+
}
|
|
28882
|
+
var init_previewRegistry = __esm({
|
|
28883
|
+
"../../packages/envrun/dist/previewRegistry.js"() {
|
|
28728
28884
|
"use strict";
|
|
28729
28885
|
}
|
|
28730
28886
|
});
|
|
28731
28887
|
|
|
28732
|
-
// ../../packages/
|
|
28733
|
-
import {
|
|
28734
|
-
|
|
28735
|
-
|
|
28736
|
-
|
|
28737
|
-
|
|
28738
|
-
|
|
28739
|
-
|
|
28740
|
-
|
|
28741
|
-
|
|
28742
|
-
|
|
28888
|
+
// ../../packages/envrun/dist/preview.js
|
|
28889
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
28890
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync12 } from "fs";
|
|
28891
|
+
import { join as join22 } from "path";
|
|
28892
|
+
function docker(client, args, timeoutMs = 6e4) {
|
|
28893
|
+
const res = client.sync([...args], { timeoutMs });
|
|
28894
|
+
return {
|
|
28895
|
+
ok: !res.error && res.status === 0,
|
|
28896
|
+
stdout: res.stdout,
|
|
28897
|
+
stderr: (res.error ? res.error.message : "") + res.stderr
|
|
28898
|
+
};
|
|
28743
28899
|
}
|
|
28744
|
-
function
|
|
28745
|
-
|
|
28900
|
+
function installReaper() {
|
|
28901
|
+
if (reaperInstalled)
|
|
28902
|
+
return;
|
|
28903
|
+
reaperInstalled = true;
|
|
28904
|
+
process.on("exit", () => {
|
|
28905
|
+
livePreviews.reapAll();
|
|
28906
|
+
});
|
|
28746
28907
|
}
|
|
28747
|
-
function
|
|
28748
|
-
|
|
28749
|
-
|
|
28750
|
-
const decoded = Buffer.from(s, "base64url");
|
|
28751
|
-
if (decoded.toString("base64url") !== s)
|
|
28908
|
+
function readHostPort(client, container) {
|
|
28909
|
+
const res = docker(client, ["port", container, `${String(GUEST_PORT)}/tcp`]);
|
|
28910
|
+
if (!res.ok)
|
|
28752
28911
|
return null;
|
|
28753
|
-
|
|
28912
|
+
for (const line of res.stdout.split("\n")) {
|
|
28913
|
+
const trimmed = line.trim();
|
|
28914
|
+
if (trimmed === "")
|
|
28915
|
+
continue;
|
|
28916
|
+
const idx = trimmed.lastIndexOf(":");
|
|
28917
|
+
if (idx === -1)
|
|
28918
|
+
continue;
|
|
28919
|
+
const port = Number(trimmed.slice(idx + 1));
|
|
28920
|
+
if (Number.isInteger(port) && port > 0)
|
|
28921
|
+
return port;
|
|
28922
|
+
}
|
|
28923
|
+
return null;
|
|
28754
28924
|
}
|
|
28755
|
-
function
|
|
28756
|
-
const bytes = strictBase64UrlDecode(segment);
|
|
28757
|
-
if (bytes === null)
|
|
28758
|
-
return null;
|
|
28759
|
-
let parsed;
|
|
28925
|
+
async function fetchInstanceToken(url, authToken) {
|
|
28760
28926
|
try {
|
|
28761
|
-
|
|
28927
|
+
const headers = {};
|
|
28928
|
+
if (authToken) {
|
|
28929
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
28930
|
+
}
|
|
28931
|
+
const res = await fetch(url, { cache: "no-store", headers });
|
|
28932
|
+
if (!res.ok)
|
|
28933
|
+
return null;
|
|
28934
|
+
const body = await res.json();
|
|
28935
|
+
return typeof body.instanceToken === "string" ? body.instanceToken : null;
|
|
28762
28936
|
} catch {
|
|
28763
28937
|
return null;
|
|
28764
28938
|
}
|
|
28765
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
28766
|
-
return null;
|
|
28767
|
-
return parsed;
|
|
28768
28939
|
}
|
|
28769
|
-
function
|
|
28770
|
-
const
|
|
28771
|
-
|
|
28772
|
-
|
|
28773
|
-
|
|
28774
|
-
|
|
28775
|
-
|
|
28776
|
-
|
|
28777
|
-
if (value === "")
|
|
28778
|
-
throw new TypeError(`verifyGceIdentityToken: expectations.${name} is empty`);
|
|
28940
|
+
async function startPreview(req) {
|
|
28941
|
+
const label = labelArgs(req.labels);
|
|
28942
|
+
const container = `${req.idBase}-preview`;
|
|
28943
|
+
const image = validateImage(req.image);
|
|
28944
|
+
const bindAddress = req.bindAddress ?? "127.0.0.1";
|
|
28945
|
+
const client = req.docker;
|
|
28946
|
+
if (!LOOPBACK_BINDS.has(bindAddress) && req.authToken === void 0) {
|
|
28947
|
+
throw new PreviewError(`refusing to publish the preview on ${bindAddress} without an explicit authToken: a bind wider than loopback puts this run \u2014 the test output tail included \u2014 on the developer's local network`);
|
|
28779
28948
|
}
|
|
28780
|
-
|
|
28781
|
-
|
|
28949
|
+
const authToken = req.authToken ?? randomBytes7(24).toString("base64url");
|
|
28950
|
+
const envArgs = ["--env", `PREVIEW_AUTH_TOKEN=${authToken}`];
|
|
28951
|
+
const probeHost = WILDCARD_BINDS.has(bindAddress) ? "127.0.0.1" : bindAddress;
|
|
28952
|
+
const probeAuthority = probeHost.includes(":") ? `[${probeHost}]` : probeHost;
|
|
28953
|
+
mkdirSync5(req.scratchDir, { recursive: true });
|
|
28954
|
+
const docPath = join22(req.scratchDir, "preview-run.json");
|
|
28955
|
+
writeFileSync12(docPath, JSON.stringify(req.document, null, 2), "utf8");
|
|
28956
|
+
const teardown = () => {
|
|
28957
|
+
livePreviews.deregister(client, container);
|
|
28958
|
+
for (let i = 0; i < 3; i += 1) {
|
|
28959
|
+
const inspect = docker(client, ["inspect", "--format", "{{.State.Status}}", container]);
|
|
28960
|
+
if (!inspect.ok)
|
|
28961
|
+
return { clean: true, leaked: [] };
|
|
28962
|
+
docker(client, ["rm", "-f", container]);
|
|
28963
|
+
}
|
|
28964
|
+
const still = docker(client, ["inspect", "--format", "{{.State.Status}}", container]);
|
|
28965
|
+
return still.ok ? { clean: false, leaked: [`container ${container}`] } : { clean: true, leaked: [] };
|
|
28966
|
+
};
|
|
28967
|
+
const startedAt = Date.now();
|
|
28968
|
+
try {
|
|
28969
|
+
const run2 = docker(client, [
|
|
28970
|
+
"run",
|
|
28971
|
+
"-d",
|
|
28972
|
+
"--init",
|
|
28973
|
+
`--name=${container}`,
|
|
28974
|
+
// A network IS granted here, unlike the verification step. It carries our
|
|
28975
|
+
// own argv over a document we wrote; the repo's code never runs in it.
|
|
28976
|
+
"--network=bridge",
|
|
28977
|
+
// Loopback by default, and anything wider was refused above unless the
|
|
28978
|
+
// caller named a token. A bare `-p 8080` would bind 0.0.0.0 and put a
|
|
28979
|
+
// developer's in-progress work on their local network.
|
|
28980
|
+
`--publish=${bindAddress}:0:${String(GUEST_PORT)}`,
|
|
28981
|
+
...envArgs,
|
|
28982
|
+
"--cap-drop=ALL",
|
|
28983
|
+
"--security-opt=no-new-privileges",
|
|
28984
|
+
"--pids-limit=64",
|
|
28985
|
+
"--memory=256m",
|
|
28986
|
+
"--read-only",
|
|
28987
|
+
"--tmpfs=/tmp:rw,noexec,nosuid,size=8m",
|
|
28988
|
+
...label,
|
|
28989
|
+
`--volume=${docPath}:${GUEST_DOC}:ro`,
|
|
28990
|
+
"--",
|
|
28991
|
+
image,
|
|
28992
|
+
"node",
|
|
28993
|
+
"-e",
|
|
28994
|
+
SERVER_SOURCE
|
|
28995
|
+
]);
|
|
28996
|
+
if (!run2.ok) {
|
|
28997
|
+
throw new PreviewError(`could not start the preview container: ${run2.stderr.trim()}`);
|
|
28998
|
+
}
|
|
28999
|
+
const deadline = Date.now() + (req.readyTimeoutMs ?? 6e4);
|
|
29000
|
+
let hostPort = null;
|
|
29001
|
+
let token = null;
|
|
29002
|
+
let lastDetail = "never answered";
|
|
29003
|
+
const throwIfExited = () => {
|
|
29004
|
+
const alive = docker(client, ["inspect", "--format", "{{.State.Running}}", container]);
|
|
29005
|
+
if (alive.stdout.trim() !== "true") {
|
|
29006
|
+
const logs = docker(client, ["logs", "--tail", "20", container]);
|
|
29007
|
+
throw new PreviewError(`the preview container exited before serving: ${logs.stdout.trim()}${logs.stderr.trim()}`);
|
|
29008
|
+
}
|
|
29009
|
+
};
|
|
29010
|
+
while (Date.now() < deadline) {
|
|
29011
|
+
hostPort ??= readHostPort(client, container);
|
|
29012
|
+
if (hostPort === null) {
|
|
29013
|
+
throwIfExited();
|
|
29014
|
+
lastDetail = "Docker never reported a published host port";
|
|
29015
|
+
await sleep4(200);
|
|
29016
|
+
continue;
|
|
29017
|
+
}
|
|
29018
|
+
token = await fetchInstanceToken(`http://${probeAuthority}:${String(hostPort)}/`, authToken);
|
|
29019
|
+
if (token !== null)
|
|
29020
|
+
break;
|
|
29021
|
+
throwIfExited();
|
|
29022
|
+
lastDetail = "the port is published but the server has not answered yet";
|
|
29023
|
+
await sleep4(150);
|
|
29024
|
+
}
|
|
29025
|
+
if (hostPort === null || token === null) {
|
|
29026
|
+
throw new PreviewError(`the preview URL never became reachable: ${lastDetail}`);
|
|
29027
|
+
}
|
|
29028
|
+
livePreviews.register(client, container);
|
|
29029
|
+
installReaper();
|
|
29030
|
+
const origin = `http://${probeAuthority}:${String(hostPort)}/`;
|
|
29031
|
+
return {
|
|
29032
|
+
url: `${origin}?token=${encodeURIComponent(authToken)}`,
|
|
29033
|
+
origin,
|
|
29034
|
+
authToken,
|
|
29035
|
+
instanceToken: token,
|
|
29036
|
+
container,
|
|
29037
|
+
hostPort,
|
|
29038
|
+
readyMs: Date.now() - startedAt,
|
|
29039
|
+
teardown
|
|
29040
|
+
};
|
|
29041
|
+
} catch (err) {
|
|
29042
|
+
teardown();
|
|
29043
|
+
throw err;
|
|
28782
29044
|
}
|
|
28783
29045
|
}
|
|
28784
|
-
|
|
28785
|
-
|
|
28786
|
-
|
|
28787
|
-
|
|
28788
|
-
|
|
28789
|
-
|
|
28790
|
-
|
|
28791
|
-
|
|
28792
|
-
|
|
28793
|
-
|
|
28794
|
-
|
|
28795
|
-
|
|
28796
|
-
|
|
28797
|
-
|
|
28798
|
-
|
|
28799
|
-
|
|
28800
|
-
|
|
28801
|
-
|
|
28802
|
-
|
|
29046
|
+
function startLocalPreview(req) {
|
|
29047
|
+
return startPreview({ ...req, docker: localDockerClient() });
|
|
29048
|
+
}
|
|
29049
|
+
var PreviewError, GUEST_PORT, GUEST_DOC, LOOPBACK_BINDS, WILDCARD_BINDS, SERVER_SOURCE, livePreviews, reaperInstalled, sleep4;
|
|
29050
|
+
var init_preview = __esm({
|
|
29051
|
+
"../../packages/envrun/dist/preview.js"() {
|
|
29052
|
+
"use strict";
|
|
29053
|
+
init_dist();
|
|
29054
|
+
init_previewRegistry();
|
|
29055
|
+
PreviewError = class extends Error {
|
|
29056
|
+
};
|
|
29057
|
+
GUEST_PORT = 8080;
|
|
29058
|
+
GUEST_DOC = "/preview/run.json";
|
|
29059
|
+
LOOPBACK_BINDS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
29060
|
+
WILDCARD_BINDS = /* @__PURE__ */ new Set(["0.0.0.0", "::"]);
|
|
29061
|
+
SERVER_SOURCE = `
|
|
29062
|
+
const http = require('node:http');
|
|
29063
|
+
const { readFileSync } = require('node:fs');
|
|
29064
|
+
const { randomUUID, timingSafeEqual } = require('node:crypto');
|
|
29065
|
+
|
|
29066
|
+
// Read ONCE, at startup, and refuse to run without it. An unset token used to
|
|
29067
|
+
// skip the check entirely, which meant the one configuration nobody sets on
|
|
29068
|
+
// purpose was also the one that served a developer's in-progress work to
|
|
29069
|
+
// anything that could reach the port. Absent must never mean permitted \u2014 the
|
|
29070
|
+
// polarity requireSecret() holds in apps/web/lib/secrets.ts.
|
|
29071
|
+
const AUTH_TOKEN = process.env.PREVIEW_AUTH_TOKEN || '';
|
|
29072
|
+
if (AUTH_TOKEN === '') {
|
|
29073
|
+
process.stderr.write(
|
|
29074
|
+
'preview: refusing to start \u2014 PREVIEW_AUTH_TOKEN is unset, and this server will not ' +
|
|
29075
|
+
'serve a run document unauthenticated\\n',
|
|
29076
|
+
);
|
|
29077
|
+
process.exit(1);
|
|
29078
|
+
}
|
|
29079
|
+
const EXPECTED = Buffer.from(AUTH_TOKEN, 'utf8');
|
|
29080
|
+
|
|
29081
|
+
/** The token the client presented, or null. */
|
|
29082
|
+
function presented(req) {
|
|
29083
|
+
const header = req.headers['authorization'];
|
|
29084
|
+
const bearer = typeof header === 'string' ? /^Bearer\\s+(.+)$/i.exec(header) : null;
|
|
29085
|
+
if (bearer !== null) return bearer[1];
|
|
29086
|
+
// A non-Bearer Authorization header FALLS THROUGH to the query parameter. The
|
|
29087
|
+
// earlier ternary branched on the header merely EXISTING, so a client sending
|
|
29088
|
+
// "Basic \u2026" produced an empty string and could never authenticate at all.
|
|
29089
|
+
//
|
|
29090
|
+
// The query form stays because the founder opens this in a browser and a
|
|
29091
|
+
// browser sends no Authorization header. That is the only reason it is
|
|
29092
|
+
// accepted: a token in a URL lands in browser history, shell history and any
|
|
29093
|
+
// proxy log on the way, where a header does not.
|
|
29094
|
+
return new URL(req.url, 'http://localhost').searchParams.get('token');
|
|
29095
|
+
}
|
|
29096
|
+
|
|
29097
|
+
function authorized(req) {
|
|
29098
|
+
const given = presented(req);
|
|
29099
|
+
if (typeof given !== 'string') return false;
|
|
29100
|
+
const got = Buffer.from(given, 'utf8');
|
|
29101
|
+
// timingSafeEqual THROWS on a length mismatch, so length is compared first and
|
|
29102
|
+
// refused here. The length is not the secret; the bytes are.
|
|
29103
|
+
if (got.length !== EXPECTED.length) return false;
|
|
29104
|
+
return timingSafeEqual(got, EXPECTED);
|
|
29105
|
+
}
|
|
29106
|
+
|
|
29107
|
+
// Per-INSTANCE, minted at boot. Not passed in, not derived from anything the
|
|
29108
|
+
// host controls \u2014 that is what makes "same token \u21D2 same instance" hold.
|
|
29109
|
+
const INSTANCE_TOKEN = randomUUID();
|
|
29110
|
+
const DOC = JSON.parse(readFileSync(${JSON.stringify(GUEST_DOC)}, 'utf8'));
|
|
29111
|
+
let served = 0;
|
|
29112
|
+
|
|
29113
|
+
http
|
|
29114
|
+
.createServer((req, res) => {
|
|
29115
|
+
if (!authorized(req)) {
|
|
29116
|
+
res.writeHead(401, { 'content-type': 'application/json; charset=utf-8' });
|
|
29117
|
+
return res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
29118
|
+
}
|
|
29119
|
+
served += 1;
|
|
29120
|
+
const body = JSON.stringify(
|
|
29121
|
+
{
|
|
29122
|
+
instanceToken: INSTANCE_TOKEN,
|
|
29123
|
+
servedCount: served,
|
|
29124
|
+
pid: process.pid,
|
|
29125
|
+
run: DOC,
|
|
29126
|
+
},
|
|
29127
|
+
null,
|
|
29128
|
+
2,
|
|
29129
|
+
);
|
|
29130
|
+
res.writeHead(200, {
|
|
29131
|
+
'content-type': 'application/json; charset=utf-8',
|
|
29132
|
+
'cache-control': 'no-store',
|
|
29133
|
+
'x-th-instance': INSTANCE_TOKEN,
|
|
29134
|
+
});
|
|
29135
|
+
res.end(body);
|
|
29136
|
+
})
|
|
29137
|
+
.listen(${String(GUEST_PORT)}, '0.0.0.0', () => {
|
|
29138
|
+
console.log('preview-ready ' + INSTANCE_TOKEN);
|
|
29139
|
+
});
|
|
29140
|
+
`;
|
|
29141
|
+
livePreviews = createPreviewRegistry();
|
|
29142
|
+
reaperInstalled = false;
|
|
29143
|
+
sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
28803
29144
|
}
|
|
28804
|
-
|
|
28805
|
-
|
|
29145
|
+
});
|
|
29146
|
+
|
|
29147
|
+
// ../../packages/envrun/dist/venue.js
|
|
29148
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
29149
|
+
import { join as join23 } from "path";
|
|
29150
|
+
function localJailPaths(scratchRoot) {
|
|
29151
|
+
return {
|
|
29152
|
+
jail: join23(scratchRoot, JAIL_SEGMENT),
|
|
29153
|
+
tmp: join23(scratchRoot, JAIL_TMP_SEGMENT)
|
|
29154
|
+
};
|
|
29155
|
+
}
|
|
29156
|
+
function localVenue() {
|
|
29157
|
+
return {
|
|
29158
|
+
kind: "local",
|
|
29159
|
+
acquire: (runId) => acquireTransactionally((allocated) => {
|
|
29160
|
+
void allocated;
|
|
29161
|
+
return Promise.resolve(acquireLocalLease(runId));
|
|
29162
|
+
})
|
|
29163
|
+
};
|
|
29164
|
+
}
|
|
29165
|
+
async function acquireTransactionally(body) {
|
|
29166
|
+
const undos = [];
|
|
29167
|
+
try {
|
|
29168
|
+
return await body({
|
|
29169
|
+
onRollback: (undo) => {
|
|
29170
|
+
undos.push(undo);
|
|
29171
|
+
}
|
|
29172
|
+
});
|
|
29173
|
+
} catch (err) {
|
|
29174
|
+
const failures = [];
|
|
29175
|
+
for (const undo of undos.reverse()) {
|
|
29176
|
+
let failed = null;
|
|
29177
|
+
try {
|
|
29178
|
+
await undo();
|
|
29179
|
+
} catch (rollbackErr) {
|
|
29180
|
+
failed = { thrown: rollbackErr };
|
|
29181
|
+
}
|
|
29182
|
+
if (failed === null)
|
|
29183
|
+
continue;
|
|
29184
|
+
let entry;
|
|
29185
|
+
try {
|
|
29186
|
+
entry = describeThrown(failed.thrown, { includeName: true });
|
|
29187
|
+
} catch {
|
|
29188
|
+
entry = UNDESCRIBABLE_THROWN;
|
|
29189
|
+
}
|
|
29190
|
+
failures.push(entry);
|
|
29191
|
+
}
|
|
29192
|
+
if (failures.length > 0)
|
|
29193
|
+
throw new VenueRollbackError(err, failures);
|
|
29194
|
+
throw err;
|
|
28806
29195
|
}
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28810
|
-
|
|
29196
|
+
}
|
|
29197
|
+
function rollbackMessage(cause, rollbackFailures) {
|
|
29198
|
+
const headline = describeThrown(cause, { includeName: false });
|
|
29199
|
+
let tail2;
|
|
29200
|
+
try {
|
|
29201
|
+
tail2 = `[venue acquisition rolled back with ${String(rollbackFailures.length)} failure(s): ${rollbackFailures.join("; ")} \u2014 one or more allocated resources may still exist]`;
|
|
29202
|
+
} catch {
|
|
29203
|
+
tail2 = UNLISTABLE_ROLLBACK_FAILURES;
|
|
28811
29204
|
}
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
29205
|
+
return `${headline} ${tail2}`;
|
|
29206
|
+
}
|
|
29207
|
+
function describeThrown(thrown, opts) {
|
|
29208
|
+
try {
|
|
29209
|
+
if (isErrorValue(thrown)) {
|
|
29210
|
+
const message2 = readErrorField(thrown, "message", UNREADABLE_MESSAGE);
|
|
29211
|
+
if (!opts.includeName)
|
|
29212
|
+
return message2;
|
|
29213
|
+
const name = readErrorField(thrown, "name", UNREADABLE_NAME);
|
|
29214
|
+
return message2 === "" ? name : `${name}: ${message2}`;
|
|
29215
|
+
}
|
|
29216
|
+
return coerceToString(thrown);
|
|
29217
|
+
} catch {
|
|
29218
|
+
return UNDESCRIBABLE_THROWN;
|
|
28815
29219
|
}
|
|
28816
|
-
|
|
29220
|
+
}
|
|
29221
|
+
function isErrorValue(thrown) {
|
|
28817
29222
|
try {
|
|
28818
|
-
|
|
28819
|
-
signatureValid = cryptoVerify2("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
29223
|
+
return thrown instanceof Error;
|
|
28820
29224
|
} catch {
|
|
28821
|
-
|
|
29225
|
+
return false;
|
|
28822
29226
|
}
|
|
28823
|
-
|
|
28824
|
-
|
|
29227
|
+
}
|
|
29228
|
+
function readErrorField(thrown, key, fallback) {
|
|
29229
|
+
let raw;
|
|
29230
|
+
try {
|
|
29231
|
+
raw = thrown[key];
|
|
29232
|
+
} catch {
|
|
29233
|
+
return fallback;
|
|
28825
29234
|
}
|
|
28826
|
-
|
|
28827
|
-
|
|
28828
|
-
|
|
29235
|
+
return typeof raw === "string" ? raw : coerceToString(raw);
|
|
29236
|
+
}
|
|
29237
|
+
function coerceToString(thrown) {
|
|
29238
|
+
try {
|
|
29239
|
+
return String(thrown);
|
|
29240
|
+
} catch {
|
|
29241
|
+
return UNCOERCIBLE_THROWN;
|
|
28829
29242
|
}
|
|
28830
|
-
|
|
28831
|
-
|
|
29243
|
+
}
|
|
29244
|
+
function localTreeOwner(ids2 = hostIds()) {
|
|
29245
|
+
if (ids2?.uid !== 0)
|
|
29246
|
+
return void 0;
|
|
29247
|
+
return localGuestUser(ids2) ?? void 0;
|
|
29248
|
+
}
|
|
29249
|
+
function handLocalTreeToGuest(local, owner, chown = chownTree) {
|
|
29250
|
+
if (owner === void 0)
|
|
29251
|
+
return;
|
|
29252
|
+
chown(local.cloneDir, owner);
|
|
29253
|
+
chown(local.scratchRoot, owner);
|
|
29254
|
+
}
|
|
29255
|
+
function localCloneVolumeName(runId, suffix = randomBytes8(4).toString("hex")) {
|
|
29256
|
+
const tail2 = `-${suffix}`;
|
|
29257
|
+
const safe = runId.replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
29258
|
+
const head = `th-clone-${safe}`.slice(0, VOLUME_NAME_MAX2 - tail2.length);
|
|
29259
|
+
return validateVolumeName(`${head}${tail2}`, "the local clone volume");
|
|
29260
|
+
}
|
|
29261
|
+
function localCloneFillArgv(from, volume, owner) {
|
|
29262
|
+
const chown = owner === null ? "" : ` && chown -R ${String(owner.uid)}:${String(owner.gid)} /dst`;
|
|
29263
|
+
return [
|
|
29264
|
+
"run",
|
|
29265
|
+
"--rm",
|
|
29266
|
+
"--network=none",
|
|
29267
|
+
`--volume=${from}:/src:ro`,
|
|
29268
|
+
`--volume=${validateVolumeName(volume, "the local clone volume")}:/dst:rw`,
|
|
29269
|
+
"--",
|
|
29270
|
+
STAGE_HELPER_IMAGE,
|
|
29271
|
+
"sh",
|
|
29272
|
+
"-c",
|
|
29273
|
+
`cp -a /src/. /dst/${chown}`
|
|
29274
|
+
];
|
|
29275
|
+
}
|
|
29276
|
+
function acquireLocalLease(runId) {
|
|
29277
|
+
const docker3 = localDockerClient();
|
|
29278
|
+
const containment = selectContainment([containerContainmentOn(docker3)]);
|
|
29279
|
+
let released = false;
|
|
29280
|
+
const stagedProxies = [];
|
|
29281
|
+
const owner = localTreeOwner();
|
|
29282
|
+
let cloneVolume;
|
|
29283
|
+
const createdVolumes = [];
|
|
29284
|
+
const fillCloneVolume = (cloneDir) => {
|
|
29285
|
+
const name = localCloneVolumeName(runId);
|
|
29286
|
+
const created = docker3.sync(["volume", "create", `--label=${STAGE_VOLUME_LABEL_KEY}=${runId}`, "--", name], { timeoutMs: LOCAL_VOLUME_CREATE_TIMEOUT_MS });
|
|
29287
|
+
if (created.error || created.status !== 0) {
|
|
29288
|
+
throw new ContainmentRefusalError(`could not create the clone volume ${name} on the local daemon: ${(created.error?.message ?? created.stderr).trim().slice(0, 300)}`);
|
|
29289
|
+
}
|
|
29290
|
+
createdVolumes.push(name);
|
|
29291
|
+
const from = resolverFor("local")(cloneDir, "clone");
|
|
29292
|
+
const filled = docker3.sync(localCloneFillArgv(from, name, localGuestUser()), {
|
|
29293
|
+
timeoutMs: LOCAL_CLONE_FILL_TIMEOUT_MS
|
|
29294
|
+
});
|
|
29295
|
+
if (filled.error || filled.status !== 0) {
|
|
29296
|
+
throw new ContainmentRefusalError(`could not copy the clone into its volume ${name}: ${(filled.error?.message ?? filled.stderr).trim().slice(0, 300)}`);
|
|
29297
|
+
}
|
|
29298
|
+
return name;
|
|
29299
|
+
};
|
|
29300
|
+
const lease = {
|
|
29301
|
+
kind: "local",
|
|
29302
|
+
runId,
|
|
29303
|
+
containment,
|
|
29304
|
+
docker: docker3,
|
|
29305
|
+
// Stated, not inferred from `kind`. On this venue it is the truth twice over:
|
|
29306
|
+
// the paths are on this machine AND `canonical()` is what should resolve
|
|
29307
|
+
// them, which is the behaviour every local run has always had.
|
|
29308
|
+
pathDomain: "local",
|
|
29309
|
+
// Spread rather than `guestUser: owner`, so a non-root lease has no such key
|
|
29310
|
+
// at all — the same object every local run has always had.
|
|
29311
|
+
...owner === void 0 ? {} : { guestUser: owner },
|
|
29312
|
+
get released() {
|
|
29313
|
+
return released;
|
|
29314
|
+
},
|
|
29315
|
+
get cloneVolume() {
|
|
29316
|
+
return cloneVolume;
|
|
29317
|
+
},
|
|
29318
|
+
stage: (local) => {
|
|
29319
|
+
if (released) {
|
|
29320
|
+
return Promise.reject(new LeaseReleasedError("this lease was already released, so a clone volume made now would never be removed."));
|
|
29321
|
+
}
|
|
29322
|
+
try {
|
|
29323
|
+
handLocalTreeToGuest(local, owner);
|
|
29324
|
+
cloneVolume = fillCloneVolume(local.cloneDir);
|
|
29325
|
+
} catch (err) {
|
|
29326
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
29327
|
+
}
|
|
29328
|
+
return Promise.resolve({
|
|
29329
|
+
cloneDir: local.cloneDir,
|
|
29330
|
+
scratchRoot: local.scratchRoot,
|
|
29331
|
+
previewDir: local.previewDir,
|
|
29332
|
+
...localJailPaths(local.scratchRoot)
|
|
29333
|
+
});
|
|
29334
|
+
},
|
|
29335
|
+
// The containment function verbatim, which is the point of the barrel
|
|
29336
|
+
// re-export rather than a copy here: this venue's daemon and this process
|
|
29337
|
+
// share a filesystem, so the directory it makes under `tmpdir()` is already
|
|
29338
|
+
// venue-side and has been on every local run since the sidecar existed.
|
|
29339
|
+
//
|
|
29340
|
+
// TRACKED, so `release()` can be the guaranteed owner the interface
|
|
29341
|
+
// promises. Returning a bare handle made that promise the CALLER's to keep,
|
|
29342
|
+
// and a caller that staged and then failed outside `runEnvironmentSpec`'s
|
|
29343
|
+
// `finally` left the directory behind while release reported success.
|
|
29344
|
+
//
|
|
29345
|
+
// REFUSING AFTER RELEASE is the other half of the same promise. Staging onto
|
|
29346
|
+
// a drained list would leak with nothing left to drain, and `release()` has
|
|
29347
|
+
// already reported it had nothing to do — the split ownership this tracking
|
|
29348
|
+
// exists to close, reached from the other side.
|
|
29349
|
+
//
|
|
29350
|
+
// ONLY THIS ARM CHECKS IT. An earlier draft of this comment said the hosted
|
|
29351
|
+
// arm refuses the same way through `check()`, and that is not what `check()`
|
|
29352
|
+
// tests: `assertVenueUnchanged` reads the tunnel's failure and classifies
|
|
29353
|
+
// the daemon, and never looks at `released`. Hosted refuses a post-release
|
|
29354
|
+
// staging only incidentally, because by then the VM is gone. Naming the
|
|
29355
|
+
// check and what it actually tests is the rule this branch spent its length
|
|
29356
|
+
// on, and that draft broke it in the same breath as stating it.
|
|
29357
|
+
//
|
|
29358
|
+
// That leaves the two arms answering one programmer error with different
|
|
29359
|
+
// exit codes — hosted's incidental failure is a `HostedVenueError` at 2,
|
|
29360
|
+
// this one is a plain throw at 1. Recorded on TERM-752 rather than fixed
|
|
29361
|
+
// here: it is unreachable while `placement.ts` refuses first.
|
|
29362
|
+
//
|
|
29363
|
+
// THE EXIT CODE IS WHY IT IS NOT A REFUSAL TYPE. Nothing catches it and
|
|
29364
|
+
// `findRunRefusal` does not match it, so it reaches the top-level catch at
|
|
29365
|
+
// exit 1 — correct, because only our own call ordering can reach this, which
|
|
29366
|
+
// is `assertDomainDeclared`'s reasoning and the same one this branch applied
|
|
29367
|
+
// to `assertProxyStagedForVenue`. Giving it `RunRefusalError` would dress
|
|
29368
|
+
// our defect as a polite refusal, TERM-649's lie inverted.
|
|
29369
|
+
//
|
|
29370
|
+
// It is a NAMED subclass rather than a bare `Error` for a hazard already
|
|
29371
|
+
// here, not a speculative one. `stageProxyCode()` in `containment` throws
|
|
29372
|
+
// `FenceError` on a damaged install, telling the developer to reinstall the
|
|
29373
|
+
// CLI. Two distinguishable failures leave this one call, so the day anyone
|
|
29374
|
+
// adds a catch to surface that instruction, an unnamed ordering bug gets
|
|
29375
|
+
// swept into it and tells a developer to reinstall over our mistake.
|
|
29376
|
+
stageProxyCode: () => {
|
|
29377
|
+
if (released) {
|
|
29378
|
+
return Promise.reject(new LeaseReleasedError("this lease was already released, so a staging made now would never be removed: release() has run and drained what it was holding."));
|
|
29379
|
+
}
|
|
29380
|
+
const staged = stageProxyCode();
|
|
29381
|
+
stagedProxies.push(staged);
|
|
29382
|
+
return Promise.resolve(staged);
|
|
29383
|
+
},
|
|
29384
|
+
census: (label) => Promise.resolve(released ? {
|
|
29385
|
+
observed: false,
|
|
29386
|
+
census: { containers: [], volumes: [], networks: [] },
|
|
29387
|
+
unobservedReason: RELEASED_LEASE_CENSUS_REASON
|
|
29388
|
+
} : censusReport(docker3, label)),
|
|
29389
|
+
publishPreview: (req) => startPreview({ ...req, docker: docker3 }),
|
|
29390
|
+
release: () => {
|
|
29391
|
+
if (released) {
|
|
29392
|
+
return Promise.resolve({
|
|
29393
|
+
kind: "local",
|
|
29394
|
+
released: false,
|
|
29395
|
+
alreadyReleased: true,
|
|
29396
|
+
error: null,
|
|
29397
|
+
detail: "already released; nothing to do"
|
|
29398
|
+
});
|
|
29399
|
+
}
|
|
29400
|
+
released = true;
|
|
29401
|
+
const staged = stagedProxies.splice(0, stagedProxies.length);
|
|
29402
|
+
for (const s of staged)
|
|
29403
|
+
s.cleanup();
|
|
29404
|
+
const volumeFailures = [];
|
|
29405
|
+
for (const name of createdVolumes.splice(0, createdVolumes.length)) {
|
|
29406
|
+
const removed = docker3.sync(["volume", "rm", "-f", "--", name], {
|
|
29407
|
+
timeoutMs: LOCAL_VOLUME_CREATE_TIMEOUT_MS
|
|
29408
|
+
});
|
|
29409
|
+
if (removed.error || removed.status !== 0) {
|
|
29410
|
+
volumeFailures.push(`${name}: ${(removed.error?.message ?? removed.stderr).trim().slice(0, 200)}`);
|
|
29411
|
+
}
|
|
29412
|
+
}
|
|
29413
|
+
cloneVolume = void 0;
|
|
29414
|
+
if (volumeFailures.length > 0) {
|
|
29415
|
+
return Promise.resolve({
|
|
29416
|
+
kind: "local",
|
|
29417
|
+
released: true,
|
|
29418
|
+
alreadyReleased: false,
|
|
29419
|
+
error: `could not remove the clone volume: ${volumeFailures.join("; ")}`,
|
|
29420
|
+
detail: `the lease is closed, but a clone volume remains (label ${STAGE_VOLUME_LABEL_KEY})`
|
|
29421
|
+
});
|
|
29422
|
+
}
|
|
29423
|
+
return Promise.resolve({
|
|
29424
|
+
kind: "local",
|
|
29425
|
+
released: true,
|
|
29426
|
+
alreadyReleased: false,
|
|
29427
|
+
error: null,
|
|
29428
|
+
detail: "the local venue owns no host resources; the lease is closed"
|
|
29429
|
+
});
|
|
29430
|
+
}
|
|
29431
|
+
};
|
|
29432
|
+
return lease;
|
|
29433
|
+
}
|
|
29434
|
+
var VenueRollbackError, UNREADABLE_MESSAGE, UNREADABLE_NAME, UNCOERCIBLE_THROWN, UNDESCRIBABLE_THROWN, UNLISTABLE_ROLLBACK_FAILURES, RELEASED_LEASE_CENSUS_REASON, LeaseReleasedError, STAGE_HELPER_IMAGE, STAGE_VOLUME_LABEL_KEY, VOLUME_NAME_MAX2, LOCAL_VOLUME_CREATE_TIMEOUT_MS, LOCAL_CLONE_FILL_TIMEOUT_MS;
|
|
29435
|
+
var init_venue = __esm({
|
|
29436
|
+
"../../packages/envrun/dist/venue.js"() {
|
|
29437
|
+
"use strict";
|
|
29438
|
+
init_dist();
|
|
29439
|
+
init_labels();
|
|
29440
|
+
init_preview();
|
|
29441
|
+
VenueRollbackError = class extends Error {
|
|
29442
|
+
/** Every undo that threw, in the order they ran (reverse allocation order). */
|
|
29443
|
+
rollbackFailures;
|
|
29444
|
+
constructor(cause, rollbackFailures) {
|
|
29445
|
+
super(rollbackMessage(cause, rollbackFailures), { cause });
|
|
29446
|
+
this.name = "VenueRollbackError";
|
|
29447
|
+
this.rollbackFailures = rollbackFailures;
|
|
29448
|
+
}
|
|
29449
|
+
};
|
|
29450
|
+
UNREADABLE_MESSAGE = "<an error whose message could not be read>";
|
|
29451
|
+
UNREADABLE_NAME = "<an error whose name could not be read>";
|
|
29452
|
+
UNCOERCIBLE_THROWN = "<a thrown value that cannot be converted to a string>";
|
|
29453
|
+
UNDESCRIBABLE_THROWN = "<a thrown value that could not be described>";
|
|
29454
|
+
UNLISTABLE_ROLLBACK_FAILURES = "[venue acquisition rolled back, and the failures could not be listed \u2014 one or more allocated resources may still exist]";
|
|
29455
|
+
RELEASED_LEASE_CENSUS_REASON = "the lease was already released, so this venue can no longer be interrogated";
|
|
29456
|
+
LeaseReleasedError = class extends Error {
|
|
29457
|
+
name = "LeaseReleasedError";
|
|
29458
|
+
};
|
|
29459
|
+
STAGE_HELPER_IMAGE = "busybox:1.37.0";
|
|
29460
|
+
STAGE_VOLUME_LABEL_KEY = "terminalhire.stage";
|
|
29461
|
+
VOLUME_NAME_MAX2 = 128;
|
|
29462
|
+
LOCAL_VOLUME_CREATE_TIMEOUT_MS = 3e4;
|
|
29463
|
+
LOCAL_CLONE_FILL_TIMEOUT_MS = 6e5;
|
|
28832
29464
|
}
|
|
28833
|
-
|
|
28834
|
-
|
|
29465
|
+
});
|
|
29466
|
+
|
|
29467
|
+
// ../../packages/envrun/dist/venueStorageWatch.js
|
|
29468
|
+
function storageWatchArgv(name, labels) {
|
|
29469
|
+
return [
|
|
29470
|
+
"run",
|
|
29471
|
+
"--rm",
|
|
29472
|
+
`--name=${name}`,
|
|
29473
|
+
"--runtime=runc",
|
|
29474
|
+
"--network=none",
|
|
29475
|
+
"--read-only",
|
|
29476
|
+
"--cap-drop=ALL",
|
|
29477
|
+
"--security-opt=no-new-privileges",
|
|
29478
|
+
"--pids-limit=32",
|
|
29479
|
+
...labelArgs(labels),
|
|
29480
|
+
"--",
|
|
29481
|
+
STAGE_HELPER_IMAGE,
|
|
29482
|
+
"sh",
|
|
29483
|
+
"-c",
|
|
29484
|
+
STORAGE_WATCH_SCRIPT
|
|
29485
|
+
];
|
|
29486
|
+
}
|
|
29487
|
+
function readStorageWatch(stream) {
|
|
29488
|
+
const lines = stream.split("\n").filter((l) => l.trim().length > 0);
|
|
29489
|
+
if (lines.length === 0) {
|
|
29490
|
+
return { ok: false, reason: "the storage watcher printed nothing" };
|
|
29491
|
+
}
|
|
29492
|
+
let prevMs = null;
|
|
29493
|
+
let minFreeMiB = Infinity;
|
|
29494
|
+
let minMemAvailableKb = Infinity;
|
|
29495
|
+
let maxGapMs = 0;
|
|
29496
|
+
for (const raw of lines) {
|
|
29497
|
+
const m = SAMPLE.exec(raw.trim());
|
|
29498
|
+
if (m === null) {
|
|
29499
|
+
return {
|
|
29500
|
+
ok: false,
|
|
29501
|
+
reason: `the storage watcher printed a line that is not a sample: ${JSON.stringify(raw.slice(0, 120))}`
|
|
29502
|
+
};
|
|
29503
|
+
}
|
|
29504
|
+
const atMs = Math.round(Number(m[1]) * 1e3);
|
|
29505
|
+
const free = Number(m[2]);
|
|
29506
|
+
const mem = Number(m[3]);
|
|
29507
|
+
if (prevMs !== null) {
|
|
29508
|
+
if (atMs < prevMs) {
|
|
29509
|
+
return { ok: false, reason: "the storage watcher\u2019s timestamps went backwards" };
|
|
29510
|
+
}
|
|
29511
|
+
maxGapMs = Math.max(maxGapMs, atMs - prevMs);
|
|
29512
|
+
}
|
|
29513
|
+
prevMs = atMs;
|
|
29514
|
+
minFreeMiB = Math.min(minFreeMiB, free);
|
|
29515
|
+
minMemAvailableKb = Math.min(minMemAvailableKb, mem);
|
|
28835
29516
|
}
|
|
28836
|
-
const
|
|
28837
|
-
|
|
28838
|
-
|
|
28839
|
-
|
|
29517
|
+
const stats = {
|
|
29518
|
+
samples: lines.length,
|
|
29519
|
+
minFreeMiB,
|
|
29520
|
+
minMemAvailableKb,
|
|
29521
|
+
maxGapMs
|
|
29522
|
+
};
|
|
29523
|
+
if (lines.length < 2) {
|
|
29524
|
+
return {
|
|
29525
|
+
ok: false,
|
|
29526
|
+
reason: "the storage watcher took one sample, which covers no step",
|
|
29527
|
+
...stats
|
|
29528
|
+
};
|
|
28840
29529
|
}
|
|
28841
|
-
|
|
28842
|
-
|
|
28843
|
-
|
|
29530
|
+
if (maxGapMs > STORAGE_WATCH_MAX_GAP_MS) {
|
|
29531
|
+
return {
|
|
29532
|
+
ok: false,
|
|
29533
|
+
reason: `the storage watcher went ${String(maxGapMs)} ms between two samples, over the ${String(STORAGE_WATCH_MAX_GAP_MS)} ms that guarantees a write through the last GiB is seen`,
|
|
29534
|
+
...stats
|
|
29535
|
+
};
|
|
28844
29536
|
}
|
|
28845
|
-
|
|
28846
|
-
|
|
29537
|
+
return { ok: true, ...stats };
|
|
29538
|
+
}
|
|
29539
|
+
function lastSampleUptime(stdout) {
|
|
29540
|
+
const end = stdout.lastIndexOf("\n");
|
|
29541
|
+
if (end === -1)
|
|
29542
|
+
return null;
|
|
29543
|
+
const lines = stdout.slice(0, end).split("\n");
|
|
29544
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
29545
|
+
const m = /^S (\d+(?:\.\d+)?) /.exec(lines[i]);
|
|
29546
|
+
if (m)
|
|
29547
|
+
return Number(m[1]);
|
|
28847
29548
|
}
|
|
28848
|
-
|
|
28849
|
-
|
|
28850
|
-
|
|
28851
|
-
|
|
29549
|
+
return null;
|
|
29550
|
+
}
|
|
29551
|
+
async function startStorageWatch(docker3, labels) {
|
|
29552
|
+
const name = `th-storage-watch-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
29553
|
+
let child;
|
|
29554
|
+
try {
|
|
29555
|
+
child = docker3.spawn(storageWatchArgv(name, labels));
|
|
29556
|
+
} catch (err) {
|
|
29557
|
+
return `the storage watcher could not be started: ${err instanceof Error ? err.message : String(err)}`;
|
|
28852
29558
|
}
|
|
28853
|
-
|
|
28854
|
-
|
|
28855
|
-
|
|
29559
|
+
let stdout = "";
|
|
29560
|
+
let stderr = "";
|
|
29561
|
+
let closed = false;
|
|
29562
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
29563
|
+
const changed = () => {
|
|
29564
|
+
for (const w of [...waiters])
|
|
29565
|
+
w();
|
|
29566
|
+
};
|
|
29567
|
+
const closedPromise = new Promise((resolve6) => {
|
|
29568
|
+
const done2 = () => {
|
|
29569
|
+
closed = true;
|
|
29570
|
+
changed();
|
|
29571
|
+
resolve6();
|
|
29572
|
+
};
|
|
29573
|
+
child.on("close", done2);
|
|
29574
|
+
child.on("error", done2);
|
|
29575
|
+
});
|
|
29576
|
+
child.stdout.on("data", (b) => {
|
|
29577
|
+
stdout += b.toString();
|
|
29578
|
+
changed();
|
|
29579
|
+
});
|
|
29580
|
+
child.stderr.on("data", (b) => {
|
|
29581
|
+
stderr += b.toString();
|
|
29582
|
+
});
|
|
29583
|
+
const waitFor = (cond, ms) => new Promise((resolve6) => {
|
|
29584
|
+
const check = () => {
|
|
29585
|
+
if (cond())
|
|
29586
|
+
finish(true);
|
|
29587
|
+
else if (closed)
|
|
29588
|
+
finish(false);
|
|
29589
|
+
};
|
|
29590
|
+
const timer = setTimeout(() => finish(cond()), ms);
|
|
29591
|
+
const finish = (v) => {
|
|
29592
|
+
clearTimeout(timer);
|
|
29593
|
+
waiters.delete(check);
|
|
29594
|
+
resolve6(v);
|
|
29595
|
+
};
|
|
29596
|
+
waiters.add(check);
|
|
29597
|
+
check();
|
|
29598
|
+
});
|
|
29599
|
+
const killByName = () => {
|
|
29600
|
+
try {
|
|
29601
|
+
docker3.sync(["kill", name], { timeoutMs: STOP_TIMEOUT_MS });
|
|
29602
|
+
} catch {
|
|
29603
|
+
}
|
|
29604
|
+
};
|
|
29605
|
+
const stop = async () => {
|
|
29606
|
+
let stepEnd = null;
|
|
29607
|
+
let stopProblem = null;
|
|
29608
|
+
if (!closed) {
|
|
29609
|
+
try {
|
|
29610
|
+
const res = docker3.sync(["exec", name, "cat", "/proc/uptime"], { timeoutMs: STOP_TIMEOUT_MS });
|
|
29611
|
+
const t = res.error || res.status !== 0 ? NaN : Number(res.stdout.trim().split(/\s+/)[0]);
|
|
29612
|
+
if (Number.isFinite(t))
|
|
29613
|
+
stepEnd = t;
|
|
29614
|
+
else
|
|
29615
|
+
stopProblem = "could not read the venue clock after the step";
|
|
29616
|
+
} catch (err) {
|
|
29617
|
+
stopProblem = `reading the venue clock threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
29618
|
+
}
|
|
29619
|
+
}
|
|
29620
|
+
const sampledAfterStep = stepEnd !== null && await waitFor(() => {
|
|
29621
|
+
const last = lastSampleUptime(stdout);
|
|
29622
|
+
return last !== null && last > stepEnd;
|
|
29623
|
+
}, AFTER_STEP_SAMPLE_TIMEOUT_MS);
|
|
29624
|
+
if (!closed) {
|
|
29625
|
+
try {
|
|
29626
|
+
const res = docker3.sync(["stop", "-t", "1", name], { timeoutMs: STOP_TIMEOUT_MS });
|
|
29627
|
+
if (res.error || res.status !== 0) {
|
|
29628
|
+
const said = `docker stop: ${(res.error?.message ?? res.stderr).trim().slice(0, 200)}`;
|
|
29629
|
+
stopProblem = stopProblem ? `${stopProblem}; ${said}` : said;
|
|
29630
|
+
}
|
|
29631
|
+
} catch (err) {
|
|
29632
|
+
const said = `docker stop threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
29633
|
+
stopProblem = stopProblem ? `${stopProblem}; ${said}` : said;
|
|
29634
|
+
}
|
|
29635
|
+
const timedOut = await Promise.race([
|
|
29636
|
+
closedPromise.then(() => false),
|
|
29637
|
+
new Promise((r) => setTimeout(() => r(true), CLOSE_TIMEOUT_MS).unref())
|
|
29638
|
+
]);
|
|
29639
|
+
if (timedOut) {
|
|
29640
|
+
stopProblem = `${stopProblem ?? ""} the watcher did not exit after docker stop`.trim();
|
|
29641
|
+
child.kill("SIGKILL");
|
|
29642
|
+
}
|
|
29643
|
+
}
|
|
29644
|
+
killByName();
|
|
29645
|
+
return { stdout, sampledAfterStep, stopProblem };
|
|
29646
|
+
};
|
|
29647
|
+
const first = await waitFor(() => stdout.includes("\n"), FIRST_SAMPLE_TIMEOUT_MS);
|
|
29648
|
+
if (!first) {
|
|
29649
|
+
const out = await stop();
|
|
29650
|
+
const said = (stderr || out.stdout).trim().slice(0, 300);
|
|
29651
|
+
return "the storage watcher did not report a first sample" + (said ? `: ${said}` : "");
|
|
29652
|
+
}
|
|
29653
|
+
return { name, stop };
|
|
29654
|
+
}
|
|
29655
|
+
function judgeStorageWatch(out) {
|
|
29656
|
+
const reading = readStorageWatch(out.stdout);
|
|
29657
|
+
if (reading.ok && !out.sampledAfterStep) {
|
|
29658
|
+
const { ok: _ok, ...stats } = reading;
|
|
29659
|
+
return {
|
|
29660
|
+
ok: false,
|
|
29661
|
+
reason: "the storage watcher printed no sample after the test step returned, so the end of the step was not sampled",
|
|
29662
|
+
...stats
|
|
29663
|
+
};
|
|
28856
29664
|
}
|
|
28857
|
-
|
|
28858
|
-
|
|
29665
|
+
return reading;
|
|
29666
|
+
}
|
|
29667
|
+
function describeStorageWatch(r) {
|
|
29668
|
+
const n = (v) => v === void 0 ? "?" : String(v);
|
|
29669
|
+
return `storage watch: min free ${n(r.minFreeMiB)} MiB, min MemAvailable ${n(r.minMemAvailableKb)} kB, max gap ${n(r.maxGapMs)} ms, ${n(r.samples)} samples`;
|
|
29670
|
+
}
|
|
29671
|
+
var STORAGE_WATCH_MAX_GAP_MS, FIRST_SAMPLE_TIMEOUT_MS, STOP_TIMEOUT_MS, CLOSE_TIMEOUT_MS, STORAGE_WATCH_SCRIPT, SAMPLE, AFTER_STEP_SAMPLE_TIMEOUT_MS;
|
|
29672
|
+
var init_venueStorageWatch = __esm({
|
|
29673
|
+
"../../packages/envrun/dist/venueStorageWatch.js"() {
|
|
29674
|
+
"use strict";
|
|
29675
|
+
init_dist();
|
|
29676
|
+
init_venue();
|
|
29677
|
+
STORAGE_WATCH_MAX_GAP_MS = 100;
|
|
29678
|
+
FIRST_SAMPLE_TIMEOUT_MS = 3e4;
|
|
29679
|
+
STOP_TIMEOUT_MS = 2e4;
|
|
29680
|
+
CLOSE_TIMEOUT_MS = 1e4;
|
|
29681
|
+
STORAGE_WATCH_SCRIPT = `trap 'exit 0' TERM; while :; do read u _ < /proc/uptime; f=$(df -P -m / | awk 'NR==2{print $(NF-2)}'); m=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo); echo "S $u $f $m"; sleep 0.05; done`;
|
|
29682
|
+
SAMPLE = /^S (\d+(?:\.\d+)?) (\d+) (\d+)$/;
|
|
29683
|
+
AFTER_STEP_SAMPLE_TIMEOUT_MS = 2e3;
|
|
29684
|
+
}
|
|
29685
|
+
});
|
|
29686
|
+
|
|
29687
|
+
// ../../packages/envrun/dist/npmScriptTimes.js
|
|
29688
|
+
function createNpmScriptTimesCollector(now = Date.now) {
|
|
29689
|
+
let buffer = "";
|
|
29690
|
+
const markers = [];
|
|
29691
|
+
let lastChunkAt = null;
|
|
29692
|
+
const onStdout = (chunk) => {
|
|
29693
|
+
const at = now();
|
|
29694
|
+
lastChunkAt = at;
|
|
29695
|
+
buffer += chunk;
|
|
29696
|
+
for (; ; ) {
|
|
29697
|
+
const nl = buffer.indexOf("\n");
|
|
29698
|
+
if (nl === -1)
|
|
29699
|
+
break;
|
|
29700
|
+
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
29701
|
+
buffer = buffer.slice(nl + 1);
|
|
29702
|
+
const name = stepNameOf(line);
|
|
29703
|
+
if (name !== null)
|
|
29704
|
+
markers.push({ name, at });
|
|
29705
|
+
}
|
|
29706
|
+
};
|
|
29707
|
+
const finish = (opts) => {
|
|
29708
|
+
if (buffer.length > 0 && lastChunkAt !== null) {
|
|
29709
|
+
const name = stepNameOf(buffer.replace(/\r$/, ""));
|
|
29710
|
+
if (name !== null)
|
|
29711
|
+
markers.push({ name, at: lastChunkAt });
|
|
29712
|
+
}
|
|
29713
|
+
if (markers.length === 0 || markers.length > MAX_NPM_SCRIPT_MARKERS)
|
|
29714
|
+
return null;
|
|
29715
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29716
|
+
for (const m of markers) {
|
|
29717
|
+
if (seen.has(m.name))
|
|
29718
|
+
return null;
|
|
29719
|
+
seen.add(m.name);
|
|
29720
|
+
}
|
|
29721
|
+
return markers.map((m, i) => {
|
|
29722
|
+
const next = markers[i + 1];
|
|
29723
|
+
if (next !== void 0)
|
|
29724
|
+
return { name: m.name, status: "ran", ms: next.at - m.at };
|
|
29725
|
+
const ms = opts.timedOut || lastChunkAt === null ? null : lastChunkAt - m.at;
|
|
29726
|
+
return { name: m.name, status: "ran", ms };
|
|
29727
|
+
});
|
|
29728
|
+
};
|
|
29729
|
+
return { onStdout, finish };
|
|
29730
|
+
}
|
|
29731
|
+
var MAX_NPM_SCRIPT_MARKERS;
|
|
29732
|
+
var init_npmScriptTimes = __esm({
|
|
29733
|
+
"../../packages/envrun/dist/npmScriptTimes.js"() {
|
|
29734
|
+
"use strict";
|
|
29735
|
+
init_classify2();
|
|
29736
|
+
MAX_NPM_SCRIPT_MARKERS = 100;
|
|
28859
29737
|
}
|
|
28860
|
-
|
|
28861
|
-
|
|
29738
|
+
});
|
|
29739
|
+
|
|
29740
|
+
// ../../packages/attest/dist/types.js
|
|
29741
|
+
var IN_TOTO_STATEMENT_TYPE, ACCEPTANCE_RUN_PREDICATE_TYPE, IN_TOTO_PAYLOAD_TYPE, TEST_COMMAND_SOURCES;
|
|
29742
|
+
var init_types4 = __esm({
|
|
29743
|
+
"../../packages/attest/dist/types.js"() {
|
|
29744
|
+
"use strict";
|
|
29745
|
+
IN_TOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
29746
|
+
ACCEPTANCE_RUN_PREDICATE_TYPE = "https://terminalhire.com/attestations/acceptance-run/v1";
|
|
29747
|
+
IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json";
|
|
29748
|
+
TEST_COMMAND_SOURCES = ["detected", "founder-declared", "none"];
|
|
28862
29749
|
}
|
|
28863
|
-
|
|
28864
|
-
|
|
29750
|
+
});
|
|
29751
|
+
|
|
29752
|
+
// ../../packages/attest/dist/pae.js
|
|
29753
|
+
function pae(payloadType, body) {
|
|
29754
|
+
const typeBytes = Buffer.from(payloadType, "utf8");
|
|
29755
|
+
return Buffer.concat([
|
|
29756
|
+
Buffer.from(`DSSEv1 ${typeBytes.length} `, "utf8"),
|
|
29757
|
+
typeBytes,
|
|
29758
|
+
Buffer.from(` ${body.length} `, "utf8"),
|
|
29759
|
+
body
|
|
29760
|
+
]);
|
|
29761
|
+
}
|
|
29762
|
+
var init_pae = __esm({
|
|
29763
|
+
"../../packages/attest/dist/pae.js"() {
|
|
29764
|
+
"use strict";
|
|
28865
29765
|
}
|
|
28866
|
-
|
|
28867
|
-
|
|
29766
|
+
});
|
|
29767
|
+
|
|
29768
|
+
// ../../packages/attest/dist/keys.js
|
|
29769
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign as cryptoSign, verify as cryptoVerify } from "crypto";
|
|
29770
|
+
function signBytes(privateKey, data) {
|
|
29771
|
+
return cryptoSign(null, data, privateKey);
|
|
29772
|
+
}
|
|
29773
|
+
var init_keys = __esm({
|
|
29774
|
+
"../../packages/attest/dist/keys.js"() {
|
|
29775
|
+
"use strict";
|
|
29776
|
+
init_pae();
|
|
28868
29777
|
}
|
|
29778
|
+
});
|
|
29779
|
+
|
|
29780
|
+
// ../../packages/attest/dist/dsse.js
|
|
29781
|
+
function createAcceptanceStatement(predicate) {
|
|
28869
29782
|
return {
|
|
28870
|
-
|
|
28871
|
-
|
|
28872
|
-
|
|
28873
|
-
|
|
28874
|
-
zone: ce.zone,
|
|
28875
|
-
projectId: expectations.projectId,
|
|
28876
|
-
email: expectations.serviceAccountEmail,
|
|
28877
|
-
iat,
|
|
28878
|
-
exp,
|
|
28879
|
-
tokenSha256: createHash8("sha256").update(token, "utf8").digest("hex")
|
|
28880
|
-
}
|
|
29783
|
+
_type: IN_TOTO_STATEMENT_TYPE,
|
|
29784
|
+
subject: [{ name: predicate.claim_id, digest: { sha256: predicate.tree_digest } }],
|
|
29785
|
+
predicateType: ACCEPTANCE_RUN_PREDICATE_TYPE,
|
|
29786
|
+
predicate
|
|
28881
29787
|
};
|
|
28882
29788
|
}
|
|
28883
|
-
|
|
28884
|
-
|
|
28885
|
-
|
|
29789
|
+
function signStatementWith(statement, sign, keyid) {
|
|
29790
|
+
const body = Buffer.from(JSON.stringify(statement), "utf8");
|
|
29791
|
+
const sig = Buffer.from(sign(pae(IN_TOTO_PAYLOAD_TYPE, body)));
|
|
29792
|
+
return {
|
|
29793
|
+
payload: body.toString("base64"),
|
|
29794
|
+
payloadType: IN_TOTO_PAYLOAD_TYPE,
|
|
29795
|
+
signatures: [{ ...keyid === void 0 ? {} : { keyid }, sig: sig.toString("base64") }]
|
|
29796
|
+
};
|
|
29797
|
+
}
|
|
29798
|
+
function signStatement(statement, privateKey, keyid) {
|
|
29799
|
+
return signStatementWith(statement, (data) => signBytes(privateKey, data), keyid);
|
|
29800
|
+
}
|
|
29801
|
+
var init_dsse = __esm({
|
|
29802
|
+
"../../packages/attest/dist/dsse.js"() {
|
|
28886
29803
|
"use strict";
|
|
28887
|
-
|
|
28888
|
-
|
|
28889
|
-
|
|
29804
|
+
init_pae();
|
|
29805
|
+
init_keys();
|
|
29806
|
+
init_types4();
|
|
28890
29807
|
}
|
|
28891
29808
|
});
|
|
28892
29809
|
|
|
28893
|
-
// ../../packages/attest/dist/
|
|
28894
|
-
import { createHash as
|
|
28895
|
-
|
|
29810
|
+
// ../../packages/attest/dist/dispatchedRun.js
|
|
29811
|
+
import { createHash as createHash7, createPublicKey as createPublicKey2 } from "crypto";
|
|
29812
|
+
var init_dispatchedRun = __esm({
|
|
29813
|
+
"../../packages/attest/dist/dispatchedRun.js"() {
|
|
29814
|
+
"use strict";
|
|
29815
|
+
init_keys();
|
|
29816
|
+
init_pae();
|
|
29817
|
+
init_types4();
|
|
29818
|
+
}
|
|
29819
|
+
});
|
|
29820
|
+
|
|
29821
|
+
// ../../packages/attest/dist/sealedbox.js
|
|
29822
|
+
import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, diffieHellman, generateKeyPairSync as generateKeyPairSync2, hkdfSync as hkdfSync2, randomBytes as randomBytes9 } from "crypto";
|
|
29823
|
+
var init_sealedbox = __esm({
|
|
29824
|
+
"../../packages/attest/dist/sealedbox.js"() {
|
|
29825
|
+
"use strict";
|
|
29826
|
+
init_keys();
|
|
29827
|
+
init_pae();
|
|
29828
|
+
}
|
|
29829
|
+
});
|
|
29830
|
+
|
|
29831
|
+
// ../../packages/attest/dist/aead.js
|
|
29832
|
+
import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes10 } from "crypto";
|
|
29833
|
+
var init_aead = __esm({
|
|
29834
|
+
"../../packages/attest/dist/aead.js"() {
|
|
29835
|
+
"use strict";
|
|
29836
|
+
init_pae();
|
|
29837
|
+
}
|
|
29838
|
+
});
|
|
29839
|
+
|
|
29840
|
+
// ../../packages/attest/dist/manifest.js
|
|
29841
|
+
var init_manifest = __esm({
|
|
29842
|
+
"../../packages/attest/dist/manifest.js"() {
|
|
29843
|
+
"use strict";
|
|
29844
|
+
init_pae();
|
|
29845
|
+
init_keys();
|
|
29846
|
+
init_types4();
|
|
29847
|
+
}
|
|
29848
|
+
});
|
|
29849
|
+
|
|
29850
|
+
// ../../packages/attest/dist/attestation.js
|
|
29851
|
+
var init_attestation = __esm({
|
|
29852
|
+
"../../packages/attest/dist/attestation.js"() {
|
|
29853
|
+
"use strict";
|
|
29854
|
+
init_manifest();
|
|
29855
|
+
init_types4();
|
|
29856
|
+
}
|
|
29857
|
+
});
|
|
29858
|
+
|
|
29859
|
+
// ../../packages/attest/dist/nonce.js
|
|
29860
|
+
var init_nonce = __esm({
|
|
29861
|
+
"../../packages/attest/dist/nonce.js"() {
|
|
29862
|
+
"use strict";
|
|
29863
|
+
}
|
|
29864
|
+
});
|
|
29865
|
+
|
|
29866
|
+
// ../../packages/attest/dist/gceIdentity.js
|
|
29867
|
+
import { createHash as createHash8, createPublicKey as createPublicKey3, verify as cryptoVerify2 } from "crypto";
|
|
29868
|
+
function expectedAudience(dispatchId) {
|
|
29869
|
+
if (dispatchId === "")
|
|
29870
|
+
throw new TypeError("expectedAudience: dispatchId is empty");
|
|
29871
|
+
return `${VERIFICATION_RUN_AUDIENCE_BASE}#${dispatchId}`;
|
|
29872
|
+
}
|
|
29873
|
+
function venueIdentityEmail(projectId) {
|
|
29874
|
+
if (projectId === "")
|
|
29875
|
+
throw new TypeError("venueIdentityEmail: projectId is empty");
|
|
29876
|
+
return `venue-identity@${projectId}.iam.gserviceaccount.com`;
|
|
29877
|
+
}
|
|
29878
|
+
function refuseIdentity(reason, detail) {
|
|
28896
29879
|
return { ok: false, reason, detail };
|
|
28897
29880
|
}
|
|
28898
|
-
function
|
|
28899
|
-
|
|
29881
|
+
function strictBase64UrlDecode(s) {
|
|
29882
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1)
|
|
29883
|
+
return null;
|
|
29884
|
+
const decoded = Buffer.from(s, "base64url");
|
|
29885
|
+
if (decoded.toString("base64url") !== s)
|
|
29886
|
+
return null;
|
|
29887
|
+
return decoded;
|
|
28900
29888
|
}
|
|
28901
|
-
function
|
|
29889
|
+
function decodeJsonSegment(segment) {
|
|
29890
|
+
const bytes = strictBase64UrlDecode(segment);
|
|
29891
|
+
if (bytes === null)
|
|
29892
|
+
return null;
|
|
29893
|
+
let parsed;
|
|
29894
|
+
try {
|
|
29895
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
29896
|
+
} catch {
|
|
29897
|
+
return null;
|
|
29898
|
+
}
|
|
29899
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
29900
|
+
return null;
|
|
29901
|
+
return parsed;
|
|
29902
|
+
}
|
|
29903
|
+
function assertExpectationsComplete(expectations) {
|
|
28902
29904
|
const required = [
|
|
28903
29905
|
["audience", expectations.audience],
|
|
28904
|
-
["
|
|
28905
|
-
["instanceName", expectations.instanceName],
|
|
29906
|
+
["serviceAccountEmail", expectations.serviceAccountEmail],
|
|
28906
29907
|
["projectId", expectations.projectId],
|
|
28907
|
-
|
|
28908
|
-
"additionalNonces[]",
|
|
28909
|
-
n
|
|
28910
|
-
]),
|
|
28911
|
-
...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
|
|
28912
|
-
["containerEnv name", name],
|
|
28913
|
-
[`containerEnv.${name}`, value]
|
|
28914
|
-
])
|
|
29908
|
+
["instanceName", expectations.instanceName]
|
|
28915
29909
|
];
|
|
28916
29910
|
for (const [name, value] of required) {
|
|
28917
|
-
if (value === "")
|
|
28918
|
-
throw new TypeError(`
|
|
28919
|
-
|
|
29911
|
+
if (value === "")
|
|
29912
|
+
throw new TypeError(`verifyGceIdentityToken: expectations.${name} is empty`);
|
|
29913
|
+
}
|
|
29914
|
+
if (expectations.instanceId === "") {
|
|
29915
|
+
throw new TypeError("verifyGceIdentityToken: expectations.instanceId is empty");
|
|
28920
29916
|
}
|
|
28921
29917
|
}
|
|
28922
|
-
function
|
|
28923
|
-
|
|
28924
|
-
return [v];
|
|
28925
|
-
if (!Array.isArray(v) || v.length === 0)
|
|
28926
|
-
return null;
|
|
28927
|
-
if (!v.every((n) => typeof n === "string"))
|
|
28928
|
-
return null;
|
|
28929
|
-
return v;
|
|
28930
|
-
}
|
|
28931
|
-
async function verifyConfidentialSpaceToken(token, expectations, deps) {
|
|
28932
|
-
assertExpectationsComplete2(expectations);
|
|
29918
|
+
async function verifyGceIdentityToken(token, expectations, deps) {
|
|
29919
|
+
assertExpectationsComplete(expectations);
|
|
28933
29920
|
if (!Number.isFinite(deps.now)) {
|
|
28934
|
-
throw new TypeError("
|
|
28935
|
-
}
|
|
28936
|
-
if (expectations.allowedImageDigests.length === 0) {
|
|
28937
|
-
return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
|
|
29921
|
+
throw new TypeError("verifyGceIdentityToken: deps.now is not a finite number");
|
|
28938
29922
|
}
|
|
28939
29923
|
if (token.length > MAX_TOKEN_LENGTH) {
|
|
28940
|
-
return
|
|
29924
|
+
return refuseIdentity("token-malformed", "longer than any GCE identity token");
|
|
28941
29925
|
}
|
|
28942
29926
|
const parts = token.split(".");
|
|
28943
29927
|
if (parts.length !== 3) {
|
|
28944
|
-
return
|
|
29928
|
+
return refuseIdentity("token-malformed", "not a three-segment compact JWT");
|
|
28945
29929
|
}
|
|
28946
29930
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
28947
29931
|
const header = decodeJsonSegment(headerB64);
|
|
28948
29932
|
if (header === null) {
|
|
28949
|
-
return
|
|
29933
|
+
return refuseIdentity("token-malformed", "header segment is not base64url-encoded JSON");
|
|
28950
29934
|
}
|
|
28951
29935
|
if (header.alg !== "RS256") {
|
|
28952
|
-
return
|
|
29936
|
+
return refuseIdentity("algorithm-not-rs256", "token names an unsupported algorithm");
|
|
28953
29937
|
}
|
|
28954
29938
|
if (typeof header.kid !== "string" || header.kid === "") {
|
|
28955
|
-
return
|
|
29939
|
+
return refuseIdentity("unknown-key", "token header names no key id");
|
|
28956
29940
|
}
|
|
28957
29941
|
const jwks = await deps.fetchJwks();
|
|
28958
29942
|
const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
|
|
28959
29943
|
if (jwk === void 0) {
|
|
28960
|
-
return
|
|
29944
|
+
return refuseIdentity("unknown-key", "token key id is not in the published key set");
|
|
28961
29945
|
}
|
|
28962
29946
|
const signature = strictBase64UrlDecode(signatureB64);
|
|
28963
29947
|
if (signature === null) {
|
|
28964
|
-
return
|
|
29948
|
+
return refuseIdentity("token-malformed", "signature segment is not base64url");
|
|
28965
29949
|
}
|
|
28966
29950
|
let signatureValid = false;
|
|
28967
29951
|
try {
|
|
28968
|
-
const publicKey =
|
|
28969
|
-
signatureValid =
|
|
29952
|
+
const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
|
|
29953
|
+
signatureValid = cryptoVerify2("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
28970
29954
|
} catch {
|
|
28971
29955
|
signatureValid = false;
|
|
28972
29956
|
}
|
|
28973
29957
|
if (!signatureValid) {
|
|
28974
|
-
return
|
|
29958
|
+
return refuseIdentity("signature-invalid", "signature does not verify under the named key");
|
|
28975
29959
|
}
|
|
28976
29960
|
const payload = decodeJsonSegment(payloadB64);
|
|
28977
29961
|
if (payload === null) {
|
|
28978
|
-
return
|
|
29962
|
+
return refuseIdentity("token-malformed", "payload segment is not base64url-encoded JSON");
|
|
28979
29963
|
}
|
|
28980
|
-
if (payload.iss !==
|
|
28981
|
-
return
|
|
29964
|
+
if (payload.iss !== "https://accounts.google.com") {
|
|
29965
|
+
return refuseIdentity("issuer-mismatch", "issuer is not https://accounts.google.com");
|
|
28982
29966
|
}
|
|
28983
29967
|
if (payload.aud !== expectations.audience) {
|
|
28984
|
-
return
|
|
29968
|
+
return refuseIdentity("audience-mismatch", "audience is not the one this run expects");
|
|
28985
29969
|
}
|
|
28986
29970
|
const nowSeconds = deps.now / 1e3;
|
|
28987
29971
|
const exp = payload.exp;
|
|
28988
29972
|
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
|
|
28989
|
-
return
|
|
29973
|
+
return refuseIdentity("token-expired", "exp is missing, malformed, or in the past");
|
|
28990
29974
|
}
|
|
28991
29975
|
const iat = payload.iat;
|
|
28992
29976
|
if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
|
|
28993
|
-
return
|
|
28994
|
-
}
|
|
28995
|
-
const nonces = nonceList(payload.eat_nonce);
|
|
28996
|
-
if (nonces === null) {
|
|
28997
|
-
return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
|
|
28998
|
-
}
|
|
28999
|
-
if (!nonces.includes(expectations.nonce)) {
|
|
29000
|
-
return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
|
|
29001
|
-
}
|
|
29002
|
-
for (const extra of expectations.additionalNonces ?? []) {
|
|
29003
|
-
if (!nonces.includes(extra)) {
|
|
29004
|
-
return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
|
|
29005
|
-
}
|
|
29006
|
-
}
|
|
29007
|
-
if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
|
|
29008
|
-
return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
|
|
29009
|
-
}
|
|
29010
|
-
if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
|
|
29011
|
-
return refuse2("debug-enabled", "the image is not a production image with debug disabled");
|
|
29012
|
-
}
|
|
29013
|
-
if (payload.secboot !== true) {
|
|
29014
|
-
return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
|
|
29015
|
-
}
|
|
29016
|
-
if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
|
|
29017
|
-
return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
|
|
29018
|
-
}
|
|
29019
|
-
const hwmodel = payload.hwmodel;
|
|
29020
|
-
const submods = payload.submods;
|
|
29021
|
-
const gce = isPlainObject2(submods) ? submods.gce : void 0;
|
|
29022
|
-
if (!isPlainObject2(gce)) {
|
|
29023
|
-
return refuse2("submods-gce-missing", "no submods.gce block names the instance");
|
|
29024
|
-
}
|
|
29025
|
-
if (gce.project_id !== expectations.projectId) {
|
|
29026
|
-
return refuse2("project-mismatch", "token names an instance in a different project");
|
|
29977
|
+
return refuseIdentity("issued-in-future", "iat is missing, malformed, or too far in the future");
|
|
29027
29978
|
}
|
|
29028
|
-
if (
|
|
29029
|
-
return
|
|
29979
|
+
if (payload.email !== expectations.serviceAccountEmail || payload.email_verified !== true) {
|
|
29980
|
+
return refuseIdentity("wrong-account", "token is not from the venue identity account");
|
|
29030
29981
|
}
|
|
29031
|
-
|
|
29032
|
-
|
|
29982
|
+
const google = payload.google;
|
|
29983
|
+
const computeEngine = typeof google === "object" && google !== null && !Array.isArray(google) ? google.compute_engine : void 0;
|
|
29984
|
+
if (typeof computeEngine !== "object" || computeEngine === null || Array.isArray(computeEngine)) {
|
|
29985
|
+
return refuseIdentity("compute-engine-block-missing", "no google.compute_engine block \u2014 was the token fetched with format=full?");
|
|
29033
29986
|
}
|
|
29034
|
-
const
|
|
29035
|
-
if (
|
|
29036
|
-
return
|
|
29987
|
+
const ce = computeEngine;
|
|
29988
|
+
if (ce.project_id !== expectations.projectId) {
|
|
29989
|
+
return refuseIdentity("project-mismatch", "token names an instance in a different project");
|
|
29037
29990
|
}
|
|
29038
|
-
|
|
29039
|
-
|
|
29040
|
-
if (!isPlainObject2(container)) {
|
|
29041
|
-
return refuse2("submods-container-missing", "no submods.container block names the workload");
|
|
29991
|
+
if (ce.instance_name !== expectations.instanceName) {
|
|
29992
|
+
return refuseIdentity("instance-name-mismatch", "token names a different instance than this run booted");
|
|
29042
29993
|
}
|
|
29043
|
-
if (typeof
|
|
29044
|
-
return
|
|
29994
|
+
if (typeof ce.instance_id !== "string" || ce.instance_id === "") {
|
|
29995
|
+
return refuseIdentity("instance-id-missing", "instance id is absent or not a string");
|
|
29045
29996
|
}
|
|
29046
|
-
|
|
29047
|
-
|
|
29048
|
-
return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
|
|
29997
|
+
if (expectations.instanceId !== void 0 && ce.instance_id !== expectations.instanceId) {
|
|
29998
|
+
return refuseIdentity("instance-id-mismatch", "token names a different instance than the boot record");
|
|
29049
29999
|
}
|
|
29050
|
-
|
|
29051
|
-
|
|
29052
|
-
if (!isPlainObject2(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
|
|
29053
|
-
return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
|
|
29054
|
-
}
|
|
30000
|
+
if (typeof ce.zone !== "string" || !expectations.allowedZones.includes(ce.zone)) {
|
|
30001
|
+
return refuseIdentity("zone-not-allowed", "zone is not one the pool is configured to use");
|
|
29055
30002
|
}
|
|
29056
30003
|
return {
|
|
29057
30004
|
ok: true,
|
|
29058
30005
|
value: {
|
|
29059
|
-
instanceId,
|
|
30006
|
+
instanceId: ce.instance_id,
|
|
29060
30007
|
instanceName: expectations.instanceName,
|
|
29061
|
-
zone,
|
|
30008
|
+
zone: ce.zone,
|
|
29062
30009
|
projectId: expectations.projectId,
|
|
29063
|
-
|
|
29064
|
-
imageReference,
|
|
29065
|
-
hwmodel,
|
|
30010
|
+
email: expectations.serviceAccountEmail,
|
|
29066
30011
|
iat,
|
|
29067
30012
|
exp,
|
|
29068
|
-
tokenSha256:
|
|
29069
|
-
}
|
|
29070
|
-
};
|
|
29071
|
-
}
|
|
29072
|
-
var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
|
|
29073
|
-
var init_confidentialSpace = __esm({
|
|
29074
|
-
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
29075
|
-
"use strict";
|
|
29076
|
-
init_gceIdentity();
|
|
29077
|
-
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
29078
|
-
CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
|
|
29079
|
-
CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
|
|
29080
|
-
CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
|
|
29081
|
-
}
|
|
29082
|
-
});
|
|
29083
|
-
|
|
29084
|
-
// ../../packages/attest/dist/venueInstanceName.js
|
|
29085
|
-
function venueInstanceName(runId) {
|
|
29086
|
-
return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
|
|
29087
|
-
}
|
|
29088
|
-
var init_venueInstanceName = __esm({
|
|
29089
|
-
"../../packages/attest/dist/venueInstanceName.js"() {
|
|
29090
|
-
"use strict";
|
|
29091
|
-
}
|
|
29092
|
-
});
|
|
29093
|
-
|
|
29094
|
-
// ../../packages/attest/dist/verify.js
|
|
29095
|
-
var init_verify = __esm({
|
|
29096
|
-
"../../packages/attest/dist/verify.js"() {
|
|
29097
|
-
"use strict";
|
|
29098
|
-
init_dsse();
|
|
29099
|
-
init_manifest();
|
|
29100
|
-
init_types4();
|
|
29101
|
-
}
|
|
29102
|
-
});
|
|
29103
|
-
|
|
29104
|
-
// ../../packages/attest/dist/index.js
|
|
29105
|
-
var init_dist2 = __esm({
|
|
29106
|
-
"../../packages/attest/dist/index.js"() {
|
|
29107
|
-
"use strict";
|
|
29108
|
-
init_types4();
|
|
29109
|
-
init_pae();
|
|
29110
|
-
init_keys();
|
|
29111
|
-
init_dsse();
|
|
29112
|
-
init_dispatchedRun();
|
|
29113
|
-
init_sealedbox();
|
|
29114
|
-
init_aead();
|
|
29115
|
-
init_manifest();
|
|
29116
|
-
init_attestation();
|
|
29117
|
-
init_nonce();
|
|
29118
|
-
init_gceIdentity();
|
|
29119
|
-
init_confidentialSpace();
|
|
29120
|
-
init_venueInstanceName();
|
|
29121
|
-
init_verify();
|
|
29122
|
-
}
|
|
29123
|
-
});
|
|
29124
|
-
|
|
29125
|
-
// ../../packages/envrun/dist/attestation.js
|
|
29126
|
-
import { createHash as createHash10, randomBytes as randomBytes9 } from "crypto";
|
|
29127
|
-
function contradicts(outcome, counts, exitCode) {
|
|
29128
|
-
const budget = OUTCOME_TO_BUDGET[outcome];
|
|
29129
|
-
if (budget === null)
|
|
29130
|
-
return null;
|
|
29131
|
-
return CONTRADICTS_COUNTS[budget](counts, exitCode);
|
|
29132
|
-
}
|
|
29133
|
-
function localMeasurement(imageReference) {
|
|
29134
|
-
return `${LOCAL_MEASUREMENT_PREFIX}${imageReference}`;
|
|
29135
|
-
}
|
|
29136
|
-
function isCanonicalRepoDigest(value) {
|
|
29137
|
-
const match2 = REPO_DIGEST_RE.exec(value);
|
|
29138
|
-
if (match2 === null)
|
|
29139
|
-
return false;
|
|
29140
|
-
const name = value.slice(0, value.indexOf("@"));
|
|
29141
|
-
const captured = match2[1];
|
|
29142
|
-
let domain = null;
|
|
29143
|
-
let path5 = name;
|
|
29144
|
-
if (captured !== void 0) {
|
|
29145
|
-
const isRegistry = captured.includes(".") || captured.includes(":") || captured === "localhost" || captured.toLowerCase() !== captured;
|
|
29146
|
-
if (isRegistry) {
|
|
29147
|
-
domain = captured;
|
|
29148
|
-
path5 = name.slice(captured.length + 1);
|
|
29149
|
-
}
|
|
29150
|
-
}
|
|
29151
|
-
const onDockerHub = domain === null || domain === "docker.io" || domain === "index.docker.io";
|
|
29152
|
-
if (onDockerHub && !path5.includes("/"))
|
|
29153
|
-
path5 = `library/${path5}`;
|
|
29154
|
-
return path5.length <= 255;
|
|
29155
|
-
}
|
|
29156
|
-
function imageRepo(ref) {
|
|
29157
|
-
const at = ref.indexOf("@");
|
|
29158
|
-
const base = at === -1 ? ref : ref.slice(0, at);
|
|
29159
|
-
const slash = base.lastIndexOf("/");
|
|
29160
|
-
const colon = base.lastIndexOf(":");
|
|
29161
|
-
return colon > slash ? base.slice(0, colon) : base;
|
|
29162
|
-
}
|
|
29163
|
-
function imageDigestOf(ref) {
|
|
29164
|
-
const at = ref.indexOf("@");
|
|
29165
|
-
return at === -1 ? null : ref.slice(at + 1);
|
|
29166
|
-
}
|
|
29167
|
-
function sha256Hex(data) {
|
|
29168
|
-
return createHash10("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
|
|
29169
|
-
}
|
|
29170
|
-
function toTestRunResult(result, outputSha256) {
|
|
29171
|
-
return {
|
|
29172
|
-
exit_code: result.exitCode ?? -1,
|
|
29173
|
-
...outputSha256 === void 0 ? {} : { output_sha256: outputSha256 },
|
|
29174
|
-
...result.counts === null ? {} : {
|
|
29175
|
-
tests_passed: result.counts.tests_passed,
|
|
29176
|
-
tests_failed: result.counts.tests_failed,
|
|
29177
|
-
...result.counts.coverage === "partial" ? { tests_coverage: "partial" } : {}
|
|
30013
|
+
tokenSha256: createHash8("sha256").update(token, "utf8").digest("hex")
|
|
29178
30014
|
}
|
|
29179
30015
|
};
|
|
29180
30016
|
}
|
|
29181
|
-
|
|
29182
|
-
|
|
29183
|
-
|
|
29184
|
-
|
|
29185
|
-
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
}
|
|
29189
|
-
if (patched.outcome === null) {
|
|
29190
|
-
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
29191
|
-
}
|
|
29192
|
-
if (baseline.status === "refused") {
|
|
29193
|
-
return refuse3("baseline-was-refused", `the baseline run was refused before executing (${baseline.boundaryRefusals.length} boundary refusal(s)), so there is no "before" to compare against`);
|
|
30017
|
+
var VERIFICATION_RUN_AUDIENCE_BASE, IAT_FUTURE_SKEW_MS, MAX_TOKEN_LENGTH;
|
|
30018
|
+
var init_gceIdentity = __esm({
|
|
30019
|
+
"../../packages/attest/dist/gceIdentity.js"() {
|
|
30020
|
+
"use strict";
|
|
30021
|
+
VERIFICATION_RUN_AUDIENCE_BASE = "https://terminalhire.com/api/verification-run";
|
|
30022
|
+
IAT_FUTURE_SKEW_MS = 5 * 60 * 1e3;
|
|
30023
|
+
MAX_TOKEN_LENGTH = 8192;
|
|
29194
30024
|
}
|
|
29195
|
-
|
|
29196
|
-
|
|
30025
|
+
});
|
|
30026
|
+
|
|
30027
|
+
// ../../packages/attest/dist/confidentialSpace.js
|
|
30028
|
+
import { createHash as createHash9, createPublicKey as createPublicKey4, verify as cryptoVerify3 } from "crypto";
|
|
30029
|
+
function refuse2(reason, detail) {
|
|
30030
|
+
return { ok: false, reason, detail };
|
|
30031
|
+
}
|
|
30032
|
+
function isPlainObject2(v) {
|
|
30033
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
30034
|
+
}
|
|
30035
|
+
function assertExpectationsComplete2(expectations) {
|
|
30036
|
+
const required = [
|
|
30037
|
+
["audience", expectations.audience],
|
|
30038
|
+
["nonce", expectations.nonce],
|
|
30039
|
+
["instanceName", expectations.instanceName],
|
|
30040
|
+
["projectId", expectations.projectId],
|
|
30041
|
+
...(expectations.additionalNonces ?? []).map((n) => [
|
|
30042
|
+
"additionalNonces[]",
|
|
30043
|
+
n
|
|
30044
|
+
]),
|
|
30045
|
+
...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
|
|
30046
|
+
["containerEnv name", name],
|
|
30047
|
+
[`containerEnv.${name}`, value]
|
|
30048
|
+
])
|
|
30049
|
+
];
|
|
30050
|
+
for (const [name, value] of required) {
|
|
30051
|
+
if (value === "") {
|
|
30052
|
+
throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
|
|
30053
|
+
}
|
|
29197
30054
|
}
|
|
29198
|
-
|
|
29199
|
-
|
|
30055
|
+
}
|
|
30056
|
+
function nonceList(v) {
|
|
30057
|
+
if (typeof v === "string")
|
|
30058
|
+
return [v];
|
|
30059
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
30060
|
+
return null;
|
|
30061
|
+
if (!v.every((n) => typeof n === "string"))
|
|
30062
|
+
return null;
|
|
30063
|
+
return v;
|
|
30064
|
+
}
|
|
30065
|
+
async function verifyConfidentialSpaceToken(token, expectations, deps) {
|
|
30066
|
+
assertExpectationsComplete2(expectations);
|
|
30067
|
+
if (!Number.isFinite(deps.now)) {
|
|
30068
|
+
throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
|
|
29200
30069
|
}
|
|
29201
|
-
|
|
29202
|
-
|
|
29203
|
-
return refuse3("our-environment-failed", `the patched run ended ${patched.outcome}: the test command could not be invoked, which is our environment failing and not a verdict about the work. No statement is emitted, because every possible value would be a claim we cannot support.`);
|
|
30070
|
+
if (expectations.allowedImageDigests.length === 0) {
|
|
30071
|
+
return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
|
|
29204
30072
|
}
|
|
29205
|
-
if (
|
|
29206
|
-
return
|
|
30073
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
30074
|
+
return refuse2("token-malformed", "longer than any attestation token");
|
|
29207
30075
|
}
|
|
29208
|
-
|
|
29209
|
-
|
|
30076
|
+
const parts = token.split(".");
|
|
30077
|
+
if (parts.length !== 3) {
|
|
30078
|
+
return refuse2("token-malformed", "not a three-segment compact JWT");
|
|
29210
30079
|
}
|
|
29211
|
-
|
|
29212
|
-
|
|
30080
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
30081
|
+
const header = decodeJsonSegment(headerB64);
|
|
30082
|
+
if (header === null) {
|
|
30083
|
+
return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
|
|
29213
30084
|
}
|
|
29214
|
-
if (
|
|
29215
|
-
return
|
|
30085
|
+
if (header.alg !== "RS256") {
|
|
30086
|
+
return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
|
|
29216
30087
|
}
|
|
29217
|
-
if (
|
|
29218
|
-
return
|
|
30088
|
+
if (typeof header.kid !== "string" || header.kid === "") {
|
|
30089
|
+
return refuse2("unknown-key", "token header names no key id");
|
|
29219
30090
|
}
|
|
29220
|
-
|
|
29221
|
-
|
|
30091
|
+
const jwks = await deps.fetchJwks();
|
|
30092
|
+
const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
|
|
30093
|
+
if (jwk === void 0) {
|
|
30094
|
+
return refuse2("unknown-key", "token key id is not in the published key set");
|
|
29222
30095
|
}
|
|
29223
|
-
|
|
29224
|
-
|
|
30096
|
+
const signature = strictBase64UrlDecode(signatureB64);
|
|
30097
|
+
if (signature === null) {
|
|
30098
|
+
return refuse2("token-malformed", "signature segment is not base64url");
|
|
29225
30099
|
}
|
|
29226
|
-
|
|
29227
|
-
|
|
30100
|
+
let signatureValid = false;
|
|
30101
|
+
try {
|
|
30102
|
+
const publicKey = createPublicKey4({ key: jwk, format: "jwk" });
|
|
30103
|
+
signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
30104
|
+
} catch {
|
|
30105
|
+
signatureValid = false;
|
|
29228
30106
|
}
|
|
29229
|
-
|
|
29230
|
-
|
|
29231
|
-
["baseline", baseline.outcome, baseline.counts, baseline.exitCode]
|
|
29232
|
-
]) {
|
|
29233
|
-
const why = contradicts(outcome, counts, exitCode);
|
|
29234
|
-
if (why !== null) {
|
|
29235
|
-
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
29236
|
-
}
|
|
30107
|
+
if (!signatureValid) {
|
|
30108
|
+
return refuse2("signature-invalid", "signature does not verify under the named key");
|
|
29237
30109
|
}
|
|
29238
|
-
|
|
29239
|
-
|
|
29240
|
-
|
|
29241
|
-
]) {
|
|
29242
|
-
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
29243
|
-
if (why !== null)
|
|
29244
|
-
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
30110
|
+
const payload = decodeJsonSegment(payloadB64);
|
|
30111
|
+
if (payload === null) {
|
|
30112
|
+
return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
|
|
29245
30113
|
}
|
|
29246
|
-
if (
|
|
29247
|
-
return
|
|
30114
|
+
if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
|
|
30115
|
+
return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
|
|
29248
30116
|
}
|
|
29249
|
-
if (
|
|
29250
|
-
return
|
|
30117
|
+
if (payload.aud !== expectations.audience) {
|
|
30118
|
+
return refuse2("wrong-audience", "audience is not the one this run expects");
|
|
29251
30119
|
}
|
|
29252
|
-
|
|
29253
|
-
|
|
29254
|
-
|
|
29255
|
-
|
|
29256
|
-
const why = IMAGE_SOURCE_IS_SIGNABLE[source](image);
|
|
29257
|
-
if (why !== null)
|
|
29258
|
-
return refuse3("image-origin-unattested", `the ${side} run ${why}`);
|
|
30120
|
+
const nowSeconds = deps.now / 1e3;
|
|
30121
|
+
const exp = payload.exp;
|
|
30122
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
|
|
30123
|
+
return refuse2("expired", "exp is missing, malformed, or in the past");
|
|
29259
30124
|
}
|
|
29260
|
-
|
|
29261
|
-
|
|
30125
|
+
const iat = payload.iat;
|
|
30126
|
+
if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
|
|
30127
|
+
return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
|
|
29262
30128
|
}
|
|
29263
|
-
|
|
29264
|
-
|
|
30129
|
+
const nonces = nonceList(payload.eat_nonce);
|
|
30130
|
+
if (nonces === null) {
|
|
30131
|
+
return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
|
|
29265
30132
|
}
|
|
29266
|
-
if (
|
|
29267
|
-
return
|
|
30133
|
+
if (!nonces.includes(expectations.nonce)) {
|
|
30134
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
|
|
29268
30135
|
}
|
|
29269
|
-
|
|
29270
|
-
|
|
30136
|
+
for (const extra of expectations.additionalNonces ?? []) {
|
|
30137
|
+
if (!nonces.includes(extra)) {
|
|
30138
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
|
|
30139
|
+
}
|
|
29271
30140
|
}
|
|
29272
|
-
if (
|
|
29273
|
-
return
|
|
30141
|
+
if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
|
|
30142
|
+
return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
|
|
29274
30143
|
}
|
|
29275
|
-
if (
|
|
29276
|
-
return
|
|
30144
|
+
if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
|
|
30145
|
+
return refuse2("debug-enabled", "the image is not a production image with debug disabled");
|
|
29277
30146
|
}
|
|
29278
|
-
if (
|
|
29279
|
-
return
|
|
30147
|
+
if (payload.secboot !== true) {
|
|
30148
|
+
return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
|
|
29280
30149
|
}
|
|
29281
|
-
if (typeof
|
|
29282
|
-
return
|
|
30150
|
+
if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
|
|
30151
|
+
return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
|
|
29283
30152
|
}
|
|
29284
|
-
|
|
29285
|
-
|
|
30153
|
+
const hwmodel = payload.hwmodel;
|
|
30154
|
+
const submods = payload.submods;
|
|
30155
|
+
const gce = isPlainObject2(submods) ? submods.gce : void 0;
|
|
30156
|
+
if (!isPlainObject2(gce)) {
|
|
30157
|
+
return refuse2("submods-gce-missing", "no submods.gce block names the instance");
|
|
29286
30158
|
}
|
|
29287
|
-
if (
|
|
29288
|
-
return
|
|
30159
|
+
if (gce.project_id !== expectations.projectId) {
|
|
30160
|
+
return refuse2("project-mismatch", "token names an instance in a different project");
|
|
29289
30161
|
}
|
|
29290
|
-
|
|
29291
|
-
|
|
29292
|
-
return refuse3("image-digest-contradicts-image", `the pair ran ${patched.containerImage}, whose reference pins digest ${claimedDigest}, but the digest field says ${String(imageDigestOf(patched.containerImageDigest))}. A measurement must not sign one digest while the record names another.`);
|
|
30162
|
+
if (gce.instance_name !== expectations.instanceName) {
|
|
30163
|
+
return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
|
|
29293
30164
|
}
|
|
29294
|
-
|
|
29295
|
-
|
|
29296
|
-
predicate: {
|
|
29297
|
-
claim_id: patched.claimId,
|
|
29298
|
-
base_commit_oid: patched.targetSha,
|
|
29299
|
-
patch_sha256: patched.patchSha256,
|
|
29300
|
-
tree_digest: patched.treeDigest,
|
|
29301
|
-
test_command: patched.testCommand,
|
|
29302
|
-
test_command_source: patched.testCommandSource,
|
|
29303
|
-
baseline_result: toTestRunResult(baseline, opts.baselineOutputSha256),
|
|
29304
|
-
patched_result: toTestRunResult(patched, opts.patchedOutputSha256),
|
|
29305
|
-
// No `?? 'unknown-image'`: `missing-image-digest` refuses above, so the value here is
|
|
29306
|
-
// always one the audit actually observed. The DIGEST, not `containerImage`: the
|
|
29307
|
-
// RepoDigest (`repo@sha256:…`) carries the repo name and the content hash, and the
|
|
29308
|
-
// tag it drops is the part a registry can re-point (TERM-893).
|
|
29309
|
-
enclave_measurement: localMeasurement(patched.containerImageDigest),
|
|
29310
|
-
nonce: opts.nonce ?? randomBytes9(16).toString("hex"),
|
|
29311
|
-
run_policy: { max_attempts: opts.maxAttempts ?? 1, budget_outcome: budget }
|
|
29312
|
-
}
|
|
29313
|
-
};
|
|
29314
|
-
}
|
|
29315
|
-
function signRunStatement(predicate, privateKey, keyid) {
|
|
29316
|
-
const statement = createAcceptanceStatement(predicate);
|
|
29317
|
-
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
29318
|
-
}
|
|
29319
|
-
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, IMAGE_SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS, refuse3, LOCAL_MEASUREMENT_PREFIX, REFERENCE_DOMAIN_COMPONENT, REFERENCE_DOMAIN_NAME, REFERENCE_IPV6, REFERENCE_DOMAIN, REFERENCE_PATH_COMPONENT, REPO_DIGEST_RE;
|
|
29320
|
-
var init_attestation2 = __esm({
|
|
29321
|
-
"../../packages/envrun/dist/attestation.js"() {
|
|
29322
|
-
"use strict";
|
|
29323
|
-
init_dist2();
|
|
29324
|
-
OUTCOME_TO_BUDGET = {
|
|
29325
|
-
completed: "completed",
|
|
29326
|
-
"tests-failed": "tests-failed",
|
|
29327
|
-
"no-tests-observed": "no-tests-observed",
|
|
29328
|
-
"budget-exceeded": "budget-exceeded",
|
|
29329
|
-
"counts-unparsed": "no-tests-observed",
|
|
29330
|
-
"test-command-unavailable": null,
|
|
29331
|
-
// null, with the same reasoning as `test-command-unavailable` and NOT
|
|
29332
|
-
// `budget-exceeded` (TERM-644). A signed `budget-exceeded` records one fact:
|
|
29333
|
-
// the run hit the time budget we applied — our default, or the one set for the
|
|
29334
|
-
// posting (TERM-1308), which is still ours to enforce. It blames no one (TERM-1289; `LIMIT_OWNER` in
|
|
29335
|
-
// `classify.ts` owns that question), and it is not an acceptable run:
|
|
29336
|
-
// `acceptRun` (`packages/attest/src/verify.ts`) refuses every `budget_outcome`
|
|
29337
|
-
// except `completed`. Exhausting the tmpfs we sized, or an OOM kill from memory we
|
|
29338
|
-
// did not cap, is a different fact, and signing it under the time-budget word
|
|
29339
|
-
// would record something that did not happen.
|
|
29340
|
-
"environment-exhausted": null
|
|
29341
|
-
};
|
|
29342
|
-
BASELINE_IS_A_VERDICT = {
|
|
29343
|
-
completed: true,
|
|
29344
|
-
"tests-failed": true,
|
|
29345
|
-
"no-tests-observed": true,
|
|
29346
|
-
"budget-exceeded": false,
|
|
29347
|
-
"counts-unparsed": false,
|
|
29348
|
-
"test-command-unavailable": false,
|
|
29349
|
-
"environment-exhausted": false
|
|
29350
|
-
};
|
|
29351
|
-
CONTRADICTS_COUNTS = {
|
|
29352
|
-
// Green asserts three things, and the first version checked one. `{0, 0}` signed as
|
|
29353
|
-
// `completed` — green with nothing having run, which is precisely what
|
|
29354
|
-
// `no-tests-observed` exists to say — and `exitCode: 1` signed as green too.
|
|
29355
|
-
completed: (counts, exitCode) => counts === null ? "`completed` with no parsed counts \u2014 green with no evidence any test asserted, which is exactly what `no-tests-observed` exists to say" : counts.tests_failed > 0 ? `\`completed\` alongside ${String(counts.tests_failed)} recorded failure(s)` : counts.tests_passed === 0 ? "`completed` with zero tests passed \u2014 green asserts a suite RAN, not merely that nothing failed" : exitCode !== 0 ? `\`completed\` with exit code ${String(exitCode)} \u2014 green cannot come from a command that reported failure` : null,
|
|
29356
|
-
// BOTH count fields, not just passes. `{0 passed, 4 failed}` signed as "evidence of zero"
|
|
29357
|
-
// for one round. A nonzero exit contradicts it too: `classify.ts` returns `tests-failed`
|
|
29358
|
-
// for that, so this member can only come from a clean exit.
|
|
29359
|
-
"no-tests-observed": (counts, exitCode) => counts === null ? null : counts.tests_passed > 0 || counts.tests_failed > 0 ? `mapped to \`no-tests-observed\` but records ${String(counts.tests_passed)} passed and ${String(counts.tests_failed)} failed, and that member asserts evidence of zero` : exitCode !== null && exitCode !== 0 ? `mapped to \`no-tests-observed\` with exit code ${String(exitCode)} \u2014 a nonzero exit is \`tests-failed\`, not evidence of zero` : null,
|
|
29360
|
-
// A killed run was never read to the end, so ANY counts beside it are a claim we cannot
|
|
29361
|
-
// support. `classify.ts` hardcodes `counts: null` on the timeout branch — but nothing in the
|
|
29362
|
-
// TYPE ties them, which is this whole family's rationale.
|
|
29363
|
-
"budget-exceeded": (counts) => counts === null ? null : `mapped to \`budget-exceeded\` \u2014 a run killed mid-flight \u2014 yet carries counts (${String(counts.tests_passed)} passed, ${String(counts.tests_failed)} failed), which asserts we read a suite that never finished`,
|
|
29364
|
-
// ABSTAINS, and on both observables. `classify.ts` reaches `tests-failed` two ways: counted
|
|
29365
|
-
// failures (any exit code, because a runner can report failures and still exit 0 under
|
|
29366
|
-
// `|| true`), and a nonzero exit with no counted failure ("something outside the assertions
|
|
29367
|
-
// failed"). So neither zero failures NOR a zero exit is a contradiction here, and guarding
|
|
29368
|
-
// either would redden honest evidence. This is a decision with its reason attached, not a
|
|
29369
|
-
// gap — the distinction the outer table exists to make visible.
|
|
29370
|
-
"tests-failed": () => null
|
|
29371
|
-
};
|
|
29372
|
-
SOURCE_IS_SIGNABLE = {
|
|
29373
|
-
detected: () => null,
|
|
29374
|
-
"founder-declared": () => null,
|
|
29375
|
-
// The party under verification chose it, and no signed member can say so. Per ADR-005
|
|
29376
|
-
// Decision 2, emit nothing rather than pick the closest word.
|
|
29377
|
-
"developer-declared": () => "had its test command supplied by the developer whose work is under verification, and the signed vocabulary has no member for that. `founder-declared` would attribute the choice to the other party; `detected` would claim the repo chose it. Both are false.",
|
|
29378
|
-
// US, out of band, per posting — the twin of `IMAGE_SOURCE_IS_SIGNABLE['operator-declared']`
|
|
29379
|
-
// and refusing for the same reason. `founder-declared` is the nearest signed member and it
|
|
29380
|
-
// is still false: the poster did not choose this, we did, and signing our own choice under
|
|
29381
|
-
// the counterparty's name is the specific false attribution the `developer-declared` entry
|
|
29382
|
-
// above exists to refuse. Decision 2 — emit nothing rather than pick the closest word.
|
|
29383
|
-
"operator-declared": () => "had its test command chosen by an operator out of band, and the signed vocabulary has no member for that. Signing it would present a hand-picked command as one the repository chose for itself.",
|
|
29384
|
-
// `none` means nobody chose a command, so a command beside it is a contradiction. Shadowed
|
|
29385
|
-
// today — `thrun.ts` sets `none` only when the derived command is null, and `no-test-command`
|
|
29386
|
-
// fires first — but that coupling runs through `runEnvironmentSpec` and lives in no type.
|
|
29387
|
-
none: (command) => command === null ? null : `records its command source as \`none\` \u2014 nobody chose one \u2014 beside the command ${JSON.stringify(command)}`
|
|
29388
|
-
};
|
|
29389
|
-
IMAGE_SOURCE_IS_SIGNABLE = {
|
|
29390
|
-
detected: () => null,
|
|
29391
|
-
// The command table's `founder-declared` reasoning, applied to the environment: the poster
|
|
29392
|
-
// chose the runtime and the poster is paying, so a runtime they declared is theirs to stand
|
|
29393
|
-
// behind. The image itself is still the runtime mapping's pick, not a name anyone typed.
|
|
29394
|
-
"founder-declared": () => null,
|
|
29395
|
-
"operator-declared": () => "had its container image chosen by an operator out of band, and the signed vocabulary has no member for that. Signing it would present a hand-picked environment as the one the runtime mapping selected.",
|
|
29396
|
-
"developer-declared": () => "had its container image supplied by the developer whose work is under verification. A chosen interpreter turns a red suite green as surely as a chosen test command.",
|
|
29397
|
-
// `none` means no image was booted, so an image name beside it is a contradiction — the
|
|
29398
|
-
// `SOURCE_IS_SIGNABLE.none` argument, applied to the other field.
|
|
29399
|
-
none: (image) => image === null ? null : `records its image source as \`none\` \u2014 nothing was booted \u2014 beside the image ${JSON.stringify(image)}`
|
|
29400
|
-
};
|
|
29401
|
-
ATTEST_REFUSAL_REASONS = [
|
|
29402
|
-
"our-environment-failed",
|
|
29403
|
-
"run-was-refused",
|
|
29404
|
-
"no-outcome-recorded",
|
|
29405
|
-
"no-test-command",
|
|
29406
|
-
"pair-disagrees-on-harness",
|
|
29407
|
-
"pair-disagrees-on-base",
|
|
29408
|
-
"pair-is-the-same-run",
|
|
29409
|
-
"missing-tree-digest",
|
|
29410
|
-
"missing-patch-digest",
|
|
29411
|
-
// Round 4: the pair-agreement axis. `testCommand` and `targetSha` were compared across the
|
|
29412
|
-
// halves and `containerImage` was not, so two different environments signed one measurement.
|
|
29413
|
-
"pair-disagrees-on-image",
|
|
29414
|
-
"missing-container-image",
|
|
29415
|
-
// TERM-893: the digest axis. The two guards above compare NAMES, and a name is
|
|
29416
|
-
// exactly what a registry can re-point between the two halves of a pair — so the
|
|
29417
|
-
// measurement signs the RepoDigest, and these refuse when it is absent or split.
|
|
29418
|
-
// `malformed` and `repo-mismatch` exist because this is a RUNTIME boundary
|
|
29419
|
-
// (Codex round 1): the .mjs audit calls through here untyped, so `undefined`,
|
|
29420
|
-
// a tag, or an arbitrary string would otherwise flow into the signed
|
|
29421
|
-
// measurement wearing a digest's name.
|
|
29422
|
-
"pair-disagrees-on-image-digest",
|
|
29423
|
-
"missing-image-digest",
|
|
29424
|
-
"malformed-image-digest",
|
|
29425
|
-
"image-digest-repo-mismatch",
|
|
29426
|
-
// Codex round 4: a digest-QUALIFIED image reference pins a digest of its
|
|
29427
|
-
// own, and the repo-mismatch guards compare repositories only — so
|
|
29428
|
-
// `node@sha256:A` could ride above digest fields saying `node@sha256:B`.
|
|
29429
|
-
"image-digest-contradicts-image",
|
|
29430
|
-
// ── The BASELINE half. Added TERM-354 review round 2: every guard above reads
|
|
29431
|
-
// `patched`, so a baseline could be anything at all and still be signed as the "before"
|
|
29432
|
-
// half of a comparison. That is the worst direction for this bug to point, because a
|
|
29433
|
-
// BROKEN baseline manufactures a favourable result: install dies, nothing runs, and the
|
|
29434
|
-
// pair reads "it was failing, the patch fixed it".
|
|
29435
|
-
"baseline-was-refused",
|
|
29436
|
-
"baseline-no-outcome",
|
|
29437
|
-
"baseline-environment-failed",
|
|
29438
|
-
"pair-has-identical-tree",
|
|
29439
|
-
// The signed triple must be reproducible from what it names — see the guard.
|
|
29440
|
-
"patch-binding-incomplete",
|
|
29441
|
-
"outcome-contradicts-counts",
|
|
29442
|
-
"test-command-origin-unattested",
|
|
29443
|
-
// The environment half of the pair above. `pair-disagrees-on-image` compares image NAMES;
|
|
29444
|
-
// this one asks who CHOSE the name, which no comparison of the two halves can reveal when
|
|
29445
|
-
// both were handed the same override.
|
|
29446
|
-
"image-origin-unattested"
|
|
29447
|
-
];
|
|
29448
|
-
refuse3 = (reason, detail) => ({
|
|
29449
|
-
ok: false,
|
|
29450
|
-
reason,
|
|
29451
|
-
detail
|
|
29452
|
-
});
|
|
29453
|
-
LOCAL_MEASUREMENT_PREFIX = "NOT-AN-ENCLAVE:local-container:";
|
|
29454
|
-
REFERENCE_DOMAIN_COMPONENT = "[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?";
|
|
29455
|
-
REFERENCE_DOMAIN_NAME = `${REFERENCE_DOMAIN_COMPONENT}(?:\\.${REFERENCE_DOMAIN_COMPONENT})*`;
|
|
29456
|
-
REFERENCE_IPV6 = "\\[(?:[a-fA-F0-9:]+)\\]";
|
|
29457
|
-
REFERENCE_DOMAIN = `(?:${REFERENCE_DOMAIN_NAME}|${REFERENCE_IPV6})(?::[0-9]+)?`;
|
|
29458
|
-
REFERENCE_PATH_COMPONENT = "[a-z0-9]+(?:(?:\\.|_{1,2}|-+)[a-z0-9]+)*";
|
|
29459
|
-
REPO_DIGEST_RE = new RegExp(`^(?:(${REFERENCE_DOMAIN})/)?${REFERENCE_PATH_COMPONENT}(?:/${REFERENCE_PATH_COMPONENT})*@sha256:[0-9a-f]{64}$`);
|
|
30165
|
+
if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
|
|
30166
|
+
return refuse2("instance-id-missing", "instance id is absent or not a string");
|
|
29460
30167
|
}
|
|
29461
|
-
|
|
29462
|
-
|
|
29463
|
-
|
|
29464
|
-
function runLabels(runId, callerLabels) {
|
|
29465
|
-
return { ...callerLabels ?? {}, [RUN_ID_LABEL_KEY]: runId, [RUN_LABEL_KEY]: "term-350" };
|
|
29466
|
-
}
|
|
29467
|
-
function censusTotal(c) {
|
|
29468
|
-
return c.containers.length + c.volumes.length + c.networks.length;
|
|
29469
|
-
}
|
|
29470
|
-
function query(docker3, args) {
|
|
29471
|
-
const res = docker3.sync([...args], { timeoutMs: 15e3 });
|
|
29472
|
-
if (res.error || res.status !== 0) {
|
|
29473
|
-
const why = res.error?.message ?? (res.stderr.trim() || `exit ${String(res.status)}`);
|
|
29474
|
-
return { ids: [], failure: `docker ${args.slice(0, 2).join(" ")}: ${why}` };
|
|
30168
|
+
const instanceId = gce.instance_id;
|
|
30169
|
+
if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
|
|
30170
|
+
return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
|
|
29475
30171
|
}
|
|
29476
|
-
const
|
|
29477
|
-
|
|
29478
|
-
|
|
29479
|
-
|
|
29480
|
-
return query(docker3, args).ids;
|
|
29481
|
-
}
|
|
29482
|
-
function census(docker3, label) {
|
|
29483
|
-
const filter = `label=${label}`;
|
|
29484
|
-
return {
|
|
29485
|
-
containers: ids(docker3, ["ps", "-aq", "--filter", filter]),
|
|
29486
|
-
volumes: ids(docker3, ["volume", "ls", "-q", "--filter", filter]),
|
|
29487
|
-
networks: ids(docker3, ["network", "ls", "-q", "--filter", filter])
|
|
29488
|
-
};
|
|
29489
|
-
}
|
|
29490
|
-
function censusReport(docker3, label) {
|
|
29491
|
-
const filter = `label=${label}`;
|
|
29492
|
-
const failures = [];
|
|
29493
|
-
const ask2 = (args) => {
|
|
29494
|
-
const q = query(docker3, args);
|
|
29495
|
-
if (q.failure !== null)
|
|
29496
|
-
failures.push(q.failure);
|
|
29497
|
-
return q.ids;
|
|
29498
|
-
};
|
|
29499
|
-
const taken = {
|
|
29500
|
-
containers: ask2(["ps", "-aq", "--filter", filter]),
|
|
29501
|
-
volumes: ask2(["volume", "ls", "-q", "--filter", filter]),
|
|
29502
|
-
networks: ask2(["network", "ls", "-q", "--filter", filter])
|
|
29503
|
-
};
|
|
29504
|
-
return failures.length === 0 ? { observed: true, census: taken, unobservedReason: null } : { observed: false, census: taken, unobservedReason: failures.join("; ") };
|
|
29505
|
-
}
|
|
29506
|
-
function localCensus(label) {
|
|
29507
|
-
return census(localDockerClient(), label);
|
|
29508
|
-
}
|
|
29509
|
-
function judgeLeaks(peak, after, observation) {
|
|
29510
|
-
const labelObserved = peak.containers.length > 0;
|
|
29511
|
-
if (!observation.observed) {
|
|
29512
|
-
return {
|
|
29513
|
-
labelObserved,
|
|
29514
|
-
reaped: false,
|
|
29515
|
-
observed: false,
|
|
29516
|
-
clean: false,
|
|
29517
|
-
state: "unobserved",
|
|
29518
|
-
peak,
|
|
29519
|
-
after,
|
|
29520
|
-
note: `UNOBSERVED, not clean: we could not look at what survived teardown (${observation.unobservedReason ?? "no reason given"}), so nothing is known about leaks on this run, in either direction.`
|
|
29521
|
-
};
|
|
30172
|
+
const zone = gce.zone;
|
|
30173
|
+
const container = isPlainObject2(submods) ? submods.container : void 0;
|
|
30174
|
+
if (!isPlainObject2(container)) {
|
|
30175
|
+
return refuse2("submods-container-missing", "no submods.container block names the workload");
|
|
29522
30176
|
}
|
|
29523
|
-
|
|
29524
|
-
|
|
29525
|
-
let state;
|
|
29526
|
-
if (!labelObserved && reaped) {
|
|
29527
|
-
state = "inconclusive";
|
|
29528
|
-
note = "INCONCLUSIVE, not clean: nothing labelled was ever seen alive, so an empty final census is equally consistent with the label never being applied. The control failed, so the denial proves nothing.";
|
|
29529
|
-
} else if (!labelObserved) {
|
|
29530
|
-
state = "leak";
|
|
29531
|
-
note = "no labelled container was observed alive AND objects remain \u2014 the label wiring is wrong.";
|
|
29532
|
-
} else if (!reaped) {
|
|
29533
|
-
state = "leak";
|
|
29534
|
-
note = `LEAK: ${String(censusTotal(after))} labelled object(s) survived teardown (containers=${String(after.containers.length)} volumes=${String(after.volumes.length)} networks=${String(after.networks.length)}).`;
|
|
29535
|
-
} else {
|
|
29536
|
-
state = "clean";
|
|
29537
|
-
note = `clean: peak ${String(peak.containers.length)} labelled container(s) observed alive, 0 labelled objects remain after teardown.`;
|
|
30177
|
+
if (typeof container.image_reference !== "string" || container.image_reference === "") {
|
|
30178
|
+
return refuse2("image-reference-missing", "the workload image reference is absent");
|
|
29538
30179
|
}
|
|
29539
|
-
|
|
29540
|
-
|
|
29541
|
-
|
|
29542
|
-
|
|
29543
|
-
|
|
29544
|
-
|
|
29545
|
-
|
|
29546
|
-
|
|
29547
|
-
|
|
29548
|
-
};
|
|
29549
|
-
}
|
|
29550
|
-
var RUN_LABEL_KEY, RUN_ID_LABEL_KEY, LabelWatch, LEAK_STATES;
|
|
29551
|
-
var init_labels = __esm({
|
|
29552
|
-
"../../packages/envrun/dist/labels.js"() {
|
|
29553
|
-
"use strict";
|
|
29554
|
-
init_dist();
|
|
29555
|
-
RUN_LABEL_KEY = "supergoal.run";
|
|
29556
|
-
RUN_ID_LABEL_KEY = "supergoal.run-id";
|
|
29557
|
-
LabelWatch = class {
|
|
29558
|
-
label;
|
|
29559
|
-
docker;
|
|
29560
|
-
intervalMs;
|
|
29561
|
-
#timer = null;
|
|
29562
|
-
#peak = { containers: [], volumes: [], networks: [] };
|
|
29563
|
-
#samples = 0;
|
|
29564
|
-
/**
|
|
29565
|
-
* `docker` is REQUIRED and second, so a sampler cannot be built without
|
|
29566
|
-
* naming the daemon it watches. A watch polling one daemon while the run
|
|
29567
|
-
* executes on another reports a high-water mark of 0 — indistinguishable
|
|
29568
|
-
* from "the label never applied", which is the exact ambiguity this class
|
|
29569
|
-
* exists to remove.
|
|
29570
|
-
*/
|
|
29571
|
-
constructor(label, docker3, intervalMs = 250) {
|
|
29572
|
-
this.label = label;
|
|
29573
|
-
this.docker = docker3;
|
|
29574
|
-
this.intervalMs = intervalMs;
|
|
29575
|
-
}
|
|
29576
|
-
start() {
|
|
29577
|
-
if (this.#timer !== null)
|
|
29578
|
-
return;
|
|
29579
|
-
this.#sample();
|
|
29580
|
-
this.#timer = setInterval(() => this.#sample(), this.intervalMs);
|
|
29581
|
-
this.#timer.unref();
|
|
29582
|
-
}
|
|
29583
|
-
#sample() {
|
|
29584
|
-
this.#samples += 1;
|
|
29585
|
-
const now = census(this.docker, this.label);
|
|
29586
|
-
this.#peak = {
|
|
29587
|
-
containers: now.containers.length > this.#peak.containers.length ? now.containers : this.#peak.containers,
|
|
29588
|
-
volumes: now.volumes.length > this.#peak.volumes.length ? now.volumes : this.#peak.volumes,
|
|
29589
|
-
networks: now.networks.length > this.#peak.networks.length ? now.networks : this.#peak.networks
|
|
29590
|
-
};
|
|
29591
|
-
}
|
|
29592
|
-
stop() {
|
|
29593
|
-
if (this.#timer !== null) {
|
|
29594
|
-
clearInterval(this.#timer);
|
|
29595
|
-
this.#timer = null;
|
|
29596
|
-
}
|
|
29597
|
-
this.#sample();
|
|
29598
|
-
}
|
|
29599
|
-
get peak() {
|
|
29600
|
-
return this.#peak;
|
|
29601
|
-
}
|
|
29602
|
-
get samples() {
|
|
29603
|
-
return this.#samples;
|
|
29604
|
-
}
|
|
29605
|
-
};
|
|
29606
|
-
LEAK_STATES = ["clean", "leak", "inconclusive", "unobserved"];
|
|
30180
|
+
const imageReference = container.image_reference;
|
|
30181
|
+
if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
|
|
30182
|
+
return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
|
|
30183
|
+
}
|
|
30184
|
+
const env = container.env;
|
|
30185
|
+
for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
|
|
30186
|
+
if (!isPlainObject2(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
|
|
30187
|
+
return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
|
|
30188
|
+
}
|
|
29607
30189
|
}
|
|
29608
|
-
});
|
|
29609
|
-
|
|
29610
|
-
// ../../packages/envrun/dist/previewRegistry.js
|
|
29611
|
-
function createPreviewRegistry() {
|
|
29612
|
-
const live = /* @__PURE__ */ new Map();
|
|
29613
30190
|
return {
|
|
29614
|
-
|
|
29615
|
-
|
|
29616
|
-
|
|
29617
|
-
|
|
29618
|
-
|
|
29619
|
-
|
|
29620
|
-
|
|
29621
|
-
|
|
29622
|
-
|
|
29623
|
-
|
|
29624
|
-
|
|
29625
|
-
|
|
29626
|
-
},
|
|
29627
|
-
pairs() {
|
|
29628
|
-
const out = [];
|
|
29629
|
-
for (const [client, names] of live) {
|
|
29630
|
-
for (const container of names)
|
|
29631
|
-
out.push({ client, container });
|
|
29632
|
-
}
|
|
29633
|
-
return out;
|
|
29634
|
-
},
|
|
29635
|
-
reapAll() {
|
|
29636
|
-
for (const [client, names] of live) {
|
|
29637
|
-
for (const container of names) {
|
|
29638
|
-
client.sync(["rm", "-f", container], { timeoutMs: 15e3 });
|
|
29639
|
-
}
|
|
29640
|
-
}
|
|
29641
|
-
live.clear();
|
|
30191
|
+
ok: true,
|
|
30192
|
+
value: {
|
|
30193
|
+
instanceId,
|
|
30194
|
+
instanceName: expectations.instanceName,
|
|
30195
|
+
zone,
|
|
30196
|
+
projectId: expectations.projectId,
|
|
30197
|
+
imageDigest: container.image_digest,
|
|
30198
|
+
imageReference,
|
|
30199
|
+
hwmodel,
|
|
30200
|
+
iat,
|
|
30201
|
+
exp,
|
|
30202
|
+
tokenSha256: createHash9("sha256").update(token, "utf8").digest("hex")
|
|
29642
30203
|
}
|
|
29643
30204
|
};
|
|
29644
30205
|
}
|
|
29645
|
-
var
|
|
29646
|
-
|
|
30206
|
+
var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
|
|
30207
|
+
var init_confidentialSpace = __esm({
|
|
30208
|
+
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
29647
30209
|
"use strict";
|
|
30210
|
+
init_gceIdentity();
|
|
30211
|
+
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
30212
|
+
CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
|
|
30213
|
+
CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
|
|
30214
|
+
CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
|
|
29648
30215
|
}
|
|
29649
30216
|
});
|
|
29650
30217
|
|
|
29651
|
-
// ../../packages/
|
|
29652
|
-
|
|
29653
|
-
|
|
29654
|
-
import { join as join22 } from "path";
|
|
29655
|
-
function docker(client, args, timeoutMs = 6e4) {
|
|
29656
|
-
const res = client.sync([...args], { timeoutMs });
|
|
29657
|
-
return {
|
|
29658
|
-
ok: !res.error && res.status === 0,
|
|
29659
|
-
stdout: res.stdout,
|
|
29660
|
-
stderr: (res.error ? res.error.message : "") + res.stderr
|
|
29661
|
-
};
|
|
29662
|
-
}
|
|
29663
|
-
function installReaper() {
|
|
29664
|
-
if (reaperInstalled)
|
|
29665
|
-
return;
|
|
29666
|
-
reaperInstalled = true;
|
|
29667
|
-
process.on("exit", () => {
|
|
29668
|
-
livePreviews.reapAll();
|
|
29669
|
-
});
|
|
29670
|
-
}
|
|
29671
|
-
function readHostPort(client, container) {
|
|
29672
|
-
const res = docker(client, ["port", container, `${String(GUEST_PORT)}/tcp`]);
|
|
29673
|
-
if (!res.ok)
|
|
29674
|
-
return null;
|
|
29675
|
-
for (const line of res.stdout.split("\n")) {
|
|
29676
|
-
const trimmed = line.trim();
|
|
29677
|
-
if (trimmed === "")
|
|
29678
|
-
continue;
|
|
29679
|
-
const idx = trimmed.lastIndexOf(":");
|
|
29680
|
-
if (idx === -1)
|
|
29681
|
-
continue;
|
|
29682
|
-
const port = Number(trimmed.slice(idx + 1));
|
|
29683
|
-
if (Number.isInteger(port) && port > 0)
|
|
29684
|
-
return port;
|
|
29685
|
-
}
|
|
29686
|
-
return null;
|
|
29687
|
-
}
|
|
29688
|
-
async function fetchInstanceToken(url, authToken) {
|
|
29689
|
-
try {
|
|
29690
|
-
const headers = {};
|
|
29691
|
-
if (authToken) {
|
|
29692
|
-
headers["Authorization"] = `Bearer ${authToken}`;
|
|
29693
|
-
}
|
|
29694
|
-
const res = await fetch(url, { cache: "no-store", headers });
|
|
29695
|
-
if (!res.ok)
|
|
29696
|
-
return null;
|
|
29697
|
-
const body = await res.json();
|
|
29698
|
-
return typeof body.instanceToken === "string" ? body.instanceToken : null;
|
|
29699
|
-
} catch {
|
|
29700
|
-
return null;
|
|
29701
|
-
}
|
|
30218
|
+
// ../../packages/attest/dist/venueInstanceName.js
|
|
30219
|
+
function venueInstanceName(runId) {
|
|
30220
|
+
return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
|
|
29702
30221
|
}
|
|
29703
|
-
|
|
29704
|
-
|
|
29705
|
-
|
|
29706
|
-
const image = validateImage(req.image);
|
|
29707
|
-
const bindAddress = req.bindAddress ?? "127.0.0.1";
|
|
29708
|
-
const client = req.docker;
|
|
29709
|
-
if (!LOOPBACK_BINDS.has(bindAddress) && req.authToken === void 0) {
|
|
29710
|
-
throw new PreviewError(`refusing to publish the preview on ${bindAddress} without an explicit authToken: a bind wider than loopback puts this run \u2014 the test output tail included \u2014 on the developer's local network`);
|
|
29711
|
-
}
|
|
29712
|
-
const authToken = req.authToken ?? randomBytes10(24).toString("base64url");
|
|
29713
|
-
const envArgs = ["--env", `PREVIEW_AUTH_TOKEN=${authToken}`];
|
|
29714
|
-
const probeHost = WILDCARD_BINDS.has(bindAddress) ? "127.0.0.1" : bindAddress;
|
|
29715
|
-
const probeAuthority = probeHost.includes(":") ? `[${probeHost}]` : probeHost;
|
|
29716
|
-
mkdirSync5(req.scratchDir, { recursive: true });
|
|
29717
|
-
const docPath = join22(req.scratchDir, "preview-run.json");
|
|
29718
|
-
writeFileSync12(docPath, JSON.stringify(req.document, null, 2), "utf8");
|
|
29719
|
-
const teardown = () => {
|
|
29720
|
-
livePreviews.deregister(client, container);
|
|
29721
|
-
for (let i = 0; i < 3; i += 1) {
|
|
29722
|
-
const inspect = docker(client, ["inspect", "--format", "{{.State.Status}}", container]);
|
|
29723
|
-
if (!inspect.ok)
|
|
29724
|
-
return { clean: true, leaked: [] };
|
|
29725
|
-
docker(client, ["rm", "-f", container]);
|
|
29726
|
-
}
|
|
29727
|
-
const still = docker(client, ["inspect", "--format", "{{.State.Status}}", container]);
|
|
29728
|
-
return still.ok ? { clean: false, leaked: [`container ${container}`] } : { clean: true, leaked: [] };
|
|
29729
|
-
};
|
|
29730
|
-
const startedAt = Date.now();
|
|
29731
|
-
try {
|
|
29732
|
-
const run2 = docker(client, [
|
|
29733
|
-
"run",
|
|
29734
|
-
"-d",
|
|
29735
|
-
"--init",
|
|
29736
|
-
`--name=${container}`,
|
|
29737
|
-
// A network IS granted here, unlike the verification step. It carries our
|
|
29738
|
-
// own argv over a document we wrote; the repo's code never runs in it.
|
|
29739
|
-
"--network=bridge",
|
|
29740
|
-
// Loopback by default, and anything wider was refused above unless the
|
|
29741
|
-
// caller named a token. A bare `-p 8080` would bind 0.0.0.0 and put a
|
|
29742
|
-
// developer's in-progress work on their local network.
|
|
29743
|
-
`--publish=${bindAddress}:0:${String(GUEST_PORT)}`,
|
|
29744
|
-
...envArgs,
|
|
29745
|
-
"--cap-drop=ALL",
|
|
29746
|
-
"--security-opt=no-new-privileges",
|
|
29747
|
-
"--pids-limit=64",
|
|
29748
|
-
"--memory=256m",
|
|
29749
|
-
"--read-only",
|
|
29750
|
-
"--tmpfs=/tmp:rw,noexec,nosuid,size=8m",
|
|
29751
|
-
...label,
|
|
29752
|
-
`--volume=${docPath}:${GUEST_DOC}:ro`,
|
|
29753
|
-
"--",
|
|
29754
|
-
image,
|
|
29755
|
-
"node",
|
|
29756
|
-
"-e",
|
|
29757
|
-
SERVER_SOURCE
|
|
29758
|
-
]);
|
|
29759
|
-
if (!run2.ok) {
|
|
29760
|
-
throw new PreviewError(`could not start the preview container: ${run2.stderr.trim()}`);
|
|
29761
|
-
}
|
|
29762
|
-
const deadline = Date.now() + (req.readyTimeoutMs ?? 6e4);
|
|
29763
|
-
let hostPort = null;
|
|
29764
|
-
let token = null;
|
|
29765
|
-
let lastDetail = "never answered";
|
|
29766
|
-
const throwIfExited = () => {
|
|
29767
|
-
const alive = docker(client, ["inspect", "--format", "{{.State.Running}}", container]);
|
|
29768
|
-
if (alive.stdout.trim() !== "true") {
|
|
29769
|
-
const logs = docker(client, ["logs", "--tail", "20", container]);
|
|
29770
|
-
throw new PreviewError(`the preview container exited before serving: ${logs.stdout.trim()}${logs.stderr.trim()}`);
|
|
29771
|
-
}
|
|
29772
|
-
};
|
|
29773
|
-
while (Date.now() < deadline) {
|
|
29774
|
-
hostPort ??= readHostPort(client, container);
|
|
29775
|
-
if (hostPort === null) {
|
|
29776
|
-
throwIfExited();
|
|
29777
|
-
lastDetail = "Docker never reported a published host port";
|
|
29778
|
-
await sleep4(200);
|
|
29779
|
-
continue;
|
|
29780
|
-
}
|
|
29781
|
-
token = await fetchInstanceToken(`http://${probeAuthority}:${String(hostPort)}/`, authToken);
|
|
29782
|
-
if (token !== null)
|
|
29783
|
-
break;
|
|
29784
|
-
throwIfExited();
|
|
29785
|
-
lastDetail = "the port is published but the server has not answered yet";
|
|
29786
|
-
await sleep4(150);
|
|
29787
|
-
}
|
|
29788
|
-
if (hostPort === null || token === null) {
|
|
29789
|
-
throw new PreviewError(`the preview URL never became reachable: ${lastDetail}`);
|
|
29790
|
-
}
|
|
29791
|
-
livePreviews.register(client, container);
|
|
29792
|
-
installReaper();
|
|
29793
|
-
const origin = `http://${probeAuthority}:${String(hostPort)}/`;
|
|
29794
|
-
return {
|
|
29795
|
-
url: `${origin}?token=${encodeURIComponent(authToken)}`,
|
|
29796
|
-
origin,
|
|
29797
|
-
authToken,
|
|
29798
|
-
instanceToken: token,
|
|
29799
|
-
container,
|
|
29800
|
-
hostPort,
|
|
29801
|
-
readyMs: Date.now() - startedAt,
|
|
29802
|
-
teardown
|
|
29803
|
-
};
|
|
29804
|
-
} catch (err) {
|
|
29805
|
-
teardown();
|
|
29806
|
-
throw err;
|
|
30222
|
+
var init_venueInstanceName = __esm({
|
|
30223
|
+
"../../packages/attest/dist/venueInstanceName.js"() {
|
|
30224
|
+
"use strict";
|
|
29807
30225
|
}
|
|
29808
|
-
}
|
|
29809
|
-
|
|
29810
|
-
|
|
29811
|
-
|
|
29812
|
-
|
|
29813
|
-
var init_preview = __esm({
|
|
29814
|
-
"../../packages/envrun/dist/preview.js"() {
|
|
30226
|
+
});
|
|
30227
|
+
|
|
30228
|
+
// ../../packages/attest/dist/verify.js
|
|
30229
|
+
var init_verify = __esm({
|
|
30230
|
+
"../../packages/attest/dist/verify.js"() {
|
|
29815
30231
|
"use strict";
|
|
29816
|
-
|
|
29817
|
-
|
|
29818
|
-
|
|
29819
|
-
|
|
29820
|
-
|
|
29821
|
-
GUEST_DOC = "/preview/run.json";
|
|
29822
|
-
LOOPBACK_BINDS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
29823
|
-
WILDCARD_BINDS = /* @__PURE__ */ new Set(["0.0.0.0", "::"]);
|
|
29824
|
-
SERVER_SOURCE = `
|
|
29825
|
-
const http = require('node:http');
|
|
29826
|
-
const { readFileSync } = require('node:fs');
|
|
29827
|
-
const { randomUUID, timingSafeEqual } = require('node:crypto');
|
|
30232
|
+
init_dsse();
|
|
30233
|
+
init_manifest();
|
|
30234
|
+
init_types4();
|
|
30235
|
+
}
|
|
30236
|
+
});
|
|
29828
30237
|
|
|
29829
|
-
//
|
|
29830
|
-
|
|
29831
|
-
|
|
29832
|
-
|
|
29833
|
-
|
|
29834
|
-
|
|
29835
|
-
|
|
29836
|
-
|
|
29837
|
-
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
|
|
29841
|
-
|
|
29842
|
-
|
|
30238
|
+
// ../../packages/attest/dist/index.js
|
|
30239
|
+
var init_dist2 = __esm({
|
|
30240
|
+
"../../packages/attest/dist/index.js"() {
|
|
30241
|
+
"use strict";
|
|
30242
|
+
init_types4();
|
|
30243
|
+
init_pae();
|
|
30244
|
+
init_keys();
|
|
30245
|
+
init_dsse();
|
|
30246
|
+
init_dispatchedRun();
|
|
30247
|
+
init_sealedbox();
|
|
30248
|
+
init_aead();
|
|
30249
|
+
init_manifest();
|
|
30250
|
+
init_attestation();
|
|
30251
|
+
init_nonce();
|
|
30252
|
+
init_gceIdentity();
|
|
30253
|
+
init_confidentialSpace();
|
|
30254
|
+
init_venueInstanceName();
|
|
30255
|
+
init_verify();
|
|
30256
|
+
}
|
|
30257
|
+
});
|
|
29843
30258
|
|
|
29844
|
-
|
|
29845
|
-
|
|
29846
|
-
|
|
29847
|
-
const
|
|
29848
|
-
if (
|
|
29849
|
-
|
|
29850
|
-
|
|
29851
|
-
// "Basic \u2026" produced an empty string and could never authenticate at all.
|
|
29852
|
-
//
|
|
29853
|
-
// The query form stays because the founder opens this in a browser and a
|
|
29854
|
-
// browser sends no Authorization header. That is the only reason it is
|
|
29855
|
-
// accepted: a token in a URL lands in browser history, shell history and any
|
|
29856
|
-
// proxy log on the way, where a header does not.
|
|
29857
|
-
return new URL(req.url, 'http://localhost').searchParams.get('token');
|
|
30259
|
+
// ../../packages/envrun/dist/attestation.js
|
|
30260
|
+
import { createHash as createHash10, randomBytes as randomBytes11 } from "crypto";
|
|
30261
|
+
function contradicts(outcome, counts, exitCode) {
|
|
30262
|
+
const budget = OUTCOME_TO_BUDGET[outcome];
|
|
30263
|
+
if (budget === null)
|
|
30264
|
+
return null;
|
|
30265
|
+
return CONTRADICTS_COUNTS[budget](counts, exitCode);
|
|
29858
30266
|
}
|
|
29859
|
-
|
|
29860
|
-
|
|
29861
|
-
const given = presented(req);
|
|
29862
|
-
if (typeof given !== 'string') return false;
|
|
29863
|
-
const got = Buffer.from(given, 'utf8');
|
|
29864
|
-
// timingSafeEqual THROWS on a length mismatch, so length is compared first and
|
|
29865
|
-
// refused here. The length is not the secret; the bytes are.
|
|
29866
|
-
if (got.length !== EXPECTED.length) return false;
|
|
29867
|
-
return timingSafeEqual(got, EXPECTED);
|
|
30267
|
+
function localMeasurement(imageReference) {
|
|
30268
|
+
return `${LOCAL_MEASUREMENT_PREFIX}${imageReference}`;
|
|
29868
30269
|
}
|
|
29869
|
-
|
|
29870
|
-
|
|
29871
|
-
|
|
29872
|
-
|
|
29873
|
-
const
|
|
29874
|
-
|
|
29875
|
-
|
|
29876
|
-
|
|
29877
|
-
|
|
29878
|
-
|
|
29879
|
-
|
|
29880
|
-
|
|
30270
|
+
function isCanonicalRepoDigest(value) {
|
|
30271
|
+
const match2 = REPO_DIGEST_RE.exec(value);
|
|
30272
|
+
if (match2 === null)
|
|
30273
|
+
return false;
|
|
30274
|
+
const name = value.slice(0, value.indexOf("@"));
|
|
30275
|
+
const captured = match2[1];
|
|
30276
|
+
let domain = null;
|
|
30277
|
+
let path5 = name;
|
|
30278
|
+
if (captured !== void 0) {
|
|
30279
|
+
const isRegistry = captured.includes(".") || captured.includes(":") || captured === "localhost" || captured.toLowerCase() !== captured;
|
|
30280
|
+
if (isRegistry) {
|
|
30281
|
+
domain = captured;
|
|
30282
|
+
path5 = name.slice(captured.length + 1);
|
|
29881
30283
|
}
|
|
29882
|
-
served += 1;
|
|
29883
|
-
const body = JSON.stringify(
|
|
29884
|
-
{
|
|
29885
|
-
instanceToken: INSTANCE_TOKEN,
|
|
29886
|
-
servedCount: served,
|
|
29887
|
-
pid: process.pid,
|
|
29888
|
-
run: DOC,
|
|
29889
|
-
},
|
|
29890
|
-
null,
|
|
29891
|
-
2,
|
|
29892
|
-
);
|
|
29893
|
-
res.writeHead(200, {
|
|
29894
|
-
'content-type': 'application/json; charset=utf-8',
|
|
29895
|
-
'cache-control': 'no-store',
|
|
29896
|
-
'x-th-instance': INSTANCE_TOKEN,
|
|
29897
|
-
});
|
|
29898
|
-
res.end(body);
|
|
29899
|
-
})
|
|
29900
|
-
.listen(${String(GUEST_PORT)}, '0.0.0.0', () => {
|
|
29901
|
-
console.log('preview-ready ' + INSTANCE_TOKEN);
|
|
29902
|
-
});
|
|
29903
|
-
`;
|
|
29904
|
-
livePreviews = createPreviewRegistry();
|
|
29905
|
-
reaperInstalled = false;
|
|
29906
|
-
sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
29907
30284
|
}
|
|
29908
|
-
|
|
29909
|
-
|
|
29910
|
-
|
|
29911
|
-
|
|
29912
|
-
import { join as join23 } from "path";
|
|
29913
|
-
function localJailPaths(scratchRoot) {
|
|
29914
|
-
return {
|
|
29915
|
-
jail: join23(scratchRoot, JAIL_SEGMENT),
|
|
29916
|
-
tmp: join23(scratchRoot, JAIL_TMP_SEGMENT)
|
|
29917
|
-
};
|
|
30285
|
+
const onDockerHub = domain === null || domain === "docker.io" || domain === "index.docker.io";
|
|
30286
|
+
if (onDockerHub && !path5.includes("/"))
|
|
30287
|
+
path5 = `library/${path5}`;
|
|
30288
|
+
return path5.length <= 255;
|
|
29918
30289
|
}
|
|
29919
|
-
function
|
|
30290
|
+
function imageRepo(ref) {
|
|
30291
|
+
const at = ref.indexOf("@");
|
|
30292
|
+
const base = at === -1 ? ref : ref.slice(0, at);
|
|
30293
|
+
const slash = base.lastIndexOf("/");
|
|
30294
|
+
const colon = base.lastIndexOf(":");
|
|
30295
|
+
return colon > slash ? base.slice(0, colon) : base;
|
|
30296
|
+
}
|
|
30297
|
+
function imageDigestOf(ref) {
|
|
30298
|
+
const at = ref.indexOf("@");
|
|
30299
|
+
return at === -1 ? null : ref.slice(at + 1);
|
|
30300
|
+
}
|
|
30301
|
+
function sha256Hex(data) {
|
|
30302
|
+
return createHash10("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
|
|
30303
|
+
}
|
|
30304
|
+
function toTestRunResult(result, outputSha256) {
|
|
29920
30305
|
return {
|
|
29921
|
-
|
|
29922
|
-
|
|
29923
|
-
|
|
29924
|
-
|
|
29925
|
-
|
|
30306
|
+
exit_code: result.exitCode ?? -1,
|
|
30307
|
+
...outputSha256 === void 0 ? {} : { output_sha256: outputSha256 },
|
|
30308
|
+
...result.counts === null ? {} : {
|
|
30309
|
+
tests_passed: result.counts.tests_passed,
|
|
30310
|
+
tests_failed: result.counts.tests_failed,
|
|
30311
|
+
...result.counts.coverage === "partial" ? { tests_coverage: "partial" } : {}
|
|
30312
|
+
}
|
|
29926
30313
|
};
|
|
29927
30314
|
}
|
|
29928
|
-
|
|
29929
|
-
const
|
|
29930
|
-
|
|
29931
|
-
return
|
|
29932
|
-
|
|
29933
|
-
|
|
29934
|
-
|
|
29935
|
-
|
|
29936
|
-
|
|
29937
|
-
|
|
29938
|
-
|
|
29939
|
-
|
|
29940
|
-
|
|
29941
|
-
|
|
29942
|
-
|
|
29943
|
-
|
|
29944
|
-
|
|
29945
|
-
|
|
29946
|
-
|
|
29947
|
-
|
|
29948
|
-
|
|
29949
|
-
|
|
29950
|
-
|
|
29951
|
-
|
|
29952
|
-
|
|
29953
|
-
|
|
30315
|
+
function toAcceptancePredicate(pair, opts = {}) {
|
|
30316
|
+
const { baseline, patched } = pair;
|
|
30317
|
+
if (baseline.runId === patched.runId) {
|
|
30318
|
+
return refuse3("pair-is-the-same-run", `both sides are run ${patched.runId}. A baseline and a patched result that are the same execution prove nothing about the patch, and every field of the pair would be equal by construction.`);
|
|
30319
|
+
}
|
|
30320
|
+
if (patched.status === "refused") {
|
|
30321
|
+
return refuse3("run-was-refused", `the patched run was refused before executing (${patched.boundaryRefusals.length} boundary refusal(s)), so there is no result to attest`);
|
|
30322
|
+
}
|
|
30323
|
+
if (patched.outcome === null) {
|
|
30324
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
30325
|
+
}
|
|
30326
|
+
if (baseline.status === "refused") {
|
|
30327
|
+
return refuse3("baseline-was-refused", `the baseline run was refused before executing (${baseline.boundaryRefusals.length} boundary refusal(s)), so there is no "before" to compare against`);
|
|
30328
|
+
}
|
|
30329
|
+
if (baseline.outcome === null) {
|
|
30330
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
30331
|
+
}
|
|
30332
|
+
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
30333
|
+
return refuse3("baseline-environment-failed", `the baseline ended ${baseline.outcome}, which is not an observed verdict about the unpatched tree. Signing it would let a baseline we could not read stand in for one that failed, and a broken baseline flatters every patch.`);
|
|
30334
|
+
}
|
|
30335
|
+
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
30336
|
+
if (budget === null) {
|
|
30337
|
+
return refuse3("our-environment-failed", `the patched run ended ${patched.outcome}: the test command could not be invoked, which is our environment failing and not a verdict about the work. No statement is emitted, because every possible value would be a claim we cannot support.`);
|
|
30338
|
+
}
|
|
30339
|
+
if (patched.testCommand === null) {
|
|
30340
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
30341
|
+
}
|
|
30342
|
+
if (baseline.testCommand !== patched.testCommand) {
|
|
30343
|
+
return refuse3("pair-disagrees-on-harness", `baseline ran ${JSON.stringify(baseline.testCommand)} and patched ran ${JSON.stringify(patched.testCommand)}. Opposite verdicts from different harnesses say nothing about the patch.`);
|
|
30344
|
+
}
|
|
30345
|
+
if (baseline.targetSha !== patched.targetSha) {
|
|
30346
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
30347
|
+
}
|
|
30348
|
+
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
30349
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
30350
|
+
}
|
|
30351
|
+
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
30352
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
30353
|
+
}
|
|
30354
|
+
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
30355
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
30356
|
+
}
|
|
30357
|
+
if (baseline.treeDigest === patched.treeDigest) {
|
|
30358
|
+
return refuse3("pair-has-identical-tree", `both sides are tree ${String(patched.treeDigest).slice(0, 12)}\u2026, so whatever the two runs disagree about, it is not the patch`);
|
|
30359
|
+
}
|
|
30360
|
+
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
30361
|
+
return refuse3("patch-binding-incomplete", `a baseline patch shaped the ${patched.baselinePatchSha256 !== null ? "patched" : "baseline"} tree, so it cannot be reproduced from (base_commit_oid, patch_sha256) alone. Fold it into the binding before signing; do not sign a triple a verifier cannot follow.`);
|
|
30362
|
+
}
|
|
30363
|
+
for (const [side, outcome, counts, exitCode] of [
|
|
30364
|
+
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
30365
|
+
["baseline", baseline.outcome, baseline.counts, baseline.exitCode]
|
|
30366
|
+
]) {
|
|
30367
|
+
const why = contradicts(outcome, counts, exitCode);
|
|
30368
|
+
if (why !== null) {
|
|
30369
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
29954
30370
|
}
|
|
29955
|
-
if (failures.length > 0)
|
|
29956
|
-
throw new VenueRollbackError(err, failures);
|
|
29957
|
-
throw err;
|
|
29958
30371
|
}
|
|
29959
|
-
|
|
29960
|
-
|
|
29961
|
-
|
|
29962
|
-
|
|
29963
|
-
|
|
29964
|
-
|
|
29965
|
-
|
|
29966
|
-
|
|
30372
|
+
for (const [side, source] of [
|
|
30373
|
+
["patched", patched.testCommandSource],
|
|
30374
|
+
["baseline", baseline.testCommandSource]
|
|
30375
|
+
]) {
|
|
30376
|
+
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
30377
|
+
if (why !== null)
|
|
30378
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
30379
|
+
}
|
|
30380
|
+
if (patched.testCommandSource === "developer-declared" || patched.testCommandSource === "operator-declared") {
|
|
30381
|
+
return refuse3("test-command-origin-unattested", `${patched.testCommandSource === "developer-declared" ? "the developer" : "an operator"} supplied the test command (unreachable \u2014 the source rules above cover it)`);
|
|
30382
|
+
}
|
|
30383
|
+
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
30384
|
+
return refuse3("test-command-origin-unattested", `the pair disagrees on where the command came from: baseline ${baseline.testCommandSource}, patched ${patched.testCommandSource}. One signed value cannot describe both.`);
|
|
30385
|
+
}
|
|
30386
|
+
for (const [side, source, image] of [
|
|
30387
|
+
["patched", patched.imageSource, patched.containerImage],
|
|
30388
|
+
["baseline", baseline.imageSource, baseline.containerImage]
|
|
30389
|
+
]) {
|
|
30390
|
+
const why = IMAGE_SOURCE_IS_SIGNABLE[source](image);
|
|
30391
|
+
if (why !== null)
|
|
30392
|
+
return refuse3("image-origin-unattested", `the ${side} run ${why}`);
|
|
30393
|
+
}
|
|
30394
|
+
if (baseline.imageSource !== patched.imageSource) {
|
|
30395
|
+
return refuse3("image-origin-unattested", `the pair disagrees on where the image came from: baseline ${baseline.imageSource}, patched ${patched.imageSource}. The environment is part of what the statement says was held constant.`);
|
|
29967
30396
|
}
|
|
29968
|
-
|
|
29969
|
-
}
|
|
29970
|
-
function describeThrown(thrown, opts) {
|
|
29971
|
-
try {
|
|
29972
|
-
if (isErrorValue(thrown)) {
|
|
29973
|
-
const message2 = readErrorField(thrown, "message", UNREADABLE_MESSAGE);
|
|
29974
|
-
if (!opts.includeName)
|
|
29975
|
-
return message2;
|
|
29976
|
-
const name = readErrorField(thrown, "name", UNREADABLE_NAME);
|
|
29977
|
-
return message2 === "" ? name : `${name}: ${message2}`;
|
|
29978
|
-
}
|
|
29979
|
-
return coerceToString(thrown);
|
|
29980
|
-
} catch {
|
|
29981
|
-
return UNDESCRIBABLE_THROWN;
|
|
30397
|
+
if (baseline.containerImage !== patched.containerImage) {
|
|
30398
|
+
return refuse3("pair-disagrees-on-image", `baseline ran in ${String(baseline.containerImage)} and patched in ${String(patched.containerImage)}. The signed measurement names one image, so a pair from two environments would attribute to the patch whatever the image changed.`);
|
|
29982
30399
|
}
|
|
29983
|
-
|
|
29984
|
-
|
|
29985
|
-
try {
|
|
29986
|
-
return thrown instanceof Error;
|
|
29987
|
-
} catch {
|
|
29988
|
-
return false;
|
|
30400
|
+
if (patched.containerImage === null || patched.containerImage === "") {
|
|
30401
|
+
return refuse3("missing-container-image", "no container image was recorded, so the measurement would name a stand-in rather than the environment the suite actually ran in");
|
|
29989
30402
|
}
|
|
29990
|
-
|
|
29991
|
-
|
|
29992
|
-
let raw;
|
|
29993
|
-
try {
|
|
29994
|
-
raw = thrown[key];
|
|
29995
|
-
} catch {
|
|
29996
|
-
return fallback;
|
|
30403
|
+
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
30404
|
+
return refuse3("missing-image-digest", "no image digest was recorded for the patched half, so the measurement would name the mutable tag rather than the bytes the suite actually ran in. A run without the digest is recordable but not attestable \u2014 the `missing-patch-digest` split.");
|
|
29997
30405
|
}
|
|
29998
|
-
|
|
29999
|
-
}
|
|
30000
|
-
function coerceToString(thrown) {
|
|
30001
|
-
try {
|
|
30002
|
-
return String(thrown);
|
|
30003
|
-
} catch {
|
|
30004
|
-
return UNCOERCIBLE_THROWN;
|
|
30406
|
+
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
30407
|
+
return refuse3("malformed-image-digest", `the patched half carries ${JSON.stringify(patched.containerImageDigest).slice(0, 120)} where a RepoDigest (\`repo@sha256:<64 hex>\`) belongs. A tag or a stand-in string here would be signed as if it were content-addressed, which is the lie the digest axis exists to refuse.`);
|
|
30005
30408
|
}
|
|
30006
|
-
|
|
30007
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
}
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30015
|
-
|
|
30016
|
-
|
|
30017
|
-
}
|
|
30018
|
-
|
|
30019
|
-
|
|
30020
|
-
|
|
30021
|
-
const
|
|
30022
|
-
|
|
30023
|
-
}
|
|
30024
|
-
|
|
30025
|
-
|
|
30026
|
-
|
|
30027
|
-
|
|
30028
|
-
|
|
30029
|
-
|
|
30030
|
-
|
|
30031
|
-
|
|
30032
|
-
|
|
30033
|
-
|
|
30034
|
-
|
|
30035
|
-
|
|
30036
|
-
|
|
30037
|
-
|
|
30038
|
-
|
|
30039
|
-
|
|
30040
|
-
|
|
30041
|
-
|
|
30042
|
-
|
|
30043
|
-
const stagedProxies = [];
|
|
30044
|
-
const owner = localTreeOwner();
|
|
30045
|
-
let cloneVolume;
|
|
30046
|
-
const createdVolumes = [];
|
|
30047
|
-
const fillCloneVolume = (cloneDir) => {
|
|
30048
|
-
const name = localCloneVolumeName(runId);
|
|
30049
|
-
const created = docker3.sync(["volume", "create", `--label=${STAGE_VOLUME_LABEL_KEY}=${runId}`, "--", name], { timeoutMs: LOCAL_VOLUME_CREATE_TIMEOUT_MS });
|
|
30050
|
-
if (created.error || created.status !== 0) {
|
|
30051
|
-
throw new ContainmentRefusalError(`could not create the clone volume ${name} on the local daemon: ${(created.error?.message ?? created.stderr).trim().slice(0, 300)}`);
|
|
30052
|
-
}
|
|
30053
|
-
createdVolumes.push(name);
|
|
30054
|
-
const from = resolverFor("local")(cloneDir, "clone");
|
|
30055
|
-
const filled = docker3.sync(localCloneFillArgv(from, name, localGuestUser()), {
|
|
30056
|
-
timeoutMs: LOCAL_CLONE_FILL_TIMEOUT_MS
|
|
30057
|
-
});
|
|
30058
|
-
if (filled.error || filled.status !== 0) {
|
|
30059
|
-
throw new ContainmentRefusalError(`could not copy the clone into its volume ${name}: ${(filled.error?.message ?? filled.stderr).trim().slice(0, 300)}`);
|
|
30060
|
-
}
|
|
30061
|
-
return name;
|
|
30062
|
-
};
|
|
30063
|
-
const lease = {
|
|
30064
|
-
kind: "local",
|
|
30065
|
-
runId,
|
|
30066
|
-
containment,
|
|
30067
|
-
docker: docker3,
|
|
30068
|
-
// Stated, not inferred from `kind`. On this venue it is the truth twice over:
|
|
30069
|
-
// the paths are on this machine AND `canonical()` is what should resolve
|
|
30070
|
-
// them, which is the behaviour every local run has always had.
|
|
30071
|
-
pathDomain: "local",
|
|
30072
|
-
// Spread rather than `guestUser: owner`, so a non-root lease has no such key
|
|
30073
|
-
// at all — the same object every local run has always had.
|
|
30074
|
-
...owner === void 0 ? {} : { guestUser: owner },
|
|
30075
|
-
get released() {
|
|
30076
|
-
return released;
|
|
30077
|
-
},
|
|
30078
|
-
get cloneVolume() {
|
|
30079
|
-
return cloneVolume;
|
|
30080
|
-
},
|
|
30081
|
-
stage: (local) => {
|
|
30082
|
-
if (released) {
|
|
30083
|
-
return Promise.reject(new LeaseReleasedError("this lease was already released, so a clone volume made now would never be removed."));
|
|
30084
|
-
}
|
|
30085
|
-
try {
|
|
30086
|
-
handLocalTreeToGuest(local, owner);
|
|
30087
|
-
cloneVolume = fillCloneVolume(local.cloneDir);
|
|
30088
|
-
} catch (err) {
|
|
30089
|
-
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
30090
|
-
}
|
|
30091
|
-
return Promise.resolve({
|
|
30092
|
-
cloneDir: local.cloneDir,
|
|
30093
|
-
scratchRoot: local.scratchRoot,
|
|
30094
|
-
previewDir: local.previewDir,
|
|
30095
|
-
...localJailPaths(local.scratchRoot)
|
|
30096
|
-
});
|
|
30097
|
-
},
|
|
30098
|
-
// The containment function verbatim, which is the point of the barrel
|
|
30099
|
-
// re-export rather than a copy here: this venue's daemon and this process
|
|
30100
|
-
// share a filesystem, so the directory it makes under `tmpdir()` is already
|
|
30101
|
-
// venue-side and has been on every local run since the sidecar existed.
|
|
30102
|
-
//
|
|
30103
|
-
// TRACKED, so `release()` can be the guaranteed owner the interface
|
|
30104
|
-
// promises. Returning a bare handle made that promise the CALLER's to keep,
|
|
30105
|
-
// and a caller that staged and then failed outside `runEnvironmentSpec`'s
|
|
30106
|
-
// `finally` left the directory behind while release reported success.
|
|
30107
|
-
//
|
|
30108
|
-
// REFUSING AFTER RELEASE is the other half of the same promise. Staging onto
|
|
30109
|
-
// a drained list would leak with nothing left to drain, and `release()` has
|
|
30110
|
-
// already reported it had nothing to do — the split ownership this tracking
|
|
30111
|
-
// exists to close, reached from the other side.
|
|
30112
|
-
//
|
|
30113
|
-
// ONLY THIS ARM CHECKS IT. An earlier draft of this comment said the hosted
|
|
30114
|
-
// arm refuses the same way through `check()`, and that is not what `check()`
|
|
30115
|
-
// tests: `assertVenueUnchanged` reads the tunnel's failure and classifies
|
|
30116
|
-
// the daemon, and never looks at `released`. Hosted refuses a post-release
|
|
30117
|
-
// staging only incidentally, because by then the VM is gone. Naming the
|
|
30118
|
-
// check and what it actually tests is the rule this branch spent its length
|
|
30119
|
-
// on, and that draft broke it in the same breath as stating it.
|
|
30120
|
-
//
|
|
30121
|
-
// That leaves the two arms answering one programmer error with different
|
|
30122
|
-
// exit codes — hosted's incidental failure is a `HostedVenueError` at 2,
|
|
30123
|
-
// this one is a plain throw at 1. Recorded on TERM-752 rather than fixed
|
|
30124
|
-
// here: it is unreachable while `placement.ts` refuses first.
|
|
30125
|
-
//
|
|
30126
|
-
// THE EXIT CODE IS WHY IT IS NOT A REFUSAL TYPE. Nothing catches it and
|
|
30127
|
-
// `findRunRefusal` does not match it, so it reaches the top-level catch at
|
|
30128
|
-
// exit 1 — correct, because only our own call ordering can reach this, which
|
|
30129
|
-
// is `assertDomainDeclared`'s reasoning and the same one this branch applied
|
|
30130
|
-
// to `assertProxyStagedForVenue`. Giving it `RunRefusalError` would dress
|
|
30131
|
-
// our defect as a polite refusal, TERM-649's lie inverted.
|
|
30132
|
-
//
|
|
30133
|
-
// It is a NAMED subclass rather than a bare `Error` for a hazard already
|
|
30134
|
-
// here, not a speculative one. `stageProxyCode()` in `containment` throws
|
|
30135
|
-
// `FenceError` on a damaged install, telling the developer to reinstall the
|
|
30136
|
-
// CLI. Two distinguishable failures leave this one call, so the day anyone
|
|
30137
|
-
// adds a catch to surface that instruction, an unnamed ordering bug gets
|
|
30138
|
-
// swept into it and tells a developer to reinstall over our mistake.
|
|
30139
|
-
stageProxyCode: () => {
|
|
30140
|
-
if (released) {
|
|
30141
|
-
return Promise.reject(new LeaseReleasedError("this lease was already released, so a staging made now would never be removed: release() has run and drained what it was holding."));
|
|
30142
|
-
}
|
|
30143
|
-
const staged = stageProxyCode();
|
|
30144
|
-
stagedProxies.push(staged);
|
|
30145
|
-
return Promise.resolve(staged);
|
|
30146
|
-
},
|
|
30147
|
-
census: (label) => Promise.resolve(released ? {
|
|
30148
|
-
observed: false,
|
|
30149
|
-
census: { containers: [], volumes: [], networks: [] },
|
|
30150
|
-
unobservedReason: RELEASED_LEASE_CENSUS_REASON
|
|
30151
|
-
} : censusReport(docker3, label)),
|
|
30152
|
-
publishPreview: (req) => startPreview({ ...req, docker: docker3 }),
|
|
30153
|
-
release: () => {
|
|
30154
|
-
if (released) {
|
|
30155
|
-
return Promise.resolve({
|
|
30156
|
-
kind: "local",
|
|
30157
|
-
released: false,
|
|
30158
|
-
alreadyReleased: true,
|
|
30159
|
-
error: null,
|
|
30160
|
-
detail: "already released; nothing to do"
|
|
30161
|
-
});
|
|
30162
|
-
}
|
|
30163
|
-
released = true;
|
|
30164
|
-
const staged = stagedProxies.splice(0, stagedProxies.length);
|
|
30165
|
-
for (const s of staged)
|
|
30166
|
-
s.cleanup();
|
|
30167
|
-
const volumeFailures = [];
|
|
30168
|
-
for (const name of createdVolumes.splice(0, createdVolumes.length)) {
|
|
30169
|
-
const removed = docker3.sync(["volume", "rm", "-f", "--", name], {
|
|
30170
|
-
timeoutMs: LOCAL_VOLUME_CREATE_TIMEOUT_MS
|
|
30171
|
-
});
|
|
30172
|
-
if (removed.error || removed.status !== 0) {
|
|
30173
|
-
volumeFailures.push(`${name}: ${(removed.error?.message ?? removed.stderr).trim().slice(0, 200)}`);
|
|
30174
|
-
}
|
|
30175
|
-
}
|
|
30176
|
-
cloneVolume = void 0;
|
|
30177
|
-
if (volumeFailures.length > 0) {
|
|
30178
|
-
return Promise.resolve({
|
|
30179
|
-
kind: "local",
|
|
30180
|
-
released: true,
|
|
30181
|
-
alreadyReleased: false,
|
|
30182
|
-
error: `could not remove the clone volume: ${volumeFailures.join("; ")}`,
|
|
30183
|
-
detail: `the lease is closed, but a clone volume remains (label ${STAGE_VOLUME_LABEL_KEY})`
|
|
30184
|
-
});
|
|
30185
|
-
}
|
|
30186
|
-
return Promise.resolve({
|
|
30187
|
-
kind: "local",
|
|
30188
|
-
released: true,
|
|
30189
|
-
alreadyReleased: false,
|
|
30190
|
-
error: null,
|
|
30191
|
-
detail: "the local venue owns no host resources; the lease is closed"
|
|
30192
|
-
});
|
|
30409
|
+
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
30410
|
+
return refuse3("image-digest-repo-mismatch", `the patched half's digest names repository ${imageRepo(patched.containerImageDigest)} but the pair ran ${patched.containerImage}. An image can carry digests for several repositories; signing one the pair did not name would attribute these bytes to a different name.`);
|
|
30411
|
+
}
|
|
30412
|
+
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
30413
|
+
return refuse3("missing-image-digest", "no image digest was recorded for the baseline half, so the measurement would name the mutable tag rather than the bytes the suite actually ran in. A run without the digest is recordable but not attestable \u2014 the `missing-patch-digest` split.");
|
|
30414
|
+
}
|
|
30415
|
+
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
30416
|
+
return refuse3("malformed-image-digest", `the baseline half carries ${JSON.stringify(baseline.containerImageDigest).slice(0, 120)} where a RepoDigest (\`repo@sha256:<64 hex>\`) belongs. A tag or a stand-in string here would be signed as if it were content-addressed, which is the lie the digest axis exists to refuse.`);
|
|
30417
|
+
}
|
|
30418
|
+
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
30419
|
+
return refuse3("image-digest-repo-mismatch", `the baseline half's digest names repository ${imageRepo(baseline.containerImageDigest)} but the pair ran ${patched.containerImage}. An image can carry digests for several repositories; signing one the pair did not name would attribute these bytes to a different name.`);
|
|
30420
|
+
}
|
|
30421
|
+
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
30422
|
+
return refuse3("pair-disagrees-on-image-digest", `baseline ran image digest ${String(baseline.containerImageDigest)} and patched ${String(patched.containerImageDigest)} under one tag. The tag agreeing is the trap: a re-pointed tag is two environments wearing one name.`);
|
|
30423
|
+
}
|
|
30424
|
+
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
30425
|
+
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
30426
|
+
return refuse3("image-digest-contradicts-image", `the pair ran ${patched.containerImage}, whose reference pins digest ${claimedDigest}, but the digest field says ${String(imageDigestOf(patched.containerImageDigest))}. A measurement must not sign one digest while the record names another.`);
|
|
30427
|
+
}
|
|
30428
|
+
return {
|
|
30429
|
+
ok: true,
|
|
30430
|
+
predicate: {
|
|
30431
|
+
claim_id: patched.claimId,
|
|
30432
|
+
base_commit_oid: patched.targetSha,
|
|
30433
|
+
patch_sha256: patched.patchSha256,
|
|
30434
|
+
tree_digest: patched.treeDigest,
|
|
30435
|
+
test_command: patched.testCommand,
|
|
30436
|
+
test_command_source: patched.testCommandSource,
|
|
30437
|
+
baseline_result: toTestRunResult(baseline, opts.baselineOutputSha256),
|
|
30438
|
+
patched_result: toTestRunResult(patched, opts.patchedOutputSha256),
|
|
30439
|
+
// No `?? 'unknown-image'`: `missing-image-digest` refuses above, so the value here is
|
|
30440
|
+
// always one the audit actually observed. The DIGEST, not `containerImage`: the
|
|
30441
|
+
// RepoDigest (`repo@sha256:…`) carries the repo name and the content hash, and the
|
|
30442
|
+
// tag it drops is the part a registry can re-point (TERM-893).
|
|
30443
|
+
enclave_measurement: localMeasurement(patched.containerImageDigest),
|
|
30444
|
+
nonce: opts.nonce ?? randomBytes11(16).toString("hex"),
|
|
30445
|
+
run_policy: { max_attempts: opts.maxAttempts ?? 1, budget_outcome: budget }
|
|
30193
30446
|
}
|
|
30194
30447
|
};
|
|
30195
|
-
return lease;
|
|
30196
30448
|
}
|
|
30197
|
-
|
|
30198
|
-
|
|
30199
|
-
|
|
30449
|
+
function signRunStatement(predicate, privateKey, keyid) {
|
|
30450
|
+
const statement = createAcceptanceStatement(predicate);
|
|
30451
|
+
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
30452
|
+
}
|
|
30453
|
+
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, IMAGE_SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS, refuse3, LOCAL_MEASUREMENT_PREFIX, REFERENCE_DOMAIN_COMPONENT, REFERENCE_DOMAIN_NAME, REFERENCE_IPV6, REFERENCE_DOMAIN, REFERENCE_PATH_COMPONENT, REPO_DIGEST_RE;
|
|
30454
|
+
var init_attestation2 = __esm({
|
|
30455
|
+
"../../packages/envrun/dist/attestation.js"() {
|
|
30200
30456
|
"use strict";
|
|
30201
|
-
|
|
30202
|
-
|
|
30203
|
-
|
|
30204
|
-
|
|
30205
|
-
|
|
30206
|
-
|
|
30207
|
-
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
|
|
30211
|
-
|
|
30457
|
+
init_dist2();
|
|
30458
|
+
OUTCOME_TO_BUDGET = {
|
|
30459
|
+
completed: "completed",
|
|
30460
|
+
"tests-failed": "tests-failed",
|
|
30461
|
+
"no-tests-observed": "no-tests-observed",
|
|
30462
|
+
"budget-exceeded": "budget-exceeded",
|
|
30463
|
+
"counts-unparsed": "no-tests-observed",
|
|
30464
|
+
"test-command-unavailable": null,
|
|
30465
|
+
// null, with the same reasoning as `test-command-unavailable` and NOT
|
|
30466
|
+
// `budget-exceeded` (TERM-644). A signed `budget-exceeded` records one fact:
|
|
30467
|
+
// the run hit the time budget we applied — our default, or the one set for the
|
|
30468
|
+
// posting (TERM-1308), which is still ours to enforce. It blames no one (TERM-1289; `LIMIT_OWNER` in
|
|
30469
|
+
// `classify.ts` owns that question), and it is not an acceptable run:
|
|
30470
|
+
// `acceptRun` (`packages/attest/src/verify.ts`) refuses every `budget_outcome`
|
|
30471
|
+
// except `completed`. Exhausting the tmpfs we sized, or an OOM kill from memory we
|
|
30472
|
+
// did not cap, is a different fact, and signing it under the time-budget word
|
|
30473
|
+
// would record something that did not happen.
|
|
30474
|
+
"environment-exhausted": null
|
|
30212
30475
|
};
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
|
|
30220
|
-
|
|
30476
|
+
BASELINE_IS_A_VERDICT = {
|
|
30477
|
+
completed: true,
|
|
30478
|
+
"tests-failed": true,
|
|
30479
|
+
"no-tests-observed": true,
|
|
30480
|
+
"budget-exceeded": false,
|
|
30481
|
+
"counts-unparsed": false,
|
|
30482
|
+
"test-command-unavailable": false,
|
|
30483
|
+
"environment-exhausted": false
|
|
30221
30484
|
};
|
|
30222
|
-
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30485
|
+
CONTRADICTS_COUNTS = {
|
|
30486
|
+
// Green asserts three things, and the first version checked one. `{0, 0}` signed as
|
|
30487
|
+
// `completed` — green with nothing having run, which is precisely what
|
|
30488
|
+
// `no-tests-observed` exists to say — and `exitCode: 1` signed as green too.
|
|
30489
|
+
completed: (counts, exitCode) => counts === null ? "`completed` with no parsed counts \u2014 green with no evidence any test asserted, which is exactly what `no-tests-observed` exists to say" : counts.tests_failed > 0 ? `\`completed\` alongside ${String(counts.tests_failed)} recorded failure(s)` : counts.tests_passed === 0 ? "`completed` with zero tests passed \u2014 green asserts a suite RAN, not merely that nothing failed" : exitCode !== 0 ? `\`completed\` with exit code ${String(exitCode)} \u2014 green cannot come from a command that reported failure` : null,
|
|
30490
|
+
// BOTH count fields, not just passes. `{0 passed, 4 failed}` signed as "evidence of zero"
|
|
30491
|
+
// for one round. A nonzero exit contradicts it too: `classify.ts` returns `tests-failed`
|
|
30492
|
+
// for that, so this member can only come from a clean exit.
|
|
30493
|
+
"no-tests-observed": (counts, exitCode) => counts === null ? null : counts.tests_passed > 0 || counts.tests_failed > 0 ? `mapped to \`no-tests-observed\` but records ${String(counts.tests_passed)} passed and ${String(counts.tests_failed)} failed, and that member asserts evidence of zero` : exitCode !== null && exitCode !== 0 ? `mapped to \`no-tests-observed\` with exit code ${String(exitCode)} \u2014 a nonzero exit is \`tests-failed\`, not evidence of zero` : null,
|
|
30494
|
+
// A killed run was never read to the end, so ANY counts beside it are a claim we cannot
|
|
30495
|
+
// support. `classify.ts` hardcodes `counts: null` on the timeout branch — but nothing in the
|
|
30496
|
+
// TYPE ties them, which is this whole family's rationale.
|
|
30497
|
+
"budget-exceeded": (counts) => counts === null ? null : `mapped to \`budget-exceeded\` \u2014 a run killed mid-flight \u2014 yet carries counts (${String(counts.tests_passed)} passed, ${String(counts.tests_failed)} failed), which asserts we read a suite that never finished`,
|
|
30498
|
+
// ABSTAINS, and on both observables. `classify.ts` reaches `tests-failed` two ways: counted
|
|
30499
|
+
// failures (any exit code, because a runner can report failures and still exit 0 under
|
|
30500
|
+
// `|| true`), and a nonzero exit with no counted failure ("something outside the assertions
|
|
30501
|
+
// failed"). So neither zero failures NOR a zero exit is a contradiction here, and guarding
|
|
30502
|
+
// either would redden honest evidence. This is a decision with its reason attached, not a
|
|
30503
|
+
// gap — the distinction the outer table exists to make visible.
|
|
30504
|
+
"tests-failed": () => null
|
|
30505
|
+
};
|
|
30506
|
+
SOURCE_IS_SIGNABLE = {
|
|
30507
|
+
detected: () => null,
|
|
30508
|
+
"founder-declared": () => null,
|
|
30509
|
+
// The party under verification chose it, and no signed member can say so. Per ADR-005
|
|
30510
|
+
// Decision 2, emit nothing rather than pick the closest word.
|
|
30511
|
+
"developer-declared": () => "had its test command supplied by the developer whose work is under verification, and the signed vocabulary has no member for that. `founder-declared` would attribute the choice to the other party; `detected` would claim the repo chose it. Both are false.",
|
|
30512
|
+
// US, out of band, per posting — the twin of `IMAGE_SOURCE_IS_SIGNABLE['operator-declared']`
|
|
30513
|
+
// and refusing for the same reason. `founder-declared` is the nearest signed member and it
|
|
30514
|
+
// is still false: the poster did not choose this, we did, and signing our own choice under
|
|
30515
|
+
// the counterparty's name is the specific false attribution the `developer-declared` entry
|
|
30516
|
+
// above exists to refuse. Decision 2 — emit nothing rather than pick the closest word.
|
|
30517
|
+
"operator-declared": () => "had its test command chosen by an operator out of band, and the signed vocabulary has no member for that. Signing it would present a hand-picked command as one the repository chose for itself.",
|
|
30518
|
+
// `none` means nobody chose a command, so a command beside it is a contradiction. Shadowed
|
|
30519
|
+
// today — `thrun.ts` sets `none` only when the derived command is null, and `no-test-command`
|
|
30520
|
+
// fires first — but that coupling runs through `runEnvironmentSpec` and lives in no type.
|
|
30521
|
+
none: (command) => command === null ? null : `records its command source as \`none\` \u2014 nobody chose one \u2014 beside the command ${JSON.stringify(command)}`
|
|
30522
|
+
};
|
|
30523
|
+
IMAGE_SOURCE_IS_SIGNABLE = {
|
|
30524
|
+
detected: () => null,
|
|
30525
|
+
// The command table's `founder-declared` reasoning, applied to the environment: the poster
|
|
30526
|
+
// chose the runtime and the poster is paying, so a runtime they declared is theirs to stand
|
|
30527
|
+
// behind. The image itself is still the runtime mapping's pick, not a name anyone typed.
|
|
30528
|
+
"founder-declared": () => null,
|
|
30529
|
+
"operator-declared": () => "had its container image chosen by an operator out of band, and the signed vocabulary has no member for that. Signing it would present a hand-picked environment as the one the runtime mapping selected.",
|
|
30530
|
+
"developer-declared": () => "had its container image supplied by the developer whose work is under verification. A chosen interpreter turns a red suite green as surely as a chosen test command.",
|
|
30531
|
+
// `none` means no image was booted, so an image name beside it is a contradiction — the
|
|
30532
|
+
// `SOURCE_IS_SIGNABLE.none` argument, applied to the other field.
|
|
30533
|
+
none: (image) => image === null ? null : `records its image source as \`none\` \u2014 nothing was booted \u2014 beside the image ${JSON.stringify(image)}`
|
|
30534
|
+
};
|
|
30535
|
+
ATTEST_REFUSAL_REASONS = [
|
|
30536
|
+
"our-environment-failed",
|
|
30537
|
+
"run-was-refused",
|
|
30538
|
+
"no-outcome-recorded",
|
|
30539
|
+
"no-test-command",
|
|
30540
|
+
"pair-disagrees-on-harness",
|
|
30541
|
+
"pair-disagrees-on-base",
|
|
30542
|
+
"pair-is-the-same-run",
|
|
30543
|
+
"missing-tree-digest",
|
|
30544
|
+
"missing-patch-digest",
|
|
30545
|
+
// Round 4: the pair-agreement axis. `testCommand` and `targetSha` were compared across the
|
|
30546
|
+
// halves and `containerImage` was not, so two different environments signed one measurement.
|
|
30547
|
+
"pair-disagrees-on-image",
|
|
30548
|
+
"missing-container-image",
|
|
30549
|
+
// TERM-893: the digest axis. The two guards above compare NAMES, and a name is
|
|
30550
|
+
// exactly what a registry can re-point between the two halves of a pair — so the
|
|
30551
|
+
// measurement signs the RepoDigest, and these refuse when it is absent or split.
|
|
30552
|
+
// `malformed` and `repo-mismatch` exist because this is a RUNTIME boundary
|
|
30553
|
+
// (Codex round 1): the .mjs audit calls through here untyped, so `undefined`,
|
|
30554
|
+
// a tag, or an arbitrary string would otherwise flow into the signed
|
|
30555
|
+
// measurement wearing a digest's name.
|
|
30556
|
+
"pair-disagrees-on-image-digest",
|
|
30557
|
+
"missing-image-digest",
|
|
30558
|
+
"malformed-image-digest",
|
|
30559
|
+
"image-digest-repo-mismatch",
|
|
30560
|
+
// Codex round 4: a digest-QUALIFIED image reference pins a digest of its
|
|
30561
|
+
// own, and the repo-mismatch guards compare repositories only — so
|
|
30562
|
+
// `node@sha256:A` could ride above digest fields saying `node@sha256:B`.
|
|
30563
|
+
"image-digest-contradicts-image",
|
|
30564
|
+
// ── The BASELINE half. Added TERM-354 review round 2: every guard above reads
|
|
30565
|
+
// `patched`, so a baseline could be anything at all and still be signed as the "before"
|
|
30566
|
+
// half of a comparison. That is the worst direction for this bug to point, because a
|
|
30567
|
+
// BROKEN baseline manufactures a favourable result: install dies, nothing runs, and the
|
|
30568
|
+
// pair reads "it was failing, the patch fixed it".
|
|
30569
|
+
"baseline-was-refused",
|
|
30570
|
+
"baseline-no-outcome",
|
|
30571
|
+
"baseline-environment-failed",
|
|
30572
|
+
"pair-has-identical-tree",
|
|
30573
|
+
// The signed triple must be reproducible from what it names — see the guard.
|
|
30574
|
+
"patch-binding-incomplete",
|
|
30575
|
+
"outcome-contradicts-counts",
|
|
30576
|
+
"test-command-origin-unattested",
|
|
30577
|
+
// The environment half of the pair above. `pair-disagrees-on-image` compares image NAMES;
|
|
30578
|
+
// this one asks who CHOSE the name, which no comparison of the two halves can reveal when
|
|
30579
|
+
// both were handed the same override.
|
|
30580
|
+
"image-origin-unattested"
|
|
30581
|
+
];
|
|
30582
|
+
refuse3 = (reason, detail) => ({
|
|
30583
|
+
ok: false,
|
|
30584
|
+
reason,
|
|
30585
|
+
detail
|
|
30586
|
+
});
|
|
30587
|
+
LOCAL_MEASUREMENT_PREFIX = "NOT-AN-ENCLAVE:local-container:";
|
|
30588
|
+
REFERENCE_DOMAIN_COMPONENT = "[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?";
|
|
30589
|
+
REFERENCE_DOMAIN_NAME = `${REFERENCE_DOMAIN_COMPONENT}(?:\\.${REFERENCE_DOMAIN_COMPONENT})*`;
|
|
30590
|
+
REFERENCE_IPV6 = "\\[(?:[a-fA-F0-9:]+)\\]";
|
|
30591
|
+
REFERENCE_DOMAIN = `(?:${REFERENCE_DOMAIN_NAME}|${REFERENCE_IPV6})(?::[0-9]+)?`;
|
|
30592
|
+
REFERENCE_PATH_COMPONENT = "[a-z0-9]+(?:(?:\\.|_{1,2}|-+)[a-z0-9]+)*";
|
|
30593
|
+
REPO_DIGEST_RE = new RegExp(`^(?:(${REFERENCE_DOMAIN})/)?${REFERENCE_PATH_COMPONENT}(?:/${REFERENCE_PATH_COMPONENT})*@sha256:[0-9a-f]{64}$`);
|
|
30227
30594
|
}
|
|
30228
30595
|
});
|
|
30229
30596
|
|
|
@@ -30470,6 +30837,7 @@ async function runEnvironmentSpec(req) {
|
|
|
30470
30837
|
let install = null;
|
|
30471
30838
|
let test = null;
|
|
30472
30839
|
let imageDigest = null;
|
|
30840
|
+
let scriptTimes = null;
|
|
30473
30841
|
let result;
|
|
30474
30842
|
let proxyCode = null;
|
|
30475
30843
|
try {
|
|
@@ -30497,6 +30865,7 @@ async function runEnvironmentSpec(req) {
|
|
|
30497
30865
|
labels,
|
|
30498
30866
|
timeoutMs: req.installTimeoutMs ?? 9e5
|
|
30499
30867
|
});
|
|
30868
|
+
req.log?.(`step install took ${(install.wallMs / 1e3).toFixed(1)}s`);
|
|
30500
30869
|
}
|
|
30501
30870
|
if (install !== null && install.exitCode !== 0) {
|
|
30502
30871
|
result = {
|
|
@@ -30516,28 +30885,65 @@ async function runEnvironmentSpec(req) {
|
|
|
30516
30885
|
} else {
|
|
30517
30886
|
imageDigest = inspectRunDigest(req.lease.docker, image, req.log ?? (() => {
|
|
30518
30887
|
}));
|
|
30519
|
-
|
|
30520
|
-
|
|
30521
|
-
|
|
30522
|
-
|
|
30523
|
-
|
|
30524
|
-
|
|
30525
|
-
|
|
30526
|
-
|
|
30527
|
-
|
|
30528
|
-
|
|
30529
|
-
|
|
30530
|
-
|
|
30531
|
-
|
|
30532
|
-
|
|
30533
|
-
|
|
30534
|
-
|
|
30535
|
-
|
|
30536
|
-
|
|
30537
|
-
|
|
30538
|
-
|
|
30888
|
+
const scriptTimesCollector = createNpmScriptTimesCollector();
|
|
30889
|
+
const started = req.lease.pathDomain === "venue" ? await startStorageWatch(req.lease.docker, labels) : null;
|
|
30890
|
+
let coverageFailure = typeof started === "string" ? started : null;
|
|
30891
|
+
const storageWatch = typeof started === "string" ? null : started;
|
|
30892
|
+
let watched = null;
|
|
30893
|
+
try {
|
|
30894
|
+
test = await runStep(containment, {
|
|
30895
|
+
step: "test",
|
|
30896
|
+
profile: "offline",
|
|
30897
|
+
command: req.spec.testCommand,
|
|
30898
|
+
repoDir: req.repoDir,
|
|
30899
|
+
jail,
|
|
30900
|
+
tmp,
|
|
30901
|
+
pathDomain: req.lease.pathDomain,
|
|
30902
|
+
// TERM-729: travels WITH pathDomain, because it answers the same
|
|
30903
|
+
// question about the same machine. Undefined on a local lease.
|
|
30904
|
+
guestUser: req.lease.guestUser,
|
|
30905
|
+
// TERM-913: the third answer about that machine — which volumes the
|
|
30906
|
+
// fence mounts in place of the noexec stage. Undefined on a local lease.
|
|
30907
|
+
stageVolumes: req.lease.stageVolumes,
|
|
30908
|
+
// TERM-1106: the local venue's copy of the clone. Undefined on a hosted lease.
|
|
30909
|
+
cloneVolume: req.lease.cloneVolume,
|
|
30910
|
+
env: testStepEnv(env, req.spec.runtime),
|
|
30911
|
+
image: imageDigest ?? image,
|
|
30912
|
+
labels,
|
|
30913
|
+
timeoutMs: req.testTimeoutMs ?? 9e5,
|
|
30914
|
+
onStdout: scriptTimesCollector.onStdout
|
|
30915
|
+
});
|
|
30916
|
+
} finally {
|
|
30917
|
+
if (storageWatch !== null)
|
|
30918
|
+
watched = await storageWatch.stop();
|
|
30919
|
+
}
|
|
30920
|
+
req.log?.(`step test took ${(test.wallMs / 1e3).toFixed(1)}s`);
|
|
30921
|
+
scriptTimes = scriptTimesCollector.finish({ timedOut: test.timedOut });
|
|
30922
|
+
let venueExhausted;
|
|
30923
|
+
if (watched !== null) {
|
|
30924
|
+
const reading = judgeStorageWatch(watched);
|
|
30925
|
+
req.log?.(describeStorageWatch(reading));
|
|
30926
|
+
if (watched.stopProblem !== null)
|
|
30927
|
+
req.log?.(`storage watch detail: ${watched.stopProblem}`);
|
|
30928
|
+
if (reading.ok) {
|
|
30929
|
+
venueExhausted = venueExhaustionFrom(reading.minFreeMiB, test.stdout, test.stderr);
|
|
30930
|
+
} else {
|
|
30931
|
+
coverageFailure = reading.reason;
|
|
30932
|
+
}
|
|
30933
|
+
}
|
|
30934
|
+
if (coverageFailure !== null) {
|
|
30935
|
+
if (outputNamesNoSpace(test.stdout, test.stderr)) {
|
|
30936
|
+
throw new RunRefusalError(`${coverageFailure}. The test output names ENOSPC, and without dense samples of the venue's layer storage a full tmpfs could read as the repo's tests failing, so we refuse rather than classify.`);
|
|
30937
|
+
}
|
|
30938
|
+
req.log?.("storage watch: coverage incomplete; the output names no ENOSPC, so the verdict stands");
|
|
30939
|
+
req.log?.(`storage watch detail: ${coverageFailure}`);
|
|
30940
|
+
venueExhausted = null;
|
|
30941
|
+
}
|
|
30942
|
+
const verdict = classifyVerification({
|
|
30943
|
+
...toExecution(test),
|
|
30944
|
+
runtime: req.spec.runtime,
|
|
30945
|
+
...venueExhausted === void 0 ? {} : { venueExhausted }
|
|
30539
30946
|
});
|
|
30540
|
-
const verdict = classifyVerification({ ...toExecution(test), runtime: req.spec.runtime });
|
|
30541
30947
|
result = {
|
|
30542
30948
|
outcome: verdict.outcome,
|
|
30543
30949
|
note: verdict.reason,
|
|
@@ -30571,7 +30977,8 @@ async function runEnvironmentSpec(req) {
|
|
|
30571
30977
|
counts: reportedCounts(result.counts, test ? readCounts(test.stdout, test.stderr) : null),
|
|
30572
30978
|
leaks: judgeLeaks(peak, after, observation),
|
|
30573
30979
|
note: result.note,
|
|
30574
|
-
wallMs: Date.now() - startedAt
|
|
30980
|
+
wallMs: Date.now() - startedAt,
|
|
30981
|
+
scriptTimes
|
|
30575
30982
|
};
|
|
30576
30983
|
}
|
|
30577
30984
|
function labelSelector(labels) {
|
|
@@ -30606,7 +31013,10 @@ async function runStep(containment, r) {
|
|
|
30606
31013
|
timeoutMs: r.timeoutMs,
|
|
30607
31014
|
image: r.image,
|
|
30608
31015
|
...r.labels ? { labels: r.labels } : {},
|
|
30609
|
-
...r.proxyCode ? { proxyCode: r.proxyCode } : {}
|
|
31016
|
+
...r.proxyCode ? { proxyCode: r.proxyCode } : {},
|
|
31017
|
+
...r.runtime === void 0 ? {} : { runtime: r.runtime },
|
|
31018
|
+
...r.networkOf === void 0 ? {} : { networkOf: r.networkOf },
|
|
31019
|
+
...r.onStdout ? { onStdout: r.onStdout } : {}
|
|
30610
31020
|
});
|
|
30611
31021
|
return {
|
|
30612
31022
|
step: r.step,
|
|
@@ -30627,8 +31037,10 @@ var init_execute = __esm({
|
|
|
30627
31037
|
"use strict";
|
|
30628
31038
|
init_dist();
|
|
30629
31039
|
init_classify2();
|
|
31040
|
+
init_venueStorageWatch();
|
|
30630
31041
|
init_attestation2();
|
|
30631
31042
|
init_labels();
|
|
31043
|
+
init_npmScriptTimes();
|
|
30632
31044
|
init_venue();
|
|
30633
31045
|
EnvRunError = class extends Error {
|
|
30634
31046
|
};
|
|
@@ -31050,7 +31462,8 @@ var init_result = __esm({
|
|
|
31050
31462
|
"venue",
|
|
31051
31463
|
"refusalOrigin",
|
|
31052
31464
|
"stoppedAt",
|
|
31053
|
-
"installFrozen"
|
|
31465
|
+
"installFrozen",
|
|
31466
|
+
"scriptTimes"
|
|
31054
31467
|
];
|
|
31055
31468
|
RENDER_NONE = null;
|
|
31056
31469
|
FIELD_VIEWS = {
|
|
@@ -31118,7 +31531,10 @@ var init_result = __esm({
|
|
|
31118
31531
|
stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
|
|
31119
31532
|
// The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
|
|
31120
31533
|
// the record of it for the worker and the intake.
|
|
31121
|
-
installFrozen: RENDER_NONE
|
|
31534
|
+
installFrozen: RENDER_NONE,
|
|
31535
|
+
// For the worker and the baseline intake, not the terminal (TERM-1351 PR2) —
|
|
31536
|
+
// the same reasoning as `refusalOrigin` and `installFrozen` above.
|
|
31537
|
+
scriptTimes: RENDER_NONE
|
|
31122
31538
|
};
|
|
31123
31539
|
}
|
|
31124
31540
|
});
|
|
@@ -31133,6 +31549,23 @@ function fail2(detail) {
|
|
|
31133
31549
|
function tarballBasename(name) {
|
|
31134
31550
|
return name.startsWith("@") ? name.slice(name.indexOf("/") + 1) : name;
|
|
31135
31551
|
}
|
|
31552
|
+
function plainRange(name, range, manager) {
|
|
31553
|
+
const archive = manager === "npm" ? /[.](?:tgz|tar\.gz|tar)$/i.test(range) : range.endsWith(".tgz") || range.endsWith(".tar.gz");
|
|
31554
|
+
return PACKAGE_NAME.test(name) && !/[:/#\\]/.test(range) && !range.startsWith(".") && !archive;
|
|
31555
|
+
}
|
|
31556
|
+
function registryRange(name, range, manager) {
|
|
31557
|
+
if (!range.startsWith("npm:"))
|
|
31558
|
+
return plainRange(name, range, manager);
|
|
31559
|
+
const target = range.slice("npm:".length);
|
|
31560
|
+
const at = target.lastIndexOf("@");
|
|
31561
|
+
return PACKAGE_NAME.test(name) && at > 0 && plainRange(target.slice(0, at), target.slice(at + 1), manager);
|
|
31562
|
+
}
|
|
31563
|
+
function notPlainRange(label, path5, dependency) {
|
|
31564
|
+
return PACKAGE_NAME.test(dependency) ? `\`${label}\` in ${path5} depends on ${dependency} through a range that is not a plain version range from the registry, so the change is refused.` : `\`${label}\` in ${path5} depends on a package whose name we could not read, so the change is refused.`;
|
|
31565
|
+
}
|
|
31566
|
+
function noOwnEntry(label, path5, dependency) {
|
|
31567
|
+
return `\`${label}\` in ${path5} depends on ${dependency}, which has no entry of its own in the lockfile, so where it installs from could not be checked.`;
|
|
31568
|
+
}
|
|
31136
31569
|
function resolvedProblem(label, path5, name, version, resolved) {
|
|
31137
31570
|
if (typeof resolved !== "string") {
|
|
31138
31571
|
return `\`${label}\` in ${path5} records no download address, so where it installs from could not be checked.`;
|
|
@@ -31228,7 +31661,7 @@ function checkNpm({ path: path5, base, post }) {
|
|
|
31228
31661
|
}
|
|
31229
31662
|
continue;
|
|
31230
31663
|
}
|
|
31231
|
-
const problem = npmPackageProblem(path5, key, entry);
|
|
31664
|
+
const problem = npmPackageProblem(path5, key, entry, afterPackages);
|
|
31232
31665
|
if (problem !== null)
|
|
31233
31666
|
return fail2(problem);
|
|
31234
31667
|
}
|
|
@@ -31240,7 +31673,7 @@ function checkNpm({ path: path5, base, post }) {
|
|
|
31240
31673
|
}
|
|
31241
31674
|
return { ok: true, judged };
|
|
31242
31675
|
}
|
|
31243
|
-
function npmPackageProblem(path5, key, entry) {
|
|
31676
|
+
function npmPackageProblem(path5, key, entry, packages) {
|
|
31244
31677
|
const match2 = NPM_INSTALL_KEY.exec(key);
|
|
31245
31678
|
const name = match2?.[1];
|
|
31246
31679
|
if (name === void 0 || !PACKAGE_NAME.test(name)) {
|
|
@@ -31260,13 +31693,63 @@ function npmPackageProblem(path5, key, entry) {
|
|
|
31260
31693
|
return `${path5} records "${name}" with no published version we can check.`;
|
|
31261
31694
|
}
|
|
31262
31695
|
const label = `${name}@${version}`;
|
|
31263
|
-
|
|
31696
|
+
if (entry["hasShrinkwrap"] !== void 0 || entry["_hasShrinkwrap"] !== void 0) {
|
|
31697
|
+
return `\`${label}\` in ${path5} carries a lockfile of its own, whose entries this check cannot read, so the change is refused.`;
|
|
31698
|
+
}
|
|
31699
|
+
return resolvedProblem(label, path5, name, version, entry["resolved"]) ?? integrityProblem(label, path5, entry["integrity"], false) ?? npmEdgesProblem(path5, key, label, entry, packages);
|
|
31700
|
+
}
|
|
31701
|
+
function npmEdgesProblem(path5, key, label, entry, packages) {
|
|
31702
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
31703
|
+
const map = entry[field];
|
|
31704
|
+
if (map === void 0)
|
|
31705
|
+
continue;
|
|
31706
|
+
const unreadable = `\`${label}\` in ${path5} has a "${field}" list we could not read, so the change is refused.`;
|
|
31707
|
+
if (!isObject(map))
|
|
31708
|
+
return unreadable;
|
|
31709
|
+
for (const [dependency, range] of Object.entries(map)) {
|
|
31710
|
+
if (typeof range !== "string")
|
|
31711
|
+
return unreadable;
|
|
31712
|
+
if (!registryRange(dependency, range, "npm")) {
|
|
31713
|
+
return notPlainRange(label, path5, dependency);
|
|
31714
|
+
}
|
|
31715
|
+
if (field === "dependencies" && !npmResolves(packages, key, dependency)) {
|
|
31716
|
+
return noOwnEntry(label, path5, dependency);
|
|
31717
|
+
}
|
|
31718
|
+
}
|
|
31719
|
+
}
|
|
31720
|
+
return null;
|
|
31721
|
+
}
|
|
31722
|
+
function npmResolves(packages, key, dependency) {
|
|
31723
|
+
const has2 = (k) => Object.prototype.hasOwnProperty.call(packages, k);
|
|
31724
|
+
let scope = key;
|
|
31725
|
+
for (; ; ) {
|
|
31726
|
+
if (has2(`${scope}/node_modules/${dependency}`))
|
|
31727
|
+
return true;
|
|
31728
|
+
const cut = scope.lastIndexOf("/node_modules/");
|
|
31729
|
+
if (cut < 0)
|
|
31730
|
+
return has2(`node_modules/${dependency}`);
|
|
31731
|
+
scope = scope.slice(0, cut);
|
|
31732
|
+
}
|
|
31264
31733
|
}
|
|
31265
31734
|
function withoutNested(entry) {
|
|
31266
31735
|
const copy = { ...entry };
|
|
31267
31736
|
delete copy["dependencies"];
|
|
31268
31737
|
return copy;
|
|
31269
31738
|
}
|
|
31739
|
+
function npmRequiresProblem(path5, label, requires) {
|
|
31740
|
+
if (requires === void 0)
|
|
31741
|
+
return null;
|
|
31742
|
+
const unreadable = `\`${label}\` in ${path5} has a "requires" list we could not read, so the change is refused.`;
|
|
31743
|
+
if (!isObject(requires))
|
|
31744
|
+
return unreadable;
|
|
31745
|
+
for (const [dependency, range] of Object.entries(requires)) {
|
|
31746
|
+
if (typeof range !== "string")
|
|
31747
|
+
return unreadable;
|
|
31748
|
+
if (!registryRange(dependency, range, "npm"))
|
|
31749
|
+
return notPlainRange(label, path5, dependency);
|
|
31750
|
+
}
|
|
31751
|
+
return null;
|
|
31752
|
+
}
|
|
31270
31753
|
function npmLegacyProblem(path5, before, after) {
|
|
31271
31754
|
if (!isObject(after))
|
|
31272
31755
|
return `${path5} has a "dependencies" section that is not an object.`;
|
|
@@ -31287,7 +31770,7 @@ function npmLegacyProblem(path5, before, after) {
|
|
|
31287
31770
|
return `${path5} records "${name}" with no published version we can check.`;
|
|
31288
31771
|
}
|
|
31289
31772
|
const label = `${name}@${version}`;
|
|
31290
|
-
const problem = resolvedProblem(label, path5, name, version, entry["resolved"]) ?? integrityProblem(label, path5, entry["integrity"], false);
|
|
31773
|
+
const problem = resolvedProblem(label, path5, name, version, entry["resolved"]) ?? integrityProblem(label, path5, entry["integrity"], false) ?? npmRequiresProblem(path5, label, entry["requires"]);
|
|
31291
31774
|
if (problem !== null)
|
|
31292
31775
|
return problem;
|
|
31293
31776
|
}
|
|
@@ -31358,13 +31841,14 @@ function parseYarn(path5, text, when) {
|
|
|
31358
31841
|
let header = null;
|
|
31359
31842
|
let block = [];
|
|
31360
31843
|
let fields = /* @__PURE__ */ new Map();
|
|
31844
|
+
let dependencies = [];
|
|
31361
31845
|
let inMap = false;
|
|
31362
31846
|
const close = () => {
|
|
31363
31847
|
if (header === null)
|
|
31364
31848
|
return null;
|
|
31365
31849
|
if (entries.has(header))
|
|
31366
31850
|
return `${path5} ${when} lists the entry ${header} twice.`;
|
|
31367
|
-
entries.set(header, { raw: block.join("\n"), fields });
|
|
31851
|
+
entries.set(header, { raw: block.join("\n"), fields, dependencies });
|
|
31368
31852
|
header = null;
|
|
31369
31853
|
return null;
|
|
31370
31854
|
};
|
|
@@ -31388,6 +31872,7 @@ function parseYarn(path5, text, when) {
|
|
|
31388
31872
|
header = line.slice(0, -1);
|
|
31389
31873
|
block = [line];
|
|
31390
31874
|
fields = /* @__PURE__ */ new Map();
|
|
31875
|
+
dependencies = [];
|
|
31391
31876
|
inMap = false;
|
|
31392
31877
|
continue;
|
|
31393
31878
|
}
|
|
@@ -31395,8 +31880,10 @@ function parseYarn(path5, text, when) {
|
|
|
31395
31880
|
return { problem: unreadable };
|
|
31396
31881
|
block.push(line);
|
|
31397
31882
|
if (line.startsWith(" ")) {
|
|
31398
|
-
|
|
31883
|
+
const pair = inMap ? mapLine(line.slice(4)) : null;
|
|
31884
|
+
if (pair === null)
|
|
31399
31885
|
return { problem: unreadable };
|
|
31886
|
+
dependencies.push({ name: pair.key, range: pair.value });
|
|
31400
31887
|
continue;
|
|
31401
31888
|
}
|
|
31402
31889
|
const field = /^ {2}([A-Za-z]+)(?: (.+)|:)$/.exec(line);
|
|
@@ -31432,7 +31919,7 @@ function headerName(header) {
|
|
|
31432
31919
|
return null;
|
|
31433
31920
|
const candidate = spec.slice(0, at);
|
|
31434
31921
|
const range = spec.slice(at + 1);
|
|
31435
|
-
if (!
|
|
31922
|
+
if (!plainRange(candidate, range, "yarn"))
|
|
31436
31923
|
return null;
|
|
31437
31924
|
if (name !== null && name !== candidate)
|
|
31438
31925
|
return null;
|
|
@@ -31447,6 +31934,14 @@ function checkYarn({ path: path5, base, post }) {
|
|
|
31447
31934
|
const after = parseYarn(path5, post, "after the change");
|
|
31448
31935
|
if ("problem" in after)
|
|
31449
31936
|
return fail2(after.problem);
|
|
31937
|
+
const claims = /* @__PURE__ */ new Map();
|
|
31938
|
+
for (const header of after.entries.keys()) {
|
|
31939
|
+
for (const part of header.split(", ")) {
|
|
31940
|
+
const spec = unquote(part);
|
|
31941
|
+
if (spec !== null)
|
|
31942
|
+
claims.set(spec, (claims.get(spec) ?? 0) + 1);
|
|
31943
|
+
}
|
|
31944
|
+
}
|
|
31450
31945
|
let judged = 0;
|
|
31451
31946
|
for (const [header, entry] of after.entries) {
|
|
31452
31947
|
if (before.entries.get(header)?.raw === entry.raw)
|
|
@@ -31467,6 +31962,18 @@ function checkYarn({ path: path5, base, post }) {
|
|
|
31467
31962
|
const problem = resolvedProblem(label, path5, name, version, url) ?? integrityProblem(label, path5, entry.fields.get("integrity"), fragment !== null);
|
|
31468
31963
|
if (problem !== null)
|
|
31469
31964
|
return fail2(problem);
|
|
31965
|
+
for (const dependency of entry.dependencies) {
|
|
31966
|
+
if (!registryRange(dependency.name, dependency.range, "yarn")) {
|
|
31967
|
+
return fail2(notPlainRange(label, path5, dependency.name));
|
|
31968
|
+
}
|
|
31969
|
+
const spec = `${dependency.name}@${dependency.range}`;
|
|
31970
|
+
const count = claims.get(spec) ?? 0;
|
|
31971
|
+
if (count === 0)
|
|
31972
|
+
return fail2(noOwnEntry(label, path5, spec));
|
|
31973
|
+
if (count > 1) {
|
|
31974
|
+
return fail2(`\`${label}\` in ${path5} depends on ${spec}, which more than one entry in the lockfile claims, so which one installs could not be checked.`);
|
|
31975
|
+
}
|
|
31976
|
+
}
|
|
31470
31977
|
}
|
|
31471
31978
|
return { ok: true, judged };
|
|
31472
31979
|
}
|
|
@@ -36637,6 +37144,7 @@ var init_dist3 = __esm({
|
|
|
36637
37144
|
});
|
|
36638
37145
|
|
|
36639
37146
|
// ../../packages/envrun/dist/screenshots.js
|
|
37147
|
+
import { randomBytes as randomBytes12 } from "crypto";
|
|
36640
37148
|
import { copyFileSync as copyFileSync4, mkdirSync as mkdirSync6 } from "fs";
|
|
36641
37149
|
import { join as join27 } from "path";
|
|
36642
37150
|
function tooBigNote(items) {
|
|
@@ -36893,10 +37401,31 @@ async function captureSide(ctx, deps, side, tree, plan, image, code, routes) {
|
|
|
36893
37401
|
const appLabel = `${ctx.runId}-${side}`;
|
|
36894
37402
|
const findApp = () => lease.docker.sync(["ps", "-aq", "--filter", `label=terminalhire.capture-app=${appLabel}`], syncOpts(ctx)).stdout.trim().split("\n")[0] || null;
|
|
36895
37403
|
const labels = { ...ctx.labels ?? {}, "terminalhire.capture-app": appLabel };
|
|
36896
|
-
ctx.
|
|
36897
|
-
const
|
|
37404
|
+
const holder = `th-netns-${ctx.runId}-${side}-${randomBytes12(4).toString("hex")}`;
|
|
37405
|
+
const removeHolder = () => {
|
|
37406
|
+
try {
|
|
37407
|
+
lease.docker.sync(["rm", "--force", holder], { timeoutMs: HOLDER_REMOVE_TIMEOUT_MS });
|
|
37408
|
+
} catch {
|
|
37409
|
+
}
|
|
37410
|
+
};
|
|
37411
|
+
let serving = Promise.resolve();
|
|
36898
37412
|
let appId = null;
|
|
36899
37413
|
try {
|
|
37414
|
+
const held = lease.docker.sync(netnsHolderArgs({ name: holder, image, labels: ctx.labels }), syncOpts(ctx));
|
|
37415
|
+
if (held.status !== 0) {
|
|
37416
|
+
return skip(`could not start the screenshot network holder: ${(held.stderr || held.stdout).trim().slice(-300)}`);
|
|
37417
|
+
}
|
|
37418
|
+
ctx.progress?.("screenshots", `${side}: ${plan.serveCommand}`);
|
|
37419
|
+
serving = step(
|
|
37420
|
+
"serve",
|
|
37421
|
+
"offline",
|
|
37422
|
+
plan.serveCommand,
|
|
37423
|
+
captureTimeout(ctx, SCREENSHOT_LIMITS.waitForPortMs + SCREENSHOT_LIMITS.captureTimeoutMs),
|
|
37424
|
+
// runc, the one step that names a runtime (TERM-1336, design D2): the capture
|
|
37425
|
+
// shares this container's network namespace and probes `localhost`, which under
|
|
37426
|
+
// gVisor stays inside the app's own sandbox. `captureArgs` pins the capture.
|
|
37427
|
+
{ labels, runtime: "runc", networkOf: holder }
|
|
37428
|
+
).catch(() => null);
|
|
36900
37429
|
for (let i = 0; i < 60 && appId === null; i++) {
|
|
36901
37430
|
appId = findApp();
|
|
36902
37431
|
if (appId === null)
|
|
@@ -36910,7 +37439,7 @@ async function captureSide(ctx, deps, side, tree, plan, image, code, routes) {
|
|
|
36910
37439
|
image,
|
|
36911
37440
|
name: `th-capture-${ctx.runId}-${side}-srv`,
|
|
36912
37441
|
site: null,
|
|
36913
|
-
network: { container:
|
|
37442
|
+
network: { container: holder },
|
|
36914
37443
|
code,
|
|
36915
37444
|
out: await outTo(serverOut, `capout-${side}-srv`),
|
|
36916
37445
|
request: { mode: "server", routes, waitForPortMs: SCREENSHOT_LIMITS.waitForPortMs },
|
|
@@ -36922,10 +37451,14 @@ async function captureSide(ctx, deps, side, tree, plan, image, code, routes) {
|
|
|
36922
37451
|
return skip(served.reason ?? "the serve script produced no screenshot");
|
|
36923
37452
|
return done(side, served, serverOut);
|
|
36924
37453
|
} finally {
|
|
36925
|
-
|
|
36926
|
-
|
|
36927
|
-
|
|
36928
|
-
|
|
37454
|
+
try {
|
|
37455
|
+
const app = appId ?? findApp();
|
|
37456
|
+
if (app !== null)
|
|
37457
|
+
lease.docker.sync(["rm", "--force", app], syncOpts(ctx));
|
|
37458
|
+
} finally {
|
|
37459
|
+
removeHolder();
|
|
37460
|
+
await serving;
|
|
37461
|
+
}
|
|
36929
37462
|
}
|
|
36930
37463
|
}
|
|
36931
37464
|
function screenshotDeadline(lease, o, now = Date.now()) {
|
|
@@ -37054,7 +37587,7 @@ function syncOpts(ctx) {
|
|
|
37054
37587
|
function message(err) {
|
|
37055
37588
|
return String(err?.message ?? err).slice(0, 500);
|
|
37056
37589
|
}
|
|
37057
|
-
var SCREENSHOT_LIMITS, GUARDED_CAPTURE_CAP_MS, SCREENSHOT_UPLOAD_MAX_BYTES, UPLOAD_CAP_TEXT, NOTE_MAX, BUILD_MARKER, realDeps, HOSTED_BEFORE_REASON, SCREENSHOT_BUDGET_MS, HOSTED_SCREENSHOT_BUDGET_MS, DEFERRED_SCREENSHOT_BUDGET_MS, SCREENSHOT_DRAIN_MS, SCREENSHOTS_ABANDONED, RENDERED_EXTENSIONS, RENDERED_DIRS;
|
|
37590
|
+
var SCREENSHOT_LIMITS, GUARDED_CAPTURE_CAP_MS, SCREENSHOT_UPLOAD_MAX_BYTES, UPLOAD_CAP_TEXT, NOTE_MAX, BUILD_MARKER, realDeps, HOLDER_REMOVE_TIMEOUT_MS, HOSTED_BEFORE_REASON, SCREENSHOT_BUDGET_MS, HOSTED_SCREENSHOT_BUDGET_MS, DEFERRED_SCREENSHOT_BUDGET_MS, SCREENSHOT_DRAIN_MS, SCREENSHOTS_ABANDONED, RENDERED_EXTENSIONS, RENDERED_DIRS;
|
|
37058
37591
|
var init_screenshots = __esm({
|
|
37059
37592
|
"../../packages/envrun/dist/screenshots.js"() {
|
|
37060
37593
|
"use strict";
|
|
@@ -37089,6 +37622,7 @@ var init_screenshots = __esm({
|
|
|
37089
37622
|
mkdir: (dir) => mkdirSync6(dir, { recursive: true, mode: 511 }),
|
|
37090
37623
|
env: (tree) => scrubEnv(process.env, scrubEnvPathsFor("container", { jail: tree.jail, tmp: tree.tmp }))
|
|
37091
37624
|
};
|
|
37625
|
+
HOLDER_REMOVE_TIMEOUT_MS = 3e4;
|
|
37092
37626
|
HOSTED_BEFORE_REASON = "before side not captured on hosted runs yet";
|
|
37093
37627
|
SCREENSHOT_BUDGET_MS = 3e5;
|
|
37094
37628
|
HOSTED_SCREENSHOT_BUDGET_MS = 36e4;
|
|
@@ -37689,7 +38223,9 @@ function refusedRun(fields) {
|
|
|
37689
38223
|
// No test step ran, so there is no output to read a step from.
|
|
37690
38224
|
stoppedAt: null,
|
|
37691
38225
|
// Refused before any install step ran.
|
|
37692
|
-
installFrozen: null
|
|
38226
|
+
installFrozen: null,
|
|
38227
|
+
// No test step ran, so there is nothing to have timed (TERM-1351 PR2).
|
|
38228
|
+
scriptTimes: null
|
|
37693
38229
|
};
|
|
37694
38230
|
}
|
|
37695
38231
|
function refusalOriginOf(refusal2) {
|
|
@@ -38165,7 +38701,11 @@ async function runVerification(req, ctx) {
|
|
|
38165
38701
|
refusalOrigin: null,
|
|
38166
38702
|
// Read from the full test output, not `outputTail`, which is an excerpt.
|
|
38167
38703
|
stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
|
|
38168
|
-
installFrozen: installFrozenFor(spec, verdict.install)
|
|
38704
|
+
installFrozen: installFrozenFor(spec, verdict.install),
|
|
38705
|
+
// Copied through, not re-derived (TERM-1351 PR2): unlike `stoppedAt` above, which
|
|
38706
|
+
// reads the full output after the fact, this was computed DURING the run from the
|
|
38707
|
+
// fence's own `onStdout` relay, and `verdict` already carries the finished value.
|
|
38708
|
+
scriptTimes: verdict.scriptTimes
|
|
38169
38709
|
};
|
|
38170
38710
|
let later = null;
|
|
38171
38711
|
if (req.screenshots !== void 0) {
|
|
@@ -38626,7 +39166,7 @@ var init_dbplan = __esm({
|
|
|
38626
39166
|
|
|
38627
39167
|
// ../../packages/envrun/dist/dbstack.js
|
|
38628
39168
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
38629
|
-
import { randomBytes as
|
|
39169
|
+
import { randomBytes as randomBytes13 } from "crypto";
|
|
38630
39170
|
import { mkdirSync as mkdirSync9 } from "fs";
|
|
38631
39171
|
function installCommandFor(runner) {
|
|
38632
39172
|
switch (runner) {
|
|
@@ -38662,7 +39202,7 @@ function docker2(args, timeoutMs = DOCKER_TIMEOUT_MS2) {
|
|
|
38662
39202
|
function generateCredentials(host) {
|
|
38663
39203
|
return {
|
|
38664
39204
|
user: "thverify",
|
|
38665
|
-
password:
|
|
39205
|
+
password: randomBytes13(24).toString("base64url"),
|
|
38666
39206
|
database: "thverify",
|
|
38667
39207
|
host,
|
|
38668
39208
|
port: 5432
|
|
@@ -39266,10 +39806,12 @@ __export(dist_exports, {
|
|
|
39266
39806
|
GOOGLE_JWKS_URL: () => GOOGLE_JWKS_URL,
|
|
39267
39807
|
HostedVenueError: () => HostedVenueError,
|
|
39268
39808
|
JWKS_FETCH_TIMEOUT_MS: () => JWKS_FETCH_TIMEOUT_MS,
|
|
39809
|
+
LAYER_STORAGE_BAND_MIB: () => LAYER_STORAGE_BAND_MIB,
|
|
39269
39810
|
LEAK_STATES: () => LEAK_STATES,
|
|
39270
39811
|
LIMIT_OWNER: () => LIMIT_OWNER,
|
|
39271
39812
|
LOCAL_MEASUREMENT_PREFIX: () => LOCAL_MEASUREMENT_PREFIX,
|
|
39272
39813
|
LabelWatch: () => LabelWatch,
|
|
39814
|
+
MAX_NPM_SCRIPT_MARKERS: () => MAX_NPM_SCRIPT_MARKERS,
|
|
39273
39815
|
MIN_GIT_VERSION_FOR_END_OF_OPTIONS: () => MIN_GIT_VERSION_FOR_END_OF_OPTIONS,
|
|
39274
39816
|
OUTCOME_TO_BUDGET: () => OUTCOME_TO_BUDGET,
|
|
39275
39817
|
PARTIAL_COUNTS_NOTE: () => PARTIAL_COUNTS_NOTE,
|
|
@@ -39297,6 +39839,7 @@ __export(dist_exports, {
|
|
|
39297
39839
|
SSH_PROBE_INTERVAL_MS: () => SSH_PROBE_INTERVAL_MS,
|
|
39298
39840
|
SSH_READY_BUDGET_MS: () => SSH_READY_BUDGET_MS,
|
|
39299
39841
|
STOCK_POSTGRES_IMAGE: () => STOCK_POSTGRES_IMAGE,
|
|
39842
|
+
STORAGE_WATCH_MAX_GAP_MS: () => STORAGE_WATCH_MAX_GAP_MS,
|
|
39300
39843
|
SUPPORTED_RUNNERS: () => SUPPORTED_RUNNERS,
|
|
39301
39844
|
ThRunError: () => ThRunError,
|
|
39302
39845
|
UNPARSEABLE_TARGET: () => UNPARSEABLE_TARGET,
|
|
@@ -39329,6 +39872,7 @@ __export(dist_exports, {
|
|
|
39329
39872
|
collectWorkingDiff: () => collectWorkingDiff,
|
|
39330
39873
|
connectionUrl: () => connectionUrl,
|
|
39331
39874
|
containmentUnavailableRefusal: () => containmentUnavailableRefusal,
|
|
39875
|
+
createNpmScriptTimesCollector: () => createNpmScriptTimesCollector,
|
|
39332
39876
|
credentialInGitUrl: () => credentialInGitUrl,
|
|
39333
39877
|
defaultHostedVenueIo: () => defaultHostedVenueIo,
|
|
39334
39878
|
deleteFoundNothing: () => deleteFoundNothing,
|
|
@@ -39385,6 +39929,7 @@ __export(dist_exports, {
|
|
|
39385
39929
|
readCounts: () => readCounts,
|
|
39386
39930
|
readSchema: () => readSchema,
|
|
39387
39931
|
readStoppedAt: () => readStoppedAt,
|
|
39932
|
+
readStorageWatch: () => readStorageWatch,
|
|
39388
39933
|
recordedApplied: () => recordedApplied,
|
|
39389
39934
|
refuseSshTransport: () => refuseSshTransport,
|
|
39390
39935
|
refuseUnbuildableSpec: () => refuseUnbuildableSpec,
|
|
@@ -39404,11 +39949,13 @@ __export(dist_exports, {
|
|
|
39404
39949
|
startDatabase: () => startDatabase,
|
|
39405
39950
|
startLocalPreview: () => startLocalPreview,
|
|
39406
39951
|
startPreview: () => startPreview,
|
|
39952
|
+
stepNameOf: () => stepNameOf,
|
|
39407
39953
|
supportedLockfile: () => supportedLockfile,
|
|
39408
39954
|
targetCarriesCredential: () => targetCarriesCredential,
|
|
39409
39955
|
toAcceptancePredicate: () => toAcceptancePredicate,
|
|
39410
39956
|
toolingImageFor: () => toolingImageFor,
|
|
39411
39957
|
unquoteDiffPath: () => unquoteDiffPath,
|
|
39958
|
+
venueExhaustionFrom: () => venueExhaustionFrom,
|
|
39412
39959
|
venueGcloudEnv: () => venueGcloudEnv,
|
|
39413
39960
|
venueImageFromEnv: () => venueImageFromEnv,
|
|
39414
39961
|
venueStagePaths: () => venueStagePaths,
|
|
@@ -39418,6 +39965,8 @@ var init_dist4 = __esm({
|
|
|
39418
39965
|
"../../packages/envrun/dist/index.js"() {
|
|
39419
39966
|
"use strict";
|
|
39420
39967
|
init_classify2();
|
|
39968
|
+
init_venueStorageWatch();
|
|
39969
|
+
init_npmScriptTimes();
|
|
39421
39970
|
init_execute();
|
|
39422
39971
|
init_labels();
|
|
39423
39972
|
init_result();
|
|
@@ -40582,6 +41131,19 @@ var BASELINE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
40582
41131
|
]);
|
|
40583
41132
|
var MIN_TEST_BUDGET_MINUTES = 5;
|
|
40584
41133
|
var MAX_TEST_BUDGET_MINUTES = 120;
|
|
41134
|
+
var FIRST_TEST_RUN_STATES = /* @__PURE__ */ new Set(["running", "passed", "failed", "could-not-run", "none"]);
|
|
41135
|
+
function wellFormedFirstTestRun(v) {
|
|
41136
|
+
if (typeof v !== "object" || v === null) return null;
|
|
41137
|
+
if (typeof v.state !== "string" || !FIRST_TEST_RUN_STATES.has(v.state)) return null;
|
|
41138
|
+
if (v.counts === void 0) return { state: v.state };
|
|
41139
|
+
if (v.counts === null) return { state: v.state, counts: null };
|
|
41140
|
+
const c = v.counts;
|
|
41141
|
+
if (typeof c !== "object" || c === null) return null;
|
|
41142
|
+
if (!Number.isInteger(c.passed) || c.passed < 0) return null;
|
|
41143
|
+
if (!Number.isInteger(c.failed) || c.failed < 0) return null;
|
|
41144
|
+
if (c.runner !== null && typeof c.runner !== "string") return null;
|
|
41145
|
+
return { state: v.state, counts: { passed: c.passed, failed: c.failed, runner: c.runner } };
|
|
41146
|
+
}
|
|
40585
41147
|
function baselineFields(b) {
|
|
40586
41148
|
const out = {};
|
|
40587
41149
|
const m = b?.testBudgetMinutes;
|
|
@@ -40594,8 +41156,27 @@ function baselineFields(b) {
|
|
|
40594
41156
|
if (typeof b?.baselineSha === "string" && /^[0-9a-f]{7,40}$/.test(b.baselineSha)) {
|
|
40595
41157
|
out.baselineSha = b.baselineSha;
|
|
40596
41158
|
}
|
|
41159
|
+
const ftr = wellFormedFirstTestRun(b?.firstTestRun);
|
|
41160
|
+
if (ftr !== null) out.firstTestRun = ftr;
|
|
40597
41161
|
return out;
|
|
40598
41162
|
}
|
|
41163
|
+
function firstTestRunLabel(info) {
|
|
41164
|
+
const counts = info?.counts ?? null;
|
|
41165
|
+
switch (info?.state) {
|
|
41166
|
+
case "none":
|
|
41167
|
+
return null;
|
|
41168
|
+
case "running":
|
|
41169
|
+
return "First test run on the posted commit: running.";
|
|
41170
|
+
case "passed":
|
|
41171
|
+
return counts === null ? "First test run on the posted commit: passed." : `${counts.passed} of ${counts.passed + counts.failed} tests passed on the posted commit.`;
|
|
41172
|
+
case "failed":
|
|
41173
|
+
return counts === null ? "Some tests failed on the posted commit." : `${counts.failed} of ${counts.passed + counts.failed} tests failed on the posted commit.`;
|
|
41174
|
+
case "could-not-run":
|
|
41175
|
+
return "First test run on the posted commit: could not run.";
|
|
41176
|
+
default:
|
|
41177
|
+
return null;
|
|
41178
|
+
}
|
|
41179
|
+
}
|
|
40599
41180
|
function baselineLine({ baselineStatus, baselineSha, testBudgetMinutes } = {}) {
|
|
40600
41181
|
const at = typeof baselineSha === "string" ? ` at ${baselineSha.slice(0, 12)}` : "";
|
|
40601
41182
|
const limit2 = typeof testBudgetMinutes === "number" ? ` Time limit ${String(testBudgetMinutes)} min.` : "";
|
|
@@ -41298,7 +41879,7 @@ async function resolveBounty(arg) {
|
|
|
41298
41879
|
if (freshPool) job = findByShortRefInPool(freshPool, arg);
|
|
41299
41880
|
}
|
|
41300
41881
|
if (job) {
|
|
41301
|
-
let testBudgetMinutes, baselineStatus, baselineSha, reviewMode;
|
|
41882
|
+
let testBudgetMinutes, baselineStatus, baselineSha, reviewMode, firstTestRun;
|
|
41302
41883
|
({
|
|
41303
41884
|
bountyId,
|
|
41304
41885
|
title,
|
|
@@ -41313,14 +41894,17 @@ async function resolveBounty(arg) {
|
|
|
41313
41894
|
testBudgetMinutes,
|
|
41314
41895
|
baselineStatus,
|
|
41315
41896
|
baselineSha,
|
|
41316
|
-
reviewMode
|
|
41897
|
+
reviewMode,
|
|
41898
|
+
firstTestRun
|
|
41317
41899
|
} = extractClaimableFields(job));
|
|
41318
41900
|
baseline = {
|
|
41319
41901
|
...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
|
|
41320
41902
|
...baselineStatus === void 0 ? {} : { baselineStatus },
|
|
41321
41903
|
...baselineSha === void 0 ? {} : { baselineSha },
|
|
41322
41904
|
// TERM-1344. Rides with the baseline fields: index-only, and no key when absent.
|
|
41323
|
-
...reviewMode === void 0 ? {} : { reviewMode }
|
|
41905
|
+
...reviewMode === void 0 ? {} : { reviewMode },
|
|
41906
|
+
// TERM-1351 PR2. Same convention: index-only, no key when absent or malformed.
|
|
41907
|
+
...firstTestRun === void 0 ? {} : { firstTestRun }
|
|
41324
41908
|
};
|
|
41325
41909
|
indexNativeId = bountyId;
|
|
41326
41910
|
} else {
|
|
@@ -44256,7 +44840,9 @@ function sectionKey(pkg, section) {
|
|
|
44256
44840
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
44257
44841
|
return JSON.stringify(value ?? null);
|
|
44258
44842
|
}
|
|
44259
|
-
return JSON.stringify(
|
|
44843
|
+
return JSON.stringify(
|
|
44844
|
+
Object.keys(value).sort().map((k) => [k, value[k]])
|
|
44845
|
+
);
|
|
44260
44846
|
}
|
|
44261
44847
|
async function patchChangesDependencies(wt, base, touched) {
|
|
44262
44848
|
for (const path5 of touched) {
|
|
@@ -44264,7 +44850,8 @@ async function patchChangesDependencies(wt, base, touched) {
|
|
|
44264
44850
|
const before = await packageJsonAt(wt, base, path5);
|
|
44265
44851
|
const after = await packageJsonAt(wt, "HEAD", path5);
|
|
44266
44852
|
if (before === null || after === null) continue;
|
|
44267
|
-
if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s)))
|
|
44853
|
+
if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s)))
|
|
44854
|
+
return true;
|
|
44268
44855
|
}
|
|
44269
44856
|
return false;
|
|
44270
44857
|
}
|
|
@@ -45926,6 +46513,7 @@ export {
|
|
|
45926
46513
|
fetchFounderApprovals,
|
|
45927
46514
|
findClaimableByShortRef,
|
|
45928
46515
|
findClaimableInCache,
|
|
46516
|
+
firstTestRunLabel,
|
|
45929
46517
|
fmtAge,
|
|
45930
46518
|
fmtContestedWarning,
|
|
45931
46519
|
founderClaimStanding,
|