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.
@@ -1466,6 +1466,7 @@ __export(github_auth_exports, {
1466
1466
  decrypt: () => decrypt,
1467
1467
  deleteGitHubToken: () => deleteGitHubToken,
1468
1468
  encrypt: () => encrypt,
1469
+ githubTokenDisplayPath: () => githubTokenDisplayPath,
1469
1470
  hasGitHubToken: () => hasGitHubToken,
1470
1471
  loadKey: () => loadKey,
1471
1472
  readGitHubToken: () => readGitHubToken,
@@ -1476,8 +1477,12 @@ __export(github_auth_exports, {
1476
1477
  });
1477
1478
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
1478
1479
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, rmSync as rmSync3, renameSync as renameSync2 } from "fs";
1479
- import { join as join10 } from "path";
1480
+ import { join as join10, sep } from "path";
1480
1481
  import { homedir as homedir8 } from "os";
1482
+ function githubTokenDisplayPath() {
1483
+ const home = homedir8();
1484
+ return TOKEN_FILE.startsWith(home + sep) ? `~${TOKEN_FILE.slice(home.length)}` : TOKEN_FILE;
1485
+ }
1481
1486
  async function loadKey() {
1482
1487
  return loadOrCreateSharedKey();
1483
1488
  }
@@ -10290,10 +10295,10 @@ var init_node_model = __esm({
10290
10295
  // ../../packages/core/src/episodes/derivers/signals.ts
10291
10296
  function mcpToolSignal(name) {
10292
10297
  const rest = name.slice("mcp__".length);
10293
- const sep5 = rest.indexOf("__");
10294
- if (sep5 <= 0) return "mcp:custom";
10295
- const server = rest.slice(0, sep5).toLowerCase();
10296
- const leaf = rest.slice(sep5 + 2);
10298
+ const sep6 = rest.indexOf("__");
10299
+ if (sep6 <= 0) return "mcp:custom";
10300
+ const server = rest.slice(0, sep6).toLowerCase();
10301
+ const leaf = rest.slice(sep6 + 2);
10297
10302
  if (leaf.length === 0) return "mcp:custom";
10298
10303
  if (MCP_SERVER_CAPABILITY.has(server)) return MCP_SERVER_CAPABILITY.get(server) ?? null;
10299
10304
  return "mcp:custom";
@@ -12903,14 +12908,14 @@ async function run() {
12903
12908
  }
12904
12909
  }
12905
12910
  async function runLogin() {
12906
- const { runDeviceFlow: runDeviceFlow2, readGitHubToken: readGitHubToken2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
12911
+ const { runDeviceFlow: runDeviceFlow2, readGitHubToken: readGitHubToken2, githubTokenDisplayPath: githubTokenDisplayPath2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
12907
12912
  const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2, computeAcceptanceCredential: computeAcceptanceCredential2 } = await Promise.resolve().then(() => (init_src(), src_exports));
12908
12913
  const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateGitHubTags: accumulateGitHubTags2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
12909
12914
  console.log("");
12910
12915
  console.log(" terminalhire \u2014 Sign in with GitHub");
12911
12916
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12912
12917
  console.log(" Scope: read:user (public profile + public repos only)");
12913
- console.log(" Your token is encrypted and stored at ~/.terminalhire/github-token.enc");
12918
+ console.log(` Your token is encrypted and stored at ${githubTokenDisplayPath2()}`);
12914
12919
  console.log(" GitHub data enriches your LOCAL profile \u2014 no data leaves your machine.");
12915
12920
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12916
12921
  try {
@@ -13035,7 +13040,7 @@ async function runLogin() {
13035
13040
  }
13036
13041
  }
13037
13042
  async function runLogout() {
13038
- const { deleteGitHubToken: deleteGitHubToken2, hasGitHubToken: hasGitHubToken2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
13043
+ const { deleteGitHubToken: deleteGitHubToken2, hasGitHubToken: hasGitHubToken2, githubTokenDisplayPath: githubTokenDisplayPath2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
13039
13044
  const { readProfile: readProfile2, writeProfile: writeProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
13040
13045
  const hasToken = await hasGitHubToken2();
13041
13046
  if (!hasToken) {
@@ -13057,7 +13062,7 @@ async function runLogout() {
13057
13062
  await writeProfile2(profile);
13058
13063
  console.log("\n GitHub identity + proof-of-work credential cleared from local profile.");
13059
13064
  }
13060
- console.log(" GitHub token deleted from ~/.terminalhire/github-token.enc");
13065
+ console.log(` GitHub token deleted from ${githubTokenDisplayPath2()}`);
13061
13066
  console.log(" Skill tags accumulated from GitHub remain in your profile.");
13062
13067
  console.log(" To also delete those: terminalhire profile --delete\n");
13063
13068
  }
@@ -32288,12 +32293,33 @@ function scrubEndpointEnv(env) {
32288
32293
  }
32289
32294
  return out;
32290
32295
  }
32291
- function remoteDockerClient(endpoint) {
32296
+ function containerFlagsAt(args5) {
32297
+ if (args5[0] === "run" || args5[0] === "create")
32298
+ return 1;
32299
+ if (args5[0] === "container" && (args5[1] === "run" || args5[1] === "create"))
32300
+ return 2;
32301
+ return -1;
32302
+ }
32303
+ function remoteDockerClient(endpoint, opts = {}) {
32292
32304
  if (endpoint.trim().length === 0) {
32293
32305
  throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
32294
32306
  }
32295
32307
  const target = endpoint.trim();
32296
- const argv = (args5) => ["--host", target, ...args5];
32308
+ const tlsFlags = [];
32309
+ if (opts.tls) {
32310
+ for (const key of ["caPath", "certPath", "keyPath"]) {
32311
+ if (opts.tls[key].trim().length === 0) {
32312
+ throw new Error(`remoteDockerClient: tls.${key} must name a file`);
32313
+ }
32314
+ }
32315
+ tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
32316
+ }
32317
+ const runArgs = opts.runArgs ?? [];
32318
+ const argv = (args5) => {
32319
+ const at = runArgs.length > 0 ? containerFlagsAt(args5) : -1;
32320
+ const sub = at < 0 ? [...args5] : [...args5.slice(0, at), ...runArgs, ...args5.slice(at)];
32321
+ return ["--host", target, ...tlsFlags, ...sub];
32322
+ };
32297
32323
  return {
32298
32324
  endpoint: target,
32299
32325
  // Always empty, and it is a fact about this client rather than about the
@@ -32310,10 +32336,10 @@ function remoteDockerClient(endpoint) {
32310
32336
  env: scrubEndpointEnv(process.env)
32311
32337
  });
32312
32338
  },
32313
- sync(args5, opts) {
32339
+ sync(args5, opts2) {
32314
32340
  return syncResult(spawnSync4(DOCKER_BIN, argv(args5), {
32315
32341
  encoding: "utf8",
32316
- timeout: opts?.timeoutMs,
32342
+ timeout: opts2?.timeoutMs,
32317
32343
  env: scrubEndpointEnv(process.env)
32318
32344
  }));
32319
32345
  },
@@ -34294,6 +34320,197 @@ var init_gceIdentity = __esm({
34294
34320
  }
34295
34321
  });
34296
34322
 
34323
+ // ../../packages/attest/dist/confidentialSpace.js
34324
+ import { createHash as createHash8, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
34325
+ function refuse2(reason, detail) {
34326
+ return { ok: false, reason, detail };
34327
+ }
34328
+ function isPlainObject(v) {
34329
+ return typeof v === "object" && v !== null && !Array.isArray(v);
34330
+ }
34331
+ function assertExpectationsComplete2(expectations) {
34332
+ const required2 = [
34333
+ ["audience", expectations.audience],
34334
+ ["nonce", expectations.nonce],
34335
+ ["instanceName", expectations.instanceName],
34336
+ ["projectId", expectations.projectId],
34337
+ ...(expectations.additionalNonces ?? []).map((n) => [
34338
+ "additionalNonces[]",
34339
+ n
34340
+ ]),
34341
+ ...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
34342
+ ["containerEnv name", name],
34343
+ [`containerEnv.${name}`, value]
34344
+ ])
34345
+ ];
34346
+ for (const [name, value] of required2) {
34347
+ if (value === "") {
34348
+ throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
34349
+ }
34350
+ }
34351
+ }
34352
+ function nonceList(v) {
34353
+ if (typeof v === "string")
34354
+ return [v];
34355
+ if (!Array.isArray(v) || v.length === 0)
34356
+ return null;
34357
+ if (!v.every((n) => typeof n === "string"))
34358
+ return null;
34359
+ return v;
34360
+ }
34361
+ async function verifyConfidentialSpaceToken(token, expectations, deps) {
34362
+ assertExpectationsComplete2(expectations);
34363
+ if (!Number.isFinite(deps.now)) {
34364
+ throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
34365
+ }
34366
+ if (expectations.allowedImageDigests.length === 0) {
34367
+ return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
34368
+ }
34369
+ if (token.length > MAX_TOKEN_LENGTH) {
34370
+ return refuse2("token-malformed", "longer than any attestation token");
34371
+ }
34372
+ const parts = token.split(".");
34373
+ if (parts.length !== 3) {
34374
+ return refuse2("token-malformed", "not a three-segment compact JWT");
34375
+ }
34376
+ const [headerB64, payloadB64, signatureB64] = parts;
34377
+ const header = decodeJsonSegment(headerB64);
34378
+ if (header === null) {
34379
+ return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
34380
+ }
34381
+ if (header.alg !== "RS256") {
34382
+ return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
34383
+ }
34384
+ if (typeof header.kid !== "string" || header.kid === "") {
34385
+ return refuse2("unknown-key", "token header names no key id");
34386
+ }
34387
+ const jwks = await deps.fetchJwks();
34388
+ const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
34389
+ if (jwk === void 0) {
34390
+ return refuse2("unknown-key", "token key id is not in the published key set");
34391
+ }
34392
+ const signature = strictBase64UrlDecode(signatureB64);
34393
+ if (signature === null) {
34394
+ return refuse2("token-malformed", "signature segment is not base64url");
34395
+ }
34396
+ let signatureValid = false;
34397
+ try {
34398
+ const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
34399
+ signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
34400
+ } catch {
34401
+ signatureValid = false;
34402
+ }
34403
+ if (!signatureValid) {
34404
+ return refuse2("signature-invalid", "signature does not verify under the named key");
34405
+ }
34406
+ const payload = decodeJsonSegment(payloadB64);
34407
+ if (payload === null) {
34408
+ return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
34409
+ }
34410
+ if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
34411
+ return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
34412
+ }
34413
+ if (payload.aud !== expectations.audience) {
34414
+ return refuse2("wrong-audience", "audience is not the one this run expects");
34415
+ }
34416
+ const nowSeconds = deps.now / 1e3;
34417
+ const exp = payload.exp;
34418
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
34419
+ return refuse2("expired", "exp is missing, malformed, or in the past");
34420
+ }
34421
+ const iat = payload.iat;
34422
+ if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
34423
+ return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
34424
+ }
34425
+ const nonces = nonceList(payload.eat_nonce);
34426
+ if (nonces === null) {
34427
+ return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
34428
+ }
34429
+ if (!nonces.includes(expectations.nonce)) {
34430
+ return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
34431
+ }
34432
+ for (const extra of expectations.additionalNonces ?? []) {
34433
+ if (!nonces.includes(extra)) {
34434
+ return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
34435
+ }
34436
+ }
34437
+ if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
34438
+ return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
34439
+ }
34440
+ if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
34441
+ return refuse2("debug-enabled", "the image is not a production image with debug disabled");
34442
+ }
34443
+ if (payload.secboot !== true) {
34444
+ return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
34445
+ }
34446
+ if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
34447
+ return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
34448
+ }
34449
+ const hwmodel = payload.hwmodel;
34450
+ const submods = payload.submods;
34451
+ const gce = isPlainObject(submods) ? submods.gce : void 0;
34452
+ if (!isPlainObject(gce)) {
34453
+ return refuse2("submods-gce-missing", "no submods.gce block names the instance");
34454
+ }
34455
+ if (gce.project_id !== expectations.projectId) {
34456
+ return refuse2("project-mismatch", "token names an instance in a different project");
34457
+ }
34458
+ if (gce.instance_name !== expectations.instanceName) {
34459
+ return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
34460
+ }
34461
+ if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
34462
+ return refuse2("instance-id-missing", "instance id is absent or not a string");
34463
+ }
34464
+ const instanceId = gce.instance_id;
34465
+ if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
34466
+ return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
34467
+ }
34468
+ const zone = gce.zone;
34469
+ const container = isPlainObject(submods) ? submods.container : void 0;
34470
+ if (!isPlainObject(container)) {
34471
+ return refuse2("submods-container-missing", "no submods.container block names the workload");
34472
+ }
34473
+ if (typeof container.image_reference !== "string" || container.image_reference === "") {
34474
+ return refuse2("image-reference-missing", "the workload image reference is absent");
34475
+ }
34476
+ const imageReference = container.image_reference;
34477
+ if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
34478
+ return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
34479
+ }
34480
+ const env = container.env;
34481
+ for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
34482
+ if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
34483
+ return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
34484
+ }
34485
+ }
34486
+ return {
34487
+ ok: true,
34488
+ value: {
34489
+ instanceId,
34490
+ instanceName: expectations.instanceName,
34491
+ zone,
34492
+ projectId: expectations.projectId,
34493
+ imageDigest: container.image_digest,
34494
+ imageReference,
34495
+ hwmodel,
34496
+ iat,
34497
+ exp,
34498
+ tokenSha256: createHash8("sha256").update(token, "utf8").digest("hex")
34499
+ }
34500
+ };
34501
+ }
34502
+ var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
34503
+ var init_confidentialSpace = __esm({
34504
+ "../../packages/attest/dist/confidentialSpace.js"() {
34505
+ "use strict";
34506
+ init_gceIdentity();
34507
+ CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
34508
+ CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
34509
+ CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
34510
+ CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
34511
+ }
34512
+ });
34513
+
34297
34514
  // ../../packages/attest/dist/venueInstanceName.js
34298
34515
  function venueInstanceName(runId) {
34299
34516
  return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
@@ -34328,6 +34545,7 @@ var init_dist2 = __esm({
34328
34545
  init_attestation();
34329
34546
  init_nonce();
34330
34547
  init_gceIdentity();
34548
+ init_confidentialSpace();
34331
34549
  init_venueInstanceName();
34332
34550
  init_verify();
34333
34551
  }
@@ -34578,7 +34796,7 @@ var init_result = __esm({
34578
34796
  });
34579
34797
 
34580
34798
  // ../../packages/envrun/dist/attestation.js
34581
- import { createHash as createHash8, randomBytes as randomBytes11 } from "crypto";
34799
+ import { createHash as createHash9, randomBytes as randomBytes11 } from "crypto";
34582
34800
  function contradicts(outcome, counts, exitCode) {
34583
34801
  const budget = OUTCOME_TO_BUDGET[outcome];
34584
34802
  if (budget === null)
@@ -34620,7 +34838,7 @@ function imageDigestOf(ref) {
34620
34838
  return at === -1 ? null : ref.slice(at + 1);
34621
34839
  }
34622
34840
  function sha256Hex(data) {
34623
- return createHash8("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
34841
+ return createHash9("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
34624
34842
  }
34625
34843
  function toTestRunResult(result, outputSha256) {
34626
34844
  return {
@@ -34632,50 +34850,50 @@ function toTestRunResult(result, outputSha256) {
34632
34850
  function toAcceptancePredicate(pair, opts = {}) {
34633
34851
  const { baseline, patched } = pair;
34634
34852
  if (baseline.runId === patched.runId) {
34635
- 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.`);
34853
+ 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.`);
34636
34854
  }
34637
34855
  if (patched.status === "refused") {
34638
- 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`);
34856
+ 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`);
34639
34857
  }
34640
34858
  if (patched.outcome === null) {
34641
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
34859
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
34642
34860
  }
34643
34861
  if (baseline.status === "refused") {
34644
- 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`);
34862
+ 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`);
34645
34863
  }
34646
34864
  if (baseline.outcome === null) {
34647
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
34865
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
34648
34866
  }
34649
34867
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
34650
- 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.`);
34868
+ 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.`);
34651
34869
  }
34652
34870
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
34653
34871
  if (budget === null) {
34654
- 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.`);
34872
+ 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.`);
34655
34873
  }
34656
34874
  if (patched.testCommand === null) {
34657
- return refuse2("no-test-command", "no test command was recorded, so none can be attested");
34875
+ return refuse3("no-test-command", "no test command was recorded, so none can be attested");
34658
34876
  }
34659
34877
  if (baseline.testCommand !== patched.testCommand) {
34660
- 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.`);
34878
+ 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.`);
34661
34879
  }
34662
34880
  if (baseline.targetSha !== patched.targetSha) {
34663
- return refuse2("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
34881
+ return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
34664
34882
  }
34665
34883
  if (patched.treeDigest === null || patched.treeDigest === "") {
34666
- return refuse2("missing-tree-digest", "the patched run recorded no tree digest to bind to");
34884
+ return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
34667
34885
  }
34668
34886
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
34669
- return refuse2("missing-patch-digest", "the patched run recorded no patch digest");
34887
+ return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
34670
34888
  }
34671
34889
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
34672
- return refuse2("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
34890
+ return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
34673
34891
  }
34674
34892
  if (baseline.treeDigest === patched.treeDigest) {
34675
- 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`);
34893
+ 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`);
34676
34894
  }
34677
34895
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
34678
- 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.`);
34896
+ 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.`);
34679
34897
  }
34680
34898
  for (const [side, outcome, counts, exitCode] of [
34681
34899
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -34683,7 +34901,7 @@ function toAcceptancePredicate(pair, opts = {}) {
34683
34901
  ]) {
34684
34902
  const why = contradicts(outcome, counts, exitCode);
34685
34903
  if (why !== null) {
34686
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
34904
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
34687
34905
  }
34688
34906
  }
34689
34907
  for (const [side, source] of [
@@ -34692,44 +34910,44 @@ function toAcceptancePredicate(pair, opts = {}) {
34692
34910
  ]) {
34693
34911
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
34694
34912
  if (why !== null)
34695
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
34913
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
34696
34914
  }
34697
34915
  if (patched.testCommandSource === "developer-declared") {
34698
- return refuse2("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
34916
+ return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
34699
34917
  }
34700
34918
  if (baseline.testCommandSource !== patched.testCommandSource) {
34701
- 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.`);
34919
+ 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.`);
34702
34920
  }
34703
34921
  if (baseline.containerImage !== patched.containerImage) {
34704
- 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.`);
34922
+ 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.`);
34705
34923
  }
34706
34924
  if (patched.containerImage === null || patched.containerImage === "") {
34707
- 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");
34925
+ 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");
34708
34926
  }
34709
34927
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
34710
- 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.");
34928
+ 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.");
34711
34929
  }
34712
34930
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
34713
- 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.`);
34931
+ 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.`);
34714
34932
  }
34715
34933
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
34716
- 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.`);
34934
+ 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.`);
34717
34935
  }
34718
34936
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
34719
- 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.");
34937
+ 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.");
34720
34938
  }
34721
34939
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
34722
- 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.`);
34940
+ 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.`);
34723
34941
  }
34724
34942
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
34725
- 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.`);
34943
+ 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.`);
34726
34944
  }
34727
34945
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
34728
- 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.`);
34946
+ 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.`);
34729
34947
  }
34730
34948
  const claimedDigest = imageDigestOf(patched.containerImage);
34731
34949
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
34732
- 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.`);
34950
+ 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.`);
34733
34951
  }
34734
34952
  return {
34735
34953
  ok: true,
@@ -34756,7 +34974,7 @@ function signRunStatement(predicate, privateKey, keyid) {
34756
34974
  const statement = createAcceptanceStatement(predicate);
34757
34975
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
34758
34976
  }
34759
- 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;
34977
+ 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;
34760
34978
  var init_attestation2 = __esm({
34761
34979
  "../../packages/envrun/dist/attestation.js"() {
34762
34980
  "use strict";
@@ -34860,7 +35078,7 @@ var init_attestation2 = __esm({
34860
35078
  "outcome-contradicts-counts",
34861
35079
  "test-command-origin-unattested"
34862
35080
  ];
34863
- refuse2 = (reason, detail) => ({
35081
+ refuse3 = (reason, detail) => ({
34864
35082
  ok: false,
34865
35083
  reason,
34866
35084
  detail
@@ -35243,6 +35461,73 @@ var init_boundary = __esm({
35243
35461
  });
35244
35462
 
35245
35463
  // ../../packages/envrun/dist/gcpPlacement.js
35464
+ function assertWorkloadImage(fn, value) {
35465
+ if (typeof value !== "string" || !WORKLOAD_IMAGE_REFERENCE.test(value)) {
35466
+ 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.`);
35467
+ }
35468
+ }
35469
+ function venueImageFromEnv(env) {
35470
+ const image = env["VENUE_IMAGE"] ?? "";
35471
+ if (image === "" || image === "cos-stable")
35472
+ return { venueImage: "cos-stable" };
35473
+ if (image !== "confidential-space") {
35474
+ 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.");
35475
+ }
35476
+ const workloadImage = env["VENUE_WORKLOAD_IMAGE"] ?? "";
35477
+ if (workloadImage === "") {
35478
+ throw new GcpPlacementError("VENUE_IMAGE=confidential-space needs VENUE_WORKLOAD_IMAGE, the image reference the Confidential Space launcher runs. Nothing boots without one.");
35479
+ }
35480
+ assertWorkloadImage("venueImageFromEnv", workloadImage);
35481
+ return { venueImage: "confidential-space", workloadImage };
35482
+ }
35483
+ function bootImageFlags(p) {
35484
+ const image = p.venueImage ?? "cos-stable";
35485
+ if (image === "cos-stable")
35486
+ return ["--image-family=cos-stable", "--image-project=cos-cloud"];
35487
+ if (image !== "confidential-space") {
35488
+ throw new GcpPlacementError("gcpBootArgv: venueImage is not one of cos-stable, confidential-space. An unknown image is refused rather than booted as the default.");
35489
+ }
35490
+ const workloadImage = p.workloadImage;
35491
+ assertWorkloadImage("gcpBootArgv", workloadImage);
35492
+ const clientCert = p.clientCert;
35493
+ if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
35494
+ 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.`);
35495
+ }
35496
+ if (p.runId.length < MIN_NONCE_BYTES) {
35497
+ 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.`);
35498
+ }
35499
+ const metadata = [
35500
+ `tee-image-reference=${workloadImage}`,
35501
+ "tee-container-log-redirect=true",
35502
+ 'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
35503
+ "tee-cgroup-ns=true",
35504
+ `tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
35505
+ `tee-env-TH_NONCE=${p.runId}`,
35506
+ `tee-env-TH_CLIENT_CERT=${clientCert}`
35507
+ ];
35508
+ return [
35509
+ // The production family. The debug family reports `dbgstat: enabled`, which the
35510
+ // intake's verifier refuses, so a debug boot could never produce a run we record.
35511
+ "--image-family=confidential-space",
35512
+ "--image-project=confidential-space-images",
35513
+ "--confidential-compute-type=SEV",
35514
+ // The deploy-workloads page says N2D with SEV may set MIGRATE for live migration,
35515
+ // and every other machine type needs TERMINATE. The spec chose TERMINATE: a
35516
+ // maintenance event ends the VM instead of moving it, which for a VM that lives at
35517
+ // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
35518
+ // combination is phase 5's measurement.
35519
+ "--maintenance-policy=TERMINATE",
35520
+ `--metadata=^;^${metadata.join(";")}`,
35521
+ // What the standing firewall rule for IAP's range matches, so the tunnels to the
35522
+ // workload's dockerd and attest ports open on these VMs and no others.
35523
+ "--tags=th-cs-venue",
35524
+ // The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
35525
+ // runs vfs, which copies every layer and every container's rootfs in full. Pulling
35526
+ // node:20-bookworm left 992 MB, and the next container create failed ENOSPC
35527
+ // (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
35528
+ "--boot-disk-size=50GB"
35529
+ ];
35530
+ }
35246
35531
  function assertInstanceIdentity(fn, id) {
35247
35532
  const fields = [
35248
35533
  ["vmName", id.vmName, GCE_INSTANCE_NAME],
@@ -35268,8 +35553,8 @@ function gcpBootArgv(p) {
35268
35553
  `--project=${p.project}`,
35269
35554
  `--zone=${p.zone}`,
35270
35555
  `--machine-type=${p.machineType}`,
35271
- "--image-family=cos-stable",
35272
- "--image-project=cos-cloud",
35556
+ // What the VM boots, and the one part of this argv the two images do not share.
35557
+ ...bootImageFlags(p),
35273
35558
  // ── THE OUTER REAP. This, not the `finally` below, is what bounds the bill. ──
35274
35559
  //
35275
35560
  // An in-process `finally` is a best effort, not a reap. SIGKILL, a crashed
@@ -35314,7 +35599,13 @@ function gcpBootArgv(p) {
35314
35599
  // expectation uses (TERM-832): two inline spellings of one value is how
35315
35600
  // the boot path and the verifier drift apart silently.
35316
35601
  `--service-account=${venueIdentityEmail(p.project)}`,
35317
- "--no-scopes",
35602
+ // A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
35603
+ // pulls the workload image and mints the attestation token with this account, and
35604
+ // under `--no-scopes` it can do neither. What bounds a lifted token there is the
35605
+ // account's two grants — `confidentialcomputing.workloadUser` and a reader on the
35606
+ // one venue image repository — plus point 2 above, which is unchanged: the fence
35607
+ // still denies untrusted code the metadata server.
35608
+ ...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
35318
35609
  "--shielded-secure-boot",
35319
35610
  "--shielded-vtpm",
35320
35611
  "--shielded-integrity-monitoring",
@@ -35347,7 +35638,7 @@ function gcpDeleteArgv(p) {
35347
35638
  "--quiet"
35348
35639
  ];
35349
35640
  }
35350
- 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;
35641
+ 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;
35351
35642
  var init_gcpPlacement = __esm({
35352
35643
  "../../packages/envrun/dist/gcpPlacement.js"() {
35353
35644
  "use strict";
@@ -35364,6 +35655,10 @@ var init_gcpPlacement = __esm({
35364
35655
  GCP_MAX_RUN_DURATION_SECONDS = 3600;
35365
35656
  GCP_MANAGED_LABEL_KEY = "th-managed";
35366
35657
  GCP_RUN_LABEL_KEY = "th-run";
35658
+ CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
35659
+ WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
35660
+ CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
35661
+ MIN_NONCE_BYTES = 10;
35367
35662
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
35368
35663
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
35369
35664
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -35405,7 +35700,10 @@ var init_emptyGitConfig = __esm({
35405
35700
 
35406
35701
  // ../../packages/envrun/dist/hostedVenue.js
35407
35702
  import { spawn as spawn6, spawnSync as spawnSync6 } from "child_process";
35408
- import { chmodSync as chmodSync3, existsSync as existsSync18, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, rmSync as rmSync10 } from "fs";
35703
+ import { createHash as createHash10, X509Certificate } from "crypto";
35704
+ import { chmodSync as chmodSync3, existsSync as existsSync18, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, rmSync as rmSync10, writeFileSync as writeFileSync22 } from "fs";
35705
+ import { request as httpsRequest } from "https";
35706
+ import { createServer } from "net";
35409
35707
  import { join as join40 } from "path";
35410
35708
  import { tmpdir as tmpdir3 } from "os";
35411
35709
  function credentialInGitConfig(text) {
@@ -35607,6 +35905,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
35607
35905
  });
35608
35906
  });
35609
35907
  }
35908
+ function socketErrorReason(err) {
35909
+ if (err.message !== "")
35910
+ return err.message;
35911
+ const code = err.code;
35912
+ if (code !== void 0)
35913
+ return code;
35914
+ const inner = err.errors;
35915
+ if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
35916
+ return inner[0].message;
35917
+ }
35918
+ return err.name;
35919
+ }
35920
+ function fetchAttestationOverTls(req) {
35921
+ return new Promise((resolve7) => {
35922
+ let whole;
35923
+ const settle = (reading) => {
35924
+ if (whole !== void 0)
35925
+ clearTimeout(whole);
35926
+ resolve7(reading);
35927
+ };
35928
+ let cert;
35929
+ let key;
35930
+ try {
35931
+ cert = readFileSync25(req.certPath);
35932
+ key = readFileSync25(req.keyPath);
35933
+ } catch (err) {
35934
+ settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
35935
+ return;
35936
+ }
35937
+ const call = httpsRequest({
35938
+ host: "localhost",
35939
+ port: req.port,
35940
+ path: "/attest",
35941
+ method: "GET",
35942
+ servername: "localhost",
35943
+ cert,
35944
+ key,
35945
+ rejectUnauthorized: false,
35946
+ agent: false,
35947
+ timeout: req.timeoutMs
35948
+ }, (res) => {
35949
+ const peer = res.socket.getPeerCertificate();
35950
+ let body = "";
35951
+ res.setEncoding("utf8");
35952
+ res.on("data", (chunk) => {
35953
+ body += chunk;
35954
+ if (body.length > MAX_TOKEN_LENGTH) {
35955
+ settle({ ok: false, reason: "the answer is longer than any attestation token" });
35956
+ res.destroy();
35957
+ }
35958
+ });
35959
+ res.on("end", () => {
35960
+ if (res.statusCode !== 200) {
35961
+ settle({
35962
+ ok: false,
35963
+ reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
35964
+ });
35965
+ return;
35966
+ }
35967
+ const raw = peer.raw;
35968
+ if (raw === void 0 || raw.length === 0) {
35969
+ settle({ ok: false, reason: "the attest endpoint presented no certificate" });
35970
+ return;
35971
+ }
35972
+ settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
35973
+ });
35974
+ res.on("error", (err) => {
35975
+ settle({ ok: false, reason: socketErrorReason(err) });
35976
+ });
35977
+ });
35978
+ whole = setTimeout(() => {
35979
+ settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
35980
+ call.destroy();
35981
+ }, req.timeoutMs);
35982
+ call.on("timeout", () => {
35983
+ call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
35984
+ });
35985
+ call.on("error", (err) => {
35986
+ settle({ ok: false, reason: socketErrorReason(err) });
35987
+ });
35988
+ call.end();
35989
+ });
35990
+ }
35610
35991
  function quoteForRemoteShell(arg) {
35611
35992
  return `'${arg.replace(/'/g, "'\\''")}'`;
35612
35993
  }
@@ -35941,25 +36322,370 @@ ${res.stderr}`);
35941
36322
  function manualDeleteCommand(vm, project, zone) {
35942
36323
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
35943
36324
  }
36325
+ function clientCertArgv(keyPath, certPath) {
36326
+ return [
36327
+ "req",
36328
+ "-x509",
36329
+ "-newkey",
36330
+ "ec",
36331
+ "-pkeyopt",
36332
+ "ec_paramgen_curve:P-256",
36333
+ "-nodes",
36334
+ "-keyout",
36335
+ keyPath,
36336
+ "-out",
36337
+ certPath,
36338
+ "-subj",
36339
+ "/CN=terminalhire-worker",
36340
+ "-days",
36341
+ "1",
36342
+ "-addext",
36343
+ "extendedKeyUsage=clientAuth"
36344
+ ];
36345
+ }
36346
+ function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
36347
+ return [
36348
+ "compute",
36349
+ "start-iap-tunnel",
36350
+ vm,
36351
+ String(remotePort),
36352
+ `--local-host-port=localhost:${String(localPort)}`,
36353
+ `--project=${project}`,
36354
+ `--zone=${zone}`,
36355
+ "--iap-tunnel-disable-connection-check"
36356
+ ];
36357
+ }
36358
+ function spkiSha256Hex(certDer) {
36359
+ const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
36360
+ return createHash10("sha256").update(spki).digest("hex");
36361
+ }
36362
+ function certMetadataValue(pem) {
36363
+ return new X509Certificate(pem).raw.toString("base64url");
36364
+ }
36365
+ function requireWorkloadDigest(reference) {
36366
+ const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
36367
+ if (named === null) {
36368
+ 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.");
36369
+ }
36370
+ return named[1];
36371
+ }
36372
+ function underStageRoot(path6) {
36373
+ if (!path6.startsWith(`${CS_STAGE_ROOT}/`)) {
36374
+ throw new Error(`${path6} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
36375
+ }
36376
+ return path6;
36377
+ }
36378
+ function userFlag(owner) {
36379
+ return `--user=${String(owner.uid)}:${String(owner.gid)}`;
36380
+ }
36381
+ function csStagePrepareArgv(stageBase, owner) {
36382
+ const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
36383
+ return [
36384
+ "run",
36385
+ "--rm",
36386
+ "--network=none",
36387
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
36388
+ "--",
36389
+ STAGE_HELPER_IMAGE,
36390
+ "sh",
36391
+ "-c",
36392
+ `mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
36393
+ ];
36394
+ }
36395
+ function csUntarArgv(dir, owner) {
36396
+ return [
36397
+ "run",
36398
+ "-i",
36399
+ "--rm",
36400
+ "--network=none",
36401
+ userFlag(owner),
36402
+ `--volume=${underStageRoot(dir)}:/dst:rw`,
36403
+ "--",
36404
+ STAGE_HELPER_IMAGE,
36405
+ "tar",
36406
+ "-xf",
36407
+ "-",
36408
+ "-C",
36409
+ "/dst"
36410
+ ];
36411
+ }
36412
+ function csUntarProxyArgv(dir, owner) {
36413
+ const d = quoteForRemoteShell(underStageRoot(dir));
36414
+ return [
36415
+ "run",
36416
+ "-i",
36417
+ "--rm",
36418
+ "--network=none",
36419
+ userFlag(owner),
36420
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
36421
+ "--",
36422
+ STAGE_HELPER_IMAGE,
36423
+ "sh",
36424
+ "-c",
36425
+ `mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
36426
+ ];
36427
+ }
36428
+ function csRemoveArgv(dir, owner) {
36429
+ return [
36430
+ "run",
36431
+ "--rm",
36432
+ "--network=none",
36433
+ userFlag(owner),
36434
+ `--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
36435
+ "--",
36436
+ STAGE_HELPER_IMAGE,
36437
+ "rm",
36438
+ "-rf",
36439
+ "--",
36440
+ underStageRoot(dir)
36441
+ ];
36442
+ }
36443
+ function sshStageTransport(vm, project, zone, env, io) {
36444
+ return {
36445
+ /**
36446
+ * `id -u` / `id -g` on the venue, as the account that untarred the tree.
36447
+ *
36448
+ * That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
36449
+ * so asking it who it is answers exactly the question the mount needs, without
36450
+ * a chown and without assuming a uid. One ssh round trip, once per run.
36451
+ *
36452
+ * Both ids come from ONE invocation. Two calls could straddle a reconnect and
36453
+ * pair a uid with another session's gid — a third wrong answer rather than a
36454
+ * half-right one, which is the reason `GuestUser` is a pair in the first place.
36455
+ */
36456
+ readOwner: () => {
36457
+ const res = io.exec(
36458
+ "gcloud",
36459
+ iapSshArgv(vm, project, zone, "id -u && id -g"),
36460
+ OWNER_PROBE_TIMEOUT_MS,
36461
+ // `env`, like every other gcloud call here. TERM-724 made the venue boot under the
36462
+ // SERVICE configuration rather than whoever is at the terminal, and a probe that
36463
+ // omitted this would ssh under the person's own credential — then report an owner
36464
+ // measured against a session the run does not use.
36465
+ env
36466
+ );
36467
+ if (!res.ok) {
36468
+ 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.`);
36469
+ }
36470
+ const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
36471
+ const ids2 = lines.slice(-2).map((l) => Number(l));
36472
+ if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
36473
+ 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.`);
36474
+ }
36475
+ return { uid: ids2[0], gid: ids2[1] };
36476
+ },
36477
+ push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
36478
+ pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
36479
+ removeProxy: (dir) => {
36480
+ io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
36481
+ }
36482
+ };
36483
+ }
36484
+ function dockerStageTransport(docker3, env, io) {
36485
+ const pushVia = (from, args5) => {
36486
+ const [file, ...rest] = docker3.commandLine(args5);
36487
+ if (file === void 0)
36488
+ throw new Error("the docker client returned an empty command line");
36489
+ return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
36490
+ };
36491
+ return {
36492
+ // Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
36493
+ readOwner: () => CS_GUEST_USER,
36494
+ push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
36495
+ pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
36496
+ removeProxy: (dir) => {
36497
+ docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
36498
+ }
36499
+ };
36500
+ }
36501
+ function bothTunnels(a, b) {
36502
+ return {
36503
+ kill: (signal) => {
36504
+ let failed = false;
36505
+ let error2;
36506
+ for (const t of [a, b]) {
36507
+ try {
36508
+ t.kill(signal);
36509
+ } catch (err) {
36510
+ if (!failed)
36511
+ error2 = err;
36512
+ failed = true;
36513
+ }
36514
+ }
36515
+ if (failed)
36516
+ throw error2;
36517
+ },
36518
+ failure: () => a.failure() ?? b.failure()
36519
+ };
36520
+ }
36521
+ function makeClientTls(io, allocated, vm) {
36522
+ let dir;
36523
+ try {
36524
+ dir = io.makePrivateDir();
36525
+ } catch (err) {
36526
+ throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
36527
+ }
36528
+ allocated.onRollback(() => {
36529
+ try {
36530
+ io.removeTree(dir);
36531
+ } catch (err) {
36532
+ throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
36533
+ }
36534
+ });
36535
+ const certPath = join40(dir, "client.pem");
36536
+ const keyPath = join40(dir, "client-key.pem");
36537
+ const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
36538
+ if (!made.ok) {
36539
+ throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
36540
+ }
36541
+ let metadataValue;
36542
+ try {
36543
+ const pem = io.readTextIfPresent(certPath);
36544
+ if (pem === null)
36545
+ throw new Error(`openssl wrote nothing to ${certPath}`);
36546
+ metadataValue = certMetadataValue(pem);
36547
+ } catch (err) {
36548
+ throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
36549
+ }
36550
+ return { dir, certPath, keyPath, metadataValue };
36551
+ }
36552
+ async function reachConfidentialSpace(a) {
36553
+ const { io, vm } = a;
36554
+ let ports;
36555
+ try {
36556
+ ports = await io.freeLocalPorts(2);
36557
+ } catch (err) {
36558
+ throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
36559
+ }
36560
+ const [attestPort, dockerPort] = ports;
36561
+ if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
36562
+ throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
36563
+ }
36564
+ const forward = (remote, local) => {
36565
+ const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
36566
+ a.allocated.onRollback(() => {
36567
+ try {
36568
+ t.kill("SIGTERM");
36569
+ } catch {
36570
+ }
36571
+ });
36572
+ return t;
36573
+ };
36574
+ const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
36575
+ const deadline = io.now() + CS_READY_BUDGET_MS;
36576
+ let reading = { ok: false, reason: "no request was made" };
36577
+ while (io.now() < deadline) {
36578
+ const dead = tunnel.failure();
36579
+ if (dead !== null) {
36580
+ throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
36581
+ }
36582
+ reading = await io.fetchAttestation({
36583
+ port: attestPort,
36584
+ certPath: a.tls.certPath,
36585
+ keyPath: a.tls.keyPath,
36586
+ timeoutMs: CS_ATTEST_TIMEOUT_MS
36587
+ });
36588
+ if (reading.ok)
36589
+ break;
36590
+ await io.sleep(CS_ATTEST_INTERVAL_MS);
36591
+ }
36592
+ if (!reading.ok) {
36593
+ throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
36594
+ }
36595
+ let spki;
36596
+ let venuePem;
36597
+ try {
36598
+ spki = spkiSha256Hex(reading.peerCertDer);
36599
+ venuePem = new X509Certificate(reading.peerCertDer).toString();
36600
+ } catch (err) {
36601
+ throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
36602
+ }
36603
+ let jwks;
36604
+ try {
36605
+ jwks = await io.fetchConfidentialSpaceJwks();
36606
+ } catch (err) {
36607
+ throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
36608
+ }
36609
+ const verdict = await verifyConfidentialSpaceToken(reading.token, {
36610
+ audience: expectedAudience(a.runId),
36611
+ nonce: a.runId,
36612
+ additionalNonces: [spki],
36613
+ instanceName: vm,
36614
+ allowedZones: [a.zone],
36615
+ projectId: a.project,
36616
+ allowedImageDigests: [a.imageDigest],
36617
+ containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
36618
+ }, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
36619
+ if (!verdict.ok) {
36620
+ throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
36621
+ }
36622
+ const caPath = join40(a.tls.dir, "venue.pem");
36623
+ try {
36624
+ io.writeFile(caPath, venuePem);
36625
+ } catch (err) {
36626
+ throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
36627
+ }
36628
+ const dockerAt = `tcp://localhost:${String(dockerPort)}`;
36629
+ const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
36630
+ const daemon = io.classifyDaemon(docker3);
36631
+ if (!daemon.distinct) {
36632
+ throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
36633
+ }
36634
+ const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
36635
+ timeoutMs: MKDIR_TIMEOUT_MS
36636
+ });
36637
+ if (prepared.error || prepared.status !== 0) {
36638
+ throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
36639
+ }
36640
+ const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
36641
+ if (pulled.error || pulled.status !== 0) {
36642
+ throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
36643
+ }
36644
+ return {
36645
+ docker: docker3,
36646
+ tunnel,
36647
+ socketDir: a.tls.dir,
36648
+ socketPath: dockerAt,
36649
+ venueDaemonId: daemon.venueDaemonId,
36650
+ venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
36651
+ transport: dockerStageTransport(docker3, a.env, io)
36652
+ };
36653
+ }
35944
36654
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
35945
36655
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
35946
36656
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
35947
36657
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
35948
36658
  const keyFile = resolveServiceAccountKeyFile(opts);
36659
+ const image = venueImageFromEnv(process.env);
36660
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
35949
36661
  const env = venueGcloudEnv(gcloudConfig);
35950
36662
  return {
35951
36663
  kind: "hosted-pool",
35952
36664
  acquire: (runId) => acquireTransactionally(async (allocated) => {
35953
36665
  const vm = venueInstanceName(runId);
35954
36666
  const stageBase = `/tmp/th-stage-${runId}`;
35955
- const bootArgv = gcpBootArgv({
36667
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
35956
36668
  vmName: vm,
35957
36669
  project,
35958
36670
  zone,
35959
36671
  runId,
35960
- machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE
35961
- });
36672
+ machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
36673
+ venueImage: "confidential-space",
36674
+ workloadImage: image.workloadImage,
36675
+ clientCert
36676
+ } : {
36677
+ vmName: vm,
36678
+ project,
36679
+ zone,
36680
+ runId,
36681
+ machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
36682
+ ...image
36683
+ };
36684
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
35962
36685
  ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
36686
+ const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
36687
+ if (clientTls !== null)
36688
+ bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
35963
36689
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
35964
36690
  let instanceState = "unknown";
35965
36691
  allocated.onRollback(() => {
@@ -35975,6 +36701,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
35975
36701
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
35976
36702
  }
35977
36703
  instanceState = "exists";
36704
+ if (clientTls !== null) {
36705
+ return finishAcquire(await reachConfidentialSpace({
36706
+ vm,
36707
+ project,
36708
+ zone,
36709
+ runId,
36710
+ stageBase,
36711
+ env,
36712
+ io,
36713
+ allocated,
36714
+ tls: clientTls,
36715
+ imageDigest: clientTls.imageDigest
36716
+ }));
36717
+ }
35978
36718
  const deadline = io.now() + SSH_READY_BUDGET_MS;
35979
36719
  let last = { retryable: true, reason: "no probe ran" };
35980
36720
  let identity = null;
@@ -36069,43 +36809,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
36069
36809
  if (!prepared.ok) {
36070
36810
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
36071
36811
  }
36072
- const stageVolumes = stageVolumeNames(vm);
36073
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36074
- const created = docker3.sync(volumeCreateArgv(name, runId), {
36075
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
36076
- });
36077
- if (created.error || created.status !== 0) {
36078
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
36079
- }
36080
- }
36081
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36082
- const probe = docker3.sync(execProbeArgv(name), {
36083
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
36084
- });
36085
- if (probe.error || probe.status !== 0) {
36086
- 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)}`);
36087
- }
36088
- }
36089
- const after = io.classifyDaemon(docker3);
36090
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
36091
- 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.`);
36092
- }
36093
- return makeLease({
36094
- runId,
36095
- vm,
36096
- project,
36097
- zone,
36098
- stageBase,
36099
- stageVolumes,
36812
+ return finishAcquire({
36100
36813
  docker: docker3,
36101
36814
  tunnel,
36102
36815
  socketDir,
36103
36816
  socketPath,
36104
36817
  venueDaemonId,
36105
36818
  venueIdentity,
36106
- env,
36107
- io
36819
+ transport: sshStageTransport(vm, project, zone, env, io)
36108
36820
  });
36821
+ function finishAcquire(r) {
36822
+ const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
36823
+ const stageVolumes = stageVolumeNames(vm);
36824
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36825
+ const created = docker4.sync(volumeCreateArgv(name, runId), {
36826
+ timeoutMs: VOLUME_CREATE_TIMEOUT_MS
36827
+ });
36828
+ if (created.error || created.status !== 0) {
36829
+ throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
36830
+ }
36831
+ }
36832
+ for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36833
+ const probe = docker4.sync(execProbeArgv(name), {
36834
+ timeoutMs: EXEC_PROBE_TIMEOUT_MS
36835
+ });
36836
+ if (probe.error || probe.status !== 0) {
36837
+ 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)}`);
36838
+ }
36839
+ }
36840
+ const after = io.classifyDaemon(docker4);
36841
+ if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
36842
+ 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.`);
36843
+ }
36844
+ return makeLease({
36845
+ runId,
36846
+ vm,
36847
+ project,
36848
+ zone,
36849
+ stageBase,
36850
+ stageVolumes,
36851
+ docker: docker4,
36852
+ tunnel: r.tunnel,
36853
+ socketDir: r.socketDir,
36854
+ socketPath: socketPath2,
36855
+ venueDaemonId: venueDaemonId2,
36856
+ venueIdentity: r.venueIdentity,
36857
+ transport: r.transport,
36858
+ env,
36859
+ io
36860
+ });
36861
+ }
36109
36862
  })
36110
36863
  };
36111
36864
  }
@@ -36164,28 +36917,7 @@ function makeLease(p) {
36164
36917
  return null;
36165
36918
  };
36166
36919
  let stagedOwner;
36167
- const readStagedOwner = () => {
36168
- const res = p.io.exec(
36169
- "gcloud",
36170
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
36171
- OWNER_PROBE_TIMEOUT_MS,
36172
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
36173
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
36174
- // omitted this would ssh under the person's own credential — then report an owner
36175
- // measured against a session the run does not use.
36176
- p.env
36177
- );
36178
- if (!res.ok) {
36179
- 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.`);
36180
- }
36181
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
36182
- const ids2 = lines.slice(-2).map((l) => Number(l));
36183
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
36184
- 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.`);
36185
- }
36186
- return { uid: ids2[0], gid: ids2[1] };
36187
- };
36188
- stagedOwner = readStagedOwner();
36920
+ stagedOwner = p.transport.readOwner();
36189
36921
  return {
36190
36922
  kind: "hosted-pool",
36191
36923
  runId: p.runId,
@@ -36220,7 +36952,7 @@ function makeLease(p) {
36220
36952
  const dir = venueProxyDir(p.stageBase);
36221
36953
  const buffer = stageProxyCode();
36222
36954
  try {
36223
- const res = await p.io.pushTree(buffer.dir, "gcloud", iapUntarProxyArgv(p.vm, p.project, p.zone, dir), STAGE_PUSH_TIMEOUT_MS, p.env);
36955
+ const res = await p.transport.pushProxy(buffer.dir, dir);
36224
36956
  if (!res.ok) {
36225
36957
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
36226
36958
  }
@@ -36230,7 +36962,7 @@ function makeLease(p) {
36230
36962
  return {
36231
36963
  dir,
36232
36964
  cleanup: () => {
36233
- p.io.exec("gcloud", iapSshArgv(p.vm, p.project, p.zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, p.env);
36965
+ p.transport.removeProxy(dir);
36234
36966
  }
36235
36967
  };
36236
36968
  },
@@ -36238,7 +36970,7 @@ function makeLease(p) {
36238
36970
  check("staging the tree");
36239
36971
  const paths = venueStagePaths(p.stageBase);
36240
36972
  const push = async (from, to) => {
36241
- const res = await p.io.pushTree(from, "gcloud", iapUntarArgv(p.vm, p.project, p.zone, to), STAGE_PUSH_TIMEOUT_MS, p.env);
36973
+ const res = await p.transport.push(from, to);
36242
36974
  if (!res.ok) {
36243
36975
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
36244
36976
  }
@@ -36396,7 +37128,7 @@ function makeLease(p) {
36396
37128
  }
36397
37129
  };
36398
37130
  }
36399
- 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;
37131
+ 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;
36400
37132
  var init_hostedVenue = __esm({
36401
37133
  "../../packages/envrun/dist/hostedVenue.js"() {
36402
37134
  "use strict";
@@ -36517,7 +37249,55 @@ var init_hostedVenue = __esm({
36517
37249
  throw new Error("JWKS fetch returned a body with no keys array");
36518
37250
  }
36519
37251
  return body;
36520
- }
37252
+ },
37253
+ fetchConfidentialSpaceJwks: async () => {
37254
+ const getJson = async (url, what) => {
37255
+ const res = await globalThis.fetch(url, {
37256
+ signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
37257
+ });
37258
+ if (!res.ok) {
37259
+ throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
37260
+ }
37261
+ return res.json();
37262
+ };
37263
+ const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
37264
+ const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
37265
+ if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
37266
+ throw new Error("Confidential Space discovery names no https jwks_uri");
37267
+ }
37268
+ const body = await getJson(jwksUri, "key set");
37269
+ if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
37270
+ throw new Error("Confidential Space key set has no keys array");
37271
+ }
37272
+ return body;
37273
+ },
37274
+ freeLocalPorts: async (n) => {
37275
+ const servers = [];
37276
+ try {
37277
+ for (let i = 0; i < n; i += 1) {
37278
+ const server = createServer();
37279
+ servers.push(server);
37280
+ await new Promise((resolve7, reject) => {
37281
+ server.once("error", reject);
37282
+ server.listen(0, "localhost", () => {
37283
+ resolve7();
37284
+ });
37285
+ });
37286
+ }
37287
+ return servers.map((s) => s.address().port);
37288
+ } finally {
37289
+ await Promise.all(servers.map((s) => new Promise((resolve7) => {
37290
+ s.close(() => {
37291
+ resolve7();
37292
+ });
37293
+ })));
37294
+ }
37295
+ },
37296
+ writeFile: (path6, text) => {
37297
+ writeFileSync22(path6, text, { mode: 384, flag: "wx" });
37298
+ },
37299
+ fetchAttestation: (req) => fetchAttestationOverTls(req),
37300
+ dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
36521
37301
  };
36522
37302
  VENUE_SSH_USER = "th-runner";
36523
37303
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -36538,6 +37318,17 @@ var init_hostedVenue = __esm({
36538
37318
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
36539
37319
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
36540
37320
  SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
37321
+ CS_ATTEST_PORT = 8443;
37322
+ CS_DOCKER_PORT = 2376;
37323
+ CS_READY_BUDGET_MS = 48e4;
37324
+ CS_ATTEST_TIMEOUT_MS = 15e3;
37325
+ CS_PULL_TIMEOUT_MS = 6e5;
37326
+ CS_ATTEST_INTERVAL_MS = 5e3;
37327
+ OPENSSL_TIMEOUT_MS = 3e4;
37328
+ CS_RUN_ARGS = ["--oom-score-adj", "1000"];
37329
+ CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
37330
+ CS_STAGE_ROOT = "/tmp";
37331
+ CLIENT_CERT_STAND_IN = "stand-in-certificate";
36541
37332
  }
36542
37333
  });
36543
37334
 
@@ -37437,10 +38228,10 @@ var init_references = __esm({
37437
38228
 
37438
38229
  // ../../packages/envspec/dist/repo.js
37439
38230
  import { readdirSync as readdirSync2, readFileSync as readFileSync26, statSync as statSync4 } from "fs";
37440
- import { join as join41, relative as relative2, sep as sep4 } from "path";
38231
+ import { join as join41, relative as relative2, sep as sep5 } from "path";
37441
38232
  function createRepoReader(repoPath) {
37442
38233
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join41(repoPath, relativePath);
37443
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
38234
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
37444
38235
  const readText = (relativePath) => {
37445
38236
  try {
37446
38237
  return readFileSync26(resolveIn(relativePath), "utf8");
@@ -38083,13 +38874,16 @@ function git(repoDir, args5, allowNonZero = false) {
38083
38874
  }
38084
38875
  return res.stdout ?? "";
38085
38876
  }
38086
- function collectWorkingDiff(repoDir) {
38877
+ function collectWorkingDiff(repoDir, opts = {}) {
38087
38878
  if (!existsSync19(join42(repoDir, ".git"))) {
38088
38879
  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.`);
38089
38880
  }
38090
38881
  const headSha = git(repoDir, ["rev-parse", "HEAD"]).trim();
38091
- const tracked = git(repoDir, ["diff", "HEAD", "--binary"]);
38092
- const trackedChanged = git(repoDir, ["diff", "HEAD", "--name-only"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
38882
+ const diffBase = opts.base === void 0 ? headSha : assertDiffBase(repoDir, opts.base);
38883
+ const trackedArgs = opts.base === void 0 ? ["diff", "HEAD"] : ["diff", diffBase];
38884
+ const pathsEnd = opts.base === void 0 ? [] : ["--"];
38885
+ const tracked = git(repoDir, [...trackedArgs, "--binary", ...pathsEnd]);
38886
+ const trackedChanged = git(repoDir, [...trackedArgs, "--name-only", ...pathsEnd]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
38093
38887
  const untracked = git(repoDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
38094
38888
  const parts = [];
38095
38889
  if (tracked.trim() !== "")
@@ -38099,7 +38893,17 @@ function collectWorkingDiff(repoDir) {
38099
38893
  if (one.trim() !== "")
38100
38894
  parts.push(one.replace(/\n*$/, "\n"));
38101
38895
  }
38102
- return { patch: parts.join(""), headSha, trackedChanged, untracked };
38896
+ return { patch: parts.join(""), headSha, diffBase, trackedChanged, untracked };
38897
+ }
38898
+ function assertDiffBase(repoDir, base) {
38899
+ if (!FULL_COMMIT_ID.test(base)) {
38900
+ throw new ThRunError(`the diff base must be a full 40-character commit id, got ${JSON.stringify(base)}.`);
38901
+ }
38902
+ const resolved = git(repoDir, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`], true);
38903
+ if (resolved.trim() !== base) {
38904
+ throw new ThRunError(`the diff base ${base} is not a commit in ${repoDir}.`);
38905
+ }
38906
+ return base;
38103
38907
  }
38104
38908
  function isLocalPath(p) {
38105
38909
  return !UNC_PATH.test(p) && (p.startsWith("/") || WINDOWS_ABSOLUTE.test(p));
@@ -38671,8 +39475,9 @@ async function runVerification(req, ctx) {
38671
39475
  let diff2 = null;
38672
39476
  if (source.kind === "working-diff") {
38673
39477
  progress("collect", `reading the working diff from ${source.localRepoDir}`);
38674
- diff2 = collectWorkingDiff(source.localRepoDir);
38675
- progress("collect", `${String(diff2.trackedChanged.length)} tracked, ${String(diff2.untracked.length)} untracked, ${String(diff2.patch.length)} bytes`);
39478
+ diff2 = collectWorkingDiff(source.localRepoDir, source.diffBase === void 0 ? {} : { base: source.diffBase });
39479
+ const from = source.diffBase === void 0 ? "" : `, from delivered baseline ${diff2.diffBase.slice(0, 12)}`;
39480
+ progress("collect", `${String(diff2.trackedChanged.length)} tracked, ${String(diff2.untracked.length)} untracked, ${String(diff2.patch.length)} bytes${from}`);
38676
39481
  } else {
38677
39482
  progress("collect", "dispatched run: the tree is the stored commit itself");
38678
39483
  }
@@ -38874,7 +39679,7 @@ async function runVerification(req, ctx) {
38874
39679
  await releaseWithoutThrowing(lease, progress);
38875
39680
  }
38876
39681
  }
38877
- 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;
39682
+ 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;
38878
39683
  var init_thrun = __esm({
38879
39684
  "../../packages/envrun/dist/thrun.js"() {
38880
39685
  "use strict";
@@ -38892,6 +39697,7 @@ var init_thrun = __esm({
38892
39697
  ThRunError = class extends Error {
38893
39698
  };
38894
39699
  OUTPUT_TAIL_BYTES = 4e3;
39700
+ FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
38895
39701
  ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["https", "http", "ssh", "git"]);
38896
39702
  SCP_STYLE = /^[A-Za-z0-9._~+-]+@[A-Za-z0-9._-]+:[^:]/;
38897
39703
  URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
@@ -39750,6 +40556,7 @@ __export(dist_exports, {
39750
40556
  ALEMBIC_IMAGE: () => ALEMBIC_IMAGE,
39751
40557
  ATTEST_REFUSAL_REASONS: () => ATTEST_REFUSAL_REASONS,
39752
40558
  BOOKKEEPING_TABLES: () => BOOKKEEPING_TABLES,
40559
+ CONFIDENTIAL_SPACE_MACHINE_TYPE: () => CONFIDENTIAL_SPACE_MACHINE_TYPE,
39753
40560
  CONTAINMENT_UNAVAILABLE_PREFIX: () => CONTAINMENT_UNAVAILABLE_PREFIX,
39754
40561
  CloneUnavailableError: () => CloneUnavailableError,
39755
40562
  DAEMON_FACTS_FORMAT: () => DAEMON_FACTS_FORMAT,
@@ -39891,6 +40698,7 @@ __export(dist_exports, {
39891
40698
  toolingImageFor: () => toolingImageFor,
39892
40699
  unquoteDiffPath: () => unquoteDiffPath,
39893
40700
  venueGcloudEnv: () => venueGcloudEnv,
40701
+ venueImageFromEnv: () => venueImageFromEnv,
39894
40702
  venueStagePaths: () => venueStagePaths,
39895
40703
  verifyWorkingDiff: () => verifyWorkingDiff
39896
40704
  });
@@ -40253,7 +41061,7 @@ __export(jpi_claim_exports, {
40253
41061
  });
40254
41062
  import {
40255
41063
  readFileSync as readFileSync27,
40256
- writeFileSync as writeFileSync22,
41064
+ writeFileSync as writeFileSync23,
40257
41065
  mkdirSync as mkdirSync9,
40258
41066
  mkdtempSync as mkdtempSync5,
40259
41067
  renameSync as renameSync11,
@@ -40264,7 +41072,7 @@ import {
40264
41072
  readdirSync as readdirSync3
40265
41073
  } from "fs";
40266
41074
  import { join as join43, dirname as dirname10, isAbsolute as isAbsolute5, resolve as pathResolve } from "path";
40267
- import { createHash as createHash9 } from "crypto";
41075
+ import { createHash as createHash11 } from "crypto";
40268
41076
  import { homedir as homedir26, hostname as osHostname } from "os";
40269
41077
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync9 } from "child_process";
40270
41078
  import { promisify as promisify3 } from "util";
@@ -40282,7 +41090,7 @@ function markClaimNudged(id) {
40282
41090
  const ids2 = readNudgedClaimIds();
40283
41091
  ids2.add(id);
40284
41092
  ensureStateDir(TERMINALHIRE_DIR17);
40285
- writeFileSync22(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
41093
+ writeFileSync23(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
40286
41094
  } catch {
40287
41095
  }
40288
41096
  }
@@ -41456,7 +42264,7 @@ async function registerFounderClaim(b) {
41456
42264
  let clearedLocalCredential = false;
41457
42265
  let refusedForPurpose = false;
41458
42266
  let preserveBackgroundToken = false;
41459
- const refuse3 = (reason) => {
42267
+ const refuse4 = (reason) => {
41460
42268
  console.error(
41461
42269
  `
41462
42270
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -41481,7 +42289,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41481
42289
  try {
41482
42290
  expectLogin = await localLoginForPaidBrowserClaim();
41483
42291
  } catch (err) {
41484
- refuse3(
42292
+ refuse4(
41485
42293
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
41486
42294
  Run \`terminalhire login\`, then retry this claim. Browser verification was
41487
42295
  not opened and no claim was sent.`
@@ -41497,16 +42305,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
41497
42305
  claimRef: opportunityShortToken(b.bountyId)
41498
42306
  });
41499
42307
  if (!proofToken) {
41500
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
42308
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
41501
42309
  }
41502
42310
  const verifiedLogin = proofTokenLogin(proofToken);
41503
42311
  if (!verifiedLogin) {
41504
- refuse3(
42312
+ refuse4(
41505
42313
  "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."
41506
42314
  );
41507
42315
  }
41508
42316
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
41509
- refuse3(
42317
+ refuse4(
41510
42318
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
41511
42319
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
41512
42320
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -41538,7 +42346,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41538
42346
  try {
41539
42347
  res = await sendRegistration(true);
41540
42348
  } catch (err) {
41541
- refuse3(
42349
+ refuse4(
41542
42350
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41543
42351
  );
41544
42352
  }
@@ -41589,7 +42397,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41589
42397
  try {
41590
42398
  res = await sendRegistration(true);
41591
42399
  } catch (err) {
41592
- refuse3(
42400
+ refuse4(
41593
42401
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41594
42402
  );
41595
42403
  }
@@ -41600,14 +42408,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
41600
42408
  res = await sendRegistration(false);
41601
42409
  refusalBody = await readRefusal(res);
41602
42410
  } catch (err) {
41603
- refuse3(
42411
+ refuse4(
41604
42412
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41605
42413
  );
41606
42414
  }
41607
42415
  }
41608
42416
  if (!res.ok) {
41609
42417
  const detail = refusalBody?.message || refusalBody?.error || "";
41610
- refuse3(
42418
+ refuse4(
41611
42419
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
41612
42420
  );
41613
42421
  }
@@ -41618,7 +42426,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41618
42426
  body = null;
41619
42427
  }
41620
42428
  if (!body || body.ok !== true) {
41621
- refuse3("malformed registration response from the server.");
42429
+ refuse4("malformed registration response from the server.");
41622
42430
  }
41623
42431
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
41624
42432
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -43048,7 +43856,7 @@ function writeSliceFiles(destDir, files) {
43048
43856
  if (typeof f.content === "string") {
43049
43857
  const abs = join43(destDir, f.path);
43050
43858
  mkdirSync9(dirname10(abs), { recursive: true });
43051
- writeFileSync22(abs, f.content, "utf8");
43859
+ writeFileSync23(abs, f.content, "utf8");
43052
43860
  written.push(f.path);
43053
43861
  } else {
43054
43862
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -43097,7 +43905,7 @@ function ensureExcludedPackDir(destDir) {
43097
43905
  const existing = existsSync20(excludeFile) ? readFileSync27(excludeFile, "utf8") : "";
43098
43906
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
43099
43907
  mkdirSync9(dirname10(excludeFile), { recursive: true });
43100
- writeFileSync22(
43908
+ writeFileSync23(
43101
43909
  excludeFile,
43102
43910
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
43103
43911
  `,
@@ -43113,23 +43921,33 @@ function writePackFile(destDir, relPath, content, what) {
43113
43921
  try {
43114
43922
  const abs = join43(destDir, relPath);
43115
43923
  mkdirSync9(dirname10(abs), { recursive: true });
43116
- writeFileSync22(abs, content, { encoding: "utf8", flag: "wx" });
43924
+ writeFileSync23(abs, content, { encoding: "utf8", flag: "wx" });
43117
43925
  } catch (err) {
43118
43926
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
43119
43927
  }
43120
43928
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
43121
43929
  }
43122
43930
  function sha256OfUtf8(content) {
43123
- return createHash9("sha256").update(content, "utf8").digest("hex");
43931
+ return createHash11("sha256").update(content, "utf8").digest("hex");
43124
43932
  }
43125
- function writeWorkspacePack(destDir, spec, claim) {
43933
+ function writeWorkspacePack(destDir, spec, claim, delivery) {
43934
+ if (delivery !== "full" && delivery !== "sparse") {
43935
+ throw new TypeError(
43936
+ `writeWorkspacePack: delivery must be 'full' or 'sparse', got ${String(delivery)}`
43937
+ );
43938
+ }
43126
43939
  const gate2 = ensureExcludedPackDir(destDir);
43127
43940
  if (!gate2.ok) {
43128
43941
  const refused = { written: false, reason: gate2.reason };
43129
43942
  return { brief: refused, verify: refused, agents: refused };
43130
43943
  }
43131
43944
  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");
43132
- const verify = writePackFile(destDir, VERIFY_REL_PATH, renderVerifyDoc(claim), "verify note");
43945
+ const verify = writePackFile(
43946
+ destDir,
43947
+ VERIFY_REL_PATH,
43948
+ renderVerifyDoc(claim, delivery),
43949
+ "verify note"
43950
+ );
43133
43951
  const agents = writePackFile(
43134
43952
  destDir,
43135
43953
  AGENTS_REL_PATH,
@@ -43142,20 +43960,30 @@ function packSafeId(claim) {
43142
43960
  const id = String(claim?.id ?? "");
43143
43961
  return PACK_SAFE_ID.test(id) ? id : "<your claim id \u2014 see: terminalhire claim list>";
43144
43962
  }
43145
- function renderVerifyDoc(claim) {
43963
+ function renderVerifyDoc(claim, delivery) {
43146
43964
  const id = packSafeId(claim);
43965
+ const runStep2 = delivery === "full" ? ` ${nextStep("terminalhire run")}
43966
+
43967
+ It tests every change since the delivered baseline, committed or not, which
43968
+ is what submit sends once it is all committed.` : ` ${nextStep("terminalhire run --target <clone URL of the full repository>")}
43969
+
43970
+ It tests every change since the delivered baseline, committed or not, which
43971
+ is what submit sends once it is all committed. This delivery contains only the
43972
+ files you were granted, so the local run needs the whole repository; if you
43973
+ don't have access to it, skip to step 4, because the poster-side run in step 5
43974
+ is the verdict that counts.`;
43147
43975
  return `# Verifying claim ${id}
43148
43976
 
43149
43977
  This workspace was delivered by terminalhire for a posting. "Done" is
43150
43978
  judged on the diff: the submitted patch is the change from the delivered
43151
43979
  baseline (this repo's root commit) to HEAD, so only committed, tracked changes
43152
- count.
43980
+ count. Submit needs a clean tree; run does not.
43153
43981
 
43154
43982
  1. Read the poster's task first, when there is one: ${BRIEF_REL_PATH}
43155
43983
  2. Work on the claim branch this delivery checked out, committing as you go.
43156
43984
  3. To verify locally in terminalhire's sandboxed runner, from this directory:
43157
43985
 
43158
- terminalhire run
43986
+ ${runStep2}
43159
43987
 
43160
43988
  4. Submit \u2014 the only step that sends anything off this machine. It prints what it
43161
43989
  is about to send first, and asks y/N only at an interactive terminal:
@@ -43521,11 +44349,13 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
43521
44349
  );
43522
44350
  process.exit(1);
43523
44351
  }
43524
- const pack = writeWorkspacePack(dest, body.spec, claim);
44352
+ const pack = writeWorkspacePack(dest, body.spec, claim, "full");
43525
44353
  const working = claims.updateClaim(claim.id, {
43526
44354
  worktreePath: dest,
43527
44355
  branch,
43528
44356
  state: "working",
44357
+ // `terminalhire run` clones this commit when run from the workspace (TERM-1098).
44358
+ baseSha,
43529
44359
  workspacePack: {
43530
44360
  brief: pack.brief.written === true,
43531
44361
  verify: pack.verify.written === true,
@@ -43681,7 +44511,7 @@ async function attemptSliceDelivery(id, flags = {}) {
43681
44511
  let pack;
43682
44512
  try {
43683
44513
  await sh("git", ["-C", dest, "init"]);
43684
- pack = writeWorkspacePack(dest, body.spec, claim);
44514
+ pack = writeWorkspacePack(dest, body.spec, claim, "sparse");
43685
44515
  await sh("git", ["-C", dest, "add", "-A"]);
43686
44516
  await sh("git", [
43687
44517
  "-C",
@@ -43718,6 +44548,11 @@ async function attemptSliceDelivery(id, flags = {}) {
43718
44548
  worktreePath: dest,
43719
44549
  branch,
43720
44550
  state: "working",
44551
+ // The commit these files were cut from, so `terminalhire run` can clone it unasked
44552
+ // (TERM-1098). Recorded only when it is a full sha: a slice confirmed before the
44553
+ // server kept one answers null, and a value of any other shape is not a commit we
44554
+ // could clone, so neither is written and `run` asks for `--sha` instead.
44555
+ ...typeof body.baseSha === "string" && /^[0-9a-f]{40}$/.test(body.baseSha) ? { baseSha: body.baseSha } : {},
43721
44556
  workspacePack: {
43722
44557
  brief: pack.brief.written === true,
43723
44558
  verify: pack.verify.written === true,
@@ -44450,7 +45285,7 @@ function readClaimPushMarker() {
44450
45285
  }
44451
45286
  function writeClaimPushMarker(marker) {
44452
45287
  ensureStateDir(TERMINALHIRE_DIR17);
44453
- writeFileSync22(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
45288
+ writeFileSync23(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
44454
45289
  }
44455
45290
  function clearClaimPushMarker() {
44456
45291
  try {
@@ -45637,7 +46472,7 @@ import {
45637
46472
  readFileSync as readFileSync28,
45638
46473
  renameSync as renameSync12,
45639
46474
  rmSync as rmSync13,
45640
- writeFileSync as writeFileSync23
46475
+ writeFileSync as writeFileSync24
45641
46476
  } from "fs";
45642
46477
  import { homedir as homedir27 } from "os";
45643
46478
  import { join as join44 } from "path";
@@ -45673,7 +46508,7 @@ function writeFile(file) {
45673
46508
  const path6 = postingDraftFilePath();
45674
46509
  const tmp = `${path6}.${process.pid}.${randomUUID4()}.tmp`;
45675
46510
  try {
45676
- writeFileSync23(tmp, `${JSON.stringify(file, null, 2)}
46511
+ writeFileSync24(tmp, `${JSON.stringify(file, null, 2)}
45677
46512
  `, {
45678
46513
  encoding: "utf8",
45679
46514
  mode: 384,
@@ -46433,7 +47268,7 @@ import {
46433
47268
  renameSync as renameSync13,
46434
47269
  statSync as statSync5,
46435
47270
  unlinkSync as unlinkSync4,
46436
- writeFileSync as writeFileSync24
47271
+ writeFileSync as writeFileSync25
46437
47272
  } from "fs";
46438
47273
  import { homedir as homedir28 } from "os";
46439
47274
  import { basename as basename7, dirname as dirname11, join as join46 } from "path";
@@ -46496,7 +47331,7 @@ function mutateCache(path6, mutate) {
46496
47331
  if (mutate(existing) === false) return false;
46497
47332
  const tmp = `${path6}.${process.pid}.${tmpCounter3 += 1}.tmp`;
46498
47333
  try {
46499
- writeFileSync24(tmp, `${JSON.stringify(existing, null, 2)}
47334
+ writeFileSync25(tmp, `${JSON.stringify(existing, null, 2)}
46500
47335
  `, { mode: 384 });
46501
47336
  renameSync13(tmp, path6);
46502
47337
  } catch {
@@ -46604,13 +47439,31 @@ var init_recall_check = __esm({
46604
47439
  }
46605
47440
  });
46606
47441
 
47442
+ // src/delivered-baseline.ts
47443
+ import { execFileSync as execFileSync3 } from "child_process";
47444
+ function deliveredBaseline(worktree) {
47445
+ const roots = execFileSync3("git", ["-C", worktree, "rev-list", "--max-parents=0", "HEAD"], {
47446
+ encoding: "utf8",
47447
+ stdio: ["ignore", "pipe", "pipe"]
47448
+ }).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
47449
+ const [only] = roots;
47450
+ return roots.length === 1 && only !== void 0 ? { kind: "baseline", sha: only } : { kind: "ambiguous", rootCount: roots.length };
47451
+ }
47452
+ var init_delivered_baseline = __esm({
47453
+ "src/delivered-baseline.ts"() {
47454
+ "use strict";
47455
+ }
47456
+ });
47457
+
46607
47458
  // bin/jpi-run.js
46608
47459
  var jpi_run_exports = {};
46609
47460
  __export(jpi_run_exports, {
46610
47461
  once: () => once,
47462
+ resolveClaimContext: () => resolveClaimContext,
46611
47463
  run: () => run10
46612
47464
  });
46613
- import { existsSync as existsSync24, readFileSync as readFileSync31 } from "fs";
47465
+ import { existsSync as existsSync24, readFileSync as readFileSync31, realpathSync as realpathSync4 } from "fs";
47466
+ import { execFileSync as execFileSync4 } from "child_process";
46614
47467
  import { join as join47, resolve as resolve5 } from "path";
46615
47468
  import { tmpdir as tmpdir5 } from "os";
46616
47469
  import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "fs";
@@ -46688,9 +47541,79 @@ function requireField(value, name, hint) {
46688
47541
  if (typeof value === "string" && value.trim() !== "") return value.trim();
46689
47542
  throw new Error(`terminalhire: run needs ${name}. ${hint}`);
46690
47543
  }
47544
+ function realToplevel(dir) {
47545
+ try {
47546
+ const top = execFileSync4("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
47547
+ encoding: "utf8",
47548
+ stdio: ["ignore", "pipe", "ignore"]
47549
+ }).trim();
47550
+ return top === "" ? null : realpathSync4(top);
47551
+ } catch {
47552
+ return null;
47553
+ }
47554
+ }
47555
+ function realpathOrNull(path6) {
47556
+ try {
47557
+ return realpathSync4(path6);
47558
+ } catch {
47559
+ return null;
47560
+ }
47561
+ }
47562
+ function resolveClaimContext({ localDir, flags, claims }) {
47563
+ const here = realToplevel(localDir);
47564
+ if (here === null) return null;
47565
+ const recordedHere = claims.filter(
47566
+ (c) => c.approval != null && typeof c.worktreePath === "string" && realpathOrNull(c.worktreePath) === here
47567
+ );
47568
+ const matches = given(flags.claim) ? recordedHere.filter((c) => c.id === flags.claim.trim()) : recordedHere;
47569
+ if (matches.length === 0) return null;
47570
+ if (matches.length > 1) {
47571
+ throw new Error(
47572
+ `terminalhire: ${String(matches.length)} claims are recorded against this workspace (${matches.map((c) => c.id).join(", ")}), so run cannot tell which one this is. Name it with --claim <id>.`
47573
+ );
47574
+ }
47575
+ const [claim] = matches;
47576
+ let sha = given(flags.sha) ? flags.sha.trim() : null;
47577
+ if (sha === null) {
47578
+ if (typeof claim.baseSha !== "string" || !/^[0-9a-f]{40}$/.test(claim.baseSha)) {
47579
+ throw new Error(
47580
+ `terminalhire: claim ${claim.id} has no recorded base commit: it was delivered before terminalhire kept one. Pass --sha <40-hex>, the commit the workspace was delivered from.`
47581
+ );
47582
+ }
47583
+ sha = claim.baseSha;
47584
+ }
47585
+ let baseline;
47586
+ try {
47587
+ baseline = deliveredBaseline(here);
47588
+ } catch (err) {
47589
+ throw new Error(
47590
+ `terminalhire: could not read the delivered baseline in ${here} (${String(err?.stderr || err?.message || err).trim()}).`
47591
+ );
47592
+ }
47593
+ if (baseline.kind !== "baseline") {
47594
+ throw new Error(
47595
+ `terminalhire: ${here} has ${String(baseline.rootCount)} root commits \u2014 cannot determine the delivered baseline to diff against. Re-fetch the slice into a fresh directory: terminalhire claim slice ${claim.id} --dir <path>`
47596
+ );
47597
+ }
47598
+ const diffBase = baseline.sha;
47599
+ const target = given(flags.target) ? flags.target.trim() : diffBase === sha ? here : null;
47600
+ if (target === null) {
47601
+ const sparse = typeof claim.baseSha === "string" && diffBase !== claim.baseSha;
47602
+ throw new Error(
47603
+ sparse ? "terminalhire: run needs --target. This delivery holds only the files you were granted, so a local run needs the clone URL of the full repository: --target <url>." : `terminalhire: run needs --target. --sha ${sha.slice(0, 12)} is not the commit this workspace was delivered as (${diffBase.slice(0, 12)}), so the workspace cannot stand in for the repository: pass --target <clone URL of the repository at that commit>.`
47604
+ );
47605
+ }
47606
+ return { claimId: claim.id, target, sha, diffBase, workspaceRoot: here };
47607
+ }
46691
47608
  async function once(engine, opts) {
46692
47609
  const { root } = opts.scratch;
46693
47610
  const started = Date.now();
47611
+ if (opts.deliveredCommit && !opts.json) {
47612
+ process.stderr.write(
47613
+ ` ${"tested".padEnd(8)} against the delivered commit ${opts.deliveredCommit.slice(0, 12)}; the poster-side run puts your change on top of the repository's current default branch, so its result can differ if that branch has moved since delivery.
47614
+ `
47615
+ );
47616
+ }
46694
47617
  const outcome = await engine.verifyWorkingDiff({
46695
47618
  claimId: opts.claimId,
46696
47619
  // `th run` is the developer's own loop, and this is the variant that says so: it tests
@@ -46698,7 +47621,10 @@ async function once(engine, opts) {
46698
47621
  source: {
46699
47622
  kind: "working-diff",
46700
47623
  localRepoDir: opts.localDir,
46701
- sliceFiles: opts.slice
47624
+ sliceFiles: opts.slice,
47625
+ // From a claim workspace, the delivered baseline: committed work is then tested,
47626
+ // exactly as `claim submit` would send it (TERM-1098). Absent means HEAD.
47627
+ ...opts.diffBase ? { diffBase: opts.diffBase } : {}
46702
47628
  },
46703
47629
  targetRepo: opts.target,
46704
47630
  targetSha: opts.sha,
@@ -46786,12 +47712,35 @@ async function run10() {
46786
47712
  `terminalhire: --keep must be at most ${String(KEEP_MAX_SECONDS)} seconds, got ${JSON.stringify(String(keepRaw))}. Past that the timer overflows its int32 of milliseconds and the wait collapses to 1ms, so asking for a longer hold would give you no hold at all and the URL printed above it would be dead before anyone could open it. Refused rather than clamped, because a preview that died on the way to you looks exactly like one that worked.`
46787
47713
  );
46788
47714
  }
47715
+ const claimContext = resolveClaimContext({
47716
+ localDir,
47717
+ flags: { claim: pick2("claim"), target: pick2("target"), sha: pick2("sha") },
47718
+ claims: readClaims()
47719
+ });
46789
47720
  const opts = {
46790
- claimId: requireField(pick2("claim"), "--claim", "It is the id the result is filed under."),
46791
- target: requireField(pick2("target"), "--target", "e.g. https://github.com/koajs/koa.git"),
46792
- sha: requireField(pick2("sha"), "--sha", "The full 40-character commit your diff applies to."),
47721
+ claimId: requireField(
47722
+ claimContext?.claimId ?? pick2("claim"),
47723
+ "--claim",
47724
+ "It is the id the result is filed under."
47725
+ ),
47726
+ // With a claim context the target is always set: `resolveClaimContext` refuses, and
47727
+ // says why, when it cannot name one.
47728
+ target: requireField(
47729
+ claimContext?.target ?? pick2("target"),
47730
+ "--target",
47731
+ "e.g. https://github.com/koajs/koa.git"
47732
+ ),
47733
+ sha: requireField(
47734
+ claimContext?.sha ?? pick2("sha"),
47735
+ "--sha",
47736
+ "The full 40-character commit your diff applies to."
47737
+ ),
47738
+ diffBase: claimContext?.diffBase ?? null,
47739
+ deliveredCommit: claimContext?.sha ?? null,
46793
47740
  slice,
46794
- localDir,
47741
+ // From a claim workspace, its root and not the directory run started in: the diff
47742
+ // collector reads `.git` there, and --watch watches the same tree (TERM-1098).
47743
+ localDir: claimContext?.workspaceRoot ?? localDir,
46795
47744
  preview: !parsed.bools.has("no-preview"),
46796
47745
  watch: parsed.bools.has("watch"),
46797
47746
  json: parsed.bools.has("json"),
@@ -46844,12 +47793,14 @@ async function run10() {
46844
47793
  });
46845
47794
  return last;
46846
47795
  }
46847
- var KEEP_MAX_SECONDS, USAGE;
47796
+ var KEEP_MAX_SECONDS, USAGE, given;
46848
47797
  var init_jpi_run = __esm({
46849
47798
  "bin/jpi-run.js"() {
46850
47799
  "use strict";
46851
47800
  init_recall_check();
46852
47801
  init_package_version();
47802
+ init_claims();
47803
+ init_delivered_baseline();
46853
47804
  KEEP_MAX_SECONDS = Math.floor((2 ** 31 - 1) / 1e3);
46854
47805
  USAGE = `terminalhire run \u2014 verify your working diff in a fresh container
46855
47806
 
@@ -46877,7 +47828,14 @@ Options:
46877
47828
  Defaults are read from .th-run.json in the local checkout when present, so a
46878
47829
  claim you run repeatedly needs no flags. Every flag overrides the file.
46879
47830
 
46880
- Needs Docker running. Needs network for the first clone of the target.`;
47831
+ Run from inside a delivered claim workspace, the claim ledger supplies --claim and
47832
+ --sha. The diff is then taken from the delivered baseline, so committed work is tested
47833
+ as \`claim submit\` would send it. A full-repository delivery is its own --target: the
47834
+ delivered commit is cloned out of the workspace by path, so a private repository needs
47835
+ no credential. A delivery of only the files you were granted needs --target.
47836
+
47837
+ Needs Docker running. Needs network for the first clone of a remote target.`;
47838
+ given = (value) => typeof value === "string" && value.trim() !== "";
46881
47839
  if (process.argv[1] && process.argv[1].endsWith("jpi-run.js")) {
46882
47840
  run10().then((code) => process.exit(code ?? 0)).catch((err) => {
46883
47841
  process.stderr.write(`${err && err.message ? err.message : String(err)}
@@ -47533,7 +48491,7 @@ __export(trajectory_exports, {
47533
48491
  runTrajectory: () => runTrajectory,
47534
48492
  runTrajectoryPush: () => runTrajectoryPush
47535
48493
  });
47536
- import { existsSync as existsSync25, readFileSync as readFileSync32, readdirSync as readdirSync5, writeFileSync as writeFileSync25 } from "fs";
48494
+ import { existsSync as existsSync25, readFileSync as readFileSync32, readdirSync as readdirSync5, writeFileSync as writeFileSync26 } from "fs";
47537
48495
  import { homedir as homedir29 } from "os";
47538
48496
  import { join as join48 } from "path";
47539
48497
  function isRecord4(value) {
@@ -47692,8 +48650,8 @@ function writeExportArtifacts(score, markdown) {
47692
48650
  ensureStateDir(dir);
47693
48651
  const jsonPath = join48(dir, "trajectory-export.json");
47694
48652
  const mdPath = join48(dir, "trajectory-export.md");
47695
- writeFileSync25(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
47696
- writeFileSync25(mdPath, markdown, "utf8");
48653
+ writeFileSync26(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
48654
+ writeFileSync26(mdPath, markdown, "utf8");
47697
48655
  return { jsonPath, mdPath };
47698
48656
  }
47699
48657
  function renderInward(allNodes, view, files) {
@@ -48526,7 +49484,7 @@ var init_jpi_intro = __esm({
48526
49484
  });
48527
49485
 
48528
49486
  // src/chat-keystore.ts
48529
- import { existsSync as existsSync26, linkSync as linkSync2, readFileSync as readFileSync33, rmSync as rmSync15, unlinkSync as unlinkSync5, writeFileSync as writeFileSync26 } from "fs";
49487
+ import { existsSync as existsSync26, linkSync as linkSync2, readFileSync as readFileSync33, rmSync as rmSync15, unlinkSync as unlinkSync5, writeFileSync as writeFileSync27 } from "fs";
48530
49488
  import { randomBytes as randomBytes13 } from "crypto";
48531
49489
  import { homedir as homedir30 } from "os";
48532
49490
  import { join as join49 } from "path";
@@ -48573,7 +49531,7 @@ Recovery: if you intend to reset your chat identity, delete the file yourself an
48573
49531
  function publishIdentityBlob(blob) {
48574
49532
  const tmpFile = `${IDENTITY_FILE}.${process.pid}.${randomBytes13(6).toString("hex")}.tmp`;
48575
49533
  try {
48576
- writeFileSync26(tmpFile, JSON.stringify(blob, null, 2), {
49534
+ writeFileSync27(tmpFile, JSON.stringify(blob, null, 2), {
48577
49535
  encoding: "utf8",
48578
49536
  mode: 384,
48579
49537
  flag: "wx"
@@ -48609,7 +49567,7 @@ var init_chat_keystore = __esm({
48609
49567
  });
48610
49568
 
48611
49569
  // src/chat-client.ts
48612
- import { existsSync as existsSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync27 } from "fs";
49570
+ import { existsSync as existsSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync28 } from "fs";
48613
49571
  import { homedir as homedir31 } from "os";
48614
49572
  import { join as join50 } from "path";
48615
49573
  function defaultReadPeerPins() {
@@ -48628,7 +49586,7 @@ function defaultReadPeerPins() {
48628
49586
  }
48629
49587
  function defaultWritePeerPins(pins) {
48630
49588
  ensureStateDir(TERMINALHIRE_DIR19);
48631
- writeFileSync27(PEERS_FILE, JSON.stringify(pins, null, 2), { mode: 384, encoding: "utf8" });
49589
+ writeFileSync28(PEERS_FILE, JSON.stringify(pins, null, 2), { mode: 384, encoding: "utf8" });
48632
49590
  }
48633
49591
  function defaultChatClientDeps() {
48634
49592
  return {
@@ -49257,7 +50215,7 @@ __export(jpi_chat_read_exports, {
49257
50215
  syncUnreadBadge: () => syncUnreadBadge,
49258
50216
  writeReadCursor: () => writeReadCursor
49259
50217
  });
49260
- import { existsSync as existsSync28, readFileSync as readFileSync35, writeFileSync as writeFileSync28 } from "fs";
50218
+ import { existsSync as existsSync28, readFileSync as readFileSync35, writeFileSync as writeFileSync29 } from "fs";
49261
50219
  import { homedir as homedir32 } from "os";
49262
50220
  import { join as join51 } from "path";
49263
50221
  async function syncUnreadBadge(deps = {}) {
@@ -49281,7 +50239,7 @@ async function syncUnreadBadge(deps = {}) {
49281
50239
  );
49282
50240
  const entry = JSON.parse(readFileSync35(cacheFile, "utf8"));
49283
50241
  entry.unreadChat = { count: total };
49284
- writeFileSync28(cacheFile, JSON.stringify(entry), "utf8");
50242
+ writeFileSync29(cacheFile, JSON.stringify(entry), "utf8");
49285
50243
  } catch {
49286
50244
  }
49287
50245
  }
@@ -49306,7 +50264,7 @@ function writeReadCursor(login, iso, deps = {}) {
49306
50264
  if (prev && iso <= prev) return;
49307
50265
  cursors[login] = iso;
49308
50266
  ensureStateDir(TERMINALHIRE_DIR20);
49309
- writeFileSync28(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
50267
+ writeFileSync29(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
49310
50268
  }
49311
50269
  async function postReadCursor(peerLogin, lastReadAt, deps = {}) {
49312
50270
  const readCookie = deps.readCookie ?? readWebSessionCookie;
@@ -52216,7 +53174,7 @@ __export(mcp_config_exports, {
52216
53174
  });
52217
53175
  import { homedir as homedir34 } from "os";
52218
53176
  import { join as join53 } from "path";
52219
- import { existsSync as existsSync30, readFileSync as readFileSync37, copyFileSync as copyFileSync3, writeFileSync as writeFileSync29, mkdirSync as mkdirSync11 } from "fs";
53177
+ import { existsSync as existsSync30, readFileSync as readFileSync37, copyFileSync as copyFileSync3, writeFileSync as writeFileSync30, mkdirSync as mkdirSync11 } from "fs";
52220
53178
  import { dirname as dirname12 } from "path";
52221
53179
  function serverEntry() {
52222
53180
  return { command: SERVER_COMMAND, args: [...SERVER_ARGS] };
@@ -52321,7 +53279,7 @@ function writeServerToFile(configPath, serversKey, entry = serverEntry()) {
52321
53279
  } else {
52322
53280
  mkdirSync11(dirname12(configPath), { recursive: true });
52323
53281
  }
52324
- writeFileSync29(configPath, merged.text, "utf8");
53282
+ writeFileSync30(configPath, merged.text, "utf8");
52325
53283
  return { status: "written", backupPath, added: merged.added };
52326
53284
  }
52327
53285
  async function initMcpStep({
@@ -52559,7 +53517,7 @@ __export(util_exports, {
52559
53517
  getSizableOrigin: () => getSizableOrigin,
52560
53518
  hexToUint8Array: () => hexToUint8Array,
52561
53519
  isObject: () => isObject,
52562
- isPlainObject: () => isPlainObject,
53520
+ isPlainObject: () => isPlainObject2,
52563
53521
  issue: () => issue,
52564
53522
  joinValues: () => joinValues,
52565
53523
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -52721,7 +53679,7 @@ function slugify(input) {
52721
53679
  function isObject(data) {
52722
53680
  return typeof data === "object" && data !== null && !Array.isArray(data);
52723
53681
  }
52724
- function isPlainObject(o) {
53682
+ function isPlainObject2(o) {
52725
53683
  if (isObject(o) === false)
52726
53684
  return false;
52727
53685
  const ctor = o.constructor;
@@ -52738,7 +53696,7 @@ function isPlainObject(o) {
52738
53696
  return true;
52739
53697
  }
52740
53698
  function shallowClone(o) {
52741
- if (isPlainObject(o))
53699
+ if (isPlainObject2(o))
52742
53700
  return { ...o };
52743
53701
  if (Array.isArray(o))
52744
53702
  return [...o];
@@ -52878,7 +53836,7 @@ function omit(schema, mask) {
52878
53836
  return clone(schema, def);
52879
53837
  }
52880
53838
  function extend(schema, shape) {
52881
- if (!isPlainObject(shape)) {
53839
+ if (!isPlainObject2(shape)) {
52882
53840
  throw new Error("Invalid input to extend: expected a plain object");
52883
53841
  }
52884
53842
  const checks = schema._zod.def.checks;
@@ -52901,7 +53859,7 @@ function extend(schema, shape) {
52901
53859
  return clone(schema, def);
52902
53860
  }
52903
53861
  function safeExtend(schema, shape) {
52904
- if (!isPlainObject(shape)) {
53862
+ if (!isPlainObject2(shape)) {
52905
53863
  throw new Error("Invalid input to safeExtend: expected a plain object");
52906
53864
  }
52907
53865
  const def = mergeDefs(schema._zod.def, {
@@ -54062,7 +55020,7 @@ function mergeValues(a, b) {
54062
55020
  if (a instanceof Date && b instanceof Date && +a === +b) {
54063
55021
  return { valid: true, data: a };
54064
55022
  }
54065
- if (isPlainObject(a) && isPlainObject(b)) {
55023
+ if (isPlainObject2(a) && isPlainObject2(b)) {
54066
55024
  const bKeys = Object.keys(b);
54067
55025
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
54068
55026
  const newObj = { ...a, ...b };
@@ -55014,7 +55972,7 @@ var init_schemas = __esm({
55014
55972
  $ZodType.init(inst, def);
55015
55973
  inst._zod.parse = (payload, ctx) => {
55016
55974
  const input = payload.value;
55017
- if (!isPlainObject(input)) {
55975
+ if (!isPlainObject2(input)) {
55018
55976
  payload.issues.push({
55019
55977
  expected: "record",
55020
55978
  code: "invalid_type",
@@ -60156,7 +61114,7 @@ var init_zod_json_schema_compat = __esm({
60156
61114
  });
60157
61115
 
60158
61116
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
60159
- function isPlainObject2(value) {
61117
+ function isPlainObject3(value) {
60160
61118
  return value !== null && typeof value === "object" && !Array.isArray(value);
60161
61119
  }
60162
61120
  function mergeCapabilities(base, additional) {
@@ -60167,7 +61125,7 @@ function mergeCapabilities(base, additional) {
60167
61125
  if (addValue === void 0)
60168
61126
  continue;
60169
61127
  const baseValue = result[k];
60170
- if (isPlainObject2(baseValue) && isPlainObject2(addValue)) {
61128
+ if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
60171
61129
  result[k] = { ...baseValue, ...addValue };
60172
61130
  } else {
60173
61131
  result[k] = addValue;
@@ -75466,7 +76424,7 @@ __export(link_exports, {
75466
76424
  runLink: () => runLink,
75467
76425
  runLinkLogout: () => runLinkLogout
75468
76426
  });
75469
- import { createServer } from "http";
76427
+ import { createServer as createServer2 } from "http";
75470
76428
  import { randomBytes as randomBytes14 } from "crypto";
75471
76429
  function resolveLoopbackRequest(rawUrl, expectedNonce) {
75472
76430
  let u;
@@ -75500,7 +76458,7 @@ function defaultStartLoopback(expectedNonce, timeoutMs) {
75500
76458
  }
75501
76459
  });
75502
76460
  };
75503
- const server = createServer((req, res) => {
76461
+ const server = createServer2((req, res) => {
75504
76462
  const outcome = resolveLoopbackRequest(req.url ?? "", expectedNonce);
75505
76463
  res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
75506
76464
  res.end(outcome.ok ? LINKED_HTML : FAILED_HTML);
@@ -75894,11 +76852,11 @@ __export(signal_exports, {
75894
76852
  extractFingerprint: () => extractFingerprint
75895
76853
  });
75896
76854
  import { readFileSync as readFileSync38, readdirSync as readdirSync6 } from "fs";
75897
- import { execFileSync as execFileSync3 } from "child_process";
76855
+ import { execFileSync as execFileSync5 } from "child_process";
75898
76856
  import { join as join54 } from "path";
75899
76857
  function safeGit(args5, cwd) {
75900
76858
  try {
75901
- return execFileSync3("git", ["-C", cwd, ...args5], {
76859
+ return execFileSync5("git", ["-C", cwd, ...args5], {
75902
76860
  timeout: 2e3,
75903
76861
  stdio: ["ignore", "pipe", "ignore"]
75904
76862
  }).toString().trim();
@@ -76390,7 +77348,7 @@ var jpi_spinner_exports = {};
76390
77348
  __export(jpi_spinner_exports, {
76391
77349
  run: () => run25
76392
77350
  });
76393
- import { readFileSync as readFileSync39, writeFileSync as writeFileSync30, copyFileSync as copyFileSync4, existsSync as existsSync31 } from "fs";
77351
+ import { readFileSync as readFileSync39, writeFileSync as writeFileSync31, copyFileSync as copyFileSync4, existsSync as existsSync31 } from "fs";
76394
77352
  import { join as join56 } from "path";
76395
77353
  import { homedir as homedir36 } from "os";
76396
77354
  import { createInterface as createInterface13 } from "readline";
@@ -76404,7 +77362,7 @@ function readConfig3() {
76404
77362
  function writeConfig2(patch) {
76405
77363
  ensureStateDir(TH_DIR);
76406
77364
  const merged = { ...readConfig3(), ...patch };
76407
- writeFileSync30(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
77365
+ writeFileSync31(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
76408
77366
  }
76409
77367
  function backupSettings() {
76410
77368
  if (!existsSync31(SETTINGS_PATH)) return null;
@@ -76650,7 +77608,7 @@ var jpi_sync_exports = {};
76650
77608
  __export(jpi_sync_exports, {
76651
77609
  run: () => run26
76652
77610
  });
76653
- import { readFileSync as readFileSync40, writeFileSync as writeFileSync31, existsSync as existsSync32, rmSync as rmSync16 } from "fs";
77611
+ import { readFileSync as readFileSync40, writeFileSync as writeFileSync32, existsSync as existsSync32, rmSync as rmSync16 } from "fs";
76654
77612
  import { join as join57 } from "path";
76655
77613
  import { homedir as homedir37, hostname as osHostname2 } from "os";
76656
77614
  import { createInterface as createInterface14 } from "readline";
@@ -76675,7 +77633,7 @@ function readMarker() {
76675
77633
  }
76676
77634
  function writeMarker(marker) {
76677
77635
  ensureStateDir(TH_DIR2);
76678
- writeFileSync31(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
77636
+ writeFileSync32(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
76679
77637
  }
76680
77638
  function clearMarker() {
76681
77639
  try {