terminalhire 0.42.21 → 0.42.22

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.
@@ -31787,7 +31787,7 @@ ${facts.stderr}`))
31787
31787
  return null;
31788
31788
  }
31789
31789
  function isCommandUnavailable(facts, countsRead) {
31790
- if (facts.exitCode === 127 || facts.exitCode === 126)
31790
+ if (facts.exitCode === 127 || facts.exitCode === 126 || facts.exitCode === 125)
31791
31791
  return true;
31792
31792
  if (countsRead)
31793
31793
  return false;
@@ -32010,6 +32010,79 @@ var init_classify2 = __esm({
32010
32010
  const p = int(passed);
32011
32011
  return { tests_passed: p, tests_failed: Math.max(0, t - p) };
32012
32012
  }
32013
+ },
32014
+ {
32015
+ /**
32016
+ * BARE TAP — assertion lines with no plan and no summary comment (TERM-1138).
32017
+ *
32018
+ * LAST ON PURPOSE. It is the loosest reader here, so anything that a more specific
32019
+ * one can read must reach that one first: `node --test` and node-tap both emit `ok`
32020
+ * lines AND a closing summary, and their readers are ordered above for that reason.
32021
+ * Moving this up would have it answer for both.
32022
+ *
32023
+ * A hand-rolled harness prints neither summary — it emits its assertions and stops.
32024
+ * That is what morebetterltd/wheelhouse does: thirty `ok N - …` lines, exit 0, and
32025
+ * before this every one of those runs reported `counts-unparsed`, which the claim
32026
+ * page renders as "RESULT UNREADABLE — THE SUITE EXITED 0" over a suite that passed.
32027
+ *
32028
+ * ON THE MISSING PLAN LINE. TAP requires `1..N` so a reader can tell a finished
32029
+ * stream from a truncated one, and there is none here, so this cannot prove the
32030
+ * suite reached its end. That is a real limit and it is NOT a reason to refuse,
32031
+ * because it holds this format to a standard no reader in this file meets — the
32032
+ * jest reader greps a line out of stdout that a repository could `echo` without
32033
+ * running anything. These are format adapters, not a trust boundary; the boundary
32034
+ * is that the command is the repository's own or is refused a signature.
32035
+ *
32036
+ * CONTIGUOUS FROM 1, which buys self-consistency rather than security:
32037
+ *
32038
+ * - starting at 1 rejects a TRUNCATION. `testOutputTail` keeps the last lines, so
32039
+ * a stream beginning `ok 47` is the middle of a longer run and counting it would
32040
+ * report three tests for a suite that ran fifty.
32041
+ * - no gaps rejects prose that happens to carry the word, and a stream interleaved
32042
+ * with another's.
32043
+ * - anchored with no leading space rejects TAP SUBTESTS, which indent. Counting
32044
+ * those would double them against their parent.
32045
+ */
32046
+ runner: "bare TAP",
32047
+ read: (out) => {
32048
+ let n = 0;
32049
+ let failed = 0;
32050
+ let passed = 0;
32051
+ let plan = null;
32052
+ for (const line of out.split("\n")) {
32053
+ if (/^Bail out!/.test(line))
32054
+ return null;
32055
+ const p = /^(\d+)\.\.(\d+)\s*$/.exec(line);
32056
+ if (p) {
32057
+ const declared = Number(p[2]) - Number(p[1]) + 1;
32058
+ if (plan !== null && plan !== declared)
32059
+ return null;
32060
+ plan = declared;
32061
+ continue;
32062
+ }
32063
+ const m = /^(not )?ok (\d+)(?:\s|$)/.exec(line);
32064
+ if (!m)
32065
+ continue;
32066
+ if (Number(m[2]) !== n + 1)
32067
+ return null;
32068
+ n += 1;
32069
+ const directive = /#\s*(skip|todo)\b/i.exec(line);
32070
+ const kind = directive?.[1]?.toLowerCase();
32071
+ if (kind === "skip")
32072
+ continue;
32073
+ if (m[1] !== void 0) {
32074
+ if (kind !== "todo")
32075
+ failed += 1;
32076
+ continue;
32077
+ }
32078
+ passed += 1;
32079
+ }
32080
+ if (n === 0)
32081
+ return null;
32082
+ if (plan !== null && plan !== n)
32083
+ return null;
32084
+ return { tests_passed: passed, tests_failed: failed };
32085
+ }
32013
32086
  }
32014
32087
  ];
32015
32088
  SUPPORTED_RUNNERS = READERS.map((r) => r.runner);
@@ -32096,6 +32169,10 @@ function scrubEnv(source, opts) {
32096
32169
  env["GOCACHE"] = join34(jailHome, ".cache", "go-build");
32097
32170
  env["GOFLAGS"] = "-modcacherw";
32098
32171
  env["CARGO_HOME"] = join34(jailHome, ".cargo");
32172
+ const bundle = join34(jailHome, ".bundle");
32173
+ env["GEM_HOME"] = bundle;
32174
+ env["BUNDLE_PATH"] = bundle;
32175
+ env["BUNDLE_APP_CONFIG"] = bundle;
32099
32176
  const proxy = opts.proxyUrl ?? DEAD_PROXY;
32100
32177
  env["HTTP_PROXY"] = proxy;
32101
32178
  env["HTTPS_PROXY"] = proxy;
@@ -32396,6 +32473,19 @@ var init_egressProxy = __esm({
32396
32473
  "static.crates.io",
32397
32474
  "index.crates.io",
32398
32475
  "crates.io",
32476
+ // TERM-1139. `ruby` has had an image since the mapping existed and `bundle install`
32477
+ // as its derived command, and this host was missing — so every ruby repo could start
32478
+ // and could never install. Install is the ONLY step with a network grant at all, so a
32479
+ // denial here is terminal, not a slow path.
32480
+ //
32481
+ // One entry, not two: `hostAllowed` is dot-anchored SUFFIX matching, so this covers
32482
+ // `index.rubygems.org` as well. Listing that separately would advertise the grant as
32483
+ // narrower than it is, which the `github.com` note below rejects for the same reason.
32484
+ "rubygems.org",
32485
+ // TERM-1139, added with the `dotnet` image mapping. `dotnet restore` fetches from
32486
+ // NuGet; the coupling test refuses a mapped runtime whose registry is unreachable, and
32487
+ // it caught this one the moment the image landed.
32488
+ "api.nuget.org",
32399
32489
  // Git dependencies. Approved by Eric 2026-07-22 after a live run measured the
32400
32490
  // chokepoint working under load (20 events, 16 allowed / 4 denied by host).
32401
32491
  //
@@ -33777,7 +33867,7 @@ function imageForRuntime(runtime, override, version2) {
33777
33867
  return override;
33778
33868
  const shape = RUNTIME_IMAGES[runtime];
33779
33869
  if (!shape) {
33780
- throw new RunRefusalError(`no container image is mapped for runtime ${JSON.stringify(runtime)}. Refusing to run it in the Node image: a missing interpreter surfaces as "command not found", which the classifier reads as tests-failed \u2014 a false red blamed on the repo.`);
33870
+ throw new RunRefusalError(`no container image is mapped for runtime ${JSON.stringify(runtime)}. Refusing to run it in the Node image: a bare "command not found" exits 127, which classifyVerification already owns as ours \u2014 but a test script that RUNS and fails inside on the missing tool exits with its own status and prints to stdout, and that falls through to tests-failed \u2014 a false red blamed on the repo.`);
33781
33871
  }
33782
33872
  if (version2 === void 0 || version2 === null)
33783
33873
  return unversionedImage(shape);
@@ -34028,7 +34118,37 @@ var init_execute = __esm({
34028
34118
  },
34029
34119
  go: { repository: "golang", suffix: "-bookworm", defaultVersion: "1.23", declaredIsFloor: true },
34030
34120
  ruby: { repository: "ruby", suffix: "-bookworm", defaultVersion: "3.3", declaredIsFloor: false },
34031
- rust: { repository: "rust", suffix: "-bookworm", defaultVersion: "1", declaredIsFloor: true }
34121
+ rust: { repository: "rust", suffix: "-bookworm", defaultVersion: "1", declaredIsFloor: true },
34122
+ /**
34123
+ * TERM-1139. `dotnet` was unmapped alongside `jvm`, and the recorded reason was that
34124
+ * neither had "a single obvious base image (gradle vs maven, sdk vs runtime)". That
34125
+ * reason holds for jvm and it does NOT hold here, which is why only this half moved.
34126
+ *
34127
+ * "sdk vs runtime" answers itself: the derived commands are `dotnet restore` and
34128
+ * `dotnet test`, and neither exists on the runtime image. There is no second build
34129
+ * tool competing for the slot the way maven and gradle compete for jvm's — and since
34130
+ * no official image carries both of those, jvm genuinely cannot be served by one key.
34131
+ *
34132
+ * MEASURED, not chosen by tag: `mcr.microsoft.com/dotnet/sdk:8.0` carries `git` and
34133
+ * `dotnet` (881MB). `git` is the property the whole `image-tooling-live` test exists
34134
+ * for, and `eclipse-temurin:21-jdk` was rejected here for lacking it.
34135
+ *
34136
+ * The tag shape fits without touching the builder: the SDK publishes a tag per
34137
+ * version, so `repository:VERSION` is a real image at every version envspec can
34138
+ * derive, with no suffix. That is NOT true of the maven images, where the leading
34139
+ * tag component is maven's version and Java's sits in the suffix.
34140
+ *
34141
+ * `declaredIsFloor` is true on the same reasoning as go and rust: an SDK builds
34142
+ * older target frameworks, so booting 8.0 for a repo asking 6.0 is the supported
34143
+ * case rather than a substitution. A repo asking for something NEWER than our
34144
+ * default still refuses, which is where TERM-643 put that line.
34145
+ */
34146
+ dotnet: {
34147
+ repository: "mcr.microsoft.com/dotnet/sdk",
34148
+ suffix: "",
34149
+ defaultVersion: "8.0",
34150
+ declaredIsFloor: true
34151
+ }
34032
34152
  };
34033
34153
  unversionedImage = (shape) => `${shape.repository}:${shape.defaultVersion}${shape.suffix}`;
34034
34154
  TAG_VERSION = /^\d+(?:\.\d+){0,2}$/;
@@ -34722,14 +34842,24 @@ function answerDidItPass(r) {
34722
34842
  const passed = r.status === "verified" && r.outcome !== null && isGreen(r.outcome);
34723
34843
  return { passed, summary: renderVerdictLine(r), lookAt: r.preview?.url ?? null };
34724
34844
  }
34725
- var RUN_TEST_COMMAND_SOURCES, RUN_RESULT_SCHEMA, RUN_RESULT_FIELDS, RENDER_NONE, FIELD_VIEWS;
34845
+ var RUN_TEST_COMMAND_SOURCES, RUN_IMAGE_SOURCES, RUN_RESULT_SCHEMA, RUN_RESULT_FIELDS, RENDER_NONE, FIELD_VIEWS;
34726
34846
  var init_result = __esm({
34727
34847
  "../../packages/envrun/dist/result.js"() {
34728
34848
  "use strict";
34729
34849
  init_dist2();
34730
34850
  init_classify2();
34731
34851
  init_venueDescriptor();
34732
- RUN_TEST_COMMAND_SOURCES = [...TEST_COMMAND_SOURCES, "developer-declared"];
34852
+ RUN_TEST_COMMAND_SOURCES = [
34853
+ ...TEST_COMMAND_SOURCES,
34854
+ "developer-declared",
34855
+ "operator-declared"
34856
+ ];
34857
+ RUN_IMAGE_SOURCES = [
34858
+ "detected",
34859
+ "none",
34860
+ "operator-declared",
34861
+ "developer-declared"
34862
+ ];
34733
34863
  RUN_RESULT_SCHEMA = "terminalhire.verification-run/1";
34734
34864
  RUN_RESULT_FIELDS = [
34735
34865
  "schema",
@@ -34754,6 +34884,7 @@ var init_result = __esm({
34754
34884
  "preview",
34755
34885
  "containerImage",
34756
34886
  "containerImageDigest",
34887
+ "imageSource",
34757
34888
  "leaksClean",
34758
34889
  "venue"
34759
34890
  ];
@@ -34786,6 +34917,9 @@ var init_result = __esm({
34786
34917
  preview: (r) => r.preview === null ? null : `preview ${r.preview.url}`,
34787
34918
  containerImage: (r) => r.containerImage === null ? null : `image ${r.containerImage}`,
34788
34919
  containerImageDigest: (r) => r.containerImageDigest === null ? null : `image digest ${r.containerImageDigest}`,
34920
+ // Shown only when a human chose the environment. `detected` is the ordinary case and
34921
+ // saying so on every run would train the reader to skip the line that matters.
34922
+ imageSource: (r) => r.imageSource === "detected" || r.imageSource === "none" ? null : `image source ${r.imageSource} (not signed)`,
34789
34923
  leaksClean: (r) => r.leaksClean === null ? null : r.leaksClean ? null : "WARNING labelled Docker objects survived teardown",
34790
34924
  // Absent on most runs, so it prints only when there is something to say. Silence
34791
34925
  // here is the honest rendering of "no venue answered": a placeholder line would
@@ -34912,12 +35046,23 @@ function toAcceptancePredicate(pair, opts = {}) {
34912
35046
  if (why !== null)
34913
35047
  return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
34914
35048
  }
34915
- if (patched.testCommandSource === "developer-declared") {
34916
- return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
35049
+ if (patched.testCommandSource === "developer-declared" || patched.testCommandSource === "operator-declared") {
35050
+ 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)`);
34917
35051
  }
34918
35052
  if (baseline.testCommandSource !== patched.testCommandSource) {
34919
35053
  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.`);
34920
35054
  }
35055
+ for (const [side, source, image] of [
35056
+ ["patched", patched.imageSource, patched.containerImage],
35057
+ ["baseline", baseline.imageSource, baseline.containerImage]
35058
+ ]) {
35059
+ const why = IMAGE_SOURCE_IS_SIGNABLE[source](image);
35060
+ if (why !== null)
35061
+ return refuse3("image-origin-unattested", `the ${side} run ${why}`);
35062
+ }
35063
+ if (baseline.imageSource !== patched.imageSource) {
35064
+ 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.`);
35065
+ }
34921
35066
  if (baseline.containerImage !== patched.containerImage) {
34922
35067
  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.`);
34923
35068
  }
@@ -34974,7 +35119,7 @@ function signRunStatement(predicate, privateKey, keyid) {
34974
35119
  const statement = createAcceptanceStatement(predicate);
34975
35120
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
34976
35121
  }
34977
- var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, 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;
35122
+ 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;
34978
35123
  var init_attestation2 = __esm({
34979
35124
  "../../packages/envrun/dist/attestation.js"() {
34980
35125
  "use strict";
@@ -35030,11 +35175,25 @@ var init_attestation2 = __esm({
35030
35175
  // The party under verification chose it, and no signed member can say so. Per ADR-005
35031
35176
  // Decision 2, emit nothing rather than pick the closest word.
35032
35177
  "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.",
35178
+ // US, out of band, per posting — the twin of `IMAGE_SOURCE_IS_SIGNABLE['operator-declared']`
35179
+ // and refusing for the same reason. `founder-declared` is the nearest signed member and it
35180
+ // is still false: the poster did not choose this, we did, and signing our own choice under
35181
+ // the counterparty's name is the specific false attribution the `developer-declared` entry
35182
+ // above exists to refuse. Decision 2 — emit nothing rather than pick the closest word.
35183
+ "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.",
35033
35184
  // `none` means nobody chose a command, so a command beside it is a contradiction. Shadowed
35034
35185
  // today — `thrun.ts` sets `none` only when the derived command is null, and `no-test-command`
35035
35186
  // fires first — but that coupling runs through `runEnvironmentSpec` and lives in no type.
35036
35187
  none: (command) => command === null ? null : `records its command source as \`none\` \u2014 nobody chose one \u2014 beside the command ${JSON.stringify(command)}`
35037
35188
  };
35189
+ IMAGE_SOURCE_IS_SIGNABLE = {
35190
+ detected: () => null,
35191
+ "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.",
35192
+ "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.",
35193
+ // `none` means no image was booted, so an image name beside it is a contradiction — the
35194
+ // `SOURCE_IS_SIGNABLE.none` argument, applied to the other field.
35195
+ none: (image) => image === null ? null : `records its image source as \`none\` \u2014 nothing was booted \u2014 beside the image ${JSON.stringify(image)}`
35196
+ };
35038
35197
  ATTEST_REFUSAL_REASONS = [
35039
35198
  "our-environment-failed",
35040
35199
  "run-was-refused",
@@ -35076,7 +35235,11 @@ var init_attestation2 = __esm({
35076
35235
  // The signed triple must be reproducible from what it names — see the guard.
35077
35236
  "patch-binding-incomplete",
35078
35237
  "outcome-contradicts-counts",
35079
- "test-command-origin-unattested"
35238
+ "test-command-origin-unattested",
35239
+ // The environment half of the pair above. `pair-disagrees-on-image` compares image NAMES;
35240
+ // this one asks who CHOSE the name, which no comparison of the two halves can reveal when
35241
+ // both were handed the same override.
35242
+ "image-origin-unattested"
35080
35243
  ];
35081
35244
  refuse3 = (reason, detail) => ({
35082
35245
  ok: false,
@@ -35580,13 +35743,20 @@ function gcpBootArgv(p) {
35580
35743
  // steal". Attaching an account removes that, so the safety now rests on two things,
35581
35744
  // and this change MUST land together with proof #2 — never ahead of it:
35582
35745
  //
35583
- // 1. `--no-scopes` + ZERO project roles bound the BLAST RADIUS of a lifted token.
35584
- // No scopes → the OAuth access-token endpoint serves nothing (measured,
35585
- // TERM-706); zero roles → the token authorizes no GCP API. But it is NOT
35586
- // valueless: it is the attestation evidence itself, so a lifted token still
35587
- // authorizes a false claim about WHERE a run happened (the steal-a-true-token
35588
- // attack in the item-2 spike). `--no-scopes` stays because it removes lateral
35589
- // GCP access; it does not make losing the identity token cheap.
35746
+ // 1. `--no-scopes` and a NARROW grant set bound the BLAST RADIUS of a lifted
35747
+ // token. No scopes → the OAuth access-token endpoint serves nothing
35748
+ // (measured, TERM-706). This said "ZERO project roles" until 2026-09-18, and
35749
+ // that was never true of the account we ship: measured live, `venue-identity@`
35750
+ // holds `roles/confidentialcomputing.workloadUser` on the project and
35751
+ // `roles/artifactregistry.reader` on the `venue-images` repository alone —
35752
+ // the same two grants the Confidential Space note below already named, so this
35753
+ // file contradicted itself. Neither reaches a second repository, a bucket or
35754
+ // another VM, which is the property "zero roles" was reaching for. But the
35755
+ // token is NOT valueless: it is the attestation evidence itself, so a lifted
35756
+ // token still authorizes a false claim about WHERE a run happened (the
35757
+ // steal-a-true-token attack in the item-2 spike). `--no-scopes` stays because
35758
+ // it removes lateral GCP access; it does not make losing the identity token
35759
+ // cheap.
35590
35760
  // 2. The untrusted code cannot reach the metadata server to lift the token at all.
35591
35761
  // The test step runs `--network=none`; the install step runs on an `--internal`
35592
35762
  // network whose only egress is a CONNECT-only, registry-allowlisted proxy that
@@ -39276,6 +39446,9 @@ function refusedRun(fields) {
39276
39446
  treeDigest: null,
39277
39447
  baselinePatchSha256: null,
39278
39448
  testCommandSource: "none",
39449
+ // Nothing was booted, so nobody chose an image. Same reasoning as `containerImage: null`
39450
+ // below: it must not read as an image we picked and cannot name.
39451
+ imageSource: "none",
39279
39452
  boundaryRefusals: fields.boundaryRefusals,
39280
39453
  touchedPaths: fields.touchedPaths,
39281
39454
  preview: null,
@@ -39624,7 +39797,7 @@ async function runVerification(req, ctx) {
39624
39797
  // that `founder-declared` (as this did for one review round) signs the counterparty's
39625
39798
  // name onto the developer's choice, which is worse than laundering it as `detected`:
39626
39799
  // it is a specific false attribution inside a field a reviewer trusts.
39627
- testCommandSource: req.testCommandOverride !== void 0 ? req.testCommandOverrideOrigin === "founder" ? "founder-declared" : "developer-declared" : derived.testCommand === null ? "none" : "detected",
39800
+ testCommandSource: req.testCommandOverride !== void 0 ? req.testCommandOverrideOrigin === "founder" ? "founder-declared" : req.testCommandOverrideOrigin === "operator" ? "operator-declared" : "developer-declared" : derived.testCommand === null ? "none" : "detected",
39628
39801
  boundaryRefusals: [],
39629
39802
  touchedPaths: pre.touchedPaths,
39630
39803
  preview: null,
@@ -39634,6 +39807,12 @@ async function runVerification(req, ctx) {
39634
39807
  // that measures one; a run without it is recordable but not attestable —
39635
39808
  // `toAcceptancePredicate` refuses `missing-image-digest` (TERM-893).
39636
39809
  containerImageDigest: null,
39810
+ // The image half of the provenance pair above. `detected` means the runtime mapping
39811
+ // chose the environment; an override names the human. Defaulting an unstated origin to
39812
+ // `developer-declared` is deliberate and matches `testCommandSource`: `th run --image`
39813
+ // runs on the developer's machine, judging the developer's work, so attributing it to
39814
+ // the operator would sign our name onto their choice.
39815
+ imageSource: req.image === void 0 ? "detected" : req.imageOrigin === "operator" ? "operator-declared" : "developer-declared",
39637
39816
  leaksClean: verdict.leaks.clean,
39638
39817
  // Built from the LEASE, over the client that ran the steps — never from
39639
39818
  // `req.placement`, which is a request. `venueDescriptor.ts` carries the
@@ -40589,6 +40768,7 @@ __export(dist_exports, {
40589
40768
  REDACTED_TARGET_SHA: () => REDACTED_TARGET_SHA,
40590
40769
  RELEASED_LEASE_CENSUS_REASON: () => RELEASED_LEASE_CENSUS_REASON,
40591
40770
  REPO_DIGEST_RE: () => REPO_DIGEST_RE,
40771
+ RUN_IMAGE_SOURCES: () => RUN_IMAGE_SOURCES,
40592
40772
  RUN_LABEL_KEY: () => RUN_LABEL_KEY,
40593
40773
  RUN_RESULT_FIELDS: () => RUN_RESULT_FIELDS,
40594
40774
  RUN_RESULT_SCHEMA: () => RUN_RESULT_SCHEMA,
@@ -47632,6 +47812,11 @@ async function once(engine, opts) {
47632
47812
  preview: opts.preview,
47633
47813
  ...opts.placement ? { placement: opts.placement } : {},
47634
47814
  ...opts.testCommand ? { testCommandOverride: opts.testCommand } : {},
47815
+ // No `imageOrigin` beside it, deliberately. This flag runs on the DEVELOPER's machine,
47816
+ // judging the developer's work, so it takes `thrun`'s default of `developer` — the same
47817
+ // reasoning that makes `--test-command` developer-declared. An operator override is a
47818
+ // server-side caller, not this flag.
47819
+ ...opts.image ? { image: opts.image } : {},
47635
47820
  onProgress: (stage, detail) => {
47636
47821
  if (!opts.json) process.stderr.write(` ${stage.padEnd(8)} ${detail}
47637
47822
  `);
@@ -47745,6 +47930,7 @@ async function run10() {
47745
47930
  watch: parsed.bools.has("watch"),
47746
47931
  json: parsed.bools.has("json"),
47747
47932
  testCommand: pick2("test-command") ?? null,
47933
+ image: pick2("image") ?? null,
47748
47934
  keepSeconds,
47749
47935
  scratch: runScratchRoot()
47750
47936
  };
@@ -47823,6 +48009,9 @@ Options:
47823
48009
  --keep <seconds> Hold the preview open this long, at most ${String(KEEP_MAX_SECONDS)}
47824
48010
  (default: until Ctrl-C).
47825
48011
  --test-command <cmd> Disclosed override of the derived test command.
48012
+ --image <ref> Disclosed override of the container image. Use when no image is
48013
+ mapped for the repo's runtime. Recorded as developer-declared
48014
+ provenance, which the attestation refuses to sign.
47826
48015
  --help
47827
48016
 
47828
48017
  Defaults are read from .th-run.json in the local checkout when present, so a