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.
@@ -482,6 +482,7 @@ __export(github_auth_exports, {
482
482
  decrypt: () => decrypt,
483
483
  deleteGitHubToken: () => deleteGitHubToken,
484
484
  encrypt: () => encrypt,
485
+ githubTokenDisplayPath: () => githubTokenDisplayPath,
485
486
  hasGitHubToken: () => hasGitHubToken,
486
487
  loadKey: () => loadKey,
487
488
  readGitHubToken: () => readGitHubToken,
@@ -492,8 +493,12 @@ __export(github_auth_exports, {
492
493
  });
493
494
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
494
495
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync4, rmSync as rmSync2, renameSync as renameSync3 } from "fs";
495
- import { join as join7 } from "path";
496
+ import { join as join7, sep } from "path";
496
497
  import { homedir as homedir6 } from "os";
498
+ function githubTokenDisplayPath() {
499
+ const home = homedir6();
500
+ return TOKEN_FILE.startsWith(home + sep) ? `~${TOKEN_FILE.slice(home.length)}` : TOKEN_FILE;
501
+ }
497
502
  async function loadKey() {
498
503
  return loadOrCreateSharedKey();
499
504
  }
@@ -27069,12 +27074,33 @@ function scrubEndpointEnv(env) {
27069
27074
  }
27070
27075
  return out;
27071
27076
  }
27072
- function remoteDockerClient(endpoint) {
27077
+ function containerFlagsAt(args) {
27078
+ if (args[0] === "run" || args[0] === "create")
27079
+ return 1;
27080
+ if (args[0] === "container" && (args[1] === "run" || args[1] === "create"))
27081
+ return 2;
27082
+ return -1;
27083
+ }
27084
+ function remoteDockerClient(endpoint, opts = {}) {
27073
27085
  if (endpoint.trim().length === 0) {
27074
27086
  throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
27075
27087
  }
27076
27088
  const target = endpoint.trim();
27077
- const argv = (args) => ["--host", target, ...args];
27089
+ const tlsFlags = [];
27090
+ if (opts.tls) {
27091
+ for (const key of ["caPath", "certPath", "keyPath"]) {
27092
+ if (opts.tls[key].trim().length === 0) {
27093
+ throw new Error(`remoteDockerClient: tls.${key} must name a file`);
27094
+ }
27095
+ }
27096
+ tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
27097
+ }
27098
+ const runArgs = opts.runArgs ?? [];
27099
+ const argv = (args) => {
27100
+ const at = runArgs.length > 0 ? containerFlagsAt(args) : -1;
27101
+ const sub = at < 0 ? [...args] : [...args.slice(0, at), ...runArgs, ...args.slice(at)];
27102
+ return ["--host", target, ...tlsFlags, ...sub];
27103
+ };
27078
27104
  return {
27079
27105
  endpoint: target,
27080
27106
  // Always empty, and it is a fact about this client rather than about the
@@ -27091,10 +27117,10 @@ function remoteDockerClient(endpoint) {
27091
27117
  env: scrubEndpointEnv(process.env)
27092
27118
  });
27093
27119
  },
27094
- sync(args, opts) {
27120
+ sync(args, opts2) {
27095
27121
  return syncResult(spawnSync3(DOCKER_BIN, argv(args), {
27096
27122
  encoding: "utf8",
27097
- timeout: opts?.timeoutMs,
27123
+ timeout: opts2?.timeoutMs,
27098
27124
  env: scrubEndpointEnv(process.env)
27099
27125
  }));
27100
27126
  },
@@ -29077,13 +29103,192 @@ var init_gceIdentity = __esm({
29077
29103
 
29078
29104
  // ../../packages/attest/dist/confidentialSpace.js
29079
29105
  import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
29080
- var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL;
29106
+ function refuse2(reason, detail) {
29107
+ return { ok: false, reason, detail };
29108
+ }
29109
+ function isPlainObject(v) {
29110
+ return typeof v === "object" && v !== null && !Array.isArray(v);
29111
+ }
29112
+ function assertExpectationsComplete2(expectations) {
29113
+ const required2 = [
29114
+ ["audience", expectations.audience],
29115
+ ["nonce", expectations.nonce],
29116
+ ["instanceName", expectations.instanceName],
29117
+ ["projectId", expectations.projectId],
29118
+ ...(expectations.additionalNonces ?? []).map((n) => [
29119
+ "additionalNonces[]",
29120
+ n
29121
+ ]),
29122
+ ...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
29123
+ ["containerEnv name", name],
29124
+ [`containerEnv.${name}`, value]
29125
+ ])
29126
+ ];
29127
+ for (const [name, value] of required2) {
29128
+ if (value === "") {
29129
+ throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
29130
+ }
29131
+ }
29132
+ }
29133
+ function nonceList(v) {
29134
+ if (typeof v === "string")
29135
+ return [v];
29136
+ if (!Array.isArray(v) || v.length === 0)
29137
+ return null;
29138
+ if (!v.every((n) => typeof n === "string"))
29139
+ return null;
29140
+ return v;
29141
+ }
29142
+ async function verifyConfidentialSpaceToken(token, expectations, deps) {
29143
+ assertExpectationsComplete2(expectations);
29144
+ if (!Number.isFinite(deps.now)) {
29145
+ throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
29146
+ }
29147
+ if (expectations.allowedImageDigests.length === 0) {
29148
+ return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
29149
+ }
29150
+ if (token.length > MAX_TOKEN_LENGTH) {
29151
+ return refuse2("token-malformed", "longer than any attestation token");
29152
+ }
29153
+ const parts = token.split(".");
29154
+ if (parts.length !== 3) {
29155
+ return refuse2("token-malformed", "not a three-segment compact JWT");
29156
+ }
29157
+ const [headerB64, payloadB64, signatureB64] = parts;
29158
+ const header = decodeJsonSegment(headerB64);
29159
+ if (header === null) {
29160
+ return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
29161
+ }
29162
+ if (header.alg !== "RS256") {
29163
+ return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
29164
+ }
29165
+ if (typeof header.kid !== "string" || header.kid === "") {
29166
+ return refuse2("unknown-key", "token header names no key id");
29167
+ }
29168
+ const jwks = await deps.fetchJwks();
29169
+ const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
29170
+ if (jwk === void 0) {
29171
+ return refuse2("unknown-key", "token key id is not in the published key set");
29172
+ }
29173
+ const signature = strictBase64UrlDecode(signatureB64);
29174
+ if (signature === null) {
29175
+ return refuse2("token-malformed", "signature segment is not base64url");
29176
+ }
29177
+ let signatureValid = false;
29178
+ try {
29179
+ const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
29180
+ signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
29181
+ } catch {
29182
+ signatureValid = false;
29183
+ }
29184
+ if (!signatureValid) {
29185
+ return refuse2("signature-invalid", "signature does not verify under the named key");
29186
+ }
29187
+ const payload = decodeJsonSegment(payloadB64);
29188
+ if (payload === null) {
29189
+ return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
29190
+ }
29191
+ if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
29192
+ return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
29193
+ }
29194
+ if (payload.aud !== expectations.audience) {
29195
+ return refuse2("wrong-audience", "audience is not the one this run expects");
29196
+ }
29197
+ const nowSeconds = deps.now / 1e3;
29198
+ const exp = payload.exp;
29199
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
29200
+ return refuse2("expired", "exp is missing, malformed, or in the past");
29201
+ }
29202
+ const iat = payload.iat;
29203
+ if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
29204
+ return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
29205
+ }
29206
+ const nonces = nonceList(payload.eat_nonce);
29207
+ if (nonces === null) {
29208
+ return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
29209
+ }
29210
+ if (!nonces.includes(expectations.nonce)) {
29211
+ return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
29212
+ }
29213
+ for (const extra of expectations.additionalNonces ?? []) {
29214
+ if (!nonces.includes(extra)) {
29215
+ return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
29216
+ }
29217
+ }
29218
+ if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
29219
+ return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
29220
+ }
29221
+ if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
29222
+ return refuse2("debug-enabled", "the image is not a production image with debug disabled");
29223
+ }
29224
+ if (payload.secboot !== true) {
29225
+ return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
29226
+ }
29227
+ if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
29228
+ return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
29229
+ }
29230
+ const hwmodel = payload.hwmodel;
29231
+ const submods = payload.submods;
29232
+ const gce = isPlainObject(submods) ? submods.gce : void 0;
29233
+ if (!isPlainObject(gce)) {
29234
+ return refuse2("submods-gce-missing", "no submods.gce block names the instance");
29235
+ }
29236
+ if (gce.project_id !== expectations.projectId) {
29237
+ return refuse2("project-mismatch", "token names an instance in a different project");
29238
+ }
29239
+ if (gce.instance_name !== expectations.instanceName) {
29240
+ return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
29241
+ }
29242
+ if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
29243
+ return refuse2("instance-id-missing", "instance id is absent or not a string");
29244
+ }
29245
+ const instanceId = gce.instance_id;
29246
+ if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
29247
+ return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
29248
+ }
29249
+ const zone = gce.zone;
29250
+ const container = isPlainObject(submods) ? submods.container : void 0;
29251
+ if (!isPlainObject(container)) {
29252
+ return refuse2("submods-container-missing", "no submods.container block names the workload");
29253
+ }
29254
+ if (typeof container.image_reference !== "string" || container.image_reference === "") {
29255
+ return refuse2("image-reference-missing", "the workload image reference is absent");
29256
+ }
29257
+ const imageReference = container.image_reference;
29258
+ if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
29259
+ return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
29260
+ }
29261
+ const env = container.env;
29262
+ for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
29263
+ if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
29264
+ return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
29265
+ }
29266
+ }
29267
+ return {
29268
+ ok: true,
29269
+ value: {
29270
+ instanceId,
29271
+ instanceName: expectations.instanceName,
29272
+ zone,
29273
+ projectId: expectations.projectId,
29274
+ imageDigest: container.image_digest,
29275
+ imageReference,
29276
+ hwmodel,
29277
+ iat,
29278
+ exp,
29279
+ tokenSha256: createHash7("sha256").update(token, "utf8").digest("hex")
29280
+ }
29281
+ };
29282
+ }
29283
+ var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
29081
29284
  var init_confidentialSpace = __esm({
29082
29285
  "../../packages/attest/dist/confidentialSpace.js"() {
29083
29286
  "use strict";
29084
29287
  init_gceIdentity();
29085
29288
  CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
29086
29289
  CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
29290
+ CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
29291
+ CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
29087
29292
  }
29088
29293
  });
29089
29294
 
@@ -29426,50 +29631,50 @@ function toTestRunResult(result, outputSha256) {
29426
29631
  function toAcceptancePredicate(pair, opts = {}) {
29427
29632
  const { baseline, patched } = pair;
29428
29633
  if (baseline.runId === patched.runId) {
29429
- 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.`);
29634
+ 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.`);
29430
29635
  }
29431
29636
  if (patched.status === "refused") {
29432
- 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`);
29637
+ 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`);
29433
29638
  }
29434
29639
  if (patched.outcome === null) {
29435
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
29640
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
29436
29641
  }
29437
29642
  if (baseline.status === "refused") {
29438
- 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`);
29643
+ 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`);
29439
29644
  }
29440
29645
  if (baseline.outcome === null) {
29441
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
29646
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
29442
29647
  }
29443
29648
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
29444
- 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.`);
29649
+ 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.`);
29445
29650
  }
29446
29651
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
29447
29652
  if (budget === null) {
29448
- 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.`);
29653
+ 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.`);
29449
29654
  }
29450
29655
  if (patched.testCommand === null) {
29451
- return refuse2("no-test-command", "no test command was recorded, so none can be attested");
29656
+ return refuse3("no-test-command", "no test command was recorded, so none can be attested");
29452
29657
  }
29453
29658
  if (baseline.testCommand !== patched.testCommand) {
29454
- 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.`);
29659
+ 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.`);
29455
29660
  }
29456
29661
  if (baseline.targetSha !== patched.targetSha) {
29457
- return refuse2("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
29662
+ return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
29458
29663
  }
29459
29664
  if (patched.treeDigest === null || patched.treeDigest === "") {
29460
- return refuse2("missing-tree-digest", "the patched run recorded no tree digest to bind to");
29665
+ return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
29461
29666
  }
29462
29667
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
29463
- return refuse2("missing-patch-digest", "the patched run recorded no patch digest");
29668
+ return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
29464
29669
  }
29465
29670
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
29466
- return refuse2("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
29671
+ return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
29467
29672
  }
29468
29673
  if (baseline.treeDigest === patched.treeDigest) {
29469
- 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`);
29674
+ 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`);
29470
29675
  }
29471
29676
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
29472
- 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.`);
29677
+ 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.`);
29473
29678
  }
29474
29679
  for (const [side, outcome, counts, exitCode] of [
29475
29680
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -29477,7 +29682,7 @@ function toAcceptancePredicate(pair, opts = {}) {
29477
29682
  ]) {
29478
29683
  const why = contradicts(outcome, counts, exitCode);
29479
29684
  if (why !== null) {
29480
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
29685
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
29481
29686
  }
29482
29687
  }
29483
29688
  for (const [side, source] of [
@@ -29486,44 +29691,44 @@ function toAcceptancePredicate(pair, opts = {}) {
29486
29691
  ]) {
29487
29692
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
29488
29693
  if (why !== null)
29489
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
29694
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
29490
29695
  }
29491
29696
  if (patched.testCommandSource === "developer-declared") {
29492
- return refuse2("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
29697
+ return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
29493
29698
  }
29494
29699
  if (baseline.testCommandSource !== patched.testCommandSource) {
29495
- 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.`);
29700
+ 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.`);
29496
29701
  }
29497
29702
  if (baseline.containerImage !== patched.containerImage) {
29498
- 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.`);
29703
+ 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.`);
29499
29704
  }
29500
29705
  if (patched.containerImage === null || patched.containerImage === "") {
29501
- 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");
29706
+ 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");
29502
29707
  }
29503
29708
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
29504
- 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.");
29709
+ 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.");
29505
29710
  }
29506
29711
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
29507
- 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.`);
29712
+ 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.`);
29508
29713
  }
29509
29714
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
29510
- 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.`);
29715
+ 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.`);
29511
29716
  }
29512
29717
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
29513
- 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.");
29718
+ 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.");
29514
29719
  }
29515
29720
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
29516
- 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.`);
29721
+ 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.`);
29517
29722
  }
29518
29723
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
29519
- 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.`);
29724
+ 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.`);
29520
29725
  }
29521
29726
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
29522
- 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.`);
29727
+ 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.`);
29523
29728
  }
29524
29729
  const claimedDigest = imageDigestOf(patched.containerImage);
29525
29730
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
29526
- 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.`);
29731
+ 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.`);
29527
29732
  }
29528
29733
  return {
29529
29734
  ok: true,
@@ -29550,7 +29755,7 @@ function signRunStatement(predicate, privateKey, keyid) {
29550
29755
  const statement = createAcceptanceStatement(predicate);
29551
29756
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
29552
29757
  }
29553
- 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;
29758
+ 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;
29554
29759
  var init_attestation2 = __esm({
29555
29760
  "../../packages/envrun/dist/attestation.js"() {
29556
29761
  "use strict";
@@ -29654,7 +29859,7 @@ var init_attestation2 = __esm({
29654
29859
  "outcome-contradicts-counts",
29655
29860
  "test-command-origin-unattested"
29656
29861
  ];
29657
- refuse2 = (reason, detail) => ({
29862
+ refuse3 = (reason, detail) => ({
29658
29863
  ok: false,
29659
29864
  reason,
29660
29865
  detail
@@ -30065,6 +30270,22 @@ function bootImageFlags(p) {
30065
30270
  }
30066
30271
  const workloadImage = p.workloadImage;
30067
30272
  assertWorkloadImage("gcpBootArgv", workloadImage);
30273
+ const clientCert = p.clientCert;
30274
+ if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
30275
+ 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.`);
30276
+ }
30277
+ if (p.runId.length < MIN_NONCE_BYTES) {
30278
+ 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.`);
30279
+ }
30280
+ const metadata = [
30281
+ `tee-image-reference=${workloadImage}`,
30282
+ "tee-container-log-redirect=true",
30283
+ 'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
30284
+ "tee-cgroup-ns=true",
30285
+ `tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
30286
+ `tee-env-TH_NONCE=${p.runId}`,
30287
+ `tee-env-TH_CLIENT_CERT=${clientCert}`
30288
+ ];
30068
30289
  return [
30069
30290
  // The production family. The debug family reports `dbgstat: enabled`, which the
30070
30291
  // intake's verifier refuses, so a debug boot could never produce a run we record.
@@ -30077,7 +30298,15 @@ function bootImageFlags(p) {
30077
30298
  // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
30078
30299
  // combination is phase 5's measurement.
30079
30300
  "--maintenance-policy=TERMINATE",
30080
- `--metadata=tee-image-reference=${workloadImage},tee-container-log-redirect=true`
30301
+ `--metadata=^;^${metadata.join(";")}`,
30302
+ // What the standing firewall rule for IAP's range matches, so the tunnels to the
30303
+ // workload's dockerd and attest ports open on these VMs and no others.
30304
+ "--tags=th-cs-venue",
30305
+ // The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
30306
+ // runs vfs, which copies every layer and every container's rootfs in full. Pulling
30307
+ // node:20-bookworm left 992 MB, and the next container create failed ENOSPC
30308
+ // (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
30309
+ "--boot-disk-size=50GB"
30081
30310
  ];
30082
30311
  }
30083
30312
  function assertInstanceIdentity(fn, id) {
@@ -30151,7 +30380,13 @@ function gcpBootArgv(p) {
30151
30380
  // expectation uses (TERM-832): two inline spellings of one value is how
30152
30381
  // the boot path and the verifier drift apart silently.
30153
30382
  `--service-account=${venueIdentityEmail(p.project)}`,
30154
- "--no-scopes",
30383
+ // A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
30384
+ // pulls the workload image and mints the attestation token with this account, and
30385
+ // under `--no-scopes` it can do neither. What bounds a lifted token there is the
30386
+ // account's two grants — `confidentialcomputing.workloadUser` and a reader on the
30387
+ // one venue image repository — plus point 2 above, which is unchanged: the fence
30388
+ // still denies untrusted code the metadata server.
30389
+ ...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
30155
30390
  "--shielded-secure-boot",
30156
30391
  "--shielded-vtpm",
30157
30392
  "--shielded-integrity-monitoring",
@@ -30184,7 +30419,7 @@ function gcpDeleteArgv(p) {
30184
30419
  "--quiet"
30185
30420
  ];
30186
30421
  }
30187
- 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;
30422
+ 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;
30188
30423
  var init_gcpPlacement = __esm({
30189
30424
  "../../packages/envrun/dist/gcpPlacement.js"() {
30190
30425
  "use strict";
@@ -30203,6 +30438,8 @@ var init_gcpPlacement = __esm({
30203
30438
  GCP_RUN_LABEL_KEY = "th-run";
30204
30439
  CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
30205
30440
  WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
30441
+ CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
30442
+ MIN_NONCE_BYTES = 10;
30206
30443
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
30207
30444
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
30208
30445
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -30244,7 +30481,10 @@ var init_emptyGitConfig = __esm({
30244
30481
 
30245
30482
  // ../../packages/envrun/dist/hostedVenue.js
30246
30483
  import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
30247
- import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync8 } from "fs";
30484
+ import { createHash as createHash9, X509Certificate } from "crypto";
30485
+ import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "fs";
30486
+ import { request as httpsRequest } from "https";
30487
+ import { createServer } from "net";
30248
30488
  import { join as join26 } from "path";
30249
30489
  import { tmpdir as tmpdir3 } from "os";
30250
30490
  function credentialInGitConfig(text) {
@@ -30446,6 +30686,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
30446
30686
  });
30447
30687
  });
30448
30688
  }
30689
+ function socketErrorReason(err) {
30690
+ if (err.message !== "")
30691
+ return err.message;
30692
+ const code = err.code;
30693
+ if (code !== void 0)
30694
+ return code;
30695
+ const inner = err.errors;
30696
+ if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
30697
+ return inner[0].message;
30698
+ }
30699
+ return err.name;
30700
+ }
30701
+ function fetchAttestationOverTls(req) {
30702
+ return new Promise((resolve4) => {
30703
+ let whole;
30704
+ const settle = (reading) => {
30705
+ if (whole !== void 0)
30706
+ clearTimeout(whole);
30707
+ resolve4(reading);
30708
+ };
30709
+ let cert;
30710
+ let key;
30711
+ try {
30712
+ cert = readFileSync13(req.certPath);
30713
+ key = readFileSync13(req.keyPath);
30714
+ } catch (err) {
30715
+ settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
30716
+ return;
30717
+ }
30718
+ const call = httpsRequest({
30719
+ host: "localhost",
30720
+ port: req.port,
30721
+ path: "/attest",
30722
+ method: "GET",
30723
+ servername: "localhost",
30724
+ cert,
30725
+ key,
30726
+ rejectUnauthorized: false,
30727
+ agent: false,
30728
+ timeout: req.timeoutMs
30729
+ }, (res) => {
30730
+ const peer = res.socket.getPeerCertificate();
30731
+ let body = "";
30732
+ res.setEncoding("utf8");
30733
+ res.on("data", (chunk) => {
30734
+ body += chunk;
30735
+ if (body.length > MAX_TOKEN_LENGTH) {
30736
+ settle({ ok: false, reason: "the answer is longer than any attestation token" });
30737
+ res.destroy();
30738
+ }
30739
+ });
30740
+ res.on("end", () => {
30741
+ if (res.statusCode !== 200) {
30742
+ settle({
30743
+ ok: false,
30744
+ reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
30745
+ });
30746
+ return;
30747
+ }
30748
+ const raw = peer.raw;
30749
+ if (raw === void 0 || raw.length === 0) {
30750
+ settle({ ok: false, reason: "the attest endpoint presented no certificate" });
30751
+ return;
30752
+ }
30753
+ settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
30754
+ });
30755
+ res.on("error", (err) => {
30756
+ settle({ ok: false, reason: socketErrorReason(err) });
30757
+ });
30758
+ });
30759
+ whole = setTimeout(() => {
30760
+ settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
30761
+ call.destroy();
30762
+ }, req.timeoutMs);
30763
+ call.on("timeout", () => {
30764
+ call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
30765
+ });
30766
+ call.on("error", (err) => {
30767
+ settle({ ok: false, reason: socketErrorReason(err) });
30768
+ });
30769
+ call.end();
30770
+ });
30771
+ }
30449
30772
  function quoteForRemoteShell(arg) {
30450
30773
  return `'${arg.replace(/'/g, "'\\''")}'`;
30451
30774
  }
@@ -30780,27 +31103,370 @@ ${res.stderr}`);
30780
31103
  function manualDeleteCommand(vm, project, zone) {
30781
31104
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
30782
31105
  }
31106
+ function clientCertArgv(keyPath, certPath) {
31107
+ return [
31108
+ "req",
31109
+ "-x509",
31110
+ "-newkey",
31111
+ "ec",
31112
+ "-pkeyopt",
31113
+ "ec_paramgen_curve:P-256",
31114
+ "-nodes",
31115
+ "-keyout",
31116
+ keyPath,
31117
+ "-out",
31118
+ certPath,
31119
+ "-subj",
31120
+ "/CN=terminalhire-worker",
31121
+ "-days",
31122
+ "1",
31123
+ "-addext",
31124
+ "extendedKeyUsage=clientAuth"
31125
+ ];
31126
+ }
31127
+ function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
31128
+ return [
31129
+ "compute",
31130
+ "start-iap-tunnel",
31131
+ vm,
31132
+ String(remotePort),
31133
+ `--local-host-port=localhost:${String(localPort)}`,
31134
+ `--project=${project}`,
31135
+ `--zone=${zone}`,
31136
+ "--iap-tunnel-disable-connection-check"
31137
+ ];
31138
+ }
31139
+ function spkiSha256Hex(certDer) {
31140
+ const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
31141
+ return createHash9("sha256").update(spki).digest("hex");
31142
+ }
31143
+ function certMetadataValue(pem) {
31144
+ return new X509Certificate(pem).raw.toString("base64url");
31145
+ }
31146
+ function requireWorkloadDigest(reference) {
31147
+ const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
31148
+ if (named === null) {
31149
+ 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.");
31150
+ }
31151
+ return named[1];
31152
+ }
31153
+ function underStageRoot(path5) {
31154
+ if (!path5.startsWith(`${CS_STAGE_ROOT}/`)) {
31155
+ throw new Error(`${path5} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
31156
+ }
31157
+ return path5;
31158
+ }
31159
+ function userFlag(owner) {
31160
+ return `--user=${String(owner.uid)}:${String(owner.gid)}`;
31161
+ }
31162
+ function csStagePrepareArgv(stageBase, owner) {
31163
+ const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
31164
+ return [
31165
+ "run",
31166
+ "--rm",
31167
+ "--network=none",
31168
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
31169
+ "--",
31170
+ STAGE_HELPER_IMAGE,
31171
+ "sh",
31172
+ "-c",
31173
+ `mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
31174
+ ];
31175
+ }
31176
+ function csUntarArgv(dir, owner) {
31177
+ return [
31178
+ "run",
31179
+ "-i",
31180
+ "--rm",
31181
+ "--network=none",
31182
+ userFlag(owner),
31183
+ `--volume=${underStageRoot(dir)}:/dst:rw`,
31184
+ "--",
31185
+ STAGE_HELPER_IMAGE,
31186
+ "tar",
31187
+ "-xf",
31188
+ "-",
31189
+ "-C",
31190
+ "/dst"
31191
+ ];
31192
+ }
31193
+ function csUntarProxyArgv(dir, owner) {
31194
+ const d = quoteForRemoteShell(underStageRoot(dir));
31195
+ return [
31196
+ "run",
31197
+ "-i",
31198
+ "--rm",
31199
+ "--network=none",
31200
+ userFlag(owner),
31201
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
31202
+ "--",
31203
+ STAGE_HELPER_IMAGE,
31204
+ "sh",
31205
+ "-c",
31206
+ `mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
31207
+ ];
31208
+ }
31209
+ function csRemoveArgv(dir, owner) {
31210
+ return [
31211
+ "run",
31212
+ "--rm",
31213
+ "--network=none",
31214
+ userFlag(owner),
31215
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
31216
+ "--",
31217
+ STAGE_HELPER_IMAGE,
31218
+ "rm",
31219
+ "-rf",
31220
+ "--",
31221
+ underStageRoot(dir)
31222
+ ];
31223
+ }
31224
+ function sshStageTransport(vm, project, zone, env, io) {
31225
+ return {
31226
+ /**
31227
+ * `id -u` / `id -g` on the venue, as the account that untarred the tree.
31228
+ *
31229
+ * That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
31230
+ * so asking it who it is answers exactly the question the mount needs, without
31231
+ * a chown and without assuming a uid. One ssh round trip, once per run.
31232
+ *
31233
+ * Both ids come from ONE invocation. Two calls could straddle a reconnect and
31234
+ * pair a uid with another session's gid — a third wrong answer rather than a
31235
+ * half-right one, which is the reason `GuestUser` is a pair in the first place.
31236
+ */
31237
+ readOwner: () => {
31238
+ const res = io.exec(
31239
+ "gcloud",
31240
+ iapSshArgv(vm, project, zone, "id -u && id -g"),
31241
+ OWNER_PROBE_TIMEOUT_MS,
31242
+ // `env`, like every other gcloud call here. TERM-724 made the venue boot under the
31243
+ // SERVICE configuration rather than whoever is at the terminal, and a probe that
31244
+ // omitted this would ssh under the person's own credential — then report an owner
31245
+ // measured against a session the run does not use.
31246
+ env
31247
+ );
31248
+ if (!res.ok) {
31249
+ 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.`);
31250
+ }
31251
+ const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
31252
+ const ids2 = lines.slice(-2).map((l) => Number(l));
31253
+ if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
31254
+ 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.`);
31255
+ }
31256
+ return { uid: ids2[0], gid: ids2[1] };
31257
+ },
31258
+ push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
31259
+ pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
31260
+ removeProxy: (dir) => {
31261
+ io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
31262
+ }
31263
+ };
31264
+ }
31265
+ function dockerStageTransport(docker3, env, io) {
31266
+ const pushVia = (from, args) => {
31267
+ const [file, ...rest] = docker3.commandLine(args);
31268
+ if (file === void 0)
31269
+ throw new Error("the docker client returned an empty command line");
31270
+ return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
31271
+ };
31272
+ return {
31273
+ // Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
31274
+ readOwner: () => CS_GUEST_USER,
31275
+ push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
31276
+ pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
31277
+ removeProxy: (dir) => {
31278
+ docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
31279
+ }
31280
+ };
31281
+ }
31282
+ function bothTunnels(a, b) {
31283
+ return {
31284
+ kill: (signal) => {
31285
+ let failed = false;
31286
+ let error2;
31287
+ for (const t of [a, b]) {
31288
+ try {
31289
+ t.kill(signal);
31290
+ } catch (err) {
31291
+ if (!failed)
31292
+ error2 = err;
31293
+ failed = true;
31294
+ }
31295
+ }
31296
+ if (failed)
31297
+ throw error2;
31298
+ },
31299
+ failure: () => a.failure() ?? b.failure()
31300
+ };
31301
+ }
31302
+ function makeClientTls(io, allocated, vm) {
31303
+ let dir;
31304
+ try {
31305
+ dir = io.makePrivateDir();
31306
+ } catch (err) {
31307
+ throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
31308
+ }
31309
+ allocated.onRollback(() => {
31310
+ try {
31311
+ io.removeTree(dir);
31312
+ } catch (err) {
31313
+ throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
31314
+ }
31315
+ });
31316
+ const certPath = join26(dir, "client.pem");
31317
+ const keyPath = join26(dir, "client-key.pem");
31318
+ const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
31319
+ if (!made.ok) {
31320
+ throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
31321
+ }
31322
+ let metadataValue;
31323
+ try {
31324
+ const pem = io.readTextIfPresent(certPath);
31325
+ if (pem === null)
31326
+ throw new Error(`openssl wrote nothing to ${certPath}`);
31327
+ metadataValue = certMetadataValue(pem);
31328
+ } catch (err) {
31329
+ throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
31330
+ }
31331
+ return { dir, certPath, keyPath, metadataValue };
31332
+ }
31333
+ async function reachConfidentialSpace(a) {
31334
+ const { io, vm } = a;
31335
+ let ports;
31336
+ try {
31337
+ ports = await io.freeLocalPorts(2);
31338
+ } catch (err) {
31339
+ throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
31340
+ }
31341
+ const [attestPort, dockerPort] = ports;
31342
+ if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
31343
+ throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
31344
+ }
31345
+ const forward = (remote, local) => {
31346
+ const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
31347
+ a.allocated.onRollback(() => {
31348
+ try {
31349
+ t.kill("SIGTERM");
31350
+ } catch {
31351
+ }
31352
+ });
31353
+ return t;
31354
+ };
31355
+ const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
31356
+ const deadline = io.now() + CS_READY_BUDGET_MS;
31357
+ let reading = { ok: false, reason: "no request was made" };
31358
+ while (io.now() < deadline) {
31359
+ const dead = tunnel.failure();
31360
+ if (dead !== null) {
31361
+ throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
31362
+ }
31363
+ reading = await io.fetchAttestation({
31364
+ port: attestPort,
31365
+ certPath: a.tls.certPath,
31366
+ keyPath: a.tls.keyPath,
31367
+ timeoutMs: CS_ATTEST_TIMEOUT_MS
31368
+ });
31369
+ if (reading.ok)
31370
+ break;
31371
+ await io.sleep(CS_ATTEST_INTERVAL_MS);
31372
+ }
31373
+ if (!reading.ok) {
31374
+ throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
31375
+ }
31376
+ let spki;
31377
+ let venuePem;
31378
+ try {
31379
+ spki = spkiSha256Hex(reading.peerCertDer);
31380
+ venuePem = new X509Certificate(reading.peerCertDer).toString();
31381
+ } catch (err) {
31382
+ throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
31383
+ }
31384
+ let jwks;
31385
+ try {
31386
+ jwks = await io.fetchConfidentialSpaceJwks();
31387
+ } catch (err) {
31388
+ throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
31389
+ }
31390
+ const verdict = await verifyConfidentialSpaceToken(reading.token, {
31391
+ audience: expectedAudience(a.runId),
31392
+ nonce: a.runId,
31393
+ additionalNonces: [spki],
31394
+ instanceName: vm,
31395
+ allowedZones: [a.zone],
31396
+ projectId: a.project,
31397
+ allowedImageDigests: [a.imageDigest],
31398
+ containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
31399
+ }, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
31400
+ if (!verdict.ok) {
31401
+ throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
31402
+ }
31403
+ const caPath = join26(a.tls.dir, "venue.pem");
31404
+ try {
31405
+ io.writeFile(caPath, venuePem);
31406
+ } catch (err) {
31407
+ throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
31408
+ }
31409
+ const dockerAt = `tcp://localhost:${String(dockerPort)}`;
31410
+ const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
31411
+ const daemon = io.classifyDaemon(docker3);
31412
+ if (!daemon.distinct) {
31413
+ throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
31414
+ }
31415
+ const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
31416
+ timeoutMs: MKDIR_TIMEOUT_MS
31417
+ });
31418
+ if (prepared.error || prepared.status !== 0) {
31419
+ throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
31420
+ }
31421
+ const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
31422
+ if (pulled.error || pulled.status !== 0) {
31423
+ throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
31424
+ }
31425
+ return {
31426
+ docker: docker3,
31427
+ tunnel,
31428
+ socketDir: a.tls.dir,
31429
+ socketPath: dockerAt,
31430
+ venueDaemonId: daemon.venueDaemonId,
31431
+ venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
31432
+ transport: dockerStageTransport(docker3, a.env, io)
31433
+ };
31434
+ }
30783
31435
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30784
31436
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
30785
31437
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
30786
31438
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
30787
31439
  const keyFile = resolveServiceAccountKeyFile(opts);
30788
31440
  const image = venueImageFromEnv(process.env);
31441
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
30789
31442
  const env = venueGcloudEnv(gcloudConfig);
30790
31443
  return {
30791
31444
  kind: "hosted-pool",
30792
31445
  acquire: (runId) => acquireTransactionally(async (allocated) => {
30793
31446
  const vm = venueInstanceName(runId);
30794
31447
  const stageBase = `/tmp/th-stage-${runId}`;
30795
- const bootArgv = gcpBootArgv({
31448
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
30796
31449
  vmName: vm,
30797
31450
  project,
30798
31451
  zone,
30799
31452
  runId,
30800
- machineType: opts.machineType ?? (image.venueImage === "confidential-space" ? CONFIDENTIAL_SPACE_MACHINE_TYPE : DEFAULT_GCP_MACHINE_TYPE),
31453
+ machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
31454
+ venueImage: "confidential-space",
31455
+ workloadImage: image.workloadImage,
31456
+ clientCert
31457
+ } : {
31458
+ vmName: vm,
31459
+ project,
31460
+ zone,
31461
+ runId,
31462
+ machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
30801
31463
  ...image
30802
- });
31464
+ };
31465
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
30803
31466
  ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
31467
+ const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
31468
+ if (clientTls !== null)
31469
+ bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
30804
31470
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
30805
31471
  let instanceState = "unknown";
30806
31472
  allocated.onRollback(() => {
@@ -30816,6 +31482,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30816
31482
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
30817
31483
  }
30818
31484
  instanceState = "exists";
31485
+ if (clientTls !== null) {
31486
+ return finishAcquire(await reachConfidentialSpace({
31487
+ vm,
31488
+ project,
31489
+ zone,
31490
+ runId,
31491
+ stageBase,
31492
+ env,
31493
+ io,
31494
+ allocated,
31495
+ tls: clientTls,
31496
+ imageDigest: clientTls.imageDigest
31497
+ }));
31498
+ }
30819
31499
  const deadline = io.now() + SSH_READY_BUDGET_MS;
30820
31500
  let last = { retryable: true, reason: "no probe ran" };
30821
31501
  let identity = null;
@@ -30910,43 +31590,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30910
31590
  if (!prepared.ok) {
30911
31591
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
30912
31592
  }
30913
- const stageVolumes = stageVolumeNames(vm);
30914
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30915
- const created = docker3.sync(volumeCreateArgv(name, runId), {
30916
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
30917
- });
30918
- if (created.error || created.status !== 0) {
30919
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
30920
- }
30921
- }
30922
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30923
- const probe = docker3.sync(execProbeArgv(name), {
30924
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
30925
- });
30926
- if (probe.error || probe.status !== 0) {
30927
- 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)}`);
30928
- }
30929
- }
30930
- const after = io.classifyDaemon(docker3);
30931
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
30932
- 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.`);
30933
- }
30934
- return makeLease({
30935
- runId,
30936
- vm,
30937
- project,
30938
- zone,
30939
- stageBase,
30940
- stageVolumes,
31593
+ return finishAcquire({
30941
31594
  docker: docker3,
30942
31595
  tunnel,
30943
31596
  socketDir,
30944
31597
  socketPath,
30945
31598
  venueDaemonId,
30946
31599
  venueIdentity,
30947
- env,
30948
- io
31600
+ transport: sshStageTransport(vm, project, zone, env, io)
30949
31601
  });
31602
+ function finishAcquire(r) {
31603
+ const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
31604
+ const stageVolumes = stageVolumeNames(vm);
31605
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
31606
+ const created = docker4.sync(volumeCreateArgv(name, runId), {
31607
+ timeoutMs: VOLUME_CREATE_TIMEOUT_MS
31608
+ });
31609
+ if (created.error || created.status !== 0) {
31610
+ throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
31611
+ }
31612
+ }
31613
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
31614
+ const probe = docker4.sync(execProbeArgv(name), {
31615
+ timeoutMs: EXEC_PROBE_TIMEOUT_MS
31616
+ });
31617
+ if (probe.error || probe.status !== 0) {
31618
+ 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)}`);
31619
+ }
31620
+ }
31621
+ const after = io.classifyDaemon(docker4);
31622
+ if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
31623
+ 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.`);
31624
+ }
31625
+ return makeLease({
31626
+ runId,
31627
+ vm,
31628
+ project,
31629
+ zone,
31630
+ stageBase,
31631
+ stageVolumes,
31632
+ docker: docker4,
31633
+ tunnel: r.tunnel,
31634
+ socketDir: r.socketDir,
31635
+ socketPath: socketPath2,
31636
+ venueDaemonId: venueDaemonId2,
31637
+ venueIdentity: r.venueIdentity,
31638
+ transport: r.transport,
31639
+ env,
31640
+ io
31641
+ });
31642
+ }
30950
31643
  })
30951
31644
  };
30952
31645
  }
@@ -31005,28 +31698,7 @@ function makeLease(p) {
31005
31698
  return null;
31006
31699
  };
31007
31700
  let stagedOwner;
31008
- const readStagedOwner = () => {
31009
- const res = p.io.exec(
31010
- "gcloud",
31011
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
31012
- OWNER_PROBE_TIMEOUT_MS,
31013
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
31014
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
31015
- // omitted this would ssh under the person's own credential — then report an owner
31016
- // measured against a session the run does not use.
31017
- p.env
31018
- );
31019
- if (!res.ok) {
31020
- 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.`);
31021
- }
31022
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
31023
- const ids2 = lines.slice(-2).map((l) => Number(l));
31024
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
31025
- 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.`);
31026
- }
31027
- return { uid: ids2[0], gid: ids2[1] };
31028
- };
31029
- stagedOwner = readStagedOwner();
31701
+ stagedOwner = p.transport.readOwner();
31030
31702
  return {
31031
31703
  kind: "hosted-pool",
31032
31704
  runId: p.runId,
@@ -31061,7 +31733,7 @@ function makeLease(p) {
31061
31733
  const dir = venueProxyDir(p.stageBase);
31062
31734
  const buffer = stageProxyCode();
31063
31735
  try {
31064
- const res = await p.io.pushTree(buffer.dir, "gcloud", iapUntarProxyArgv(p.vm, p.project, p.zone, dir), STAGE_PUSH_TIMEOUT_MS, p.env);
31736
+ const res = await p.transport.pushProxy(buffer.dir, dir);
31065
31737
  if (!res.ok) {
31066
31738
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
31067
31739
  }
@@ -31071,7 +31743,7 @@ function makeLease(p) {
31071
31743
  return {
31072
31744
  dir,
31073
31745
  cleanup: () => {
31074
- p.io.exec("gcloud", iapSshArgv(p.vm, p.project, p.zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, p.env);
31746
+ p.transport.removeProxy(dir);
31075
31747
  }
31076
31748
  };
31077
31749
  },
@@ -31079,7 +31751,7 @@ function makeLease(p) {
31079
31751
  check("staging the tree");
31080
31752
  const paths = venueStagePaths(p.stageBase);
31081
31753
  const push = async (from, to) => {
31082
- const res = await p.io.pushTree(from, "gcloud", iapUntarArgv(p.vm, p.project, p.zone, to), STAGE_PUSH_TIMEOUT_MS, p.env);
31754
+ const res = await p.transport.push(from, to);
31083
31755
  if (!res.ok) {
31084
31756
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
31085
31757
  }
@@ -31237,7 +31909,7 @@ function makeLease(p) {
31237
31909
  }
31238
31910
  };
31239
31911
  }
31240
- 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;
31912
+ 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;
31241
31913
  var init_hostedVenue = __esm({
31242
31914
  "../../packages/envrun/dist/hostedVenue.js"() {
31243
31915
  "use strict";
@@ -31358,7 +32030,55 @@ var init_hostedVenue = __esm({
31358
32030
  throw new Error("JWKS fetch returned a body with no keys array");
31359
32031
  }
31360
32032
  return body;
31361
- }
32033
+ },
32034
+ fetchConfidentialSpaceJwks: async () => {
32035
+ const getJson = async (url, what) => {
32036
+ const res = await globalThis.fetch(url, {
32037
+ signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
32038
+ });
32039
+ if (!res.ok) {
32040
+ throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
32041
+ }
32042
+ return res.json();
32043
+ };
32044
+ const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
32045
+ const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
32046
+ if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
32047
+ throw new Error("Confidential Space discovery names no https jwks_uri");
32048
+ }
32049
+ const body = await getJson(jwksUri, "key set");
32050
+ if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
32051
+ throw new Error("Confidential Space key set has no keys array");
32052
+ }
32053
+ return body;
32054
+ },
32055
+ freeLocalPorts: async (n) => {
32056
+ const servers = [];
32057
+ try {
32058
+ for (let i = 0; i < n; i += 1) {
32059
+ const server = createServer();
32060
+ servers.push(server);
32061
+ await new Promise((resolve4, reject) => {
32062
+ server.once("error", reject);
32063
+ server.listen(0, "localhost", () => {
32064
+ resolve4();
32065
+ });
32066
+ });
32067
+ }
32068
+ return servers.map((s) => s.address().port);
32069
+ } finally {
32070
+ await Promise.all(servers.map((s) => new Promise((resolve4) => {
32071
+ s.close(() => {
32072
+ resolve4();
32073
+ });
32074
+ })));
32075
+ }
32076
+ },
32077
+ writeFile: (path5, text) => {
32078
+ writeFileSync15(path5, text, { mode: 384, flag: "wx" });
32079
+ },
32080
+ fetchAttestation: (req) => fetchAttestationOverTls(req),
32081
+ dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
31362
32082
  };
31363
32083
  VENUE_SSH_USER = "th-runner";
31364
32084
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -31379,6 +32099,17 @@ var init_hostedVenue = __esm({
31379
32099
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
31380
32100
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
31381
32101
  SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
32102
+ CS_ATTEST_PORT = 8443;
32103
+ CS_DOCKER_PORT = 2376;
32104
+ CS_READY_BUDGET_MS = 48e4;
32105
+ CS_ATTEST_TIMEOUT_MS = 15e3;
32106
+ CS_PULL_TIMEOUT_MS = 6e5;
32107
+ CS_ATTEST_INTERVAL_MS = 5e3;
32108
+ OPENSSL_TIMEOUT_MS = 3e4;
32109
+ CS_RUN_ARGS = ["--oom-score-adj", "1000"];
32110
+ CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
32111
+ CS_STAGE_ROOT = "/tmp";
32112
+ CLIENT_CERT_STAND_IN = "stand-in-certificate";
31382
32113
  }
31383
32114
  });
31384
32115
 
@@ -32278,10 +33009,10 @@ var init_references = __esm({
32278
33009
 
32279
33010
  // ../../packages/envspec/dist/repo.js
32280
33011
  import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
32281
- import { join as join27, relative as relative2, sep as sep4 } from "path";
33012
+ import { join as join27, relative as relative2, sep as sep5 } from "path";
32282
33013
  function createRepoReader(repoPath) {
32283
33014
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join27(repoPath, relativePath);
32284
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
33015
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
32285
33016
  const readText = (relativePath) => {
32286
33017
  try {
32287
33018
  return readFileSync14(resolveIn(relativePath), "utf8");
@@ -35111,7 +35842,7 @@ __export(jpi_claim_exports, {
35111
35842
  });
35112
35843
  import {
35113
35844
  readFileSync as readFileSync15,
35114
- writeFileSync as writeFileSync15,
35845
+ writeFileSync as writeFileSync16,
35115
35846
  mkdirSync as mkdirSync7,
35116
35847
  mkdtempSync as mkdtempSync5,
35117
35848
  renameSync as renameSync7,
@@ -35122,7 +35853,7 @@ import {
35122
35853
  readdirSync as readdirSync3
35123
35854
  } from "fs";
35124
35855
  import { join as join29, dirname as dirname9, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
35125
- import { createHash as createHash9 } from "crypto";
35856
+ import { createHash as createHash10 } from "crypto";
35126
35857
  import { homedir as homedir15, hostname as osHostname } from "os";
35127
35858
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
35128
35859
  import { promisify as promisify3 } from "util";
@@ -35140,7 +35871,7 @@ function markClaimNudged(id) {
35140
35871
  const ids2 = readNudgedClaimIds();
35141
35872
  ids2.add(id);
35142
35873
  ensureStateDir(TERMINALHIRE_DIR11);
35143
- writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35874
+ writeFileSync16(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35144
35875
  } catch {
35145
35876
  }
35146
35877
  }
@@ -36314,7 +37045,7 @@ async function registerFounderClaim(b) {
36314
37045
  let clearedLocalCredential = false;
36315
37046
  let refusedForPurpose = false;
36316
37047
  let preserveBackgroundToken = false;
36317
- const refuse3 = (reason) => {
37048
+ const refuse4 = (reason) => {
36318
37049
  console.error(
36319
37050
  `
36320
37051
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -36339,7 +37070,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36339
37070
  try {
36340
37071
  expectLogin = await localLoginForPaidBrowserClaim();
36341
37072
  } catch (err) {
36342
- refuse3(
37073
+ refuse4(
36343
37074
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
36344
37075
  Run \`terminalhire login\`, then retry this claim. Browser verification was
36345
37076
  not opened and no claim was sent.`
@@ -36355,16 +37086,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
36355
37086
  claimRef: opportunityShortToken(b.bountyId)
36356
37087
  });
36357
37088
  if (!proofToken) {
36358
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
37089
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
36359
37090
  }
36360
37091
  const verifiedLogin = proofTokenLogin(proofToken);
36361
37092
  if (!verifiedLogin) {
36362
- refuse3(
37093
+ refuse4(
36363
37094
  "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."
36364
37095
  );
36365
37096
  }
36366
37097
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
36367
- refuse3(
37098
+ refuse4(
36368
37099
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
36369
37100
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
36370
37101
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -36396,7 +37127,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36396
37127
  try {
36397
37128
  res = await sendRegistration(true);
36398
37129
  } catch (err) {
36399
- refuse3(
37130
+ refuse4(
36400
37131
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36401
37132
  );
36402
37133
  }
@@ -36447,7 +37178,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36447
37178
  try {
36448
37179
  res = await sendRegistration(true);
36449
37180
  } catch (err) {
36450
- refuse3(
37181
+ refuse4(
36451
37182
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36452
37183
  );
36453
37184
  }
@@ -36458,14 +37189,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
36458
37189
  res = await sendRegistration(false);
36459
37190
  refusalBody = await readRefusal(res);
36460
37191
  } catch (err) {
36461
- refuse3(
37192
+ refuse4(
36462
37193
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36463
37194
  );
36464
37195
  }
36465
37196
  }
36466
37197
  if (!res.ok) {
36467
37198
  const detail = refusalBody?.message || refusalBody?.error || "";
36468
- refuse3(
37199
+ refuse4(
36469
37200
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
36470
37201
  );
36471
37202
  }
@@ -36476,7 +37207,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36476
37207
  body = null;
36477
37208
  }
36478
37209
  if (!body || body.ok !== true) {
36479
- refuse3("malformed registration response from the server.");
37210
+ refuse4("malformed registration response from the server.");
36480
37211
  }
36481
37212
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
36482
37213
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -37906,7 +38637,7 @@ function writeSliceFiles(destDir, files) {
37906
38637
  if (typeof f.content === "string") {
37907
38638
  const abs = join29(destDir, f.path);
37908
38639
  mkdirSync7(dirname9(abs), { recursive: true });
37909
- writeFileSync15(abs, f.content, "utf8");
38640
+ writeFileSync16(abs, f.content, "utf8");
37910
38641
  written.push(f.path);
37911
38642
  } else {
37912
38643
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -37955,7 +38686,7 @@ function ensureExcludedPackDir(destDir) {
37955
38686
  const existing = existsSync13(excludeFile) ? readFileSync15(excludeFile, "utf8") : "";
37956
38687
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
37957
38688
  mkdirSync7(dirname9(excludeFile), { recursive: true });
37958
- writeFileSync15(
38689
+ writeFileSync16(
37959
38690
  excludeFile,
37960
38691
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
37961
38692
  `,
@@ -37971,14 +38702,14 @@ function writePackFile(destDir, relPath, content, what) {
37971
38702
  try {
37972
38703
  const abs = join29(destDir, relPath);
37973
38704
  mkdirSync7(dirname9(abs), { recursive: true });
37974
- writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
38705
+ writeFileSync16(abs, content, { encoding: "utf8", flag: "wx" });
37975
38706
  } catch (err) {
37976
38707
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
37977
38708
  }
37978
38709
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
37979
38710
  }
37980
38711
  function sha256OfUtf8(content) {
37981
- return createHash9("sha256").update(content, "utf8").digest("hex");
38712
+ return createHash10("sha256").update(content, "utf8").digest("hex");
37982
38713
  }
37983
38714
  function writeWorkspacePack(destDir, spec, claim, delivery) {
37984
38715
  if (delivery !== "full" && delivery !== "sparse") {
@@ -39335,7 +40066,7 @@ function readClaimPushMarker() {
39335
40066
  }
39336
40067
  function writeClaimPushMarker(marker) {
39337
40068
  ensureStateDir(TERMINALHIRE_DIR11);
39338
- writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
40069
+ writeFileSync16(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
39339
40070
  }
39340
40071
  function clearClaimPushMarker() {
39341
40072
  try {
@@ -40487,7 +41218,7 @@ __export(util_exports, {
40487
41218
  getSizableOrigin: () => getSizableOrigin,
40488
41219
  hexToUint8Array: () => hexToUint8Array,
40489
41220
  isObject: () => isObject,
40490
- isPlainObject: () => isPlainObject,
41221
+ isPlainObject: () => isPlainObject2,
40491
41222
  issue: () => issue,
40492
41223
  joinValues: () => joinValues,
40493
41224
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -40649,7 +41380,7 @@ function slugify(input) {
40649
41380
  function isObject(data) {
40650
41381
  return typeof data === "object" && data !== null && !Array.isArray(data);
40651
41382
  }
40652
- function isPlainObject(o) {
41383
+ function isPlainObject2(o) {
40653
41384
  if (isObject(o) === false)
40654
41385
  return false;
40655
41386
  const ctor = o.constructor;
@@ -40666,7 +41397,7 @@ function isPlainObject(o) {
40666
41397
  return true;
40667
41398
  }
40668
41399
  function shallowClone(o) {
40669
- if (isPlainObject(o))
41400
+ if (isPlainObject2(o))
40670
41401
  return { ...o };
40671
41402
  if (Array.isArray(o))
40672
41403
  return [...o];
@@ -40806,7 +41537,7 @@ function omit(schema, mask) {
40806
41537
  return clone(schema, def);
40807
41538
  }
40808
41539
  function extend(schema, shape) {
40809
- if (!isPlainObject(shape)) {
41540
+ if (!isPlainObject2(shape)) {
40810
41541
  throw new Error("Invalid input to extend: expected a plain object");
40811
41542
  }
40812
41543
  const checks = schema._zod.def.checks;
@@ -40829,7 +41560,7 @@ function extend(schema, shape) {
40829
41560
  return clone(schema, def);
40830
41561
  }
40831
41562
  function safeExtend(schema, shape) {
40832
- if (!isPlainObject(shape)) {
41563
+ if (!isPlainObject2(shape)) {
40833
41564
  throw new Error("Invalid input to safeExtend: expected a plain object");
40834
41565
  }
40835
41566
  const def = mergeDefs(schema._zod.def, {
@@ -41990,7 +42721,7 @@ function mergeValues(a, b) {
41990
42721
  if (a instanceof Date && b instanceof Date && +a === +b) {
41991
42722
  return { valid: true, data: a };
41992
42723
  }
41993
- if (isPlainObject(a) && isPlainObject(b)) {
42724
+ if (isPlainObject2(a) && isPlainObject2(b)) {
41994
42725
  const bKeys = Object.keys(b);
41995
42726
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
41996
42727
  const newObj = { ...a, ...b };
@@ -42942,7 +43673,7 @@ var init_schemas = __esm({
42942
43673
  $ZodType.init(inst, def);
42943
43674
  inst._zod.parse = (payload, ctx) => {
42944
43675
  const input = payload.value;
42945
- if (!isPlainObject(input)) {
43676
+ if (!isPlainObject2(input)) {
42946
43677
  payload.issues.push({
42947
43678
  expected: "record",
42948
43679
  code: "invalid_type",
@@ -48084,7 +48815,7 @@ var init_zod_json_schema_compat = __esm({
48084
48815
  });
48085
48816
 
48086
48817
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
48087
- function isPlainObject2(value) {
48818
+ function isPlainObject3(value) {
48088
48819
  return value !== null && typeof value === "object" && !Array.isArray(value);
48089
48820
  }
48090
48821
  function mergeCapabilities(base, additional) {
@@ -48095,7 +48826,7 @@ function mergeCapabilities(base, additional) {
48095
48826
  if (addValue === void 0)
48096
48827
  continue;
48097
48828
  const baseValue = result[k];
48098
- if (isPlainObject2(baseValue) && isPlainObject2(addValue)) {
48829
+ if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
48099
48830
  result[k] = { ...baseValue, ...addValue };
48100
48831
  } else {
48101
48832
  result[k] = addValue;
@@ -62453,7 +63184,7 @@ __export(config_exports, {
62453
63184
  readConfig: () => readConfig,
62454
63185
  writeConfig: () => writeConfig
62455
63186
  });
62456
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync16, existsSync as existsSync14 } from "fs";
63187
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync17, existsSync as existsSync14 } from "fs";
62457
63188
  import { join as join30 } from "path";
62458
63189
  import { homedir as homedir16 } from "os";
62459
63190
  function readConfig() {
@@ -62476,7 +63207,7 @@ function writeConfig(config2) {
62476
63207
  }
62477
63208
  delete merged.contributePrompted;
62478
63209
  }
62479
- writeFileSync16(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
63210
+ writeFileSync17(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
62480
63211
  }
62481
63212
  function parseNudgeMode(raw) {
62482
63213
  if (raw === "session" || raw === "always") return raw;