terminalhire 0.42.19 → 0.42.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  },
@@ -29075,6 +29101,197 @@ var init_gceIdentity = __esm({
29075
29101
  }
29076
29102
  });
29077
29103
 
29104
+ // ../../packages/attest/dist/confidentialSpace.js
29105
+ import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
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;
29284
+ var init_confidentialSpace = __esm({
29285
+ "../../packages/attest/dist/confidentialSpace.js"() {
29286
+ "use strict";
29287
+ init_gceIdentity();
29288
+ CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
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";
29292
+ }
29293
+ });
29294
+
29078
29295
  // ../../packages/attest/dist/venueInstanceName.js
29079
29296
  function venueInstanceName(runId) {
29080
29297
  return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
@@ -29109,6 +29326,7 @@ var init_dist2 = __esm({
29109
29326
  init_attestation();
29110
29327
  init_nonce();
29111
29328
  init_gceIdentity();
29329
+ init_confidentialSpace();
29112
29330
  init_venueInstanceName();
29113
29331
  init_verify();
29114
29332
  }
@@ -29359,7 +29577,7 @@ var init_result = __esm({
29359
29577
  });
29360
29578
 
29361
29579
  // ../../packages/envrun/dist/attestation.js
29362
- import { createHash as createHash7, randomBytes as randomBytes10 } from "crypto";
29580
+ import { createHash as createHash8, randomBytes as randomBytes10 } from "crypto";
29363
29581
  function contradicts(outcome, counts, exitCode) {
29364
29582
  const budget = OUTCOME_TO_BUDGET[outcome];
29365
29583
  if (budget === null)
@@ -29401,7 +29619,7 @@ function imageDigestOf(ref) {
29401
29619
  return at === -1 ? null : ref.slice(at + 1);
29402
29620
  }
29403
29621
  function sha256Hex(data) {
29404
- return createHash7("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
29622
+ return createHash8("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
29405
29623
  }
29406
29624
  function toTestRunResult(result, outputSha256) {
29407
29625
  return {
@@ -29413,50 +29631,50 @@ function toTestRunResult(result, outputSha256) {
29413
29631
  function toAcceptancePredicate(pair, opts = {}) {
29414
29632
  const { baseline, patched } = pair;
29415
29633
  if (baseline.runId === patched.runId) {
29416
- 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.`);
29417
29635
  }
29418
29636
  if (patched.status === "refused") {
29419
- 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`);
29420
29638
  }
29421
29639
  if (patched.outcome === null) {
29422
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
29640
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
29423
29641
  }
29424
29642
  if (baseline.status === "refused") {
29425
- 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`);
29426
29644
  }
29427
29645
  if (baseline.outcome === null) {
29428
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
29646
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
29429
29647
  }
29430
29648
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
29431
- 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.`);
29432
29650
  }
29433
29651
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
29434
29652
  if (budget === null) {
29435
- 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.`);
29436
29654
  }
29437
29655
  if (patched.testCommand === null) {
29438
- 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");
29439
29657
  }
29440
29658
  if (baseline.testCommand !== patched.testCommand) {
29441
- 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.`);
29442
29660
  }
29443
29661
  if (baseline.targetSha !== patched.targetSha) {
29444
- 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}`);
29445
29663
  }
29446
29664
  if (patched.treeDigest === null || patched.treeDigest === "") {
29447
- 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");
29448
29666
  }
29449
29667
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
29450
- 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");
29451
29669
  }
29452
29670
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
29453
- 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");
29454
29672
  }
29455
29673
  if (baseline.treeDigest === patched.treeDigest) {
29456
- 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`);
29457
29675
  }
29458
29676
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
29459
- 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.`);
29460
29678
  }
29461
29679
  for (const [side, outcome, counts, exitCode] of [
29462
29680
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -29464,7 +29682,7 @@ function toAcceptancePredicate(pair, opts = {}) {
29464
29682
  ]) {
29465
29683
  const why = contradicts(outcome, counts, exitCode);
29466
29684
  if (why !== null) {
29467
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
29685
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
29468
29686
  }
29469
29687
  }
29470
29688
  for (const [side, source] of [
@@ -29473,44 +29691,44 @@ function toAcceptancePredicate(pair, opts = {}) {
29473
29691
  ]) {
29474
29692
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
29475
29693
  if (why !== null)
29476
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
29694
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
29477
29695
  }
29478
29696
  if (patched.testCommandSource === "developer-declared") {
29479
- 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)");
29480
29698
  }
29481
29699
  if (baseline.testCommandSource !== patched.testCommandSource) {
29482
- 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.`);
29483
29701
  }
29484
29702
  if (baseline.containerImage !== patched.containerImage) {
29485
- 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.`);
29486
29704
  }
29487
29705
  if (patched.containerImage === null || patched.containerImage === "") {
29488
- 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");
29489
29707
  }
29490
29708
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
29491
- 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.");
29492
29710
  }
29493
29711
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
29494
- 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.`);
29495
29713
  }
29496
29714
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
29497
- 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.`);
29498
29716
  }
29499
29717
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
29500
- 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.");
29501
29719
  }
29502
29720
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
29503
- 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.`);
29504
29722
  }
29505
29723
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
29506
- 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.`);
29507
29725
  }
29508
29726
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
29509
- 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.`);
29510
29728
  }
29511
29729
  const claimedDigest = imageDigestOf(patched.containerImage);
29512
29730
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
29513
- 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.`);
29514
29732
  }
29515
29733
  return {
29516
29734
  ok: true,
@@ -29537,7 +29755,7 @@ function signRunStatement(predicate, privateKey, keyid) {
29537
29755
  const statement = createAcceptanceStatement(predicate);
29538
29756
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
29539
29757
  }
29540
- 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;
29541
29759
  var init_attestation2 = __esm({
29542
29760
  "../../packages/envrun/dist/attestation.js"() {
29543
29761
  "use strict";
@@ -29641,7 +29859,7 @@ var init_attestation2 = __esm({
29641
29859
  "outcome-contradicts-counts",
29642
29860
  "test-command-origin-unattested"
29643
29861
  ];
29644
- refuse2 = (reason, detail) => ({
29862
+ refuse3 = (reason, detail) => ({
29645
29863
  ok: false,
29646
29864
  reason,
29647
29865
  detail
@@ -30024,6 +30242,73 @@ var init_boundary = __esm({
30024
30242
  });
30025
30243
 
30026
30244
  // ../../packages/envrun/dist/gcpPlacement.js
30245
+ function assertWorkloadImage(fn, value) {
30246
+ if (typeof value !== "string" || !WORKLOAD_IMAGE_REFERENCE.test(value)) {
30247
+ throw new GcpPlacementError(`${fn}: the workload image is missing or is not one plain image reference (${String(WORKLOAD_IMAGE_REFERENCE)}). It lands inside a comma-separated --metadata= list, so anything that could start a second key is refused.`);
30248
+ }
30249
+ }
30250
+ function venueImageFromEnv(env) {
30251
+ const image = env["VENUE_IMAGE"] ?? "";
30252
+ if (image === "" || image === "cos-stable")
30253
+ return { venueImage: "cos-stable" };
30254
+ if (image !== "confidential-space") {
30255
+ throw new GcpPlacementError("VENUE_IMAGE is set to something other than cos-stable or confidential-space. An unknown value is refused rather than booted as the default.");
30256
+ }
30257
+ const workloadImage = env["VENUE_WORKLOAD_IMAGE"] ?? "";
30258
+ if (workloadImage === "") {
30259
+ throw new GcpPlacementError("VENUE_IMAGE=confidential-space needs VENUE_WORKLOAD_IMAGE, the image reference the Confidential Space launcher runs. Nothing boots without one.");
30260
+ }
30261
+ assertWorkloadImage("venueImageFromEnv", workloadImage);
30262
+ return { venueImage: "confidential-space", workloadImage };
30263
+ }
30264
+ function bootImageFlags(p) {
30265
+ const image = p.venueImage ?? "cos-stable";
30266
+ if (image === "cos-stable")
30267
+ return ["--image-family=cos-stable", "--image-project=cos-cloud"];
30268
+ if (image !== "confidential-space") {
30269
+ throw new GcpPlacementError("gcpBootArgv: venueImage is not one of cos-stable, confidential-space. An unknown image is refused rather than booted as the default.");
30270
+ }
30271
+ const workloadImage = p.workloadImage;
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
+ ];
30289
+ return [
30290
+ // The production family. The debug family reports `dbgstat: enabled`, which the
30291
+ // intake's verifier refuses, so a debug boot could never produce a run we record.
30292
+ "--image-family=confidential-space",
30293
+ "--image-project=confidential-space-images",
30294
+ "--confidential-compute-type=SEV",
30295
+ // The deploy-workloads page says N2D with SEV may set MIGRATE for live migration,
30296
+ // and every other machine type needs TERMINATE. The spec chose TERMINATE: a
30297
+ // maintenance event ends the VM instead of moving it, which for a VM that lives at
30298
+ // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
30299
+ // combination is phase 5's measurement.
30300
+ "--maintenance-policy=TERMINATE",
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"
30310
+ ];
30311
+ }
30027
30312
  function assertInstanceIdentity(fn, id) {
30028
30313
  const fields = [
30029
30314
  ["vmName", id.vmName, GCE_INSTANCE_NAME],
@@ -30049,8 +30334,8 @@ function gcpBootArgv(p) {
30049
30334
  `--project=${p.project}`,
30050
30335
  `--zone=${p.zone}`,
30051
30336
  `--machine-type=${p.machineType}`,
30052
- "--image-family=cos-stable",
30053
- "--image-project=cos-cloud",
30337
+ // What the VM boots, and the one part of this argv the two images do not share.
30338
+ ...bootImageFlags(p),
30054
30339
  // ── THE OUTER REAP. This, not the `finally` below, is what bounds the bill. ──
30055
30340
  //
30056
30341
  // An in-process `finally` is a best effort, not a reap. SIGKILL, a crashed
@@ -30095,7 +30380,13 @@ function gcpBootArgv(p) {
30095
30380
  // expectation uses (TERM-832): two inline spellings of one value is how
30096
30381
  // the boot path and the verifier drift apart silently.
30097
30382
  `--service-account=${venueIdentityEmail(p.project)}`,
30098
- "--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"],
30099
30390
  "--shielded-secure-boot",
30100
30391
  "--shielded-vtpm",
30101
30392
  "--shielded-integrity-monitoring",
@@ -30128,7 +30419,7 @@ function gcpDeleteArgv(p) {
30128
30419
  "--quiet"
30129
30420
  ];
30130
30421
  }
30131
- var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
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;
30132
30423
  var init_gcpPlacement = __esm({
30133
30424
  "../../packages/envrun/dist/gcpPlacement.js"() {
30134
30425
  "use strict";
@@ -30145,6 +30436,10 @@ var init_gcpPlacement = __esm({
30145
30436
  GCP_MAX_RUN_DURATION_SECONDS = 3600;
30146
30437
  GCP_MANAGED_LABEL_KEY = "th-managed";
30147
30438
  GCP_RUN_LABEL_KEY = "th-run";
30439
+ CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
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;
30148
30443
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
30149
30444
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
30150
30445
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -30186,7 +30481,10 @@ var init_emptyGitConfig = __esm({
30186
30481
 
30187
30482
  // ../../packages/envrun/dist/hostedVenue.js
30188
30483
  import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
30189
- 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";
30190
30488
  import { join as join26 } from "path";
30191
30489
  import { tmpdir as tmpdir3 } from "os";
30192
30490
  function credentialInGitConfig(text) {
@@ -30388,6 +30686,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
30388
30686
  });
30389
30687
  });
30390
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
+ }
30391
30772
  function quoteForRemoteShell(arg) {
30392
30773
  return `'${arg.replace(/'/g, "'\\''")}'`;
30393
30774
  }
@@ -30722,25 +31103,370 @@ ${res.stderr}`);
30722
31103
  function manualDeleteCommand(vm, project, zone) {
30723
31104
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
30724
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
+ }
30725
31435
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30726
31436
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
30727
31437
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
30728
31438
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
30729
31439
  const keyFile = resolveServiceAccountKeyFile(opts);
31440
+ const image = venueImageFromEnv(process.env);
31441
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
30730
31442
  const env = venueGcloudEnv(gcloudConfig);
30731
31443
  return {
30732
31444
  kind: "hosted-pool",
30733
31445
  acquire: (runId) => acquireTransactionally(async (allocated) => {
30734
31446
  const vm = venueInstanceName(runId);
30735
31447
  const stageBase = `/tmp/th-stage-${runId}`;
30736
- const bootArgv = gcpBootArgv({
31448
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
30737
31449
  vmName: vm,
30738
31450
  project,
30739
31451
  zone,
30740
31452
  runId,
30741
- machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE
30742
- });
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,
31463
+ ...image
31464
+ };
31465
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
30743
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));
30744
31470
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
30745
31471
  let instanceState = "unknown";
30746
31472
  allocated.onRollback(() => {
@@ -30756,6 +31482,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30756
31482
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
30757
31483
  }
30758
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
+ }
30759
31499
  const deadline = io.now() + SSH_READY_BUDGET_MS;
30760
31500
  let last = { retryable: true, reason: "no probe ran" };
30761
31501
  let identity = null;
@@ -30850,43 +31590,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
30850
31590
  if (!prepared.ok) {
30851
31591
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
30852
31592
  }
30853
- const stageVolumes = stageVolumeNames(vm);
30854
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30855
- const created = docker3.sync(volumeCreateArgv(name, runId), {
30856
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
30857
- });
30858
- if (created.error || created.status !== 0) {
30859
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
30860
- }
30861
- }
30862
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
30863
- const probe = docker3.sync(execProbeArgv(name), {
30864
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
30865
- });
30866
- if (probe.error || probe.status !== 0) {
30867
- 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)}`);
30868
- }
30869
- }
30870
- const after = io.classifyDaemon(docker3);
30871
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
30872
- 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.`);
30873
- }
30874
- return makeLease({
30875
- runId,
30876
- vm,
30877
- project,
30878
- zone,
30879
- stageBase,
30880
- stageVolumes,
31593
+ return finishAcquire({
30881
31594
  docker: docker3,
30882
31595
  tunnel,
30883
31596
  socketDir,
30884
31597
  socketPath,
30885
31598
  venueDaemonId,
30886
31599
  venueIdentity,
30887
- env,
30888
- io
31600
+ transport: sshStageTransport(vm, project, zone, env, io)
30889
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
+ }
30890
31643
  })
30891
31644
  };
30892
31645
  }
@@ -30945,28 +31698,7 @@ function makeLease(p) {
30945
31698
  return null;
30946
31699
  };
30947
31700
  let stagedOwner;
30948
- const readStagedOwner = () => {
30949
- const res = p.io.exec(
30950
- "gcloud",
30951
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
30952
- OWNER_PROBE_TIMEOUT_MS,
30953
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
30954
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
30955
- // omitted this would ssh under the person's own credential — then report an owner
30956
- // measured against a session the run does not use.
30957
- p.env
30958
- );
30959
- if (!res.ok) {
30960
- 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.`);
30961
- }
30962
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
30963
- const ids2 = lines.slice(-2).map((l) => Number(l));
30964
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
30965
- 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.`);
30966
- }
30967
- return { uid: ids2[0], gid: ids2[1] };
30968
- };
30969
- stagedOwner = readStagedOwner();
31701
+ stagedOwner = p.transport.readOwner();
30970
31702
  return {
30971
31703
  kind: "hosted-pool",
30972
31704
  runId: p.runId,
@@ -31001,7 +31733,7 @@ function makeLease(p) {
31001
31733
  const dir = venueProxyDir(p.stageBase);
31002
31734
  const buffer = stageProxyCode();
31003
31735
  try {
31004
- 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);
31005
31737
  if (!res.ok) {
31006
31738
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
31007
31739
  }
@@ -31011,7 +31743,7 @@ function makeLease(p) {
31011
31743
  return {
31012
31744
  dir,
31013
31745
  cleanup: () => {
31014
- 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);
31015
31747
  }
31016
31748
  };
31017
31749
  },
@@ -31019,7 +31751,7 @@ function makeLease(p) {
31019
31751
  check("staging the tree");
31020
31752
  const paths = venueStagePaths(p.stageBase);
31021
31753
  const push = async (from, to) => {
31022
- 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);
31023
31755
  if (!res.ok) {
31024
31756
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
31025
31757
  }
@@ -31177,7 +31909,7 @@ function makeLease(p) {
31177
31909
  }
31178
31910
  };
31179
31911
  }
31180
- 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;
31181
31913
  var init_hostedVenue = __esm({
31182
31914
  "../../packages/envrun/dist/hostedVenue.js"() {
31183
31915
  "use strict";
@@ -31298,7 +32030,55 @@ var init_hostedVenue = __esm({
31298
32030
  throw new Error("JWKS fetch returned a body with no keys array");
31299
32031
  }
31300
32032
  return body;
31301
- }
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 })
31302
32082
  };
31303
32083
  VENUE_SSH_USER = "th-runner";
31304
32084
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -31319,6 +32099,17 @@ var init_hostedVenue = __esm({
31319
32099
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
31320
32100
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
31321
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";
31322
32113
  }
31323
32114
  });
31324
32115
 
@@ -32218,10 +33009,10 @@ var init_references = __esm({
32218
33009
 
32219
33010
  // ../../packages/envspec/dist/repo.js
32220
33011
  import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
32221
- 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";
32222
33013
  function createRepoReader(repoPath) {
32223
33014
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join27(repoPath, relativePath);
32224
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
33015
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
32225
33016
  const readText = (relativePath) => {
32226
33017
  try {
32227
33018
  return readFileSync14(resolveIn(relativePath), "utf8");
@@ -32864,13 +33655,16 @@ function git(repoDir, args, allowNonZero = false) {
32864
33655
  }
32865
33656
  return res.stdout ?? "";
32866
33657
  }
32867
- function collectWorkingDiff(repoDir) {
33658
+ function collectWorkingDiff(repoDir, opts = {}) {
32868
33659
  if (!existsSync12(join28(repoDir, ".git"))) {
32869
33660
  throw new ThRunError(`${repoDir} is not a git checkout (no .git). \`th run\` ships the working diff, so it needs a repository to read one from.`);
32870
33661
  }
32871
33662
  const headSha = git(repoDir, ["rev-parse", "HEAD"]).trim();
32872
- const tracked = git(repoDir, ["diff", "HEAD", "--binary"]);
32873
- const trackedChanged = git(repoDir, ["diff", "HEAD", "--name-only"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
33663
+ const diffBase = opts.base === void 0 ? headSha : assertDiffBase(repoDir, opts.base);
33664
+ const trackedArgs = opts.base === void 0 ? ["diff", "HEAD"] : ["diff", diffBase];
33665
+ const pathsEnd = opts.base === void 0 ? [] : ["--"];
33666
+ const tracked = git(repoDir, [...trackedArgs, "--binary", ...pathsEnd]);
33667
+ const trackedChanged = git(repoDir, [...trackedArgs, "--name-only", ...pathsEnd]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
32874
33668
  const untracked = git(repoDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
32875
33669
  const parts = [];
32876
33670
  if (tracked.trim() !== "")
@@ -32880,7 +33674,17 @@ function collectWorkingDiff(repoDir) {
32880
33674
  if (one.trim() !== "")
32881
33675
  parts.push(one.replace(/\n*$/, "\n"));
32882
33676
  }
32883
- return { patch: parts.join(""), headSha, trackedChanged, untracked };
33677
+ return { patch: parts.join(""), headSha, diffBase, trackedChanged, untracked };
33678
+ }
33679
+ function assertDiffBase(repoDir, base) {
33680
+ if (!FULL_COMMIT_ID.test(base)) {
33681
+ throw new ThRunError(`the diff base must be a full 40-character commit id, got ${JSON.stringify(base)}.`);
33682
+ }
33683
+ const resolved = git(repoDir, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`], true);
33684
+ if (resolved.trim() !== base) {
33685
+ throw new ThRunError(`the diff base ${base} is not a commit in ${repoDir}.`);
33686
+ }
33687
+ return base;
32884
33688
  }
32885
33689
  function isLocalPath(p) {
32886
33690
  return !UNC_PATH.test(p) && (p.startsWith("/") || WINDOWS_ABSOLUTE.test(p));
@@ -33452,8 +34256,9 @@ async function runVerification(req, ctx) {
33452
34256
  let diff = null;
33453
34257
  if (source.kind === "working-diff") {
33454
34258
  progress("collect", `reading the working diff from ${source.localRepoDir}`);
33455
- diff = collectWorkingDiff(source.localRepoDir);
33456
- progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes`);
34259
+ diff = collectWorkingDiff(source.localRepoDir, source.diffBase === void 0 ? {} : { base: source.diffBase });
34260
+ const from = source.diffBase === void 0 ? "" : `, from delivered baseline ${diff.diffBase.slice(0, 12)}`;
34261
+ progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes${from}`);
33457
34262
  } else {
33458
34263
  progress("collect", "dispatched run: the tree is the stored commit itself");
33459
34264
  }
@@ -33655,7 +34460,7 @@ async function runVerification(req, ctx) {
33655
34460
  await releaseWithoutThrowing(lease, progress);
33656
34461
  }
33657
34462
  }
33658
- var ThRunError, OUTPUT_TAIL_BYTES, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
34463
+ var ThRunError, OUTPUT_TAIL_BYTES, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
33659
34464
  var init_thrun = __esm({
33660
34465
  "../../packages/envrun/dist/thrun.js"() {
33661
34466
  "use strict";
@@ -33673,6 +34478,7 @@ var init_thrun = __esm({
33673
34478
  ThRunError = class extends Error {
33674
34479
  };
33675
34480
  OUTPUT_TAIL_BYTES = 4e3;
34481
+ FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
33676
34482
  ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["https", "http", "ssh", "git"]);
33677
34483
  SCP_STYLE = /^[A-Za-z0-9._~+-]+@[A-Za-z0-9._-]+:[^:]/;
33678
34484
  URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
@@ -34531,6 +35337,7 @@ __export(dist_exports, {
34531
35337
  ALEMBIC_IMAGE: () => ALEMBIC_IMAGE,
34532
35338
  ATTEST_REFUSAL_REASONS: () => ATTEST_REFUSAL_REASONS,
34533
35339
  BOOKKEEPING_TABLES: () => BOOKKEEPING_TABLES,
35340
+ CONFIDENTIAL_SPACE_MACHINE_TYPE: () => CONFIDENTIAL_SPACE_MACHINE_TYPE,
34534
35341
  CONTAINMENT_UNAVAILABLE_PREFIX: () => CONTAINMENT_UNAVAILABLE_PREFIX,
34535
35342
  CloneUnavailableError: () => CloneUnavailableError,
34536
35343
  DAEMON_FACTS_FORMAT: () => DAEMON_FACTS_FORMAT,
@@ -34672,6 +35479,7 @@ __export(dist_exports, {
34672
35479
  toolingImageFor: () => toolingImageFor,
34673
35480
  unquoteDiffPath: () => unquoteDiffPath,
34674
35481
  venueGcloudEnv: () => venueGcloudEnv,
35482
+ venueImageFromEnv: () => venueImageFromEnv,
34675
35483
  venueStagePaths: () => venueStagePaths,
34676
35484
  verifyWorkingDiff: () => verifyWorkingDiff
34677
35485
  });
@@ -35034,7 +35842,7 @@ __export(jpi_claim_exports, {
35034
35842
  });
35035
35843
  import {
35036
35844
  readFileSync as readFileSync15,
35037
- writeFileSync as writeFileSync15,
35845
+ writeFileSync as writeFileSync16,
35038
35846
  mkdirSync as mkdirSync7,
35039
35847
  mkdtempSync as mkdtempSync5,
35040
35848
  renameSync as renameSync7,
@@ -35045,7 +35853,7 @@ import {
35045
35853
  readdirSync as readdirSync3
35046
35854
  } from "fs";
35047
35855
  import { join as join29, dirname as dirname9, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
35048
- import { createHash as createHash8 } from "crypto";
35856
+ import { createHash as createHash10 } from "crypto";
35049
35857
  import { homedir as homedir15, hostname as osHostname } from "os";
35050
35858
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
35051
35859
  import { promisify as promisify3 } from "util";
@@ -35063,7 +35871,7 @@ function markClaimNudged(id) {
35063
35871
  const ids2 = readNudgedClaimIds();
35064
35872
  ids2.add(id);
35065
35873
  ensureStateDir(TERMINALHIRE_DIR11);
35066
- writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35874
+ writeFileSync16(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
35067
35875
  } catch {
35068
35876
  }
35069
35877
  }
@@ -36237,7 +37045,7 @@ async function registerFounderClaim(b) {
36237
37045
  let clearedLocalCredential = false;
36238
37046
  let refusedForPurpose = false;
36239
37047
  let preserveBackgroundToken = false;
36240
- const refuse3 = (reason) => {
37048
+ const refuse4 = (reason) => {
36241
37049
  console.error(
36242
37050
  `
36243
37051
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -36262,7 +37070,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36262
37070
  try {
36263
37071
  expectLogin = await localLoginForPaidBrowserClaim();
36264
37072
  } catch (err) {
36265
- refuse3(
37073
+ refuse4(
36266
37074
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
36267
37075
  Run \`terminalhire login\`, then retry this claim. Browser verification was
36268
37076
  not opened and no claim was sent.`
@@ -36278,16 +37086,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
36278
37086
  claimRef: opportunityShortToken(b.bountyId)
36279
37087
  });
36280
37088
  if (!proofToken) {
36281
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
37089
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
36282
37090
  }
36283
37091
  const verifiedLogin = proofTokenLogin(proofToken);
36284
37092
  if (!verifiedLogin) {
36285
- refuse3(
37093
+ refuse4(
36286
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."
36287
37095
  );
36288
37096
  }
36289
37097
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
36290
- refuse3(
37098
+ refuse4(
36291
37099
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
36292
37100
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
36293
37101
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -36319,7 +37127,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36319
37127
  try {
36320
37128
  res = await sendRegistration(true);
36321
37129
  } catch (err) {
36322
- refuse3(
37130
+ refuse4(
36323
37131
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36324
37132
  );
36325
37133
  }
@@ -36370,7 +37178,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36370
37178
  try {
36371
37179
  res = await sendRegistration(true);
36372
37180
  } catch (err) {
36373
- refuse3(
37181
+ refuse4(
36374
37182
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36375
37183
  );
36376
37184
  }
@@ -36381,14 +37189,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
36381
37189
  res = await sendRegistration(false);
36382
37190
  refusalBody = await readRefusal(res);
36383
37191
  } catch (err) {
36384
- refuse3(
37192
+ refuse4(
36385
37193
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
36386
37194
  );
36387
37195
  }
36388
37196
  }
36389
37197
  if (!res.ok) {
36390
37198
  const detail = refusalBody?.message || refusalBody?.error || "";
36391
- refuse3(
37199
+ refuse4(
36392
37200
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
36393
37201
  );
36394
37202
  }
@@ -36399,7 +37207,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
36399
37207
  body = null;
36400
37208
  }
36401
37209
  if (!body || body.ok !== true) {
36402
- refuse3("malformed registration response from the server.");
37210
+ refuse4("malformed registration response from the server.");
36403
37211
  }
36404
37212
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
36405
37213
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -37829,7 +38637,7 @@ function writeSliceFiles(destDir, files) {
37829
38637
  if (typeof f.content === "string") {
37830
38638
  const abs = join29(destDir, f.path);
37831
38639
  mkdirSync7(dirname9(abs), { recursive: true });
37832
- writeFileSync15(abs, f.content, "utf8");
38640
+ writeFileSync16(abs, f.content, "utf8");
37833
38641
  written.push(f.path);
37834
38642
  } else {
37835
38643
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -37878,7 +38686,7 @@ function ensureExcludedPackDir(destDir) {
37878
38686
  const existing = existsSync13(excludeFile) ? readFileSync15(excludeFile, "utf8") : "";
37879
38687
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
37880
38688
  mkdirSync7(dirname9(excludeFile), { recursive: true });
37881
- writeFileSync15(
38689
+ writeFileSync16(
37882
38690
  excludeFile,
37883
38691
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
37884
38692
  `,
@@ -37894,23 +38702,33 @@ function writePackFile(destDir, relPath, content, what) {
37894
38702
  try {
37895
38703
  const abs = join29(destDir, relPath);
37896
38704
  mkdirSync7(dirname9(abs), { recursive: true });
37897
- writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
38705
+ writeFileSync16(abs, content, { encoding: "utf8", flag: "wx" });
37898
38706
  } catch (err) {
37899
38707
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
37900
38708
  }
37901
38709
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
37902
38710
  }
37903
38711
  function sha256OfUtf8(content) {
37904
- return createHash8("sha256").update(content, "utf8").digest("hex");
38712
+ return createHash10("sha256").update(content, "utf8").digest("hex");
37905
38713
  }
37906
- function writeWorkspacePack(destDir, spec, claim) {
38714
+ function writeWorkspacePack(destDir, spec, claim, delivery) {
38715
+ if (delivery !== "full" && delivery !== "sparse") {
38716
+ throw new TypeError(
38717
+ `writeWorkspacePack: delivery must be 'full' or 'sparse', got ${String(delivery)}`
38718
+ );
38719
+ }
37907
38720
  const gate = ensureExcludedPackDir(destDir);
37908
38721
  if (!gate.ok) {
37909
38722
  const refused = { written: false, reason: gate.reason };
37910
38723
  return { brief: refused, verify: refused, agents: refused };
37911
38724
  }
37912
38725
  const brief = typeof spec !== "string" || spec.trim() === "" ? { written: false, reason: "the server sent no task for this posting" } : writePackFile(destDir, BRIEF_REL_PATH, spec, "task");
37913
- const verify = writePackFile(destDir, VERIFY_REL_PATH, renderVerifyDoc(claim), "verify note");
38726
+ const verify = writePackFile(
38727
+ destDir,
38728
+ VERIFY_REL_PATH,
38729
+ renderVerifyDoc(claim, delivery),
38730
+ "verify note"
38731
+ );
37914
38732
  const agents = writePackFile(
37915
38733
  destDir,
37916
38734
  AGENTS_REL_PATH,
@@ -37923,20 +38741,30 @@ function packSafeId(claim) {
37923
38741
  const id = String(claim?.id ?? "");
37924
38742
  return PACK_SAFE_ID.test(id) ? id : "<your claim id \u2014 see: terminalhire claim list>";
37925
38743
  }
37926
- function renderVerifyDoc(claim) {
38744
+ function renderVerifyDoc(claim, delivery) {
37927
38745
  const id = packSafeId(claim);
38746
+ const runStep2 = delivery === "full" ? ` ${nextStep("terminalhire run")}
38747
+
38748
+ It tests every change since the delivered baseline, committed or not, which
38749
+ is what submit sends once it is all committed.` : ` ${nextStep("terminalhire run --target <clone URL of the full repository>")}
38750
+
38751
+ It tests every change since the delivered baseline, committed or not, which
38752
+ is what submit sends once it is all committed. This delivery contains only the
38753
+ files you were granted, so the local run needs the whole repository; if you
38754
+ don't have access to it, skip to step 4, because the poster-side run in step 5
38755
+ is the verdict that counts.`;
37928
38756
  return `# Verifying claim ${id}
37929
38757
 
37930
38758
  This workspace was delivered by terminalhire for a posting. "Done" is
37931
38759
  judged on the diff: the submitted patch is the change from the delivered
37932
38760
  baseline (this repo's root commit) to HEAD, so only committed, tracked changes
37933
- count.
38761
+ count. Submit needs a clean tree; run does not.
37934
38762
 
37935
38763
  1. Read the poster's task first, when there is one: ${BRIEF_REL_PATH}
37936
38764
  2. Work on the claim branch this delivery checked out, committing as you go.
37937
38765
  3. To verify locally in terminalhire's sandboxed runner, from this directory:
37938
38766
 
37939
- terminalhire run
38767
+ ${runStep2}
37940
38768
 
37941
38769
  4. Submit \u2014 the only step that sends anything off this machine. It prints what it
37942
38770
  is about to send first, and asks y/N only at an interactive terminal:
@@ -38302,11 +39130,13 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
38302
39130
  );
38303
39131
  process.exit(1);
38304
39132
  }
38305
- const pack = writeWorkspacePack(dest, body.spec, claim);
39133
+ const pack = writeWorkspacePack(dest, body.spec, claim, "full");
38306
39134
  const working = claims.updateClaim(claim.id, {
38307
39135
  worktreePath: dest,
38308
39136
  branch,
38309
39137
  state: "working",
39138
+ // `terminalhire run` clones this commit when run from the workspace (TERM-1098).
39139
+ baseSha,
38310
39140
  workspacePack: {
38311
39141
  brief: pack.brief.written === true,
38312
39142
  verify: pack.verify.written === true,
@@ -38462,7 +39292,7 @@ async function attemptSliceDelivery(id, flags = {}) {
38462
39292
  let pack;
38463
39293
  try {
38464
39294
  await sh("git", ["-C", dest, "init"]);
38465
- pack = writeWorkspacePack(dest, body.spec, claim);
39295
+ pack = writeWorkspacePack(dest, body.spec, claim, "sparse");
38466
39296
  await sh("git", ["-C", dest, "add", "-A"]);
38467
39297
  await sh("git", [
38468
39298
  "-C",
@@ -38499,6 +39329,11 @@ async function attemptSliceDelivery(id, flags = {}) {
38499
39329
  worktreePath: dest,
38500
39330
  branch,
38501
39331
  state: "working",
39332
+ // The commit these files were cut from, so `terminalhire run` can clone it unasked
39333
+ // (TERM-1098). Recorded only when it is a full sha: a slice confirmed before the
39334
+ // server kept one answers null, and a value of any other shape is not a commit we
39335
+ // could clone, so neither is written and `run` asks for `--sha` instead.
39336
+ ...typeof body.baseSha === "string" && /^[0-9a-f]{40}$/.test(body.baseSha) ? { baseSha: body.baseSha } : {},
38502
39337
  workspacePack: {
38503
39338
  brief: pack.brief.written === true,
38504
39339
  verify: pack.verify.written === true,
@@ -39231,7 +40066,7 @@ function readClaimPushMarker() {
39231
40066
  }
39232
40067
  function writeClaimPushMarker(marker) {
39233
40068
  ensureStateDir(TERMINALHIRE_DIR11);
39234
- writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
40069
+ writeFileSync16(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
39235
40070
  }
39236
40071
  function clearClaimPushMarker() {
39237
40072
  try {
@@ -40383,7 +41218,7 @@ __export(util_exports, {
40383
41218
  getSizableOrigin: () => getSizableOrigin,
40384
41219
  hexToUint8Array: () => hexToUint8Array,
40385
41220
  isObject: () => isObject,
40386
- isPlainObject: () => isPlainObject,
41221
+ isPlainObject: () => isPlainObject2,
40387
41222
  issue: () => issue,
40388
41223
  joinValues: () => joinValues,
40389
41224
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -40545,7 +41380,7 @@ function slugify(input) {
40545
41380
  function isObject(data) {
40546
41381
  return typeof data === "object" && data !== null && !Array.isArray(data);
40547
41382
  }
40548
- function isPlainObject(o) {
41383
+ function isPlainObject2(o) {
40549
41384
  if (isObject(o) === false)
40550
41385
  return false;
40551
41386
  const ctor = o.constructor;
@@ -40562,7 +41397,7 @@ function isPlainObject(o) {
40562
41397
  return true;
40563
41398
  }
40564
41399
  function shallowClone(o) {
40565
- if (isPlainObject(o))
41400
+ if (isPlainObject2(o))
40566
41401
  return { ...o };
40567
41402
  if (Array.isArray(o))
40568
41403
  return [...o];
@@ -40702,7 +41537,7 @@ function omit(schema, mask) {
40702
41537
  return clone(schema, def);
40703
41538
  }
40704
41539
  function extend(schema, shape) {
40705
- if (!isPlainObject(shape)) {
41540
+ if (!isPlainObject2(shape)) {
40706
41541
  throw new Error("Invalid input to extend: expected a plain object");
40707
41542
  }
40708
41543
  const checks = schema._zod.def.checks;
@@ -40725,7 +41560,7 @@ function extend(schema, shape) {
40725
41560
  return clone(schema, def);
40726
41561
  }
40727
41562
  function safeExtend(schema, shape) {
40728
- if (!isPlainObject(shape)) {
41563
+ if (!isPlainObject2(shape)) {
40729
41564
  throw new Error("Invalid input to safeExtend: expected a plain object");
40730
41565
  }
40731
41566
  const def = mergeDefs(schema._zod.def, {
@@ -41886,7 +42721,7 @@ function mergeValues(a, b) {
41886
42721
  if (a instanceof Date && b instanceof Date && +a === +b) {
41887
42722
  return { valid: true, data: a };
41888
42723
  }
41889
- if (isPlainObject(a) && isPlainObject(b)) {
42724
+ if (isPlainObject2(a) && isPlainObject2(b)) {
41890
42725
  const bKeys = Object.keys(b);
41891
42726
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
41892
42727
  const newObj = { ...a, ...b };
@@ -42838,7 +43673,7 @@ var init_schemas = __esm({
42838
43673
  $ZodType.init(inst, def);
42839
43674
  inst._zod.parse = (payload, ctx) => {
42840
43675
  const input = payload.value;
42841
- if (!isPlainObject(input)) {
43676
+ if (!isPlainObject2(input)) {
42842
43677
  payload.issues.push({
42843
43678
  expected: "record",
42844
43679
  code: "invalid_type",
@@ -47980,7 +48815,7 @@ var init_zod_json_schema_compat = __esm({
47980
48815
  });
47981
48816
 
47982
48817
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
47983
- function isPlainObject2(value) {
48818
+ function isPlainObject3(value) {
47984
48819
  return value !== null && typeof value === "object" && !Array.isArray(value);
47985
48820
  }
47986
48821
  function mergeCapabilities(base, additional) {
@@ -47991,7 +48826,7 @@ function mergeCapabilities(base, additional) {
47991
48826
  if (addValue === void 0)
47992
48827
  continue;
47993
48828
  const baseValue = result[k];
47994
- if (isPlainObject2(baseValue) && isPlainObject2(addValue)) {
48829
+ if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
47995
48830
  result[k] = { ...baseValue, ...addValue };
47996
48831
  } else {
47997
48832
  result[k] = addValue;
@@ -62349,7 +63184,7 @@ __export(config_exports, {
62349
63184
  readConfig: () => readConfig,
62350
63185
  writeConfig: () => writeConfig
62351
63186
  });
62352
- 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";
62353
63188
  import { join as join30 } from "path";
62354
63189
  import { homedir as homedir16 } from "os";
62355
63190
  function readConfig() {
@@ -62372,7 +63207,7 @@ function writeConfig(config2) {
62372
63207
  }
62373
63208
  delete merged.contributePrompted;
62374
63209
  }
62375
- writeFileSync16(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
63210
+ writeFileSync17(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
62376
63211
  }
62377
63212
  function parseNudgeMode(raw) {
62378
63213
  if (raw === "session" || raw === "always") return raw;