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.
@@ -551,12 +551,33 @@ function scrubEndpointEnv(env) {
551
551
  }
552
552
  return out;
553
553
  }
554
- function remoteDockerClient(endpoint) {
554
+ function containerFlagsAt(args) {
555
+ if (args[0] === "run" || args[0] === "create")
556
+ return 1;
557
+ if (args[0] === "container" && (args[1] === "run" || args[1] === "create"))
558
+ return 2;
559
+ return -1;
560
+ }
561
+ function remoteDockerClient(endpoint, opts = {}) {
555
562
  if (endpoint.trim().length === 0) {
556
563
  throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
557
564
  }
558
565
  const target = endpoint.trim();
559
- const argv = (args) => ["--host", target, ...args];
566
+ const tlsFlags = [];
567
+ if (opts.tls) {
568
+ for (const key of ["caPath", "certPath", "keyPath"]) {
569
+ if (opts.tls[key].trim().length === 0) {
570
+ throw new Error(`remoteDockerClient: tls.${key} must name a file`);
571
+ }
572
+ }
573
+ tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
574
+ }
575
+ const runArgs = opts.runArgs ?? [];
576
+ const argv = (args) => {
577
+ const at = runArgs.length > 0 ? containerFlagsAt(args) : -1;
578
+ const sub = at < 0 ? [...args] : [...args.slice(0, at), ...runArgs, ...args.slice(at)];
579
+ return ["--host", target, ...tlsFlags, ...sub];
580
+ };
560
581
  return {
561
582
  endpoint: target,
562
583
  // Always empty, and it is a fact about this client rather than about the
@@ -573,10 +594,10 @@ function remoteDockerClient(endpoint) {
573
594
  env: scrubEndpointEnv(process.env)
574
595
  });
575
596
  },
576
- sync(args, opts) {
597
+ sync(args, opts2) {
577
598
  return syncResult(spawnSync3(DOCKER_BIN, argv(args), {
578
599
  encoding: "utf8",
579
- timeout: opts?.timeoutMs,
600
+ timeout: opts2?.timeoutMs,
580
601
  env: scrubEndpointEnv(process.env)
581
602
  }));
582
603
  },
@@ -2559,13 +2580,192 @@ var init_gceIdentity = __esm({
2559
2580
 
2560
2581
  // ../../packages/attest/dist/confidentialSpace.js
2561
2582
  import { createHash as createHash2, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
2562
- var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL;
2583
+ function refuse2(reason, detail) {
2584
+ return { ok: false, reason, detail };
2585
+ }
2586
+ function isPlainObject(v) {
2587
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2588
+ }
2589
+ function assertExpectationsComplete2(expectations) {
2590
+ const required = [
2591
+ ["audience", expectations.audience],
2592
+ ["nonce", expectations.nonce],
2593
+ ["instanceName", expectations.instanceName],
2594
+ ["projectId", expectations.projectId],
2595
+ ...(expectations.additionalNonces ?? []).map((n) => [
2596
+ "additionalNonces[]",
2597
+ n
2598
+ ]),
2599
+ ...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
2600
+ ["containerEnv name", name],
2601
+ [`containerEnv.${name}`, value]
2602
+ ])
2603
+ ];
2604
+ for (const [name, value] of required) {
2605
+ if (value === "") {
2606
+ throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
2607
+ }
2608
+ }
2609
+ }
2610
+ function nonceList(v) {
2611
+ if (typeof v === "string")
2612
+ return [v];
2613
+ if (!Array.isArray(v) || v.length === 0)
2614
+ return null;
2615
+ if (!v.every((n) => typeof n === "string"))
2616
+ return null;
2617
+ return v;
2618
+ }
2619
+ async function verifyConfidentialSpaceToken(token, expectations, deps) {
2620
+ assertExpectationsComplete2(expectations);
2621
+ if (!Number.isFinite(deps.now)) {
2622
+ throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
2623
+ }
2624
+ if (expectations.allowedImageDigests.length === 0) {
2625
+ return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
2626
+ }
2627
+ if (token.length > MAX_TOKEN_LENGTH) {
2628
+ return refuse2("token-malformed", "longer than any attestation token");
2629
+ }
2630
+ const parts = token.split(".");
2631
+ if (parts.length !== 3) {
2632
+ return refuse2("token-malformed", "not a three-segment compact JWT");
2633
+ }
2634
+ const [headerB64, payloadB64, signatureB64] = parts;
2635
+ const header = decodeJsonSegment(headerB64);
2636
+ if (header === null) {
2637
+ return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
2638
+ }
2639
+ if (header.alg !== "RS256") {
2640
+ return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
2641
+ }
2642
+ if (typeof header.kid !== "string" || header.kid === "") {
2643
+ return refuse2("unknown-key", "token header names no key id");
2644
+ }
2645
+ const jwks = await deps.fetchJwks();
2646
+ const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
2647
+ if (jwk === void 0) {
2648
+ return refuse2("unknown-key", "token key id is not in the published key set");
2649
+ }
2650
+ const signature = strictBase64UrlDecode(signatureB64);
2651
+ if (signature === null) {
2652
+ return refuse2("token-malformed", "signature segment is not base64url");
2653
+ }
2654
+ let signatureValid = false;
2655
+ try {
2656
+ const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
2657
+ signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
2658
+ } catch {
2659
+ signatureValid = false;
2660
+ }
2661
+ if (!signatureValid) {
2662
+ return refuse2("signature-invalid", "signature does not verify under the named key");
2663
+ }
2664
+ const payload = decodeJsonSegment(payloadB64);
2665
+ if (payload === null) {
2666
+ return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
2667
+ }
2668
+ if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
2669
+ return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
2670
+ }
2671
+ if (payload.aud !== expectations.audience) {
2672
+ return refuse2("wrong-audience", "audience is not the one this run expects");
2673
+ }
2674
+ const nowSeconds = deps.now / 1e3;
2675
+ const exp = payload.exp;
2676
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
2677
+ return refuse2("expired", "exp is missing, malformed, or in the past");
2678
+ }
2679
+ const iat = payload.iat;
2680
+ if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
2681
+ return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
2682
+ }
2683
+ const nonces = nonceList(payload.eat_nonce);
2684
+ if (nonces === null) {
2685
+ return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
2686
+ }
2687
+ if (!nonces.includes(expectations.nonce)) {
2688
+ return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
2689
+ }
2690
+ for (const extra of expectations.additionalNonces ?? []) {
2691
+ if (!nonces.includes(extra)) {
2692
+ return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
2693
+ }
2694
+ }
2695
+ if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
2696
+ return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
2697
+ }
2698
+ if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
2699
+ return refuse2("debug-enabled", "the image is not a production image with debug disabled");
2700
+ }
2701
+ if (payload.secboot !== true) {
2702
+ return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
2703
+ }
2704
+ if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
2705
+ return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
2706
+ }
2707
+ const hwmodel = payload.hwmodel;
2708
+ const submods = payload.submods;
2709
+ const gce = isPlainObject(submods) ? submods.gce : void 0;
2710
+ if (!isPlainObject(gce)) {
2711
+ return refuse2("submods-gce-missing", "no submods.gce block names the instance");
2712
+ }
2713
+ if (gce.project_id !== expectations.projectId) {
2714
+ return refuse2("project-mismatch", "token names an instance in a different project");
2715
+ }
2716
+ if (gce.instance_name !== expectations.instanceName) {
2717
+ return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
2718
+ }
2719
+ if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
2720
+ return refuse2("instance-id-missing", "instance id is absent or not a string");
2721
+ }
2722
+ const instanceId = gce.instance_id;
2723
+ if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
2724
+ return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
2725
+ }
2726
+ const zone = gce.zone;
2727
+ const container = isPlainObject(submods) ? submods.container : void 0;
2728
+ if (!isPlainObject(container)) {
2729
+ return refuse2("submods-container-missing", "no submods.container block names the workload");
2730
+ }
2731
+ if (typeof container.image_reference !== "string" || container.image_reference === "") {
2732
+ return refuse2("image-reference-missing", "the workload image reference is absent");
2733
+ }
2734
+ const imageReference = container.image_reference;
2735
+ if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
2736
+ return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
2737
+ }
2738
+ const env = container.env;
2739
+ for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
2740
+ if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
2741
+ return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
2742
+ }
2743
+ }
2744
+ return {
2745
+ ok: true,
2746
+ value: {
2747
+ instanceId,
2748
+ instanceName: expectations.instanceName,
2749
+ zone,
2750
+ projectId: expectations.projectId,
2751
+ imageDigest: container.image_digest,
2752
+ imageReference,
2753
+ hwmodel,
2754
+ iat,
2755
+ exp,
2756
+ tokenSha256: createHash2("sha256").update(token, "utf8").digest("hex")
2757
+ }
2758
+ };
2759
+ }
2760
+ var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
2563
2761
  var init_confidentialSpace = __esm({
2564
2762
  "../../packages/attest/dist/confidentialSpace.js"() {
2565
2763
  "use strict";
2566
2764
  init_gceIdentity();
2567
2765
  CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
2568
2766
  CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
2767
+ CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
2768
+ CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
2569
2769
  }
2570
2770
  });
2571
2771
 
@@ -2908,50 +3108,50 @@ function toTestRunResult(result, outputSha256) {
2908
3108
  function toAcceptancePredicate(pair, opts = {}) {
2909
3109
  const { baseline, patched } = pair;
2910
3110
  if (baseline.runId === patched.runId) {
2911
- 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.`);
3111
+ 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.`);
2912
3112
  }
2913
3113
  if (patched.status === "refused") {
2914
- 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`);
3114
+ 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`);
2915
3115
  }
2916
3116
  if (patched.outcome === null) {
2917
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
3117
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
2918
3118
  }
2919
3119
  if (baseline.status === "refused") {
2920
- 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`);
3120
+ 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`);
2921
3121
  }
2922
3122
  if (baseline.outcome === null) {
2923
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
3123
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
2924
3124
  }
2925
3125
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
2926
- 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.`);
3126
+ 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.`);
2927
3127
  }
2928
3128
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
2929
3129
  if (budget === null) {
2930
- 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.`);
3130
+ 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.`);
2931
3131
  }
2932
3132
  if (patched.testCommand === null) {
2933
- return refuse2("no-test-command", "no test command was recorded, so none can be attested");
3133
+ return refuse3("no-test-command", "no test command was recorded, so none can be attested");
2934
3134
  }
2935
3135
  if (baseline.testCommand !== patched.testCommand) {
2936
- 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.`);
3136
+ 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.`);
2937
3137
  }
2938
3138
  if (baseline.targetSha !== patched.targetSha) {
2939
- return refuse2("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
3139
+ return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
2940
3140
  }
2941
3141
  if (patched.treeDigest === null || patched.treeDigest === "") {
2942
- return refuse2("missing-tree-digest", "the patched run recorded no tree digest to bind to");
3142
+ return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
2943
3143
  }
2944
3144
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
2945
- return refuse2("missing-patch-digest", "the patched run recorded no patch digest");
3145
+ return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
2946
3146
  }
2947
3147
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
2948
- return refuse2("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
3148
+ return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
2949
3149
  }
2950
3150
  if (baseline.treeDigest === patched.treeDigest) {
2951
- 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`);
3151
+ 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`);
2952
3152
  }
2953
3153
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
2954
- 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.`);
3154
+ 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.`);
2955
3155
  }
2956
3156
  for (const [side, outcome, counts, exitCode] of [
2957
3157
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -2959,7 +3159,7 @@ function toAcceptancePredicate(pair, opts = {}) {
2959
3159
  ]) {
2960
3160
  const why = contradicts(outcome, counts, exitCode);
2961
3161
  if (why !== null) {
2962
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
3162
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
2963
3163
  }
2964
3164
  }
2965
3165
  for (const [side, source] of [
@@ -2968,44 +3168,44 @@ function toAcceptancePredicate(pair, opts = {}) {
2968
3168
  ]) {
2969
3169
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
2970
3170
  if (why !== null)
2971
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
3171
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
2972
3172
  }
2973
3173
  if (patched.testCommandSource === "developer-declared") {
2974
- return refuse2("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
3174
+ return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
2975
3175
  }
2976
3176
  if (baseline.testCommandSource !== patched.testCommandSource) {
2977
- 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.`);
3177
+ 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.`);
2978
3178
  }
2979
3179
  if (baseline.containerImage !== patched.containerImage) {
2980
- 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.`);
3180
+ 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.`);
2981
3181
  }
2982
3182
  if (patched.containerImage === null || patched.containerImage === "") {
2983
- 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");
3183
+ 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");
2984
3184
  }
2985
3185
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
2986
- 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.");
3186
+ 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.");
2987
3187
  }
2988
3188
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
2989
- 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.`);
3189
+ 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.`);
2990
3190
  }
2991
3191
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
2992
- 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.`);
3192
+ 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.`);
2993
3193
  }
2994
3194
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
2995
- 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.");
3195
+ 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.");
2996
3196
  }
2997
3197
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
2998
- 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.`);
3198
+ 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.`);
2999
3199
  }
3000
3200
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
3001
- 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.`);
3201
+ 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.`);
3002
3202
  }
3003
3203
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
3004
- 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.`);
3204
+ 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.`);
3005
3205
  }
3006
3206
  const claimedDigest = imageDigestOf(patched.containerImage);
3007
3207
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
3008
- 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.`);
3208
+ 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.`);
3009
3209
  }
3010
3210
  return {
3011
3211
  ok: true,
@@ -3032,7 +3232,7 @@ function signRunStatement(predicate, privateKey, keyid) {
3032
3232
  const statement = createAcceptanceStatement(predicate);
3033
3233
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
3034
3234
  }
3035
- 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;
3235
+ 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;
3036
3236
  var init_attestation2 = __esm({
3037
3237
  "../../packages/envrun/dist/attestation.js"() {
3038
3238
  "use strict";
@@ -3136,7 +3336,7 @@ var init_attestation2 = __esm({
3136
3336
  "outcome-contradicts-counts",
3137
3337
  "test-command-origin-unattested"
3138
3338
  ];
3139
- refuse2 = (reason, detail) => ({
3339
+ refuse3 = (reason, detail) => ({
3140
3340
  ok: false,
3141
3341
  reason,
3142
3342
  detail
@@ -3547,6 +3747,22 @@ function bootImageFlags(p) {
3547
3747
  }
3548
3748
  const workloadImage = p.workloadImage;
3549
3749
  assertWorkloadImage("gcpBootArgv", workloadImage);
3750
+ const clientCert = p.clientCert;
3751
+ if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
3752
+ 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.`);
3753
+ }
3754
+ if (p.runId.length < MIN_NONCE_BYTES) {
3755
+ 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.`);
3756
+ }
3757
+ const metadata = [
3758
+ `tee-image-reference=${workloadImage}`,
3759
+ "tee-container-log-redirect=true",
3760
+ 'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
3761
+ "tee-cgroup-ns=true",
3762
+ `tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
3763
+ `tee-env-TH_NONCE=${p.runId}`,
3764
+ `tee-env-TH_CLIENT_CERT=${clientCert}`
3765
+ ];
3550
3766
  return [
3551
3767
  // The production family. The debug family reports `dbgstat: enabled`, which the
3552
3768
  // intake's verifier refuses, so a debug boot could never produce a run we record.
@@ -3559,7 +3775,15 @@ function bootImageFlags(p) {
3559
3775
  // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
3560
3776
  // combination is phase 5's measurement.
3561
3777
  "--maintenance-policy=TERMINATE",
3562
- `--metadata=tee-image-reference=${workloadImage},tee-container-log-redirect=true`
3778
+ `--metadata=^;^${metadata.join(";")}`,
3779
+ // What the standing firewall rule for IAP's range matches, so the tunnels to the
3780
+ // workload's dockerd and attest ports open on these VMs and no others.
3781
+ "--tags=th-cs-venue",
3782
+ // The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
3783
+ // runs vfs, which copies every layer and every container's rootfs in full. Pulling
3784
+ // node:20-bookworm left 992 MB, and the next container create failed ENOSPC
3785
+ // (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
3786
+ "--boot-disk-size=50GB"
3563
3787
  ];
3564
3788
  }
3565
3789
  function assertInstanceIdentity(fn, id) {
@@ -3633,7 +3857,13 @@ function gcpBootArgv(p) {
3633
3857
  // expectation uses (TERM-832): two inline spellings of one value is how
3634
3858
  // the boot path and the verifier drift apart silently.
3635
3859
  `--service-account=${venueIdentityEmail(p.project)}`,
3636
- "--no-scopes",
3860
+ // A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
3861
+ // pulls the workload image and mints the attestation token with this account, and
3862
+ // under `--no-scopes` it can do neither. What bounds a lifted token there is the
3863
+ // account's two grants — `confidentialcomputing.workloadUser` and a reader on the
3864
+ // one venue image repository — plus point 2 above, which is unchanged: the fence
3865
+ // still denies untrusted code the metadata server.
3866
+ ...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
3637
3867
  "--shielded-secure-boot",
3638
3868
  "--shielded-vtpm",
3639
3869
  "--shielded-integrity-monitoring",
@@ -3666,7 +3896,7 @@ function gcpDeleteArgv(p) {
3666
3896
  "--quiet"
3667
3897
  ];
3668
3898
  }
3669
- 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;
3899
+ 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;
3670
3900
  var init_gcpPlacement = __esm({
3671
3901
  "../../packages/envrun/dist/gcpPlacement.js"() {
3672
3902
  "use strict";
@@ -3685,6 +3915,8 @@ var init_gcpPlacement = __esm({
3685
3915
  GCP_RUN_LABEL_KEY = "th-run";
3686
3916
  CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
3687
3917
  WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
3918
+ CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
3919
+ MIN_NONCE_BYTES = 10;
3688
3920
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
3689
3921
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
3690
3922
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -3726,7 +3958,10 @@ var init_emptyGitConfig = __esm({
3726
3958
 
3727
3959
  // ../../packages/envrun/dist/hostedVenue.js
3728
3960
  import { spawn as spawn3, spawnSync as spawnSync5 } from "child_process";
3729
- import { chmodSync as chmodSync2, existsSync as existsSync6, mkdtempSync as mkdtempSync3, readFileSync as readFileSync4, rmSync as rmSync4 } from "fs";
3961
+ import { createHash as createHash4, X509Certificate } from "crypto";
3962
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdtempSync as mkdtempSync3, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
3963
+ import { request as httpsRequest } from "https";
3964
+ import { createServer } from "net";
3730
3965
  import { join as join11 } from "path";
3731
3966
  import { tmpdir as tmpdir3 } from "os";
3732
3967
  function credentialInGitConfig(text) {
@@ -3928,6 +4163,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
3928
4163
  });
3929
4164
  });
3930
4165
  }
4166
+ function socketErrorReason(err) {
4167
+ if (err.message !== "")
4168
+ return err.message;
4169
+ const code = err.code;
4170
+ if (code !== void 0)
4171
+ return code;
4172
+ const inner = err.errors;
4173
+ if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
4174
+ return inner[0].message;
4175
+ }
4176
+ return err.name;
4177
+ }
4178
+ function fetchAttestationOverTls(req) {
4179
+ return new Promise((resolve2) => {
4180
+ let whole;
4181
+ const settle = (reading) => {
4182
+ if (whole !== void 0)
4183
+ clearTimeout(whole);
4184
+ resolve2(reading);
4185
+ };
4186
+ let cert;
4187
+ let key;
4188
+ try {
4189
+ cert = readFileSync4(req.certPath);
4190
+ key = readFileSync4(req.keyPath);
4191
+ } catch (err) {
4192
+ settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
4193
+ return;
4194
+ }
4195
+ const call = httpsRequest({
4196
+ host: "localhost",
4197
+ port: req.port,
4198
+ path: "/attest",
4199
+ method: "GET",
4200
+ servername: "localhost",
4201
+ cert,
4202
+ key,
4203
+ rejectUnauthorized: false,
4204
+ agent: false,
4205
+ timeout: req.timeoutMs
4206
+ }, (res) => {
4207
+ const peer = res.socket.getPeerCertificate();
4208
+ let body = "";
4209
+ res.setEncoding("utf8");
4210
+ res.on("data", (chunk) => {
4211
+ body += chunk;
4212
+ if (body.length > MAX_TOKEN_LENGTH) {
4213
+ settle({ ok: false, reason: "the answer is longer than any attestation token" });
4214
+ res.destroy();
4215
+ }
4216
+ });
4217
+ res.on("end", () => {
4218
+ if (res.statusCode !== 200) {
4219
+ settle({
4220
+ ok: false,
4221
+ reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
4222
+ });
4223
+ return;
4224
+ }
4225
+ const raw = peer.raw;
4226
+ if (raw === void 0 || raw.length === 0) {
4227
+ settle({ ok: false, reason: "the attest endpoint presented no certificate" });
4228
+ return;
4229
+ }
4230
+ settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
4231
+ });
4232
+ res.on("error", (err) => {
4233
+ settle({ ok: false, reason: socketErrorReason(err) });
4234
+ });
4235
+ });
4236
+ whole = setTimeout(() => {
4237
+ settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
4238
+ call.destroy();
4239
+ }, req.timeoutMs);
4240
+ call.on("timeout", () => {
4241
+ call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
4242
+ });
4243
+ call.on("error", (err) => {
4244
+ settle({ ok: false, reason: socketErrorReason(err) });
4245
+ });
4246
+ call.end();
4247
+ });
4248
+ }
3931
4249
  function quoteForRemoteShell(arg) {
3932
4250
  return `'${arg.replace(/'/g, "'\\''")}'`;
3933
4251
  }
@@ -4262,27 +4580,370 @@ ${res.stderr}`);
4262
4580
  function manualDeleteCommand(vm, project, zone) {
4263
4581
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
4264
4582
  }
4583
+ function clientCertArgv(keyPath, certPath) {
4584
+ return [
4585
+ "req",
4586
+ "-x509",
4587
+ "-newkey",
4588
+ "ec",
4589
+ "-pkeyopt",
4590
+ "ec_paramgen_curve:P-256",
4591
+ "-nodes",
4592
+ "-keyout",
4593
+ keyPath,
4594
+ "-out",
4595
+ certPath,
4596
+ "-subj",
4597
+ "/CN=terminalhire-worker",
4598
+ "-days",
4599
+ "1",
4600
+ "-addext",
4601
+ "extendedKeyUsage=clientAuth"
4602
+ ];
4603
+ }
4604
+ function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
4605
+ return [
4606
+ "compute",
4607
+ "start-iap-tunnel",
4608
+ vm,
4609
+ String(remotePort),
4610
+ `--local-host-port=localhost:${String(localPort)}`,
4611
+ `--project=${project}`,
4612
+ `--zone=${zone}`,
4613
+ "--iap-tunnel-disable-connection-check"
4614
+ ];
4615
+ }
4616
+ function spkiSha256Hex(certDer) {
4617
+ const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
4618
+ return createHash4("sha256").update(spki).digest("hex");
4619
+ }
4620
+ function certMetadataValue(pem) {
4621
+ return new X509Certificate(pem).raw.toString("base64url");
4622
+ }
4623
+ function requireWorkloadDigest(reference) {
4624
+ const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
4625
+ if (named === null) {
4626
+ 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.");
4627
+ }
4628
+ return named[1];
4629
+ }
4630
+ function underStageRoot(path) {
4631
+ if (!path.startsWith(`${CS_STAGE_ROOT}/`)) {
4632
+ throw new Error(`${path} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
4633
+ }
4634
+ return path;
4635
+ }
4636
+ function userFlag(owner) {
4637
+ return `--user=${String(owner.uid)}:${String(owner.gid)}`;
4638
+ }
4639
+ function csStagePrepareArgv(stageBase, owner) {
4640
+ const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
4641
+ return [
4642
+ "run",
4643
+ "--rm",
4644
+ "--network=none",
4645
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
4646
+ "--",
4647
+ STAGE_HELPER_IMAGE,
4648
+ "sh",
4649
+ "-c",
4650
+ `mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
4651
+ ];
4652
+ }
4653
+ function csUntarArgv(dir, owner) {
4654
+ return [
4655
+ "run",
4656
+ "-i",
4657
+ "--rm",
4658
+ "--network=none",
4659
+ userFlag(owner),
4660
+ `--volume=${underStageRoot(dir)}:/dst:rw`,
4661
+ "--",
4662
+ STAGE_HELPER_IMAGE,
4663
+ "tar",
4664
+ "-xf",
4665
+ "-",
4666
+ "-C",
4667
+ "/dst"
4668
+ ];
4669
+ }
4670
+ function csUntarProxyArgv(dir, owner) {
4671
+ const d = quoteForRemoteShell(underStageRoot(dir));
4672
+ return [
4673
+ "run",
4674
+ "-i",
4675
+ "--rm",
4676
+ "--network=none",
4677
+ userFlag(owner),
4678
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
4679
+ "--",
4680
+ STAGE_HELPER_IMAGE,
4681
+ "sh",
4682
+ "-c",
4683
+ `mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
4684
+ ];
4685
+ }
4686
+ function csRemoveArgv(dir, owner) {
4687
+ return [
4688
+ "run",
4689
+ "--rm",
4690
+ "--network=none",
4691
+ userFlag(owner),
4692
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
4693
+ "--",
4694
+ STAGE_HELPER_IMAGE,
4695
+ "rm",
4696
+ "-rf",
4697
+ "--",
4698
+ underStageRoot(dir)
4699
+ ];
4700
+ }
4701
+ function sshStageTransport(vm, project, zone, env, io) {
4702
+ return {
4703
+ /**
4704
+ * `id -u` / `id -g` on the venue, as the account that untarred the tree.
4705
+ *
4706
+ * That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
4707
+ * so asking it who it is answers exactly the question the mount needs, without
4708
+ * a chown and without assuming a uid. One ssh round trip, once per run.
4709
+ *
4710
+ * Both ids come from ONE invocation. Two calls could straddle a reconnect and
4711
+ * pair a uid with another session's gid — a third wrong answer rather than a
4712
+ * half-right one, which is the reason `GuestUser` is a pair in the first place.
4713
+ */
4714
+ readOwner: () => {
4715
+ const res = io.exec(
4716
+ "gcloud",
4717
+ iapSshArgv(vm, project, zone, "id -u && id -g"),
4718
+ OWNER_PROBE_TIMEOUT_MS,
4719
+ // `env`, like every other gcloud call here. TERM-724 made the venue boot under the
4720
+ // SERVICE configuration rather than whoever is at the terminal, and a probe that
4721
+ // omitted this would ssh under the person's own credential — then report an owner
4722
+ // measured against a session the run does not use.
4723
+ env
4724
+ );
4725
+ if (!res.ok) {
4726
+ 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.`);
4727
+ }
4728
+ const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
4729
+ const ids2 = lines.slice(-2).map((l) => Number(l));
4730
+ if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
4731
+ 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.`);
4732
+ }
4733
+ return { uid: ids2[0], gid: ids2[1] };
4734
+ },
4735
+ push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
4736
+ pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
4737
+ removeProxy: (dir) => {
4738
+ io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
4739
+ }
4740
+ };
4741
+ }
4742
+ function dockerStageTransport(docker3, env, io) {
4743
+ const pushVia = (from, args) => {
4744
+ const [file, ...rest] = docker3.commandLine(args);
4745
+ if (file === void 0)
4746
+ throw new Error("the docker client returned an empty command line");
4747
+ return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
4748
+ };
4749
+ return {
4750
+ // Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
4751
+ readOwner: () => CS_GUEST_USER,
4752
+ push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
4753
+ pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
4754
+ removeProxy: (dir) => {
4755
+ docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
4756
+ }
4757
+ };
4758
+ }
4759
+ function bothTunnels(a, b) {
4760
+ return {
4761
+ kill: (signal) => {
4762
+ let failed = false;
4763
+ let error;
4764
+ for (const t of [a, b]) {
4765
+ try {
4766
+ t.kill(signal);
4767
+ } catch (err) {
4768
+ if (!failed)
4769
+ error = err;
4770
+ failed = true;
4771
+ }
4772
+ }
4773
+ if (failed)
4774
+ throw error;
4775
+ },
4776
+ failure: () => a.failure() ?? b.failure()
4777
+ };
4778
+ }
4779
+ function makeClientTls(io, allocated, vm) {
4780
+ let dir;
4781
+ try {
4782
+ dir = io.makePrivateDir();
4783
+ } catch (err) {
4784
+ throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
4785
+ }
4786
+ allocated.onRollback(() => {
4787
+ try {
4788
+ io.removeTree(dir);
4789
+ } catch (err) {
4790
+ throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
4791
+ }
4792
+ });
4793
+ const certPath = join11(dir, "client.pem");
4794
+ const keyPath = join11(dir, "client-key.pem");
4795
+ const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
4796
+ if (!made.ok) {
4797
+ throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
4798
+ }
4799
+ let metadataValue;
4800
+ try {
4801
+ const pem = io.readTextIfPresent(certPath);
4802
+ if (pem === null)
4803
+ throw new Error(`openssl wrote nothing to ${certPath}`);
4804
+ metadataValue = certMetadataValue(pem);
4805
+ } catch (err) {
4806
+ throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
4807
+ }
4808
+ return { dir, certPath, keyPath, metadataValue };
4809
+ }
4810
+ async function reachConfidentialSpace(a) {
4811
+ const { io, vm } = a;
4812
+ let ports;
4813
+ try {
4814
+ ports = await io.freeLocalPorts(2);
4815
+ } catch (err) {
4816
+ throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
4817
+ }
4818
+ const [attestPort, dockerPort] = ports;
4819
+ if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
4820
+ throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
4821
+ }
4822
+ const forward = (remote, local) => {
4823
+ const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
4824
+ a.allocated.onRollback(() => {
4825
+ try {
4826
+ t.kill("SIGTERM");
4827
+ } catch {
4828
+ }
4829
+ });
4830
+ return t;
4831
+ };
4832
+ const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
4833
+ const deadline = io.now() + CS_READY_BUDGET_MS;
4834
+ let reading = { ok: false, reason: "no request was made" };
4835
+ while (io.now() < deadline) {
4836
+ const dead = tunnel.failure();
4837
+ if (dead !== null) {
4838
+ throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
4839
+ }
4840
+ reading = await io.fetchAttestation({
4841
+ port: attestPort,
4842
+ certPath: a.tls.certPath,
4843
+ keyPath: a.tls.keyPath,
4844
+ timeoutMs: CS_ATTEST_TIMEOUT_MS
4845
+ });
4846
+ if (reading.ok)
4847
+ break;
4848
+ await io.sleep(CS_ATTEST_INTERVAL_MS);
4849
+ }
4850
+ if (!reading.ok) {
4851
+ throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
4852
+ }
4853
+ let spki;
4854
+ let venuePem;
4855
+ try {
4856
+ spki = spkiSha256Hex(reading.peerCertDer);
4857
+ venuePem = new X509Certificate(reading.peerCertDer).toString();
4858
+ } catch (err) {
4859
+ throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
4860
+ }
4861
+ let jwks;
4862
+ try {
4863
+ jwks = await io.fetchConfidentialSpaceJwks();
4864
+ } catch (err) {
4865
+ throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
4866
+ }
4867
+ const verdict = await verifyConfidentialSpaceToken(reading.token, {
4868
+ audience: expectedAudience(a.runId),
4869
+ nonce: a.runId,
4870
+ additionalNonces: [spki],
4871
+ instanceName: vm,
4872
+ allowedZones: [a.zone],
4873
+ projectId: a.project,
4874
+ allowedImageDigests: [a.imageDigest],
4875
+ containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
4876
+ }, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
4877
+ if (!verdict.ok) {
4878
+ throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
4879
+ }
4880
+ const caPath = join11(a.tls.dir, "venue.pem");
4881
+ try {
4882
+ io.writeFile(caPath, venuePem);
4883
+ } catch (err) {
4884
+ throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
4885
+ }
4886
+ const dockerAt = `tcp://localhost:${String(dockerPort)}`;
4887
+ const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
4888
+ const daemon = io.classifyDaemon(docker3);
4889
+ if (!daemon.distinct) {
4890
+ throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
4891
+ }
4892
+ const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
4893
+ timeoutMs: MKDIR_TIMEOUT_MS
4894
+ });
4895
+ if (prepared.error || prepared.status !== 0) {
4896
+ throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
4897
+ }
4898
+ const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
4899
+ if (pulled.error || pulled.status !== 0) {
4900
+ throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
4901
+ }
4902
+ return {
4903
+ docker: docker3,
4904
+ tunnel,
4905
+ socketDir: a.tls.dir,
4906
+ socketPath: dockerAt,
4907
+ venueDaemonId: daemon.venueDaemonId,
4908
+ venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
4909
+ transport: dockerStageTransport(docker3, a.env, io)
4910
+ };
4911
+ }
4265
4912
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
4266
4913
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
4267
4914
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
4268
4915
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
4269
4916
  const keyFile = resolveServiceAccountKeyFile(opts);
4270
4917
  const image = venueImageFromEnv(process.env);
4918
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
4271
4919
  const env = venueGcloudEnv(gcloudConfig);
4272
4920
  return {
4273
4921
  kind: "hosted-pool",
4274
4922
  acquire: (runId) => acquireTransactionally(async (allocated) => {
4275
4923
  const vm = venueInstanceName(runId);
4276
4924
  const stageBase = `/tmp/th-stage-${runId}`;
4277
- const bootArgv = gcpBootArgv({
4925
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
4278
4926
  vmName: vm,
4279
4927
  project,
4280
4928
  zone,
4281
4929
  runId,
4282
- machineType: opts.machineType ?? (image.venueImage === "confidential-space" ? CONFIDENTIAL_SPACE_MACHINE_TYPE : DEFAULT_GCP_MACHINE_TYPE),
4930
+ machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
4931
+ venueImage: "confidential-space",
4932
+ workloadImage: image.workloadImage,
4933
+ clientCert
4934
+ } : {
4935
+ vmName: vm,
4936
+ project,
4937
+ zone,
4938
+ runId,
4939
+ machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
4283
4940
  ...image
4284
- });
4941
+ };
4942
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
4285
4943
  ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
4944
+ const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
4945
+ if (clientTls !== null)
4946
+ bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
4286
4947
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
4287
4948
  let instanceState = "unknown";
4288
4949
  allocated.onRollback(() => {
@@ -4298,6 +4959,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
4298
4959
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
4299
4960
  }
4300
4961
  instanceState = "exists";
4962
+ if (clientTls !== null) {
4963
+ return finishAcquire(await reachConfidentialSpace({
4964
+ vm,
4965
+ project,
4966
+ zone,
4967
+ runId,
4968
+ stageBase,
4969
+ env,
4970
+ io,
4971
+ allocated,
4972
+ tls: clientTls,
4973
+ imageDigest: clientTls.imageDigest
4974
+ }));
4975
+ }
4301
4976
  const deadline = io.now() + SSH_READY_BUDGET_MS;
4302
4977
  let last = { retryable: true, reason: "no probe ran" };
4303
4978
  let identity = null;
@@ -4392,43 +5067,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
4392
5067
  if (!prepared.ok) {
4393
5068
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
4394
5069
  }
4395
- const stageVolumes = stageVolumeNames(vm);
4396
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
4397
- const created = docker3.sync(volumeCreateArgv(name, runId), {
4398
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
4399
- });
4400
- if (created.error || created.status !== 0) {
4401
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
4402
- }
4403
- }
4404
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
4405
- const probe = docker3.sync(execProbeArgv(name), {
4406
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
4407
- });
4408
- if (probe.error || probe.status !== 0) {
4409
- 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)}`);
4410
- }
4411
- }
4412
- const after = io.classifyDaemon(docker3);
4413
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
4414
- 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.`);
4415
- }
4416
- return makeLease({
4417
- runId,
4418
- vm,
4419
- project,
4420
- zone,
4421
- stageBase,
4422
- stageVolumes,
5070
+ return finishAcquire({
4423
5071
  docker: docker3,
4424
5072
  tunnel,
4425
5073
  socketDir,
4426
5074
  socketPath,
4427
5075
  venueDaemonId,
4428
5076
  venueIdentity,
4429
- env,
4430
- io
5077
+ transport: sshStageTransport(vm, project, zone, env, io)
4431
5078
  });
5079
+ function finishAcquire(r) {
5080
+ const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
5081
+ const stageVolumes = stageVolumeNames(vm);
5082
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
5083
+ const created = docker4.sync(volumeCreateArgv(name, runId), {
5084
+ timeoutMs: VOLUME_CREATE_TIMEOUT_MS
5085
+ });
5086
+ if (created.error || created.status !== 0) {
5087
+ throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
5088
+ }
5089
+ }
5090
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
5091
+ const probe = docker4.sync(execProbeArgv(name), {
5092
+ timeoutMs: EXEC_PROBE_TIMEOUT_MS
5093
+ });
5094
+ if (probe.error || probe.status !== 0) {
5095
+ 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)}`);
5096
+ }
5097
+ }
5098
+ const after = io.classifyDaemon(docker4);
5099
+ if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
5100
+ 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.`);
5101
+ }
5102
+ return makeLease({
5103
+ runId,
5104
+ vm,
5105
+ project,
5106
+ zone,
5107
+ stageBase,
5108
+ stageVolumes,
5109
+ docker: docker4,
5110
+ tunnel: r.tunnel,
5111
+ socketDir: r.socketDir,
5112
+ socketPath: socketPath2,
5113
+ venueDaemonId: venueDaemonId2,
5114
+ venueIdentity: r.venueIdentity,
5115
+ transport: r.transport,
5116
+ env,
5117
+ io
5118
+ });
5119
+ }
4432
5120
  })
4433
5121
  };
4434
5122
  }
@@ -4487,28 +5175,7 @@ function makeLease(p) {
4487
5175
  return null;
4488
5176
  };
4489
5177
  let stagedOwner;
4490
- const readStagedOwner = () => {
4491
- const res = p.io.exec(
4492
- "gcloud",
4493
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
4494
- OWNER_PROBE_TIMEOUT_MS,
4495
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
4496
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
4497
- // omitted this would ssh under the person's own credential — then report an owner
4498
- // measured against a session the run does not use.
4499
- p.env
4500
- );
4501
- if (!res.ok) {
4502
- 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.`);
4503
- }
4504
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
4505
- const ids2 = lines.slice(-2).map((l) => Number(l));
4506
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
4507
- 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.`);
4508
- }
4509
- return { uid: ids2[0], gid: ids2[1] };
4510
- };
4511
- stagedOwner = readStagedOwner();
5178
+ stagedOwner = p.transport.readOwner();
4512
5179
  return {
4513
5180
  kind: "hosted-pool",
4514
5181
  runId: p.runId,
@@ -4543,7 +5210,7 @@ function makeLease(p) {
4543
5210
  const dir = venueProxyDir(p.stageBase);
4544
5211
  const buffer = stageProxyCode();
4545
5212
  try {
4546
- const res = await p.io.pushTree(buffer.dir, "gcloud", iapUntarProxyArgv(p.vm, p.project, p.zone, dir), STAGE_PUSH_TIMEOUT_MS, p.env);
5213
+ const res = await p.transport.pushProxy(buffer.dir, dir);
4547
5214
  if (!res.ok) {
4548
5215
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
4549
5216
  }
@@ -4553,7 +5220,7 @@ function makeLease(p) {
4553
5220
  return {
4554
5221
  dir,
4555
5222
  cleanup: () => {
4556
- p.io.exec("gcloud", iapSshArgv(p.vm, p.project, p.zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, p.env);
5223
+ p.transport.removeProxy(dir);
4557
5224
  }
4558
5225
  };
4559
5226
  },
@@ -4561,7 +5228,7 @@ function makeLease(p) {
4561
5228
  check("staging the tree");
4562
5229
  const paths = venueStagePaths(p.stageBase);
4563
5230
  const push = async (from, to) => {
4564
- const res = await p.io.pushTree(from, "gcloud", iapUntarArgv(p.vm, p.project, p.zone, to), STAGE_PUSH_TIMEOUT_MS, p.env);
5231
+ const res = await p.transport.push(from, to);
4565
5232
  if (!res.ok) {
4566
5233
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
4567
5234
  }
@@ -4719,7 +5386,7 @@ function makeLease(p) {
4719
5386
  }
4720
5387
  };
4721
5388
  }
4722
- 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;
5389
+ 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;
4723
5390
  var init_hostedVenue = __esm({
4724
5391
  "../../packages/envrun/dist/hostedVenue.js"() {
4725
5392
  "use strict";
@@ -4840,7 +5507,55 @@ var init_hostedVenue = __esm({
4840
5507
  throw new Error("JWKS fetch returned a body with no keys array");
4841
5508
  }
4842
5509
  return body;
4843
- }
5510
+ },
5511
+ fetchConfidentialSpaceJwks: async () => {
5512
+ const getJson = async (url, what) => {
5513
+ const res = await globalThis.fetch(url, {
5514
+ signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
5515
+ });
5516
+ if (!res.ok) {
5517
+ throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
5518
+ }
5519
+ return res.json();
5520
+ };
5521
+ const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
5522
+ const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
5523
+ if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
5524
+ throw new Error("Confidential Space discovery names no https jwks_uri");
5525
+ }
5526
+ const body = await getJson(jwksUri, "key set");
5527
+ if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
5528
+ throw new Error("Confidential Space key set has no keys array");
5529
+ }
5530
+ return body;
5531
+ },
5532
+ freeLocalPorts: async (n) => {
5533
+ const servers = [];
5534
+ try {
5535
+ for (let i = 0; i < n; i += 1) {
5536
+ const server = createServer();
5537
+ servers.push(server);
5538
+ await new Promise((resolve2, reject) => {
5539
+ server.once("error", reject);
5540
+ server.listen(0, "localhost", () => {
5541
+ resolve2();
5542
+ });
5543
+ });
5544
+ }
5545
+ return servers.map((s) => s.address().port);
5546
+ } finally {
5547
+ await Promise.all(servers.map((s) => new Promise((resolve2) => {
5548
+ s.close(() => {
5549
+ resolve2();
5550
+ });
5551
+ })));
5552
+ }
5553
+ },
5554
+ writeFile: (path, text) => {
5555
+ writeFileSync6(path, text, { mode: 384, flag: "wx" });
5556
+ },
5557
+ fetchAttestation: (req) => fetchAttestationOverTls(req),
5558
+ dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
4844
5559
  };
4845
5560
  VENUE_SSH_USER = "th-runner";
4846
5561
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -4861,6 +5576,17 @@ var init_hostedVenue = __esm({
4861
5576
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
4862
5577
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
4863
5578
  SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
5579
+ CS_ATTEST_PORT = 8443;
5580
+ CS_DOCKER_PORT = 2376;
5581
+ CS_READY_BUDGET_MS = 48e4;
5582
+ CS_ATTEST_TIMEOUT_MS = 15e3;
5583
+ CS_PULL_TIMEOUT_MS = 6e5;
5584
+ CS_ATTEST_INTERVAL_MS = 5e3;
5585
+ OPENSSL_TIMEOUT_MS = 3e4;
5586
+ CS_RUN_ARGS = ["--oom-score-adj", "1000"];
5587
+ CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
5588
+ CS_STAGE_ROOT = "/tmp";
5589
+ CLIENT_CERT_STAND_IN = "stand-in-certificate";
4864
5590
  }
4865
5591
  });
4866
5592