terminalhire 0.42.19 → 0.42.21

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.
@@ -11137,6 +11137,7 @@ __export(github_auth_exports, {
11137
11137
  decrypt: () => decrypt,
11138
11138
  deleteGitHubToken: () => deleteGitHubToken,
11139
11139
  encrypt: () => encrypt,
11140
+ githubTokenDisplayPath: () => githubTokenDisplayPath,
11140
11141
  hasGitHubToken: () => hasGitHubToken,
11141
11142
  loadKey: () => loadKey,
11142
11143
  readGitHubToken: () => readGitHubToken,
@@ -11147,8 +11148,12 @@ __export(github_auth_exports, {
11147
11148
  });
11148
11149
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes5 } from "crypto";
11149
11150
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync5, rmSync as rmSync3, renameSync as renameSync2 } from "fs";
11150
- import { join as join8 } from "path";
11151
+ import { join as join8, sep } from "path";
11151
11152
  import { homedir as homedir6 } from "os";
11153
+ function githubTokenDisplayPath() {
11154
+ const home = homedir6();
11155
+ return TOKEN_FILE.startsWith(home + sep) ? `~${TOKEN_FILE.slice(home.length)}` : TOKEN_FILE;
11156
+ }
11152
11157
  async function loadKey() {
11153
11158
  return loadOrCreateSharedKey();
11154
11159
  }
@@ -26551,12 +26556,33 @@ function scrubEndpointEnv(env) {
26551
26556
  }
26552
26557
  return out;
26553
26558
  }
26554
- function remoteDockerClient(endpoint) {
26559
+ function containerFlagsAt(args) {
26560
+ if (args[0] === "run" || args[0] === "create")
26561
+ return 1;
26562
+ if (args[0] === "container" && (args[1] === "run" || args[1] === "create"))
26563
+ return 2;
26564
+ return -1;
26565
+ }
26566
+ function remoteDockerClient(endpoint, opts = {}) {
26555
26567
  if (endpoint.trim().length === 0) {
26556
26568
  throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
26557
26569
  }
26558
26570
  const target = endpoint.trim();
26559
- const argv = (args) => ["--host", target, ...args];
26571
+ const tlsFlags = [];
26572
+ if (opts.tls) {
26573
+ for (const key of ["caPath", "certPath", "keyPath"]) {
26574
+ if (opts.tls[key].trim().length === 0) {
26575
+ throw new Error(`remoteDockerClient: tls.${key} must name a file`);
26576
+ }
26577
+ }
26578
+ tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
26579
+ }
26580
+ const runArgs = opts.runArgs ?? [];
26581
+ const argv = (args) => {
26582
+ const at = runArgs.length > 0 ? containerFlagsAt(args) : -1;
26583
+ const sub = at < 0 ? [...args] : [...args.slice(0, at), ...runArgs, ...args.slice(at)];
26584
+ return ["--host", target, ...tlsFlags, ...sub];
26585
+ };
26560
26586
  return {
26561
26587
  endpoint: target,
26562
26588
  // Always empty, and it is a fact about this client rather than about the
@@ -26573,10 +26599,10 @@ function remoteDockerClient(endpoint) {
26573
26599
  env: scrubEndpointEnv(process.env)
26574
26600
  });
26575
26601
  },
26576
- sync(args, opts) {
26602
+ sync(args, opts2) {
26577
26603
  return syncResult(spawnSync3(DOCKER_BIN, argv(args), {
26578
26604
  encoding: "utf8",
26579
- timeout: opts?.timeoutMs,
26605
+ timeout: opts2?.timeoutMs,
26580
26606
  env: scrubEndpointEnv(process.env)
26581
26607
  }));
26582
26608
  },
@@ -28557,6 +28583,197 @@ var init_gceIdentity = __esm({
28557
28583
  }
28558
28584
  });
28559
28585
 
28586
+ // ../../packages/attest/dist/confidentialSpace.js
28587
+ import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
28588
+ function refuse2(reason, detail) {
28589
+ return { ok: false, reason, detail };
28590
+ }
28591
+ function isPlainObject(v) {
28592
+ return typeof v === "object" && v !== null && !Array.isArray(v);
28593
+ }
28594
+ function assertExpectationsComplete2(expectations) {
28595
+ const required = [
28596
+ ["audience", expectations.audience],
28597
+ ["nonce", expectations.nonce],
28598
+ ["instanceName", expectations.instanceName],
28599
+ ["projectId", expectations.projectId],
28600
+ ...(expectations.additionalNonces ?? []).map((n) => [
28601
+ "additionalNonces[]",
28602
+ n
28603
+ ]),
28604
+ ...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
28605
+ ["containerEnv name", name],
28606
+ [`containerEnv.${name}`, value]
28607
+ ])
28608
+ ];
28609
+ for (const [name, value] of required) {
28610
+ if (value === "") {
28611
+ throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
28612
+ }
28613
+ }
28614
+ }
28615
+ function nonceList(v) {
28616
+ if (typeof v === "string")
28617
+ return [v];
28618
+ if (!Array.isArray(v) || v.length === 0)
28619
+ return null;
28620
+ if (!v.every((n) => typeof n === "string"))
28621
+ return null;
28622
+ return v;
28623
+ }
28624
+ async function verifyConfidentialSpaceToken(token, expectations, deps) {
28625
+ assertExpectationsComplete2(expectations);
28626
+ if (!Number.isFinite(deps.now)) {
28627
+ throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
28628
+ }
28629
+ if (expectations.allowedImageDigests.length === 0) {
28630
+ return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
28631
+ }
28632
+ if (token.length > MAX_TOKEN_LENGTH) {
28633
+ return refuse2("token-malformed", "longer than any attestation token");
28634
+ }
28635
+ const parts = token.split(".");
28636
+ if (parts.length !== 3) {
28637
+ return refuse2("token-malformed", "not a three-segment compact JWT");
28638
+ }
28639
+ const [headerB64, payloadB64, signatureB64] = parts;
28640
+ const header = decodeJsonSegment(headerB64);
28641
+ if (header === null) {
28642
+ return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
28643
+ }
28644
+ if (header.alg !== "RS256") {
28645
+ return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
28646
+ }
28647
+ if (typeof header.kid !== "string" || header.kid === "") {
28648
+ return refuse2("unknown-key", "token header names no key id");
28649
+ }
28650
+ const jwks = await deps.fetchJwks();
28651
+ const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
28652
+ if (jwk === void 0) {
28653
+ return refuse2("unknown-key", "token key id is not in the published key set");
28654
+ }
28655
+ const signature = strictBase64UrlDecode(signatureB64);
28656
+ if (signature === null) {
28657
+ return refuse2("token-malformed", "signature segment is not base64url");
28658
+ }
28659
+ let signatureValid = false;
28660
+ try {
28661
+ const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
28662
+ signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
28663
+ } catch {
28664
+ signatureValid = false;
28665
+ }
28666
+ if (!signatureValid) {
28667
+ return refuse2("signature-invalid", "signature does not verify under the named key");
28668
+ }
28669
+ const payload = decodeJsonSegment(payloadB64);
28670
+ if (payload === null) {
28671
+ return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
28672
+ }
28673
+ if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
28674
+ return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
28675
+ }
28676
+ if (payload.aud !== expectations.audience) {
28677
+ return refuse2("wrong-audience", "audience is not the one this run expects");
28678
+ }
28679
+ const nowSeconds = deps.now / 1e3;
28680
+ const exp = payload.exp;
28681
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
28682
+ return refuse2("expired", "exp is missing, malformed, or in the past");
28683
+ }
28684
+ const iat = payload.iat;
28685
+ if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
28686
+ return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
28687
+ }
28688
+ const nonces = nonceList(payload.eat_nonce);
28689
+ if (nonces === null) {
28690
+ return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
28691
+ }
28692
+ if (!nonces.includes(expectations.nonce)) {
28693
+ return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
28694
+ }
28695
+ for (const extra of expectations.additionalNonces ?? []) {
28696
+ if (!nonces.includes(extra)) {
28697
+ return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
28698
+ }
28699
+ }
28700
+ if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
28701
+ return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
28702
+ }
28703
+ if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
28704
+ return refuse2("debug-enabled", "the image is not a production image with debug disabled");
28705
+ }
28706
+ if (payload.secboot !== true) {
28707
+ return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
28708
+ }
28709
+ if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
28710
+ return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
28711
+ }
28712
+ const hwmodel = payload.hwmodel;
28713
+ const submods = payload.submods;
28714
+ const gce = isPlainObject(submods) ? submods.gce : void 0;
28715
+ if (!isPlainObject(gce)) {
28716
+ return refuse2("submods-gce-missing", "no submods.gce block names the instance");
28717
+ }
28718
+ if (gce.project_id !== expectations.projectId) {
28719
+ return refuse2("project-mismatch", "token names an instance in a different project");
28720
+ }
28721
+ if (gce.instance_name !== expectations.instanceName) {
28722
+ return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
28723
+ }
28724
+ if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
28725
+ return refuse2("instance-id-missing", "instance id is absent or not a string");
28726
+ }
28727
+ const instanceId = gce.instance_id;
28728
+ if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
28729
+ return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
28730
+ }
28731
+ const zone = gce.zone;
28732
+ const container = isPlainObject(submods) ? submods.container : void 0;
28733
+ if (!isPlainObject(container)) {
28734
+ return refuse2("submods-container-missing", "no submods.container block names the workload");
28735
+ }
28736
+ if (typeof container.image_reference !== "string" || container.image_reference === "") {
28737
+ return refuse2("image-reference-missing", "the workload image reference is absent");
28738
+ }
28739
+ const imageReference = container.image_reference;
28740
+ if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
28741
+ return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
28742
+ }
28743
+ const env = container.env;
28744
+ for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
28745
+ if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
28746
+ return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
28747
+ }
28748
+ }
28749
+ return {
28750
+ ok: true,
28751
+ value: {
28752
+ instanceId,
28753
+ instanceName: expectations.instanceName,
28754
+ zone,
28755
+ projectId: expectations.projectId,
28756
+ imageDigest: container.image_digest,
28757
+ imageReference,
28758
+ hwmodel,
28759
+ iat,
28760
+ exp,
28761
+ tokenSha256: createHash7("sha256").update(token, "utf8").digest("hex")
28762
+ }
28763
+ };
28764
+ }
28765
+ var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
28766
+ var init_confidentialSpace = __esm({
28767
+ "../../packages/attest/dist/confidentialSpace.js"() {
28768
+ "use strict";
28769
+ init_gceIdentity();
28770
+ CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
28771
+ CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
28772
+ CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
28773
+ CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
28774
+ }
28775
+ });
28776
+
28560
28777
  // ../../packages/attest/dist/venueInstanceName.js
28561
28778
  function venueInstanceName(runId) {
28562
28779
  return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
@@ -28591,6 +28808,7 @@ var init_dist2 = __esm({
28591
28808
  init_attestation();
28592
28809
  init_nonce();
28593
28810
  init_gceIdentity();
28811
+ init_confidentialSpace();
28594
28812
  init_venueInstanceName();
28595
28813
  init_verify();
28596
28814
  }
@@ -28841,7 +29059,7 @@ var init_result = __esm({
28841
29059
  });
28842
29060
 
28843
29061
  // ../../packages/envrun/dist/attestation.js
28844
- import { createHash as createHash7, randomBytes as randomBytes10 } from "crypto";
29062
+ import { createHash as createHash8, randomBytes as randomBytes10 } from "crypto";
28845
29063
  function contradicts(outcome, counts, exitCode) {
28846
29064
  const budget = OUTCOME_TO_BUDGET[outcome];
28847
29065
  if (budget === null)
@@ -28883,7 +29101,7 @@ function imageDigestOf(ref) {
28883
29101
  return at === -1 ? null : ref.slice(at + 1);
28884
29102
  }
28885
29103
  function sha256Hex(data) {
28886
- return createHash7("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
29104
+ return createHash8("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
28887
29105
  }
28888
29106
  function toTestRunResult(result, outputSha256) {
28889
29107
  return {
@@ -28895,50 +29113,50 @@ function toTestRunResult(result, outputSha256) {
28895
29113
  function toAcceptancePredicate(pair, opts = {}) {
28896
29114
  const { baseline, patched } = pair;
28897
29115
  if (baseline.runId === patched.runId) {
28898
- return refuse2("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.`);
29116
+ 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.`);
28899
29117
  }
28900
29118
  if (patched.status === "refused") {
28901
- return refuse2("run-was-refused", `the patched run was refused before executing (${patched.boundaryRefusals.length} boundary refusal(s)), so there is no result to attest`);
29119
+ 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`);
28902
29120
  }
28903
29121
  if (patched.outcome === null) {
28904
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
29122
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
28905
29123
  }
28906
29124
  if (baseline.status === "refused") {
28907
- return refuse2("baseline-was-refused", `the baseline run was refused before executing (${baseline.boundaryRefusals.length} boundary refusal(s)), so there is no "before" to compare against`);
29125
+ 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`);
28908
29126
  }
28909
29127
  if (baseline.outcome === null) {
28910
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
29128
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
28911
29129
  }
28912
29130
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
28913
- return refuse2("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.`);
29131
+ 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.`);
28914
29132
  }
28915
29133
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
28916
29134
  if (budget === null) {
28917
- return refuse2("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.`);
29135
+ 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.`);
28918
29136
  }
28919
29137
  if (patched.testCommand === null) {
28920
- return refuse2("no-test-command", "no test command was recorded, so none can be attested");
29138
+ return refuse3("no-test-command", "no test command was recorded, so none can be attested");
28921
29139
  }
28922
29140
  if (baseline.testCommand !== patched.testCommand) {
28923
- return refuse2("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.`);
29141
+ 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.`);
28924
29142
  }
28925
29143
  if (baseline.targetSha !== patched.targetSha) {
28926
- return refuse2("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
29144
+ return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
28927
29145
  }
28928
29146
  if (patched.treeDigest === null || patched.treeDigest === "") {
28929
- return refuse2("missing-tree-digest", "the patched run recorded no tree digest to bind to");
29147
+ return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
28930
29148
  }
28931
29149
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
28932
- return refuse2("missing-patch-digest", "the patched run recorded no patch digest");
29150
+ return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
28933
29151
  }
28934
29152
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
28935
- return refuse2("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
29153
+ return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
28936
29154
  }
28937
29155
  if (baseline.treeDigest === patched.treeDigest) {
28938
- return refuse2("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`);
29156
+ 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`);
28939
29157
  }
28940
29158
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
28941
- return refuse2("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.`);
29159
+ 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.`);
28942
29160
  }
28943
29161
  for (const [side, outcome, counts, exitCode] of [
28944
29162
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -28946,7 +29164,7 @@ function toAcceptancePredicate(pair, opts = {}) {
28946
29164
  ]) {
28947
29165
  const why = contradicts(outcome, counts, exitCode);
28948
29166
  if (why !== null) {
28949
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
29167
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
28950
29168
  }
28951
29169
  }
28952
29170
  for (const [side, source] of [
@@ -28955,44 +29173,44 @@ function toAcceptancePredicate(pair, opts = {}) {
28955
29173
  ]) {
28956
29174
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
28957
29175
  if (why !== null)
28958
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
29176
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
28959
29177
  }
28960
29178
  if (patched.testCommandSource === "developer-declared") {
28961
- return refuse2("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
29179
+ return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
28962
29180
  }
28963
29181
  if (baseline.testCommandSource !== patched.testCommandSource) {
28964
- return refuse2("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.`);
29182
+ 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.`);
28965
29183
  }
28966
29184
  if (baseline.containerImage !== patched.containerImage) {
28967
- return refuse2("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.`);
29185
+ 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.`);
28968
29186
  }
28969
29187
  if (patched.containerImage === null || patched.containerImage === "") {
28970
- return refuse2("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");
29188
+ 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");
28971
29189
  }
28972
29190
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
28973
- return refuse2("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.");
29191
+ 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.");
28974
29192
  }
28975
29193
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
28976
- return refuse2("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.`);
29194
+ 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.`);
28977
29195
  }
28978
29196
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
28979
- return refuse2("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.`);
29197
+ 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.`);
28980
29198
  }
28981
29199
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
28982
- return refuse2("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.");
29200
+ 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.");
28983
29201
  }
28984
29202
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
28985
- return refuse2("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.`);
29203
+ 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.`);
28986
29204
  }
28987
29205
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
28988
- return refuse2("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.`);
29206
+ 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.`);
28989
29207
  }
28990
29208
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
28991
- return refuse2("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.`);
29209
+ 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.`);
28992
29210
  }
28993
29211
  const claimedDigest = imageDigestOf(patched.containerImage);
28994
29212
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
28995
- return refuse2("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.`);
29213
+ 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.`);
28996
29214
  }
28997
29215
  return {
28998
29216
  ok: true,
@@ -29019,7 +29237,7 @@ function signRunStatement(predicate, privateKey, keyid) {
29019
29237
  const statement = createAcceptanceStatement(predicate);
29020
29238
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
29021
29239
  }
29022
- var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS, refuse2, LOCAL_MEASUREMENT_PREFIX, REFERENCE_DOMAIN_COMPONENT, REFERENCE_DOMAIN_NAME, REFERENCE_IPV6, REFERENCE_DOMAIN, REFERENCE_PATH_COMPONENT, REPO_DIGEST_RE;
29240
+ 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;
29023
29241
  var init_attestation2 = __esm({
29024
29242
  "../../packages/envrun/dist/attestation.js"() {
29025
29243
  "use strict";
@@ -29123,7 +29341,7 @@ var init_attestation2 = __esm({
29123
29341
  "outcome-contradicts-counts",
29124
29342
  "test-command-origin-unattested"
29125
29343
  ];
29126
- refuse2 = (reason, detail) => ({
29344
+ refuse3 = (reason, detail) => ({
29127
29345
  ok: false,
29128
29346
  reason,
29129
29347
  detail
@@ -29506,6 +29724,73 @@ var init_boundary = __esm({
29506
29724
  });
29507
29725
 
29508
29726
  // ../../packages/envrun/dist/gcpPlacement.js
29727
+ function assertWorkloadImage(fn, value) {
29728
+ if (typeof value !== "string" || !WORKLOAD_IMAGE_REFERENCE.test(value)) {
29729
+ throw new GcpPlacementError(`${fn}: the workload image is missing or is not one plain image reference (${String(WORKLOAD_IMAGE_REFERENCE)}). It lands inside a comma-separated --metadata= list, so anything that could start a second key is refused.`);
29730
+ }
29731
+ }
29732
+ function venueImageFromEnv(env) {
29733
+ const image = env["VENUE_IMAGE"] ?? "";
29734
+ if (image === "" || image === "cos-stable")
29735
+ return { venueImage: "cos-stable" };
29736
+ if (image !== "confidential-space") {
29737
+ throw new GcpPlacementError("VENUE_IMAGE is set to something other than cos-stable or confidential-space. An unknown value is refused rather than booted as the default.");
29738
+ }
29739
+ const workloadImage = env["VENUE_WORKLOAD_IMAGE"] ?? "";
29740
+ if (workloadImage === "") {
29741
+ throw new GcpPlacementError("VENUE_IMAGE=confidential-space needs VENUE_WORKLOAD_IMAGE, the image reference the Confidential Space launcher runs. Nothing boots without one.");
29742
+ }
29743
+ assertWorkloadImage("venueImageFromEnv", workloadImage);
29744
+ return { venueImage: "confidential-space", workloadImage };
29745
+ }
29746
+ function bootImageFlags(p) {
29747
+ const image = p.venueImage ?? "cos-stable";
29748
+ if (image === "cos-stable")
29749
+ return ["--image-family=cos-stable", "--image-project=cos-cloud"];
29750
+ if (image !== "confidential-space") {
29751
+ throw new GcpPlacementError("gcpBootArgv: venueImage is not one of cos-stable, confidential-space. An unknown image is refused rather than booted as the default.");
29752
+ }
29753
+ const workloadImage = p.workloadImage;
29754
+ assertWorkloadImage("gcpBootArgv", workloadImage);
29755
+ const clientCert = p.clientCert;
29756
+ if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
29757
+ throw new GcpPlacementError(`gcpBootArgv: the client certificate is missing or is not one base64url value (${String(CLIENT_CERT_BASE64URL)}). It lands inside the --metadata value, so anything that could start a second key is refused.`);
29758
+ }
29759
+ if (p.runId.length < MIN_NONCE_BYTES) {
29760
+ throw new GcpPlacementError(`gcpBootArgv: a Confidential Space run id must be at least ${String(MIN_NONCE_BYTES)} characters, because it is the attestation token's nonce and the launcher refuses a shorter one. Refused here, before a VM is billed for a token it cannot mint.`);
29761
+ }
29762
+ const metadata = [
29763
+ `tee-image-reference=${workloadImage}`,
29764
+ "tee-container-log-redirect=true",
29765
+ 'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
29766
+ "tee-cgroup-ns=true",
29767
+ `tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
29768
+ `tee-env-TH_NONCE=${p.runId}`,
29769
+ `tee-env-TH_CLIENT_CERT=${clientCert}`
29770
+ ];
29771
+ return [
29772
+ // The production family. The debug family reports `dbgstat: enabled`, which the
29773
+ // intake's verifier refuses, so a debug boot could never produce a run we record.
29774
+ "--image-family=confidential-space",
29775
+ "--image-project=confidential-space-images",
29776
+ "--confidential-compute-type=SEV",
29777
+ // The deploy-workloads page says N2D with SEV may set MIGRATE for live migration,
29778
+ // and every other machine type needs TERMINATE. The spec chose TERMINATE: a
29779
+ // maintenance event ends the VM instead of moving it, which for a VM that lives at
29780
+ // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
29781
+ // combination is phase 5's measurement.
29782
+ "--maintenance-policy=TERMINATE",
29783
+ `--metadata=^;^${metadata.join(";")}`,
29784
+ // What the standing firewall rule for IAP's range matches, so the tunnels to the
29785
+ // workload's dockerd and attest ports open on these VMs and no others.
29786
+ "--tags=th-cs-venue",
29787
+ // The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
29788
+ // runs vfs, which copies every layer and every container's rootfs in full. Pulling
29789
+ // node:20-bookworm left 992 MB, and the next container create failed ENOSPC
29790
+ // (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
29791
+ "--boot-disk-size=50GB"
29792
+ ];
29793
+ }
29509
29794
  function assertInstanceIdentity(fn, id) {
29510
29795
  const fields = [
29511
29796
  ["vmName", id.vmName, GCE_INSTANCE_NAME],
@@ -29531,8 +29816,8 @@ function gcpBootArgv(p) {
29531
29816
  `--project=${p.project}`,
29532
29817
  `--zone=${p.zone}`,
29533
29818
  `--machine-type=${p.machineType}`,
29534
- "--image-family=cos-stable",
29535
- "--image-project=cos-cloud",
29819
+ // What the VM boots, and the one part of this argv the two images do not share.
29820
+ ...bootImageFlags(p),
29536
29821
  // ── THE OUTER REAP. This, not the `finally` below, is what bounds the bill. ──
29537
29822
  //
29538
29823
  // An in-process `finally` is a best effort, not a reap. SIGKILL, a crashed
@@ -29577,7 +29862,13 @@ function gcpBootArgv(p) {
29577
29862
  // expectation uses (TERM-832): two inline spellings of one value is how
29578
29863
  // the boot path and the verifier drift apart silently.
29579
29864
  `--service-account=${venueIdentityEmail(p.project)}`,
29580
- "--no-scopes",
29865
+ // A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
29866
+ // pulls the workload image and mints the attestation token with this account, and
29867
+ // under `--no-scopes` it can do neither. What bounds a lifted token there is the
29868
+ // account's two grants — `confidentialcomputing.workloadUser` and a reader on the
29869
+ // one venue image repository — plus point 2 above, which is unchanged: the fence
29870
+ // still denies untrusted code the metadata server.
29871
+ ...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
29581
29872
  "--shielded-secure-boot",
29582
29873
  "--shielded-vtpm",
29583
29874
  "--shielded-integrity-monitoring",
@@ -29610,7 +29901,7 @@ function gcpDeleteArgv(p) {
29610
29901
  "--quiet"
29611
29902
  ];
29612
29903
  }
29613
- var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
29904
+ var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, CONFIDENTIAL_SPACE_MACHINE_TYPE, WORKLOAD_IMAGE_REFERENCE, CLIENT_CERT_BASE64URL, MIN_NONCE_BYTES, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
29614
29905
  var init_gcpPlacement = __esm({
29615
29906
  "../../packages/envrun/dist/gcpPlacement.js"() {
29616
29907
  "use strict";
@@ -29627,6 +29918,10 @@ var init_gcpPlacement = __esm({
29627
29918
  GCP_MAX_RUN_DURATION_SECONDS = 3600;
29628
29919
  GCP_MANAGED_LABEL_KEY = "th-managed";
29629
29920
  GCP_RUN_LABEL_KEY = "th-run";
29921
+ CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
29922
+ WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
29923
+ CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
29924
+ MIN_NONCE_BYTES = 10;
29630
29925
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
29631
29926
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
29632
29927
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -29668,7 +29963,10 @@ var init_emptyGitConfig = __esm({
29668
29963
 
29669
29964
  // ../../packages/envrun/dist/hostedVenue.js
29670
29965
  import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
29671
- import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync8 } from "fs";
29966
+ import { createHash as createHash9, X509Certificate } from "crypto";
29967
+ import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
29968
+ import { request as httpsRequest } from "https";
29969
+ import { createServer } from "net";
29672
29970
  import { join as join24 } from "path";
29673
29971
  import { tmpdir as tmpdir3 } from "os";
29674
29972
  function credentialInGitConfig(text) {
@@ -29870,6 +30168,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
29870
30168
  });
29871
30169
  });
29872
30170
  }
30171
+ function socketErrorReason(err) {
30172
+ if (err.message !== "")
30173
+ return err.message;
30174
+ const code = err.code;
30175
+ if (code !== void 0)
30176
+ return code;
30177
+ const inner = err.errors;
30178
+ if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
30179
+ return inner[0].message;
30180
+ }
30181
+ return err.name;
30182
+ }
30183
+ function fetchAttestationOverTls(req) {
30184
+ return new Promise((resolve4) => {
30185
+ let whole;
30186
+ const settle = (reading) => {
30187
+ if (whole !== void 0)
30188
+ clearTimeout(whole);
30189
+ resolve4(reading);
30190
+ };
30191
+ let cert;
30192
+ let key;
30193
+ try {
30194
+ cert = readFileSync11(req.certPath);
30195
+ key = readFileSync11(req.keyPath);
30196
+ } catch (err) {
30197
+ settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
30198
+ return;
30199
+ }
30200
+ const call = httpsRequest({
30201
+ host: "localhost",
30202
+ port: req.port,
30203
+ path: "/attest",
30204
+ method: "GET",
30205
+ servername: "localhost",
30206
+ cert,
30207
+ key,
30208
+ rejectUnauthorized: false,
30209
+ agent: false,
30210
+ timeout: req.timeoutMs
30211
+ }, (res) => {
30212
+ const peer = res.socket.getPeerCertificate();
30213
+ let body = "";
30214
+ res.setEncoding("utf8");
30215
+ res.on("data", (chunk) => {
30216
+ body += chunk;
30217
+ if (body.length > MAX_TOKEN_LENGTH) {
30218
+ settle({ ok: false, reason: "the answer is longer than any attestation token" });
30219
+ res.destroy();
30220
+ }
30221
+ });
30222
+ res.on("end", () => {
30223
+ if (res.statusCode !== 200) {
30224
+ settle({
30225
+ ok: false,
30226
+ reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
30227
+ });
30228
+ return;
30229
+ }
30230
+ const raw = peer.raw;
30231
+ if (raw === void 0 || raw.length === 0) {
30232
+ settle({ ok: false, reason: "the attest endpoint presented no certificate" });
30233
+ return;
30234
+ }
30235
+ settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
30236
+ });
30237
+ res.on("error", (err) => {
30238
+ settle({ ok: false, reason: socketErrorReason(err) });
30239
+ });
30240
+ });
30241
+ whole = setTimeout(() => {
30242
+ settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
30243
+ call.destroy();
30244
+ }, req.timeoutMs);
30245
+ call.on("timeout", () => {
30246
+ call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
30247
+ });
30248
+ call.on("error", (err) => {
30249
+ settle({ ok: false, reason: socketErrorReason(err) });
30250
+ });
30251
+ call.end();
30252
+ });
30253
+ }
29873
30254
  function quoteForRemoteShell(arg) {
29874
30255
  return `'${arg.replace(/'/g, "'\\''")}'`;
29875
30256
  }
@@ -30204,25 +30585,370 @@ ${res.stderr}`);
30204
30585
  function manualDeleteCommand(vm, project, zone) {
30205
30586
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
30206
30587
  }
30588
+ function clientCertArgv(keyPath, certPath) {
30589
+ return [
30590
+ "req",
30591
+ "-x509",
30592
+ "-newkey",
30593
+ "ec",
30594
+ "-pkeyopt",
30595
+ "ec_paramgen_curve:P-256",
30596
+ "-nodes",
30597
+ "-keyout",
30598
+ keyPath,
30599
+ "-out",
30600
+ certPath,
30601
+ "-subj",
30602
+ "/CN=terminalhire-worker",
30603
+ "-days",
30604
+ "1",
30605
+ "-addext",
30606
+ "extendedKeyUsage=clientAuth"
30607
+ ];
30608
+ }
30609
+ function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
30610
+ return [
30611
+ "compute",
30612
+ "start-iap-tunnel",
30613
+ vm,
30614
+ String(remotePort),
30615
+ `--local-host-port=localhost:${String(localPort)}`,
30616
+ `--project=${project}`,
30617
+ `--zone=${zone}`,
30618
+ "--iap-tunnel-disable-connection-check"
30619
+ ];
30620
+ }
30621
+ function spkiSha256Hex(certDer) {
30622
+ const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
30623
+ return createHash9("sha256").update(spki).digest("hex");
30624
+ }
30625
+ function certMetadataValue(pem) {
30626
+ return new X509Certificate(pem).raw.toString("base64url");
30627
+ }
30628
+ function requireWorkloadDigest(reference) {
30629
+ const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
30630
+ if (named === null) {
30631
+ throw new GcpPlacementError("VENUE_WORKLOAD_IMAGE must name its image by digest (\u2026@sha256:<64 hex>) for a Confidential Space venue: the attestation is checked against that digest, and a tag names whatever was pushed last.");
30632
+ }
30633
+ return named[1];
30634
+ }
30635
+ function underStageRoot(path5) {
30636
+ if (!path5.startsWith(`${CS_STAGE_ROOT}/`)) {
30637
+ throw new Error(`${path5} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
30638
+ }
30639
+ return path5;
30640
+ }
30641
+ function userFlag(owner) {
30642
+ return `--user=${String(owner.uid)}:${String(owner.gid)}`;
30643
+ }
30644
+ function csStagePrepareArgv(stageBase, owner) {
30645
+ const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
30646
+ return [
30647
+ "run",
30648
+ "--rm",
30649
+ "--network=none",
30650
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
30651
+ "--",
30652
+ STAGE_HELPER_IMAGE,
30653
+ "sh",
30654
+ "-c",
30655
+ `mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
30656
+ ];
30657
+ }
30658
+ function csUntarArgv(dir, owner) {
30659
+ return [
30660
+ "run",
30661
+ "-i",
30662
+ "--rm",
30663
+ "--network=none",
30664
+ userFlag(owner),
30665
+ `--volume=${underStageRoot(dir)}:/dst:rw`,
30666
+ "--",
30667
+ STAGE_HELPER_IMAGE,
30668
+ "tar",
30669
+ "-xf",
30670
+ "-",
30671
+ "-C",
30672
+ "/dst"
30673
+ ];
30674
+ }
30675
+ function csUntarProxyArgv(dir, owner) {
30676
+ const d = quoteForRemoteShell(underStageRoot(dir));
30677
+ return [
30678
+ "run",
30679
+ "-i",
30680
+ "--rm",
30681
+ "--network=none",
30682
+ userFlag(owner),
30683
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
30684
+ "--",
30685
+ STAGE_HELPER_IMAGE,
30686
+ "sh",
30687
+ "-c",
30688
+ `mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
30689
+ ];
30690
+ }
30691
+ function csRemoveArgv(dir, owner) {
30692
+ return [
30693
+ "run",
30694
+ "--rm",
30695
+ "--network=none",
30696
+ userFlag(owner),
30697
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
30698
+ "--",
30699
+ STAGE_HELPER_IMAGE,
30700
+ "rm",
30701
+ "-rf",
30702
+ "--",
30703
+ underStageRoot(dir)
30704
+ ];
30705
+ }
30706
+ function sshStageTransport(vm, project, zone, env, io) {
30707
+ return {
30708
+ /**
30709
+ * `id -u` / `id -g` on the venue, as the account that untarred the tree.
30710
+ *
30711
+ * That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
30712
+ * so asking it who it is answers exactly the question the mount needs, without
30713
+ * a chown and without assuming a uid. One ssh round trip, once per run.
30714
+ *
30715
+ * Both ids come from ONE invocation. Two calls could straddle a reconnect and
30716
+ * pair a uid with another session's gid — a third wrong answer rather than a
30717
+ * half-right one, which is the reason `GuestUser` is a pair in the first place.
30718
+ */
30719
+ readOwner: () => {
30720
+ const res = io.exec(
30721
+ "gcloud",
30722
+ iapSshArgv(vm, project, zone, "id -u && id -g"),
30723
+ OWNER_PROBE_TIMEOUT_MS,
30724
+ // `env`, like every other gcloud call here. TERM-724 made the venue boot under the
30725
+ // SERVICE configuration rather than whoever is at the terminal, and a probe that
30726
+ // omitted this would ssh under the person's own credential — then report an owner
30727
+ // measured against a session the run does not use.
30728
+ env
30729
+ );
30730
+ if (!res.ok) {
30731
+ throw new HostedVenueError(`could not read the staged tree's owner on ${vm}: ${execDetail(res).slice(0, 300)}. Refusing rather than falling back to this machine\u2019s uid, which would hand the guest an id with no write access to its own clone.`);
30732
+ }
30733
+ const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
30734
+ const ids2 = lines.slice(-2).map((l) => Number(l));
30735
+ if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
30736
+ throw new HostedVenueError(`the venue ${vm} answered 'id -u && id -g' with ${JSON.stringify(res.stdout.slice(0, 120))}, which is not a uid/gid pair. Refusing rather than guessing an owner.`);
30737
+ }
30738
+ return { uid: ids2[0], gid: ids2[1] };
30739
+ },
30740
+ push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
30741
+ pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
30742
+ removeProxy: (dir) => {
30743
+ io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
30744
+ }
30745
+ };
30746
+ }
30747
+ function dockerStageTransport(docker3, env, io) {
30748
+ const pushVia = (from, args) => {
30749
+ const [file, ...rest] = docker3.commandLine(args);
30750
+ if (file === void 0)
30751
+ throw new Error("the docker client returned an empty command line");
30752
+ return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
30753
+ };
30754
+ return {
30755
+ // Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
30756
+ readOwner: () => CS_GUEST_USER,
30757
+ push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
30758
+ pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
30759
+ removeProxy: (dir) => {
30760
+ docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
30761
+ }
30762
+ };
30763
+ }
30764
+ function bothTunnels(a, b) {
30765
+ return {
30766
+ kill: (signal) => {
30767
+ let failed = false;
30768
+ let error;
30769
+ for (const t of [a, b]) {
30770
+ try {
30771
+ t.kill(signal);
30772
+ } catch (err) {
30773
+ if (!failed)
30774
+ error = err;
30775
+ failed = true;
30776
+ }
30777
+ }
30778
+ if (failed)
30779
+ throw error;
30780
+ },
30781
+ failure: () => a.failure() ?? b.failure()
30782
+ };
30783
+ }
30784
+ function makeClientTls(io, allocated, vm) {
30785
+ let dir;
30786
+ try {
30787
+ dir = io.makePrivateDir();
30788
+ } catch (err) {
30789
+ throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
30790
+ }
30791
+ allocated.onRollback(() => {
30792
+ try {
30793
+ io.removeTree(dir);
30794
+ } catch (err) {
30795
+ throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
30796
+ }
30797
+ });
30798
+ const certPath = join24(dir, "client.pem");
30799
+ const keyPath = join24(dir, "client-key.pem");
30800
+ const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
30801
+ if (!made.ok) {
30802
+ throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
30803
+ }
30804
+ let metadataValue;
30805
+ try {
30806
+ const pem = io.readTextIfPresent(certPath);
30807
+ if (pem === null)
30808
+ throw new Error(`openssl wrote nothing to ${certPath}`);
30809
+ metadataValue = certMetadataValue(pem);
30810
+ } catch (err) {
30811
+ throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
30812
+ }
30813
+ return { dir, certPath, keyPath, metadataValue };
30814
+ }
30815
+ async function reachConfidentialSpace(a) {
30816
+ const { io, vm } = a;
30817
+ let ports;
30818
+ try {
30819
+ ports = await io.freeLocalPorts(2);
30820
+ } catch (err) {
30821
+ throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
30822
+ }
30823
+ const [attestPort, dockerPort] = ports;
30824
+ if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
30825
+ throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
30826
+ }
30827
+ const forward = (remote, local) => {
30828
+ const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
30829
+ a.allocated.onRollback(() => {
30830
+ try {
30831
+ t.kill("SIGTERM");
30832
+ } catch {
30833
+ }
30834
+ });
30835
+ return t;
30836
+ };
30837
+ const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
30838
+ const deadline = io.now() + CS_READY_BUDGET_MS;
30839
+ let reading = { ok: false, reason: "no request was made" };
30840
+ while (io.now() < deadline) {
30841
+ const dead = tunnel.failure();
30842
+ if (dead !== null) {
30843
+ throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
30844
+ }
30845
+ reading = await io.fetchAttestation({
30846
+ port: attestPort,
30847
+ certPath: a.tls.certPath,
30848
+ keyPath: a.tls.keyPath,
30849
+ timeoutMs: CS_ATTEST_TIMEOUT_MS
30850
+ });
30851
+ if (reading.ok)
30852
+ break;
30853
+ await io.sleep(CS_ATTEST_INTERVAL_MS);
30854
+ }
30855
+ if (!reading.ok) {
30856
+ throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
30857
+ }
30858
+ let spki;
30859
+ let venuePem;
30860
+ try {
30861
+ spki = spkiSha256Hex(reading.peerCertDer);
30862
+ venuePem = new X509Certificate(reading.peerCertDer).toString();
30863
+ } catch (err) {
30864
+ throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
30865
+ }
30866
+ let jwks;
30867
+ try {
30868
+ jwks = await io.fetchConfidentialSpaceJwks();
30869
+ } catch (err) {
30870
+ throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
30871
+ }
30872
+ const verdict = await verifyConfidentialSpaceToken(reading.token, {
30873
+ audience: expectedAudience(a.runId),
30874
+ nonce: a.runId,
30875
+ additionalNonces: [spki],
30876
+ instanceName: vm,
30877
+ allowedZones: [a.zone],
30878
+ projectId: a.project,
30879
+ allowedImageDigests: [a.imageDigest],
30880
+ containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
30881
+ }, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
30882
+ if (!verdict.ok) {
30883
+ throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
30884
+ }
30885
+ const caPath = join24(a.tls.dir, "venue.pem");
30886
+ try {
30887
+ io.writeFile(caPath, venuePem);
30888
+ } catch (err) {
30889
+ throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
30890
+ }
30891
+ const dockerAt = `tcp://localhost:${String(dockerPort)}`;
30892
+ const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
30893
+ const daemon = io.classifyDaemon(docker3);
30894
+ if (!daemon.distinct) {
30895
+ throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
30896
+ }
30897
+ const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
30898
+ timeoutMs: MKDIR_TIMEOUT_MS
30899
+ });
30900
+ if (prepared.error || prepared.status !== 0) {
30901
+ throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
30902
+ }
30903
+ const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
30904
+ if (pulled.error || pulled.status !== 0) {
30905
+ throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
30906
+ }
30907
+ return {
30908
+ docker: docker3,
30909
+ tunnel,
30910
+ socketDir: a.tls.dir,
30911
+ socketPath: dockerAt,
30912
+ venueDaemonId: daemon.venueDaemonId,
30913
+ venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
30914
+ transport: dockerStageTransport(docker3, a.env, io)
30915
+ };
30916
+ }
30207
30917
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30208
30918
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
30209
30919
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
30210
30920
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
30211
30921
  const keyFile = resolveServiceAccountKeyFile(opts);
30922
+ const image = venueImageFromEnv(process.env);
30923
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
30212
30924
  const env = venueGcloudEnv(gcloudConfig);
30213
30925
  return {
30214
30926
  kind: "hosted-pool",
30215
30927
  acquire: (runId) => acquireTransactionally(async (allocated) => {
30216
30928
  const vm = venueInstanceName(runId);
30217
30929
  const stageBase = `/tmp/th-stage-${runId}`;
30218
- const bootArgv = gcpBootArgv({
30930
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
30219
30931
  vmName: vm,
30220
30932
  project,
30221
30933
  zone,
30222
30934
  runId,
30223
- machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE
30224
- });
30935
+ machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
30936
+ venueImage: "confidential-space",
30937
+ workloadImage: image.workloadImage,
30938
+ clientCert
30939
+ } : {
30940
+ vmName: vm,
30941
+ project,
30942
+ zone,
30943
+ runId,
30944
+ machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
30945
+ ...image
30946
+ };
30947
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
30225
30948
  ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
30949
+ const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
30950
+ if (clientTls !== null)
30951
+ bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
30226
30952
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
30227
30953
  let instanceState = "unknown";
30228
30954
  allocated.onRollback(() => {
@@ -30238,6 +30964,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30238
30964
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
30239
30965
  }
30240
30966
  instanceState = "exists";
30967
+ if (clientTls !== null) {
30968
+ return finishAcquire(await reachConfidentialSpace({
30969
+ vm,
30970
+ project,
30971
+ zone,
30972
+ runId,
30973
+ stageBase,
30974
+ env,
30975
+ io,
30976
+ allocated,
30977
+ tls: clientTls,
30978
+ imageDigest: clientTls.imageDigest
30979
+ }));
30980
+ }
30241
30981
  const deadline = io.now() + SSH_READY_BUDGET_MS;
30242
30982
  let last = { retryable: true, reason: "no probe ran" };
30243
30983
  let identity = null;
@@ -30332,43 +31072,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30332
31072
  if (!prepared.ok) {
30333
31073
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
30334
31074
  }
30335
- const stageVolumes = stageVolumeNames(vm);
30336
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30337
- const created = docker3.sync(volumeCreateArgv(name, runId), {
30338
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
30339
- });
30340
- if (created.error || created.status !== 0) {
30341
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
30342
- }
30343
- }
30344
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30345
- const probe = docker3.sync(execProbeArgv(name), {
30346
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
30347
- });
30348
- if (probe.error || probe.status !== 0) {
30349
- throw new HostedVenueError(`${vm} cannot execute a file written to its own Docker volume ${name}, so no install step with a native postinstall could run there. Refusing before staging anything: ${dockerDetail(probe).slice(0, 300)}`);
30350
- }
30351
- }
30352
- const after = io.classifyDaemon(docker3);
30353
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
30354
- throw new HostedVenueError(`the daemon at ${socketPath} changed while the stage volumes were being created and proved: it now answers as ${after.distinct ? `daemon ${after.venueDaemonId}` : describeVenueDaemon(after)}, not ${venueDaemonId}. Refusing to hand out a lease whose proof was made elsewhere.`);
30355
- }
30356
- return makeLease({
30357
- runId,
30358
- vm,
30359
- project,
30360
- zone,
30361
- stageBase,
30362
- stageVolumes,
31075
+ return finishAcquire({
30363
31076
  docker: docker3,
30364
31077
  tunnel,
30365
31078
  socketDir,
30366
31079
  socketPath,
30367
31080
  venueDaemonId,
30368
31081
  venueIdentity,
30369
- env,
30370
- io
31082
+ transport: sshStageTransport(vm, project, zone, env, io)
30371
31083
  });
31084
+ function finishAcquire(r) {
31085
+ const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
31086
+ const stageVolumes = stageVolumeNames(vm);
31087
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
31088
+ const created = docker4.sync(volumeCreateArgv(name, runId), {
31089
+ timeoutMs: VOLUME_CREATE_TIMEOUT_MS
31090
+ });
31091
+ if (created.error || created.status !== 0) {
31092
+ throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
31093
+ }
31094
+ }
31095
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
31096
+ const probe = docker4.sync(execProbeArgv(name), {
31097
+ timeoutMs: EXEC_PROBE_TIMEOUT_MS
31098
+ });
31099
+ if (probe.error || probe.status !== 0) {
31100
+ throw new HostedVenueError(`${vm} cannot execute a file written to its own Docker volume ${name}, so no install step with a native postinstall could run there. Refusing before staging anything: ${dockerDetail(probe).slice(0, 300)}`);
31101
+ }
31102
+ }
31103
+ const after = io.classifyDaemon(docker4);
31104
+ if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
31105
+ throw new HostedVenueError(`the daemon at ${socketPath2} changed while the stage volumes were being created and proved: it now answers as ${after.distinct ? `daemon ${after.venueDaemonId}` : describeVenueDaemon(after)}, not ${venueDaemonId2}. Refusing to hand out a lease whose proof was made elsewhere.`);
31106
+ }
31107
+ return makeLease({
31108
+ runId,
31109
+ vm,
31110
+ project,
31111
+ zone,
31112
+ stageBase,
31113
+ stageVolumes,
31114
+ docker: docker4,
31115
+ tunnel: r.tunnel,
31116
+ socketDir: r.socketDir,
31117
+ socketPath: socketPath2,
31118
+ venueDaemonId: venueDaemonId2,
31119
+ venueIdentity: r.venueIdentity,
31120
+ transport: r.transport,
31121
+ env,
31122
+ io
31123
+ });
31124
+ }
30372
31125
  })
30373
31126
  };
30374
31127
  }
@@ -30427,28 +31180,7 @@ function makeLease(p) {
30427
31180
  return null;
30428
31181
  };
30429
31182
  let stagedOwner;
30430
- const readStagedOwner = () => {
30431
- const res = p.io.exec(
30432
- "gcloud",
30433
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
30434
- OWNER_PROBE_TIMEOUT_MS,
30435
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
30436
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
30437
- // omitted this would ssh under the person's own credential — then report an owner
30438
- // measured against a session the run does not use.
30439
- p.env
30440
- );
30441
- if (!res.ok) {
30442
- throw new HostedVenueError(`could not read the staged tree's owner on ${p.vm}: ${execDetail(res).slice(0, 300)}. Refusing rather than falling back to this machine\u2019s uid, which would hand the guest an id with no write access to its own clone.`);
30443
- }
30444
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
30445
- const ids2 = lines.slice(-2).map((l) => Number(l));
30446
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
30447
- throw new HostedVenueError(`the venue ${p.vm} answered 'id -u && id -g' with ${JSON.stringify(res.stdout.slice(0, 120))}, which is not a uid/gid pair. Refusing rather than guessing an owner.`);
30448
- }
30449
- return { uid: ids2[0], gid: ids2[1] };
30450
- };
30451
- stagedOwner = readStagedOwner();
31183
+ stagedOwner = p.transport.readOwner();
30452
31184
  return {
30453
31185
  kind: "hosted-pool",
30454
31186
  runId: p.runId,
@@ -30483,7 +31215,7 @@ function makeLease(p) {
30483
31215
  const dir = venueProxyDir(p.stageBase);
30484
31216
  const buffer = stageProxyCode();
30485
31217
  try {
30486
- const res = await p.io.pushTree(buffer.dir, "gcloud", iapUntarProxyArgv(p.vm, p.project, p.zone, dir), STAGE_PUSH_TIMEOUT_MS, p.env);
31218
+ const res = await p.transport.pushProxy(buffer.dir, dir);
30487
31219
  if (!res.ok) {
30488
31220
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
30489
31221
  }
@@ -30493,7 +31225,7 @@ function makeLease(p) {
30493
31225
  return {
30494
31226
  dir,
30495
31227
  cleanup: () => {
30496
- p.io.exec("gcloud", iapSshArgv(p.vm, p.project, p.zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, p.env);
31228
+ p.transport.removeProxy(dir);
30497
31229
  }
30498
31230
  };
30499
31231
  },
@@ -30501,7 +31233,7 @@ function makeLease(p) {
30501
31233
  check("staging the tree");
30502
31234
  const paths = venueStagePaths(p.stageBase);
30503
31235
  const push = async (from, to) => {
30504
- const res = await p.io.pushTree(from, "gcloud", iapUntarArgv(p.vm, p.project, p.zone, to), STAGE_PUSH_TIMEOUT_MS, p.env);
31236
+ const res = await p.transport.push(from, to);
30505
31237
  if (!res.ok) {
30506
31238
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
30507
31239
  }
@@ -30659,7 +31391,7 @@ function makeLease(p) {
30659
31391
  }
30660
31392
  };
30661
31393
  }
30662
- var SSH_READY_BUDGET_MS, SSH_PROBE_INTERVAL_MS, SSH_PROBE_TIMEOUT_MS, TUNNEL_BUDGET_MS, TUNNEL_POLL_INTERVAL_MS, GOOGLE_JWKS_URL, JWKS_FETCH_TIMEOUT_MS, CREDENTIAL_QUERY_PARAM, UNDECODABLE, STAGE_PUSH_TIMEOUT_MS, DISPATCHED_PROBE_TIMEOUT_MS, DISPATCHED_STATUS_ARGV, DISPATCHED_GIT_CANDIDATES, PROXY_CLEANUP_TIMEOUT_MS, OWNER_PROBE_TIMEOUT_MS, BOOT_TIMEOUT_MS, MKDIR_TIMEOUT_MS, DELETE_TIMEOUT_MS, LOCAL_GCLOUD_TIMEOUT_MS, SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS, SOCKET_DIR_PREFIX, VENUE_SOCKET_NAME, HostedVenueError, VENUE_GCLOUD_CONFIG, SERVICE_ACCOUNT_SUFFIX, GCLOUD_PRINCIPAL_OVERRIDES, defaultHostedVenueIo, VENUE_SSH_USER, GCE_METADATA_IDENTITY_URL, COMPACT_JWT, STAGE_HELPER_IMAGE, VOLUME_CREATE_TIMEOUT_MS, EXEC_PROBE_TIMEOUT_MS, POPULATE_TIMEOUT_MS, STAGE_VOLUME_LABEL_KEY, STAGE_PROOF_PREFIX, IAP_NOT_READY, IAP_BACKEND_UNREACHABLE, IAP_DENIED, TERMINAL_GCP, INSTANCE_NOT_RUNNING, PREEMPTED, HOST_KEY_MISMATCH, SSH_KEY_NOT_READY, DAEMON_NOT_READY, SSH_NOT_ANSWERING;
31394
+ var SSH_READY_BUDGET_MS, SSH_PROBE_INTERVAL_MS, SSH_PROBE_TIMEOUT_MS, TUNNEL_BUDGET_MS, TUNNEL_POLL_INTERVAL_MS, GOOGLE_JWKS_URL, JWKS_FETCH_TIMEOUT_MS, CREDENTIAL_QUERY_PARAM, UNDECODABLE, STAGE_PUSH_TIMEOUT_MS, DISPATCHED_PROBE_TIMEOUT_MS, DISPATCHED_STATUS_ARGV, DISPATCHED_GIT_CANDIDATES, PROXY_CLEANUP_TIMEOUT_MS, OWNER_PROBE_TIMEOUT_MS, BOOT_TIMEOUT_MS, MKDIR_TIMEOUT_MS, DELETE_TIMEOUT_MS, LOCAL_GCLOUD_TIMEOUT_MS, SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS, SOCKET_DIR_PREFIX, VENUE_SOCKET_NAME, HostedVenueError, VENUE_GCLOUD_CONFIG, SERVICE_ACCOUNT_SUFFIX, GCLOUD_PRINCIPAL_OVERRIDES, defaultHostedVenueIo, VENUE_SSH_USER, GCE_METADATA_IDENTITY_URL, COMPACT_JWT, STAGE_HELPER_IMAGE, VOLUME_CREATE_TIMEOUT_MS, EXEC_PROBE_TIMEOUT_MS, POPULATE_TIMEOUT_MS, STAGE_VOLUME_LABEL_KEY, STAGE_PROOF_PREFIX, IAP_NOT_READY, IAP_BACKEND_UNREACHABLE, IAP_DENIED, TERMINAL_GCP, INSTANCE_NOT_RUNNING, PREEMPTED, HOST_KEY_MISMATCH, SSH_KEY_NOT_READY, DAEMON_NOT_READY, SSH_NOT_ANSWERING, CS_ATTEST_PORT, CS_DOCKER_PORT, CS_READY_BUDGET_MS, CS_ATTEST_TIMEOUT_MS, CS_PULL_TIMEOUT_MS, CS_ATTEST_INTERVAL_MS, OPENSSL_TIMEOUT_MS, CS_RUN_ARGS, CS_GUEST_USER, CS_STAGE_ROOT, CLIENT_CERT_STAND_IN;
30663
31395
  var init_hostedVenue = __esm({
30664
31396
  "../../packages/envrun/dist/hostedVenue.js"() {
30665
31397
  "use strict";
@@ -30780,7 +31512,55 @@ var init_hostedVenue = __esm({
30780
31512
  throw new Error("JWKS fetch returned a body with no keys array");
30781
31513
  }
30782
31514
  return body;
30783
- }
31515
+ },
31516
+ fetchConfidentialSpaceJwks: async () => {
31517
+ const getJson = async (url, what) => {
31518
+ const res = await globalThis.fetch(url, {
31519
+ signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
31520
+ });
31521
+ if (!res.ok) {
31522
+ throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
31523
+ }
31524
+ return res.json();
31525
+ };
31526
+ const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
31527
+ const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
31528
+ if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
31529
+ throw new Error("Confidential Space discovery names no https jwks_uri");
31530
+ }
31531
+ const body = await getJson(jwksUri, "key set");
31532
+ if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
31533
+ throw new Error("Confidential Space key set has no keys array");
31534
+ }
31535
+ return body;
31536
+ },
31537
+ freeLocalPorts: async (n) => {
31538
+ const servers = [];
31539
+ try {
31540
+ for (let i = 0; i < n; i += 1) {
31541
+ const server = createServer();
31542
+ servers.push(server);
31543
+ await new Promise((resolve4, reject) => {
31544
+ server.once("error", reject);
31545
+ server.listen(0, "localhost", () => {
31546
+ resolve4();
31547
+ });
31548
+ });
31549
+ }
31550
+ return servers.map((s) => s.address().port);
31551
+ } finally {
31552
+ await Promise.all(servers.map((s) => new Promise((resolve4) => {
31553
+ s.close(() => {
31554
+ resolve4();
31555
+ });
31556
+ })));
31557
+ }
31558
+ },
31559
+ writeFile: (path5, text) => {
31560
+ writeFileSync13(path5, text, { mode: 384, flag: "wx" });
31561
+ },
31562
+ fetchAttestation: (req) => fetchAttestationOverTls(req),
31563
+ dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
30784
31564
  };
30785
31565
  VENUE_SSH_USER = "th-runner";
30786
31566
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -30801,6 +31581,17 @@ var init_hostedVenue = __esm({
30801
31581
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
30802
31582
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
30803
31583
  SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
31584
+ CS_ATTEST_PORT = 8443;
31585
+ CS_DOCKER_PORT = 2376;
31586
+ CS_READY_BUDGET_MS = 48e4;
31587
+ CS_ATTEST_TIMEOUT_MS = 15e3;
31588
+ CS_PULL_TIMEOUT_MS = 6e5;
31589
+ CS_ATTEST_INTERVAL_MS = 5e3;
31590
+ OPENSSL_TIMEOUT_MS = 3e4;
31591
+ CS_RUN_ARGS = ["--oom-score-adj", "1000"];
31592
+ CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
31593
+ CS_STAGE_ROOT = "/tmp";
31594
+ CLIENT_CERT_STAND_IN = "stand-in-certificate";
30804
31595
  }
30805
31596
  });
30806
31597
 
@@ -31700,10 +32491,10 @@ var init_references = __esm({
31700
32491
 
31701
32492
  // ../../packages/envspec/dist/repo.js
31702
32493
  import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
31703
- import { join as join25, relative as relative2, sep as sep4 } from "path";
32494
+ import { join as join25, relative as relative2, sep as sep5 } from "path";
31704
32495
  function createRepoReader(repoPath) {
31705
32496
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join25(repoPath, relativePath);
31706
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
32497
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
31707
32498
  const readText = (relativePath) => {
31708
32499
  try {
31709
32500
  return readFileSync12(resolveIn(relativePath), "utf8");
@@ -32346,13 +33137,16 @@ function git(repoDir, args, allowNonZero = false) {
32346
33137
  }
32347
33138
  return res.stdout ?? "";
32348
33139
  }
32349
- function collectWorkingDiff(repoDir) {
33140
+ function collectWorkingDiff(repoDir, opts = {}) {
32350
33141
  if (!existsSync12(join26(repoDir, ".git"))) {
32351
33142
  throw new ThRunError(`${repoDir} is not a git checkout (no .git). \`th run\` ships the working diff, so it needs a repository to read one from.`);
32352
33143
  }
32353
33144
  const headSha = git(repoDir, ["rev-parse", "HEAD"]).trim();
32354
- const tracked = git(repoDir, ["diff", "HEAD", "--binary"]);
32355
- const trackedChanged = git(repoDir, ["diff", "HEAD", "--name-only"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
33145
+ const diffBase = opts.base === void 0 ? headSha : assertDiffBase(repoDir, opts.base);
33146
+ const trackedArgs = opts.base === void 0 ? ["diff", "HEAD"] : ["diff", diffBase];
33147
+ const pathsEnd = opts.base === void 0 ? [] : ["--"];
33148
+ const tracked = git(repoDir, [...trackedArgs, "--binary", ...pathsEnd]);
33149
+ const trackedChanged = git(repoDir, [...trackedArgs, "--name-only", ...pathsEnd]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
32356
33150
  const untracked = git(repoDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
32357
33151
  const parts = [];
32358
33152
  if (tracked.trim() !== "")
@@ -32362,7 +33156,17 @@ function collectWorkingDiff(repoDir) {
32362
33156
  if (one.trim() !== "")
32363
33157
  parts.push(one.replace(/\n*$/, "\n"));
32364
33158
  }
32365
- return { patch: parts.join(""), headSha, trackedChanged, untracked };
33159
+ return { patch: parts.join(""), headSha, diffBase, trackedChanged, untracked };
33160
+ }
33161
+ function assertDiffBase(repoDir, base) {
33162
+ if (!FULL_COMMIT_ID.test(base)) {
33163
+ throw new ThRunError(`the diff base must be a full 40-character commit id, got ${JSON.stringify(base)}.`);
33164
+ }
33165
+ const resolved = git(repoDir, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`], true);
33166
+ if (resolved.trim() !== base) {
33167
+ throw new ThRunError(`the diff base ${base} is not a commit in ${repoDir}.`);
33168
+ }
33169
+ return base;
32366
33170
  }
32367
33171
  function isLocalPath(p) {
32368
33172
  return !UNC_PATH.test(p) && (p.startsWith("/") || WINDOWS_ABSOLUTE.test(p));
@@ -32934,8 +33738,9 @@ async function runVerification(req, ctx) {
32934
33738
  let diff = null;
32935
33739
  if (source.kind === "working-diff") {
32936
33740
  progress("collect", `reading the working diff from ${source.localRepoDir}`);
32937
- diff = collectWorkingDiff(source.localRepoDir);
32938
- progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes`);
33741
+ diff = collectWorkingDiff(source.localRepoDir, source.diffBase === void 0 ? {} : { base: source.diffBase });
33742
+ const from = source.diffBase === void 0 ? "" : `, from delivered baseline ${diff.diffBase.slice(0, 12)}`;
33743
+ progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes${from}`);
32939
33744
  } else {
32940
33745
  progress("collect", "dispatched run: the tree is the stored commit itself");
32941
33746
  }
@@ -33137,7 +33942,7 @@ async function runVerification(req, ctx) {
33137
33942
  await releaseWithoutThrowing(lease, progress);
33138
33943
  }
33139
33944
  }
33140
- var ThRunError, OUTPUT_TAIL_BYTES, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
33945
+ var ThRunError, OUTPUT_TAIL_BYTES, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
33141
33946
  var init_thrun = __esm({
33142
33947
  "../../packages/envrun/dist/thrun.js"() {
33143
33948
  "use strict";
@@ -33155,6 +33960,7 @@ var init_thrun = __esm({
33155
33960
  ThRunError = class extends Error {
33156
33961
  };
33157
33962
  OUTPUT_TAIL_BYTES = 4e3;
33963
+ FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
33158
33964
  ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["https", "http", "ssh", "git"]);
33159
33965
  SCP_STYLE = /^[A-Za-z0-9._~+-]+@[A-Za-z0-9._-]+:[^:]/;
33160
33966
  URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
@@ -34013,6 +34819,7 @@ __export(dist_exports, {
34013
34819
  ALEMBIC_IMAGE: () => ALEMBIC_IMAGE,
34014
34820
  ATTEST_REFUSAL_REASONS: () => ATTEST_REFUSAL_REASONS,
34015
34821
  BOOKKEEPING_TABLES: () => BOOKKEEPING_TABLES,
34822
+ CONFIDENTIAL_SPACE_MACHINE_TYPE: () => CONFIDENTIAL_SPACE_MACHINE_TYPE,
34016
34823
  CONTAINMENT_UNAVAILABLE_PREFIX: () => CONTAINMENT_UNAVAILABLE_PREFIX,
34017
34824
  CloneUnavailableError: () => CloneUnavailableError,
34018
34825
  DAEMON_FACTS_FORMAT: () => DAEMON_FACTS_FORMAT,
@@ -34154,6 +34961,7 @@ __export(dist_exports, {
34154
34961
  toolingImageFor: () => toolingImageFor,
34155
34962
  unquoteDiffPath: () => unquoteDiffPath,
34156
34963
  venueGcloudEnv: () => venueGcloudEnv,
34964
+ venueImageFromEnv: () => venueImageFromEnv,
34157
34965
  venueStagePaths: () => venueStagePaths,
34158
34966
  verifyWorkingDiff: () => verifyWorkingDiff
34159
34967
  });
@@ -34221,7 +35029,7 @@ __export(cache_store_exports, {
34221
35029
  updateIndexCache: () => updateIndexCache,
34222
35030
  writeIndexCache: () => writeIndexCache
34223
35031
  });
34224
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync13, renameSync as renameSync5 } from "fs";
35032
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync14, renameSync as renameSync5 } from "fs";
34225
35033
  import { join as join27 } from "path";
34226
35034
  import { homedir as homedir13 } from "os";
34227
35035
  function readCacheEntry() {
@@ -34241,7 +35049,7 @@ function updateIndexCache(patch) {
34241
35049
  ts: Date.now()
34242
35050
  };
34243
35051
  const tmp = `${INDEX_CACHE_FILE}.${process.pid}.${tmpCounter++}.tmp`;
34244
- writeFileSync13(tmp, JSON.stringify(entry), "utf8");
35052
+ writeFileSync14(tmp, JSON.stringify(entry), "utf8");
34245
35053
  renameSync5(tmp, INDEX_CACHE_FILE);
34246
35054
  return entry;
34247
35055
  }
@@ -34454,7 +35262,7 @@ var init_audit2 = __esm({
34454
35262
  init_src();
34455
35263
  import {
34456
35264
  readFileSync as readFileSync14,
34457
- writeFileSync as writeFileSync14,
35265
+ writeFileSync as writeFileSync15,
34458
35266
  mkdirSync as mkdirSync7,
34459
35267
  mkdtempSync as mkdtempSync5,
34460
35268
  renameSync as renameSync6,
@@ -34465,7 +35273,7 @@ import {
34465
35273
  readdirSync as readdirSync3
34466
35274
  } from "fs";
34467
35275
  import { join as join28, dirname as dirname8, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
34468
- import { createHash as createHash8 } from "crypto";
35276
+ import { createHash as createHash10 } from "crypto";
34469
35277
  import { homedir as homedir14, hostname as osHostname } from "os";
34470
35278
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
34471
35279
  import { promisify as promisify3 } from "util";
@@ -34928,7 +35736,7 @@ function markClaimNudged(id) {
34928
35736
  const ids2 = readNudgedClaimIds();
34929
35737
  ids2.add(id);
34930
35738
  ensureStateDir(TERMINALHIRE_DIR11);
34931
- writeFileSync14(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35739
+ writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
34932
35740
  } catch {
34933
35741
  }
34934
35742
  }
@@ -36169,7 +36977,7 @@ async function registerFounderClaim(b) {
36169
36977
  let clearedLocalCredential = false;
36170
36978
  let refusedForPurpose = false;
36171
36979
  let preserveBackgroundToken = false;
36172
- const refuse3 = (reason) => {
36980
+ const refuse4 = (reason) => {
36173
36981
  console.error(
36174
36982
  `
36175
36983
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -36194,7 +37002,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36194
37002
  try {
36195
37003
  expectLogin = await localLoginForPaidBrowserClaim();
36196
37004
  } catch (err) {
36197
- refuse3(
37005
+ refuse4(
36198
37006
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
36199
37007
  Run \`terminalhire login\`, then retry this claim. Browser verification was
36200
37008
  not opened and no claim was sent.`
@@ -36210,16 +37018,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
36210
37018
  claimRef: opportunityShortToken(b.bountyId)
36211
37019
  });
36212
37020
  if (!proofToken) {
36213
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
37021
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
36214
37022
  }
36215
37023
  const verifiedLogin = proofTokenLogin(proofToken);
36216
37024
  if (!verifiedLogin) {
36217
- refuse3(
37025
+ refuse4(
36218
37026
  "the browser verification response did not name a GitHub account, so it could not be compared with this machine\u2019s sign-in. Nothing was sent; retry the claim."
36219
37027
  );
36220
37028
  }
36221
37029
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
36222
- refuse3(
37030
+ refuse4(
36223
37031
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
36224
37032
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
36225
37033
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -36251,7 +37059,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36251
37059
  try {
36252
37060
  res = await sendRegistration(true);
36253
37061
  } catch (err) {
36254
- refuse3(
37062
+ refuse4(
36255
37063
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36256
37064
  );
36257
37065
  }
@@ -36302,7 +37110,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36302
37110
  try {
36303
37111
  res = await sendRegistration(true);
36304
37112
  } catch (err) {
36305
- refuse3(
37113
+ refuse4(
36306
37114
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36307
37115
  );
36308
37116
  }
@@ -36313,14 +37121,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
36313
37121
  res = await sendRegistration(false);
36314
37122
  refusalBody = await readRefusal(res);
36315
37123
  } catch (err) {
36316
- refuse3(
37124
+ refuse4(
36317
37125
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36318
37126
  );
36319
37127
  }
36320
37128
  }
36321
37129
  if (!res.ok) {
36322
37130
  const detail = refusalBody?.message || refusalBody?.error || "";
36323
- refuse3(
37131
+ refuse4(
36324
37132
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
36325
37133
  );
36326
37134
  }
@@ -36331,7 +37139,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36331
37139
  body = null;
36332
37140
  }
36333
37141
  if (!body || body.ok !== true) {
36334
- refuse3("malformed registration response from the server.");
37142
+ refuse4("malformed registration response from the server.");
36335
37143
  }
36336
37144
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
36337
37145
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -37772,7 +38580,7 @@ function writeSliceFiles(destDir, files) {
37772
38580
  if (typeof f.content === "string") {
37773
38581
  const abs = join28(destDir, f.path);
37774
38582
  mkdirSync7(dirname8(abs), { recursive: true });
37775
- writeFileSync14(abs, f.content, "utf8");
38583
+ writeFileSync15(abs, f.content, "utf8");
37776
38584
  written.push(f.path);
37777
38585
  } else {
37778
38586
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -37826,7 +38634,7 @@ function ensureExcludedPackDir(destDir) {
37826
38634
  const existing = existsSync13(excludeFile) ? readFileSync14(excludeFile, "utf8") : "";
37827
38635
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
37828
38636
  mkdirSync7(dirname8(excludeFile), { recursive: true });
37829
- writeFileSync14(
38637
+ writeFileSync15(
37830
38638
  excludeFile,
37831
38639
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
37832
38640
  `,
@@ -37842,23 +38650,33 @@ function writePackFile(destDir, relPath, content, what) {
37842
38650
  try {
37843
38651
  const abs = join28(destDir, relPath);
37844
38652
  mkdirSync7(dirname8(abs), { recursive: true });
37845
- writeFileSync14(abs, content, { encoding: "utf8", flag: "wx" });
38653
+ writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
37846
38654
  } catch (err) {
37847
38655
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
37848
38656
  }
37849
38657
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
37850
38658
  }
37851
38659
  function sha256OfUtf8(content) {
37852
- return createHash8("sha256").update(content, "utf8").digest("hex");
38660
+ return createHash10("sha256").update(content, "utf8").digest("hex");
37853
38661
  }
37854
- function writeWorkspacePack(destDir, spec, claim) {
38662
+ function writeWorkspacePack(destDir, spec, claim, delivery) {
38663
+ if (delivery !== "full" && delivery !== "sparse") {
38664
+ throw new TypeError(
38665
+ `writeWorkspacePack: delivery must be 'full' or 'sparse', got ${String(delivery)}`
38666
+ );
38667
+ }
37855
38668
  const gate = ensureExcludedPackDir(destDir);
37856
38669
  if (!gate.ok) {
37857
38670
  const refused = { written: false, reason: gate.reason };
37858
38671
  return { brief: refused, verify: refused, agents: refused };
37859
38672
  }
37860
38673
  const brief = typeof spec !== "string" || spec.trim() === "" ? { written: false, reason: "the server sent no task for this posting" } : writePackFile(destDir, BRIEF_REL_PATH, spec, "task");
37861
- const verify = writePackFile(destDir, VERIFY_REL_PATH, renderVerifyDoc(claim), "verify note");
38674
+ const verify = writePackFile(
38675
+ destDir,
38676
+ VERIFY_REL_PATH,
38677
+ renderVerifyDoc(claim, delivery),
38678
+ "verify note"
38679
+ );
37862
38680
  const agents = writePackFile(
37863
38681
  destDir,
37864
38682
  AGENTS_REL_PATH,
@@ -37872,20 +38690,30 @@ function packSafeId(claim) {
37872
38690
  const id = String(claim?.id ?? "");
37873
38691
  return PACK_SAFE_ID.test(id) ? id : "<your claim id \u2014 see: terminalhire claim list>";
37874
38692
  }
37875
- function renderVerifyDoc(claim) {
38693
+ function renderVerifyDoc(claim, delivery) {
37876
38694
  const id = packSafeId(claim);
38695
+ const runStep2 = delivery === "full" ? ` ${nextStep("terminalhire run")}
38696
+
38697
+ It tests every change since the delivered baseline, committed or not, which
38698
+ is what submit sends once it is all committed.` : ` ${nextStep("terminalhire run --target <clone URL of the full repository>")}
38699
+
38700
+ It tests every change since the delivered baseline, committed or not, which
38701
+ is what submit sends once it is all committed. This delivery contains only the
38702
+ files you were granted, so the local run needs the whole repository; if you
38703
+ don't have access to it, skip to step 4, because the poster-side run in step 5
38704
+ is the verdict that counts.`;
37877
38705
  return `# Verifying claim ${id}
37878
38706
 
37879
38707
  This workspace was delivered by terminalhire for a posting. "Done" is
37880
38708
  judged on the diff: the submitted patch is the change from the delivered
37881
38709
  baseline (this repo's root commit) to HEAD, so only committed, tracked changes
37882
- count.
38710
+ count. Submit needs a clean tree; run does not.
37883
38711
 
37884
38712
  1. Read the poster's task first, when there is one: ${BRIEF_REL_PATH}
37885
38713
  2. Work on the claim branch this delivery checked out, committing as you go.
37886
38714
  3. To verify locally in terminalhire's sandboxed runner, from this directory:
37887
38715
 
37888
- terminalhire run
38716
+ ${runStep2}
37889
38717
 
37890
38718
  4. Submit \u2014 the only step that sends anything off this machine. It prints what it
37891
38719
  is about to send first, and asks y/N only at an interactive terminal:
@@ -38264,11 +39092,13 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
38264
39092
  );
38265
39093
  process.exit(1);
38266
39094
  }
38267
- const pack = writeWorkspacePack(dest, body.spec, claim);
39095
+ const pack = writeWorkspacePack(dest, body.spec, claim, "full");
38268
39096
  const working = claims.updateClaim(claim.id, {
38269
39097
  worktreePath: dest,
38270
39098
  branch,
38271
39099
  state: "working",
39100
+ // `terminalhire run` clones this commit when run from the workspace (TERM-1098).
39101
+ baseSha,
38272
39102
  workspacePack: {
38273
39103
  brief: pack.brief.written === true,
38274
39104
  verify: pack.verify.written === true,
@@ -38424,7 +39254,7 @@ async function attemptSliceDelivery(id, flags = {}) {
38424
39254
  let pack;
38425
39255
  try {
38426
39256
  await sh("git", ["-C", dest, "init"]);
38427
- pack = writeWorkspacePack(dest, body.spec, claim);
39257
+ pack = writeWorkspacePack(dest, body.spec, claim, "sparse");
38428
39258
  await sh("git", ["-C", dest, "add", "-A"]);
38429
39259
  await sh("git", [
38430
39260
  "-C",
@@ -38461,6 +39291,11 @@ async function attemptSliceDelivery(id, flags = {}) {
38461
39291
  worktreePath: dest,
38462
39292
  branch,
38463
39293
  state: "working",
39294
+ // The commit these files were cut from, so `terminalhire run` can clone it unasked
39295
+ // (TERM-1098). Recorded only when it is a full sha: a slice confirmed before the
39296
+ // server kept one answers null, and a value of any other shape is not a commit we
39297
+ // could clone, so neither is written and `run` asks for `--sha` instead.
39298
+ ...typeof body.baseSha === "string" && /^[0-9a-f]{40}$/.test(body.baseSha) ? { baseSha: body.baseSha } : {},
38464
39299
  workspacePack: {
38465
39300
  brief: pack.brief.written === true,
38466
39301
  verify: pack.verify.written === true,
@@ -39193,7 +40028,7 @@ function readClaimPushMarker() {
39193
40028
  }
39194
40029
  function writeClaimPushMarker(marker) {
39195
40030
  ensureStateDir(TERMINALHIRE_DIR11);
39196
- writeFileSync14(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
40031
+ writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
39197
40032
  }
39198
40033
  function clearClaimPushMarker() {
39199
40034
  try {