terminalhire 0.42.20 → 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
  },
@@ -28559,13 +28585,192 @@ var init_gceIdentity = __esm({
28559
28585
 
28560
28586
  // ../../packages/attest/dist/confidentialSpace.js
28561
28587
  import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
28562
- var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL;
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;
28563
28766
  var init_confidentialSpace = __esm({
28564
28767
  "../../packages/attest/dist/confidentialSpace.js"() {
28565
28768
  "use strict";
28566
28769
  init_gceIdentity();
28567
28770
  CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
28568
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";
28569
28774
  }
28570
28775
  });
28571
28776
 
@@ -28908,50 +29113,50 @@ function toTestRunResult(result, outputSha256) {
28908
29113
  function toAcceptancePredicate(pair, opts = {}) {
28909
29114
  const { baseline, patched } = pair;
28910
29115
  if (baseline.runId === patched.runId) {
28911
- 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.`);
28912
29117
  }
28913
29118
  if (patched.status === "refused") {
28914
- 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`);
28915
29120
  }
28916
29121
  if (patched.outcome === null) {
28917
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
29122
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
28918
29123
  }
28919
29124
  if (baseline.status === "refused") {
28920
- 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`);
28921
29126
  }
28922
29127
  if (baseline.outcome === null) {
28923
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
29128
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
28924
29129
  }
28925
29130
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
28926
- 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.`);
28927
29132
  }
28928
29133
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
28929
29134
  if (budget === null) {
28930
- 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.`);
28931
29136
  }
28932
29137
  if (patched.testCommand === null) {
28933
- 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");
28934
29139
  }
28935
29140
  if (baseline.testCommand !== patched.testCommand) {
28936
- 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.`);
28937
29142
  }
28938
29143
  if (baseline.targetSha !== patched.targetSha) {
28939
- 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}`);
28940
29145
  }
28941
29146
  if (patched.treeDigest === null || patched.treeDigest === "") {
28942
- 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");
28943
29148
  }
28944
29149
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
28945
- 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");
28946
29151
  }
28947
29152
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
28948
- 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");
28949
29154
  }
28950
29155
  if (baseline.treeDigest === patched.treeDigest) {
28951
- 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`);
28952
29157
  }
28953
29158
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
28954
- 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.`);
28955
29160
  }
28956
29161
  for (const [side, outcome, counts, exitCode] of [
28957
29162
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -28959,7 +29164,7 @@ function toAcceptancePredicate(pair, opts = {}) {
28959
29164
  ]) {
28960
29165
  const why = contradicts(outcome, counts, exitCode);
28961
29166
  if (why !== null) {
28962
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
29167
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
28963
29168
  }
28964
29169
  }
28965
29170
  for (const [side, source] of [
@@ -28968,44 +29173,44 @@ function toAcceptancePredicate(pair, opts = {}) {
28968
29173
  ]) {
28969
29174
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
28970
29175
  if (why !== null)
28971
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
29176
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
28972
29177
  }
28973
29178
  if (patched.testCommandSource === "developer-declared") {
28974
- 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)");
28975
29180
  }
28976
29181
  if (baseline.testCommandSource !== patched.testCommandSource) {
28977
- 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.`);
28978
29183
  }
28979
29184
  if (baseline.containerImage !== patched.containerImage) {
28980
- 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.`);
28981
29186
  }
28982
29187
  if (patched.containerImage === null || patched.containerImage === "") {
28983
- 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");
28984
29189
  }
28985
29190
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
28986
- 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.");
28987
29192
  }
28988
29193
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
28989
- 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.`);
28990
29195
  }
28991
29196
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
28992
- 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.`);
28993
29198
  }
28994
29199
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
28995
- 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.");
28996
29201
  }
28997
29202
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
28998
- 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.`);
28999
29204
  }
29000
29205
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
29001
- 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.`);
29002
29207
  }
29003
29208
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
29004
- 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.`);
29005
29210
  }
29006
29211
  const claimedDigest = imageDigestOf(patched.containerImage);
29007
29212
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
29008
- 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.`);
29009
29214
  }
29010
29215
  return {
29011
29216
  ok: true,
@@ -29032,7 +29237,7 @@ function signRunStatement(predicate, privateKey, keyid) {
29032
29237
  const statement = createAcceptanceStatement(predicate);
29033
29238
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
29034
29239
  }
29035
- 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;
29036
29241
  var init_attestation2 = __esm({
29037
29242
  "../../packages/envrun/dist/attestation.js"() {
29038
29243
  "use strict";
@@ -29136,7 +29341,7 @@ var init_attestation2 = __esm({
29136
29341
  "outcome-contradicts-counts",
29137
29342
  "test-command-origin-unattested"
29138
29343
  ];
29139
- refuse2 = (reason, detail) => ({
29344
+ refuse3 = (reason, detail) => ({
29140
29345
  ok: false,
29141
29346
  reason,
29142
29347
  detail
@@ -29547,6 +29752,22 @@ function bootImageFlags(p) {
29547
29752
  }
29548
29753
  const workloadImage = p.workloadImage;
29549
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
+ ];
29550
29771
  return [
29551
29772
  // The production family. The debug family reports `dbgstat: enabled`, which the
29552
29773
  // intake's verifier refuses, so a debug boot could never produce a run we record.
@@ -29559,7 +29780,15 @@ function bootImageFlags(p) {
29559
29780
  // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
29560
29781
  // combination is phase 5's measurement.
29561
29782
  "--maintenance-policy=TERMINATE",
29562
- `--metadata=tee-image-reference=${workloadImage},tee-container-log-redirect=true`
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"
29563
29792
  ];
29564
29793
  }
29565
29794
  function assertInstanceIdentity(fn, id) {
@@ -29633,7 +29862,13 @@ function gcpBootArgv(p) {
29633
29862
  // expectation uses (TERM-832): two inline spellings of one value is how
29634
29863
  // the boot path and the verifier drift apart silently.
29635
29864
  `--service-account=${venueIdentityEmail(p.project)}`,
29636
- "--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"],
29637
29872
  "--shielded-secure-boot",
29638
29873
  "--shielded-vtpm",
29639
29874
  "--shielded-integrity-monitoring",
@@ -29666,7 +29901,7 @@ function gcpDeleteArgv(p) {
29666
29901
  "--quiet"
29667
29902
  ];
29668
29903
  }
29669
- 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, 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;
29670
29905
  var init_gcpPlacement = __esm({
29671
29906
  "../../packages/envrun/dist/gcpPlacement.js"() {
29672
29907
  "use strict";
@@ -29685,6 +29920,8 @@ var init_gcpPlacement = __esm({
29685
29920
  GCP_RUN_LABEL_KEY = "th-run";
29686
29921
  CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
29687
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;
29688
29925
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
29689
29926
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
29690
29927
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -29726,7 +29963,10 @@ var init_emptyGitConfig = __esm({
29726
29963
 
29727
29964
  // ../../packages/envrun/dist/hostedVenue.js
29728
29965
  import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
29729
- 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";
29730
29970
  import { join as join24 } from "path";
29731
29971
  import { tmpdir as tmpdir3 } from "os";
29732
29972
  function credentialInGitConfig(text) {
@@ -29928,6 +30168,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
29928
30168
  });
29929
30169
  });
29930
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
+ }
29931
30254
  function quoteForRemoteShell(arg) {
29932
30255
  return `'${arg.replace(/'/g, "'\\''")}'`;
29933
30256
  }
@@ -30262,27 +30585,370 @@ ${res.stderr}`);
30262
30585
  function manualDeleteCommand(vm, project, zone) {
30263
30586
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
30264
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
+ }
30265
30917
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30266
30918
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
30267
30919
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
30268
30920
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
30269
30921
  const keyFile = resolveServiceAccountKeyFile(opts);
30270
30922
  const image = venueImageFromEnv(process.env);
30923
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
30271
30924
  const env = venueGcloudEnv(gcloudConfig);
30272
30925
  return {
30273
30926
  kind: "hosted-pool",
30274
30927
  acquire: (runId) => acquireTransactionally(async (allocated) => {
30275
30928
  const vm = venueInstanceName(runId);
30276
30929
  const stageBase = `/tmp/th-stage-${runId}`;
30277
- const bootArgv = gcpBootArgv({
30930
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
30278
30931
  vmName: vm,
30279
30932
  project,
30280
30933
  zone,
30281
30934
  runId,
30282
- machineType: opts.machineType ?? (image.venueImage === "confidential-space" ? CONFIDENTIAL_SPACE_MACHINE_TYPE : DEFAULT_GCP_MACHINE_TYPE),
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,
30283
30945
  ...image
30284
- });
30946
+ };
30947
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
30285
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));
30286
30952
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
30287
30953
  let instanceState = "unknown";
30288
30954
  allocated.onRollback(() => {
@@ -30298,6 +30964,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30298
30964
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
30299
30965
  }
30300
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
+ }
30301
30981
  const deadline = io.now() + SSH_READY_BUDGET_MS;
30302
30982
  let last = { retryable: true, reason: "no probe ran" };
30303
30983
  let identity = null;
@@ -30392,43 +31072,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30392
31072
  if (!prepared.ok) {
30393
31073
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
30394
31074
  }
30395
- const stageVolumes = stageVolumeNames(vm);
30396
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30397
- const created = docker3.sync(volumeCreateArgv(name, runId), {
30398
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
30399
- });
30400
- if (created.error || created.status !== 0) {
30401
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
30402
- }
30403
- }
30404
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30405
- const probe = docker3.sync(execProbeArgv(name), {
30406
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
30407
- });
30408
- if (probe.error || probe.status !== 0) {
30409
- 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)}`);
30410
- }
30411
- }
30412
- const after = io.classifyDaemon(docker3);
30413
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
30414
- 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.`);
30415
- }
30416
- return makeLease({
30417
- runId,
30418
- vm,
30419
- project,
30420
- zone,
30421
- stageBase,
30422
- stageVolumes,
31075
+ return finishAcquire({
30423
31076
  docker: docker3,
30424
31077
  tunnel,
30425
31078
  socketDir,
30426
31079
  socketPath,
30427
31080
  venueDaemonId,
30428
31081
  venueIdentity,
30429
- env,
30430
- io
31082
+ transport: sshStageTransport(vm, project, zone, env, io)
30431
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
+ }
30432
31125
  })
30433
31126
  };
30434
31127
  }
@@ -30487,28 +31180,7 @@ function makeLease(p) {
30487
31180
  return null;
30488
31181
  };
30489
31182
  let stagedOwner;
30490
- const readStagedOwner = () => {
30491
- const res = p.io.exec(
30492
- "gcloud",
30493
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
30494
- OWNER_PROBE_TIMEOUT_MS,
30495
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
30496
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
30497
- // omitted this would ssh under the person's own credential — then report an owner
30498
- // measured against a session the run does not use.
30499
- p.env
30500
- );
30501
- if (!res.ok) {
30502
- 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.`);
30503
- }
30504
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
30505
- const ids2 = lines.slice(-2).map((l) => Number(l));
30506
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
30507
- 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.`);
30508
- }
30509
- return { uid: ids2[0], gid: ids2[1] };
30510
- };
30511
- stagedOwner = readStagedOwner();
31183
+ stagedOwner = p.transport.readOwner();
30512
31184
  return {
30513
31185
  kind: "hosted-pool",
30514
31186
  runId: p.runId,
@@ -30543,7 +31215,7 @@ function makeLease(p) {
30543
31215
  const dir = venueProxyDir(p.stageBase);
30544
31216
  const buffer = stageProxyCode();
30545
31217
  try {
30546
- 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);
30547
31219
  if (!res.ok) {
30548
31220
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
30549
31221
  }
@@ -30553,7 +31225,7 @@ function makeLease(p) {
30553
31225
  return {
30554
31226
  dir,
30555
31227
  cleanup: () => {
30556
- 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);
30557
31229
  }
30558
31230
  };
30559
31231
  },
@@ -30561,7 +31233,7 @@ function makeLease(p) {
30561
31233
  check("staging the tree");
30562
31234
  const paths = venueStagePaths(p.stageBase);
30563
31235
  const push = async (from, to) => {
30564
- 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);
30565
31237
  if (!res.ok) {
30566
31238
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
30567
31239
  }
@@ -30719,7 +31391,7 @@ function makeLease(p) {
30719
31391
  }
30720
31392
  };
30721
31393
  }
30722
- 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;
30723
31395
  var init_hostedVenue = __esm({
30724
31396
  "../../packages/envrun/dist/hostedVenue.js"() {
30725
31397
  "use strict";
@@ -30840,7 +31512,55 @@ var init_hostedVenue = __esm({
30840
31512
  throw new Error("JWKS fetch returned a body with no keys array");
30841
31513
  }
30842
31514
  return body;
30843
- }
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 })
30844
31564
  };
30845
31565
  VENUE_SSH_USER = "th-runner";
30846
31566
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -30861,6 +31581,17 @@ var init_hostedVenue = __esm({
30861
31581
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
30862
31582
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
30863
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";
30864
31595
  }
30865
31596
  });
30866
31597
 
@@ -31760,10 +32491,10 @@ var init_references = __esm({
31760
32491
 
31761
32492
  // ../../packages/envspec/dist/repo.js
31762
32493
  import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
31763
- 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";
31764
32495
  function createRepoReader(repoPath) {
31765
32496
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join25(repoPath, relativePath);
31766
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
32497
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
31767
32498
  const readText = (relativePath) => {
31768
32499
  try {
31769
32500
  return readFileSync12(resolveIn(relativePath), "utf8");
@@ -34298,7 +35029,7 @@ __export(cache_store_exports, {
34298
35029
  updateIndexCache: () => updateIndexCache,
34299
35030
  writeIndexCache: () => writeIndexCache
34300
35031
  });
34301
- 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";
34302
35033
  import { join as join27 } from "path";
34303
35034
  import { homedir as homedir13 } from "os";
34304
35035
  function readCacheEntry() {
@@ -34318,7 +35049,7 @@ function updateIndexCache(patch) {
34318
35049
  ts: Date.now()
34319
35050
  };
34320
35051
  const tmp = `${INDEX_CACHE_FILE}.${process.pid}.${tmpCounter++}.tmp`;
34321
- writeFileSync13(tmp, JSON.stringify(entry), "utf8");
35052
+ writeFileSync14(tmp, JSON.stringify(entry), "utf8");
34322
35053
  renameSync5(tmp, INDEX_CACHE_FILE);
34323
35054
  return entry;
34324
35055
  }
@@ -34531,7 +35262,7 @@ var init_audit2 = __esm({
34531
35262
  init_src();
34532
35263
  import {
34533
35264
  readFileSync as readFileSync14,
34534
- writeFileSync as writeFileSync14,
35265
+ writeFileSync as writeFileSync15,
34535
35266
  mkdirSync as mkdirSync7,
34536
35267
  mkdtempSync as mkdtempSync5,
34537
35268
  renameSync as renameSync6,
@@ -34542,7 +35273,7 @@ import {
34542
35273
  readdirSync as readdirSync3
34543
35274
  } from "fs";
34544
35275
  import { join as join28, dirname as dirname8, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
34545
- import { createHash as createHash9 } from "crypto";
35276
+ import { createHash as createHash10 } from "crypto";
34546
35277
  import { homedir as homedir14, hostname as osHostname } from "os";
34547
35278
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
34548
35279
  import { promisify as promisify3 } from "util";
@@ -35005,7 +35736,7 @@ function markClaimNudged(id) {
35005
35736
  const ids2 = readNudgedClaimIds();
35006
35737
  ids2.add(id);
35007
35738
  ensureStateDir(TERMINALHIRE_DIR11);
35008
- writeFileSync14(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35739
+ writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35009
35740
  } catch {
35010
35741
  }
35011
35742
  }
@@ -36246,7 +36977,7 @@ async function registerFounderClaim(b) {
36246
36977
  let clearedLocalCredential = false;
36247
36978
  let refusedForPurpose = false;
36248
36979
  let preserveBackgroundToken = false;
36249
- const refuse3 = (reason) => {
36980
+ const refuse4 = (reason) => {
36250
36981
  console.error(
36251
36982
  `
36252
36983
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -36271,7 +37002,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36271
37002
  try {
36272
37003
  expectLogin = await localLoginForPaidBrowserClaim();
36273
37004
  } catch (err) {
36274
- refuse3(
37005
+ refuse4(
36275
37006
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
36276
37007
  Run \`terminalhire login\`, then retry this claim. Browser verification was
36277
37008
  not opened and no claim was sent.`
@@ -36287,16 +37018,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
36287
37018
  claimRef: opportunityShortToken(b.bountyId)
36288
37019
  });
36289
37020
  if (!proofToken) {
36290
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
37021
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
36291
37022
  }
36292
37023
  const verifiedLogin = proofTokenLogin(proofToken);
36293
37024
  if (!verifiedLogin) {
36294
- refuse3(
37025
+ refuse4(
36295
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."
36296
37027
  );
36297
37028
  }
36298
37029
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
36299
- refuse3(
37030
+ refuse4(
36300
37031
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
36301
37032
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
36302
37033
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -36328,7 +37059,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36328
37059
  try {
36329
37060
  res = await sendRegistration(true);
36330
37061
  } catch (err) {
36331
- refuse3(
37062
+ refuse4(
36332
37063
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36333
37064
  );
36334
37065
  }
@@ -36379,7 +37110,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36379
37110
  try {
36380
37111
  res = await sendRegistration(true);
36381
37112
  } catch (err) {
36382
- refuse3(
37113
+ refuse4(
36383
37114
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36384
37115
  );
36385
37116
  }
@@ -36390,14 +37121,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
36390
37121
  res = await sendRegistration(false);
36391
37122
  refusalBody = await readRefusal(res);
36392
37123
  } catch (err) {
36393
- refuse3(
37124
+ refuse4(
36394
37125
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36395
37126
  );
36396
37127
  }
36397
37128
  }
36398
37129
  if (!res.ok) {
36399
37130
  const detail = refusalBody?.message || refusalBody?.error || "";
36400
- refuse3(
37131
+ refuse4(
36401
37132
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
36402
37133
  );
36403
37134
  }
@@ -36408,7 +37139,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36408
37139
  body = null;
36409
37140
  }
36410
37141
  if (!body || body.ok !== true) {
36411
- refuse3("malformed registration response from the server.");
37142
+ refuse4("malformed registration response from the server.");
36412
37143
  }
36413
37144
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
36414
37145
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -37849,7 +38580,7 @@ function writeSliceFiles(destDir, files) {
37849
38580
  if (typeof f.content === "string") {
37850
38581
  const abs = join28(destDir, f.path);
37851
38582
  mkdirSync7(dirname8(abs), { recursive: true });
37852
- writeFileSync14(abs, f.content, "utf8");
38583
+ writeFileSync15(abs, f.content, "utf8");
37853
38584
  written.push(f.path);
37854
38585
  } else {
37855
38586
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -37903,7 +38634,7 @@ function ensureExcludedPackDir(destDir) {
37903
38634
  const existing = existsSync13(excludeFile) ? readFileSync14(excludeFile, "utf8") : "";
37904
38635
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
37905
38636
  mkdirSync7(dirname8(excludeFile), { recursive: true });
37906
- writeFileSync14(
38637
+ writeFileSync15(
37907
38638
  excludeFile,
37908
38639
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
37909
38640
  `,
@@ -37919,14 +38650,14 @@ function writePackFile(destDir, relPath, content, what) {
37919
38650
  try {
37920
38651
  const abs = join28(destDir, relPath);
37921
38652
  mkdirSync7(dirname8(abs), { recursive: true });
37922
- writeFileSync14(abs, content, { encoding: "utf8", flag: "wx" });
38653
+ writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
37923
38654
  } catch (err) {
37924
38655
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
37925
38656
  }
37926
38657
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
37927
38658
  }
37928
38659
  function sha256OfUtf8(content) {
37929
- return createHash9("sha256").update(content, "utf8").digest("hex");
38660
+ return createHash10("sha256").update(content, "utf8").digest("hex");
37930
38661
  }
37931
38662
  function writeWorkspacePack(destDir, spec, claim, delivery) {
37932
38663
  if (delivery !== "full" && delivery !== "sparse") {
@@ -39297,7 +40028,7 @@ function readClaimPushMarker() {
39297
40028
  }
39298
40029
  function writeClaimPushMarker(marker) {
39299
40030
  ensureStateDir(TERMINALHIRE_DIR11);
39300
- writeFileSync14(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
40031
+ writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
39301
40032
  }
39302
40033
  function clearClaimPushMarker() {
39303
40034
  try {