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.
@@ -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 randomBytes13 = eddsaOpts.randomBytes || randomBytes;
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 = randomBytes13(lengths.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 (/no space left on device|ENOSPC/i.test(facts.stderr))
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
- var VERIFICATION_OUTCOMES, 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;
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
- throw new FenceError(`could not start the proxy sidecar: ${run2.stderr.trim()}`);
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: proxyName, proxyPort: SIDECAR_PROXY_PORT },
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) => stdout += d2.toString());
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 (!CONTAINER_NAME.test(o.network.container)) {
28195
- throw new FenceError(`refusing to join container ${JSON.stringify(o.network.container)}: not a container name`);
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/attest/dist/types.js
28607
- var IN_TOTO_STATEMENT_TYPE, ACCEPTANCE_RUN_PREDICATE_TYPE, IN_TOTO_PAYLOAD_TYPE, TEST_COMMAND_SOURCES;
28608
- var init_types4 = __esm({
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
- var init_pae = __esm({
28629
- "../../packages/attest/dist/pae.js"() {
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
- var init_keys = __esm({
28640
- "../../packages/attest/dist/keys.js"() {
28641
- "use strict";
28642
- init_pae();
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
- // ../../packages/attest/dist/dsse.js
28647
- function createAcceptanceStatement(predicate) {
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
- _type: IN_TOTO_STATEMENT_TYPE,
28650
- subject: [{ name: predicate.claim_id, digest: { sha256: predicate.tree_digest } }],
28651
- predicateType: ACCEPTANCE_RUN_PREDICATE_TYPE,
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 signStatementWith(statement, sign, keyid) {
28656
- const body = Buffer.from(JSON.stringify(statement), "utf8");
28657
- const sig = Buffer.from(sign(pae(IN_TOTO_PAYLOAD_TYPE, body)));
28658
- return {
28659
- payload: body.toString("base64"),
28660
- payloadType: IN_TOTO_PAYLOAD_TYPE,
28661
- signatures: [{ ...keyid === void 0 ? {} : { keyid }, sig: sig.toString("base64") }]
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 signStatement(statement, privateKey, keyid) {
28665
- return signStatementWith(statement, (data) => signBytes(privateKey, data), keyid);
28743
+ function localCensus(label) {
28744
+ return census(localDockerClient(), label);
28666
28745
  }
28667
- var init_dsse = __esm({
28668
- "../../packages/attest/dist/dsse.js"() {
28669
- "use strict";
28670
- init_pae();
28671
- init_keys();
28672
- init_types4();
28673
- }
28674
- });
28675
-
28676
- // ../../packages/attest/dist/dispatchedRun.js
28677
- import { createHash as createHash7, createPublicKey as createPublicKey2 } from "crypto";
28678
- var init_dispatchedRun = __esm({
28679
- "../../packages/attest/dist/dispatchedRun.js"() {
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
- // ../../packages/attest/dist/manifest.js
28707
- var init_manifest = __esm({
28708
- "../../packages/attest/dist/manifest.js"() {
28709
- "use strict";
28710
- init_pae();
28711
- init_keys();
28712
- init_types4();
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
- // ../../packages/attest/dist/attestation.js
28717
- var init_attestation = __esm({
28718
- "../../packages/attest/dist/attestation.js"() {
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
- init_manifest();
28721
- init_types4();
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/attest/dist/nonce.js
28726
- var init_nonce = __esm({
28727
- "../../packages/attest/dist/nonce.js"() {
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/attest/dist/gceIdentity.js
28733
- import { createHash as createHash8, createPublicKey as createPublicKey3, verify as cryptoVerify2 } from "crypto";
28734
- function expectedAudience(dispatchId) {
28735
- if (dispatchId === "")
28736
- throw new TypeError("expectedAudience: dispatchId is empty");
28737
- return `${VERIFICATION_RUN_AUDIENCE_BASE}#${dispatchId}`;
28738
- }
28739
- function venueIdentityEmail(projectId) {
28740
- if (projectId === "")
28741
- throw new TypeError("venueIdentityEmail: projectId is empty");
28742
- return `venue-identity@${projectId}.iam.gserviceaccount.com`;
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 refuseIdentity(reason, detail) {
28745
- return { ok: false, reason, detail };
28900
+ function installReaper() {
28901
+ if (reaperInstalled)
28902
+ return;
28903
+ reaperInstalled = true;
28904
+ process.on("exit", () => {
28905
+ livePreviews.reapAll();
28906
+ });
28746
28907
  }
28747
- function strictBase64UrlDecode(s) {
28748
- if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1)
28749
- return null;
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
- return decoded;
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 decodeJsonSegment(segment) {
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
- parsed = JSON.parse(bytes.toString("utf8"));
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 assertExpectationsComplete(expectations) {
28770
- const required = [
28771
- ["audience", expectations.audience],
28772
- ["serviceAccountEmail", expectations.serviceAccountEmail],
28773
- ["projectId", expectations.projectId],
28774
- ["instanceName", expectations.instanceName]
28775
- ];
28776
- for (const [name, value] of required) {
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
- if (expectations.instanceId === "") {
28781
- throw new TypeError("verifyGceIdentityToken: expectations.instanceId is empty");
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
- async function verifyGceIdentityToken(token, expectations, deps) {
28785
- assertExpectationsComplete(expectations);
28786
- if (!Number.isFinite(deps.now)) {
28787
- throw new TypeError("verifyGceIdentityToken: deps.now is not a finite number");
28788
- }
28789
- if (token.length > MAX_TOKEN_LENGTH) {
28790
- return refuseIdentity("token-malformed", "longer than any GCE identity token");
28791
- }
28792
- const parts = token.split(".");
28793
- if (parts.length !== 3) {
28794
- return refuseIdentity("token-malformed", "not a three-segment compact JWT");
28795
- }
28796
- const [headerB64, payloadB64, signatureB64] = parts;
28797
- const header = decodeJsonSegment(headerB64);
28798
- if (header === null) {
28799
- return refuseIdentity("token-malformed", "header segment is not base64url-encoded JSON");
28800
- }
28801
- if (header.alg !== "RS256") {
28802
- return refuseIdentity("algorithm-not-rs256", "token names an unsupported algorithm");
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
- if (typeof header.kid !== "string" || header.kid === "") {
28805
- return refuseIdentity("unknown-key", "token header names no key id");
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
- const jwks = await deps.fetchJwks();
28808
- const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
28809
- if (jwk === void 0) {
28810
- return refuseIdentity("unknown-key", "token key id is not in the published key set");
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
- const signature = strictBase64UrlDecode(signatureB64);
28813
- if (signature === null) {
28814
- return refuseIdentity("token-malformed", "signature segment is not base64url");
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
- let signatureValid = false;
29220
+ }
29221
+ function isErrorValue(thrown) {
28817
29222
  try {
28818
- const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
28819
- signatureValid = cryptoVerify2("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
29223
+ return thrown instanceof Error;
28820
29224
  } catch {
28821
- signatureValid = false;
29225
+ return false;
28822
29226
  }
28823
- if (!signatureValid) {
28824
- return refuseIdentity("signature-invalid", "signature does not verify under the named key");
29227
+ }
29228
+ function readErrorField(thrown, key, fallback) {
29229
+ let raw;
29230
+ try {
29231
+ raw = thrown[key];
29232
+ } catch {
29233
+ return fallback;
28825
29234
  }
28826
- const payload = decodeJsonSegment(payloadB64);
28827
- if (payload === null) {
28828
- return refuseIdentity("token-malformed", "payload segment is not base64url-encoded JSON");
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
- if (payload.iss !== "https://accounts.google.com") {
28831
- return refuseIdentity("issuer-mismatch", "issuer is not https://accounts.google.com");
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
- if (payload.aud !== expectations.audience) {
28834
- return refuseIdentity("audience-mismatch", "audience is not the one this run expects");
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 nowSeconds = deps.now / 1e3;
28837
- const exp = payload.exp;
28838
- if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
28839
- return refuseIdentity("token-expired", "exp is missing, malformed, or in the past");
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
- const iat = payload.iat;
28842
- if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
28843
- return refuseIdentity("issued-in-future", "iat is missing, malformed, or too far in the future");
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
- if (payload.email !== expectations.serviceAccountEmail || payload.email_verified !== true) {
28846
- return refuseIdentity("wrong-account", "token is not from the venue identity account");
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
- const google = payload.google;
28849
- const computeEngine = typeof google === "object" && google !== null && !Array.isArray(google) ? google.compute_engine : void 0;
28850
- if (typeof computeEngine !== "object" || computeEngine === null || Array.isArray(computeEngine)) {
28851
- return refuseIdentity("compute-engine-block-missing", "no google.compute_engine block \u2014 was the token fetched with format=full?");
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
- const ce = computeEngine;
28854
- if (ce.project_id !== expectations.projectId) {
28855
- return refuseIdentity("project-mismatch", "token names an instance in a different project");
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
- if (ce.instance_name !== expectations.instanceName) {
28858
- return refuseIdentity("instance-name-mismatch", "token names a different instance than this run booted");
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
- if (typeof ce.instance_id !== "string" || ce.instance_id === "") {
28861
- return refuseIdentity("instance-id-missing", "instance id is absent or not a string");
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
- if (expectations.instanceId !== void 0 && ce.instance_id !== expectations.instanceId) {
28864
- return refuseIdentity("instance-id-mismatch", "token names a different instance than the boot record");
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
- if (typeof ce.zone !== "string" || !expectations.allowedZones.includes(ce.zone)) {
28867
- return refuseIdentity("zone-not-allowed", "zone is not one the pool is configured to use");
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
- ok: true,
28871
- value: {
28872
- instanceId: ce.instance_id,
28873
- instanceName: expectations.instanceName,
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
- var VERIFICATION_RUN_AUDIENCE_BASE, IAT_FUTURE_SKEW_MS, MAX_TOKEN_LENGTH;
28884
- var init_gceIdentity = __esm({
28885
- "../../packages/attest/dist/gceIdentity.js"() {
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
- VERIFICATION_RUN_AUDIENCE_BASE = "https://terminalhire.com/api/verification-run";
28888
- IAT_FUTURE_SKEW_MS = 5 * 60 * 1e3;
28889
- MAX_TOKEN_LENGTH = 8192;
29804
+ init_pae();
29805
+ init_keys();
29806
+ init_types4();
28890
29807
  }
28891
29808
  });
28892
29809
 
28893
- // ../../packages/attest/dist/confidentialSpace.js
28894
- import { createHash as createHash9, createPublicKey as createPublicKey4, verify as cryptoVerify3 } from "crypto";
28895
- function refuse2(reason, detail) {
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 isPlainObject2(v) {
28899
- return typeof v === "object" && v !== null && !Array.isArray(v);
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 assertExpectationsComplete2(expectations) {
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
- ["nonce", expectations.nonce],
28905
- ["instanceName", expectations.instanceName],
29906
+ ["serviceAccountEmail", expectations.serviceAccountEmail],
28906
29907
  ["projectId", expectations.projectId],
28907
- ...(expectations.additionalNonces ?? []).map((n) => [
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(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
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 nonceList(v) {
28923
- if (typeof v === "string")
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("verifyConfidentialSpaceToken: deps.now is not a finite number");
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 refuse2("token-malformed", "longer than any attestation token");
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 refuse2("token-malformed", "not a three-segment compact JWT");
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 refuse2("token-malformed", "header segment is not base64url-encoded JSON");
29933
+ return refuseIdentity("token-malformed", "header segment is not base64url-encoded JSON");
28950
29934
  }
28951
29935
  if (header.alg !== "RS256") {
28952
- return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
29936
+ return refuseIdentity("algorithm-not-rs256", "token names an unsupported algorithm");
28953
29937
  }
28954
29938
  if (typeof header.kid !== "string" || header.kid === "") {
28955
- return refuse2("unknown-key", "token header names no key id");
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 refuse2("unknown-key", "token key id is not in the published key set");
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 refuse2("token-malformed", "signature segment is not base64url");
29948
+ return refuseIdentity("token-malformed", "signature segment is not base64url");
28965
29949
  }
28966
29950
  let signatureValid = false;
28967
29951
  try {
28968
- const publicKey = createPublicKey4({ key: jwk, format: "jwk" });
28969
- signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
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 refuse2("signature-invalid", "signature does not verify under the named key");
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 refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
29962
+ return refuseIdentity("token-malformed", "payload segment is not base64url-encoded JSON");
28979
29963
  }
28980
- if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
28981
- return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
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 refuse2("wrong-audience", "audience is not the one this run expects");
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 refuse2("expired", "exp is missing, malformed, or in the past");
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 refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
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 (gce.instance_name !== expectations.instanceName) {
29029
- return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
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
- if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
29032
- return refuse2("instance-id-missing", "instance id is absent or not a string");
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 instanceId = gce.instance_id;
29035
- if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
29036
- return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
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
- const zone = gce.zone;
29039
- const container = isPlainObject2(submods) ? submods.container : void 0;
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 container.image_reference !== "string" || container.image_reference === "") {
29044
- return refuse2("image-reference-missing", "the workload image reference is absent");
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
- const imageReference = container.image_reference;
29047
- if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
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
- const env = container.env;
29051
- for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
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
- imageDigest: container.image_digest,
29064
- imageReference,
29065
- hwmodel,
30010
+ email: expectations.serviceAccountEmail,
29066
30011
  iat,
29067
30012
  exp,
29068
- tokenSha256: createHash9("sha256").update(token, "utf8").digest("hex")
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
- function toAcceptancePredicate(pair, opts = {}) {
29182
- const { baseline, patched } = pair;
29183
- if (baseline.runId === patched.runId) {
29184
- 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.`);
29185
- }
29186
- if (patched.status === "refused") {
29187
- 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`);
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
- if (baseline.outcome === null) {
29196
- return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
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
- if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
29199
- 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.`);
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
- const budget = OUTCOME_TO_BUDGET[patched.outcome];
29202
- if (budget === null) {
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 (patched.testCommand === null) {
29206
- return refuse3("no-test-command", "no test command was recorded, so none can be attested");
30073
+ if (token.length > MAX_TOKEN_LENGTH) {
30074
+ return refuse2("token-malformed", "longer than any attestation token");
29207
30075
  }
29208
- if (baseline.testCommand !== patched.testCommand) {
29209
- 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.`);
30076
+ const parts = token.split(".");
30077
+ if (parts.length !== 3) {
30078
+ return refuse2("token-malformed", "not a three-segment compact JWT");
29210
30079
  }
29211
- if (baseline.targetSha !== patched.targetSha) {
29212
- return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
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 (patched.treeDigest === null || patched.treeDigest === "") {
29215
- return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
30085
+ if (header.alg !== "RS256") {
30086
+ return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
29216
30087
  }
29217
- if (patched.patchSha256 === null || patched.patchSha256 === "") {
29218
- return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
30088
+ if (typeof header.kid !== "string" || header.kid === "") {
30089
+ return refuse2("unknown-key", "token header names no key id");
29219
30090
  }
29220
- if (baseline.treeDigest === null || baseline.treeDigest === "") {
29221
- return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
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
- if (baseline.treeDigest === patched.treeDigest) {
29224
- 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`);
30096
+ const signature = strictBase64UrlDecode(signatureB64);
30097
+ if (signature === null) {
30098
+ return refuse2("token-malformed", "signature segment is not base64url");
29225
30099
  }
29226
- if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
29227
- 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.`);
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
- for (const [side, outcome, counts, exitCode] of [
29230
- ["patched", patched.outcome, patched.counts, patched.exitCode],
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
- for (const [side, source] of [
29239
- ["patched", patched.testCommandSource],
29240
- ["baseline", baseline.testCommandSource]
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 (patched.testCommandSource === "developer-declared" || patched.testCommandSource === "operator-declared") {
29247
- 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)`);
30114
+ if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
30115
+ return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
29248
30116
  }
29249
- if (baseline.testCommandSource !== patched.testCommandSource) {
29250
- 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.`);
30117
+ if (payload.aud !== expectations.audience) {
30118
+ return refuse2("wrong-audience", "audience is not the one this run expects");
29251
30119
  }
29252
- for (const [side, source, image] of [
29253
- ["patched", patched.imageSource, patched.containerImage],
29254
- ["baseline", baseline.imageSource, baseline.containerImage]
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
- if (baseline.imageSource !== patched.imageSource) {
29261
- 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.`);
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
- if (baseline.containerImage !== patched.containerImage) {
29264
- 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.`);
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 (patched.containerImage === null || patched.containerImage === "") {
29267
- 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");
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
- if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
29270
- 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.");
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 (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
29273
- 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.`);
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 (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
29276
- 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.`);
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 (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
29279
- 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.");
30147
+ if (payload.secboot !== true) {
30148
+ return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
29280
30149
  }
29281
- if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
29282
- 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.`);
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
- if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
29285
- 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.`);
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 (baseline.containerImageDigest !== patched.containerImageDigest) {
29288
- 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.`);
30159
+ if (gce.project_id !== expectations.projectId) {
30160
+ return refuse2("project-mismatch", "token names an instance in a different project");
29289
30161
  }
29290
- const claimedDigest = imageDigestOf(patched.containerImage);
29291
- if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
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
- return {
29295
- ok: true,
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
- // ../../packages/envrun/dist/labels.js
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 ids2 = res.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
29477
- return { ids: ids2, failure: null };
29478
- }
29479
- function ids(docker3, args) {
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
- const reaped = censusTotal(after) === 0;
29524
- let note;
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
- return {
29540
- labelObserved,
29541
- reaped,
29542
- observed: true,
29543
- clean: labelObserved && reaped,
29544
- state,
29545
- peak,
29546
- after,
29547
- note
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
- register(client, container) {
29615
- const names = live.get(client) ?? /* @__PURE__ */ new Set();
29616
- names.add(container);
29617
- live.set(client, names);
29618
- },
29619
- deregister(client, container) {
29620
- const names = live.get(client);
29621
- if (names === void 0)
29622
- return;
29623
- names.delete(container);
29624
- if (names.size === 0)
29625
- live.delete(client);
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 init_previewRegistry = __esm({
29646
- "../../packages/envrun/dist/previewRegistry.js"() {
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/envrun/dist/preview.js
29652
- import { randomBytes as randomBytes10 } from "crypto";
29653
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync12 } from "fs";
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
- async function startPreview(req) {
29704
- const label = labelArgs(req.labels);
29705
- const container = `${req.idBase}-preview`;
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
- function startLocalPreview(req) {
29810
- return startPreview({ ...req, docker: localDockerClient() });
29811
- }
29812
- var PreviewError, GUEST_PORT, GUEST_DOC, LOOPBACK_BINDS, WILDCARD_BINDS, SERVER_SOURCE, livePreviews, reaperInstalled, sleep4;
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
- init_dist();
29817
- init_previewRegistry();
29818
- PreviewError = class extends Error {
29819
- };
29820
- GUEST_PORT = 8080;
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
- // Read ONCE, at startup, and refuse to run without it. An unset token used to
29830
- // skip the check entirely, which meant the one configuration nobody sets on
29831
- // purpose was also the one that served a developer's in-progress work to
29832
- // anything that could reach the port. Absent must never mean permitted \u2014 the
29833
- // polarity requireSecret() holds in apps/web/lib/secrets.ts.
29834
- const AUTH_TOKEN = process.env.PREVIEW_AUTH_TOKEN || '';
29835
- if (AUTH_TOKEN === '') {
29836
- process.stderr.write(
29837
- 'preview: refusing to start \u2014 PREVIEW_AUTH_TOKEN is unset, and this server will not ' +
29838
- 'serve a run document unauthenticated\\n',
29839
- );
29840
- process.exit(1);
29841
- }
29842
- const EXPECTED = Buffer.from(AUTH_TOKEN, 'utf8');
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
- /** The token the client presented, or null. */
29845
- function presented(req) {
29846
- const header = req.headers['authorization'];
29847
- const bearer = typeof header === 'string' ? /^Bearer\\s+(.+)$/i.exec(header) : null;
29848
- if (bearer !== null) return bearer[1];
29849
- // A non-Bearer Authorization header FALLS THROUGH to the query parameter. The
29850
- // earlier ternary branched on the header merely EXISTING, so a client sending
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
- function authorized(req) {
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
- // Per-INSTANCE, minted at boot. Not passed in, not derived from anything the
29871
- // host controls \u2014 that is what makes "same token \u21D2 same instance" hold.
29872
- const INSTANCE_TOKEN = randomUUID();
29873
- const DOC = JSON.parse(readFileSync(${JSON.stringify(GUEST_DOC)}, 'utf8'));
29874
- let served = 0;
29875
-
29876
- http
29877
- .createServer((req, res) => {
29878
- if (!authorized(req)) {
29879
- res.writeHead(401, { 'content-type': 'application/json; charset=utf-8' });
29880
- return res.end(JSON.stringify({ error: 'unauthorized' }));
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
- // ../../packages/envrun/dist/venue.js
29911
- import { randomBytes as randomBytes11 } from "crypto";
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 localVenue() {
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
- kind: "local",
29922
- acquire: (runId) => acquireTransactionally((allocated) => {
29923
- void allocated;
29924
- return Promise.resolve(acquireLocalLease(runId));
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
- async function acquireTransactionally(body) {
29929
- const undos = [];
29930
- try {
29931
- return await body({
29932
- onRollback: (undo) => {
29933
- undos.push(undo);
29934
- }
29935
- });
29936
- } catch (err) {
29937
- const failures = [];
29938
- for (const undo of undos.reverse()) {
29939
- let failed = null;
29940
- try {
29941
- await undo();
29942
- } catch (rollbackErr) {
29943
- failed = { thrown: rollbackErr };
29944
- }
29945
- if (failed === null)
29946
- continue;
29947
- let entry;
29948
- try {
29949
- entry = describeThrown(failed.thrown, { includeName: true });
29950
- } catch {
29951
- entry = UNDESCRIBABLE_THROWN;
29952
- }
29953
- failures.push(entry);
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
- function rollbackMessage(cause, rollbackFailures) {
29961
- const headline = describeThrown(cause, { includeName: false });
29962
- let tail2;
29963
- try {
29964
- tail2 = `[venue acquisition rolled back with ${String(rollbackFailures.length)} failure(s): ${rollbackFailures.join("; ")} \u2014 one or more allocated resources may still exist]`;
29965
- } catch {
29966
- tail2 = UNLISTABLE_ROLLBACK_FAILURES;
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
- return `${headline} ${tail2}`;
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
- function isErrorValue(thrown) {
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
- function readErrorField(thrown, key, fallback) {
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
- return typeof raw === "string" ? raw : coerceToString(raw);
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
- function localTreeOwner(ids2 = hostIds()) {
30008
- if (ids2?.uid !== 0)
30009
- return void 0;
30010
- return localGuestUser(ids2) ?? void 0;
30011
- }
30012
- function handLocalTreeToGuest(local, owner, chown = chownTree) {
30013
- if (owner === void 0)
30014
- return;
30015
- chown(local.cloneDir, owner);
30016
- chown(local.scratchRoot, owner);
30017
- }
30018
- function localCloneVolumeName(runId, suffix = randomBytes11(4).toString("hex")) {
30019
- const tail2 = `-${suffix}`;
30020
- const safe = runId.replace(/[^a-zA-Z0-9_.-]/g, "-");
30021
- const head = `th-clone-${safe}`.slice(0, VOLUME_NAME_MAX2 - tail2.length);
30022
- return validateVolumeName(`${head}${tail2}`, "the local clone volume");
30023
- }
30024
- function localCloneFillArgv(from, volume, owner) {
30025
- const chown = owner === null ? "" : ` && chown -R ${String(owner.uid)}:${String(owner.gid)} /dst`;
30026
- return [
30027
- "run",
30028
- "--rm",
30029
- "--network=none",
30030
- `--volume=${from}:/src:ro`,
30031
- `--volume=${validateVolumeName(volume, "the local clone volume")}:/dst:rw`,
30032
- "--",
30033
- STAGE_HELPER_IMAGE,
30034
- "sh",
30035
- "-c",
30036
- `cp -a /src/. /dst/${chown}`
30037
- ];
30038
- }
30039
- function acquireLocalLease(runId) {
30040
- const docker3 = localDockerClient();
30041
- const containment = selectContainment([containerContainmentOn(docker3)]);
30042
- let released = false;
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
- 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;
30198
- var init_venue = __esm({
30199
- "../../packages/envrun/dist/venue.js"() {
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
- init_dist();
30202
- init_labels();
30203
- init_preview();
30204
- VenueRollbackError = class extends Error {
30205
- /** Every undo that threw, in the order they ran (reverse allocation order). */
30206
- rollbackFailures;
30207
- constructor(cause, rollbackFailures) {
30208
- super(rollbackMessage(cause, rollbackFailures), { cause });
30209
- this.name = "VenueRollbackError";
30210
- this.rollbackFailures = rollbackFailures;
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
- UNREADABLE_MESSAGE = "<an error whose message could not be read>";
30214
- UNREADABLE_NAME = "<an error whose name could not be read>";
30215
- UNCOERCIBLE_THROWN = "<a thrown value that cannot be converted to a string>";
30216
- UNDESCRIBABLE_THROWN = "<a thrown value that could not be described>";
30217
- UNLISTABLE_ROLLBACK_FAILURES = "[venue acquisition rolled back, and the failures could not be listed \u2014 one or more allocated resources may still exist]";
30218
- RELEASED_LEASE_CENSUS_REASON = "the lease was already released, so this venue can no longer be interrogated";
30219
- LeaseReleasedError = class extends Error {
30220
- name = "LeaseReleasedError";
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
- STAGE_HELPER_IMAGE = "busybox:1.37.0";
30223
- STAGE_VOLUME_LABEL_KEY = "terminalhire.stage";
30224
- VOLUME_NAME_MAX2 = 128;
30225
- LOCAL_VOLUME_CREATE_TIMEOUT_MS = 3e4;
30226
- LOCAL_CLONE_FILL_TIMEOUT_MS = 6e5;
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
- test = await runStep(containment, {
30520
- step: "test",
30521
- profile: "offline",
30522
- command: req.spec.testCommand,
30523
- repoDir: req.repoDir,
30524
- jail,
30525
- tmp,
30526
- pathDomain: req.lease.pathDomain,
30527
- // TERM-729: travels WITH pathDomain, because it answers the same
30528
- // question about the same machine. Undefined on a local lease.
30529
- guestUser: req.lease.guestUser,
30530
- // TERM-913: the third answer about that machine — which volumes the
30531
- // fence mounts in place of the noexec stage. Undefined on a local lease.
30532
- stageVolumes: req.lease.stageVolumes,
30533
- // TERM-1106: the local venue's copy of the clone. Undefined on a hosted lease.
30534
- cloneVolume: req.lease.cloneVolume,
30535
- env: testStepEnv(env, req.spec.runtime),
30536
- image: imageDigest ?? image,
30537
- labels,
30538
- timeoutMs: req.testTimeoutMs ?? 9e5
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
- return resolvedProblem(label, path5, name, version, entry["resolved"]) ?? integrityProblem(label, path5, entry["integrity"], false);
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
- if (!inMap || mapLine(line.slice(4)) === null)
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 (!PACKAGE_NAME.test(candidate) || /[:/#\\]/.test(range))
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.progress?.("screenshots", `${side}: ${plan.serveCommand}`);
36897
- const serving = step("serve", "offline", plan.serveCommand, captureTimeout(ctx, SCREENSHOT_LIMITS.waitForPortMs + SCREENSHOT_LIMITS.captureTimeoutMs), { labels }).catch(() => null);
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: appId },
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
- const app = appId ?? findApp();
36926
- if (app !== null)
36927
- lease.docker.sync(["rm", "--force", app], syncOpts(ctx));
36928
- await serving;
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 randomBytes12 } from "crypto";
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: randomBytes12(24).toString("base64url"),
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(Object.keys(value).sort().map((k) => [k, value[k]]));
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))) return true;
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,