terminalhire 0.42.20 → 0.42.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  },
@@ -34296,13 +34322,192 @@ var init_gceIdentity = __esm({
34296
34322
 
34297
34323
  // ../../packages/attest/dist/confidentialSpace.js
34298
34324
  import { createHash as createHash8, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
34299
- var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL;
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;
34300
34503
  var init_confidentialSpace = __esm({
34301
34504
  "../../packages/attest/dist/confidentialSpace.js"() {
34302
34505
  "use strict";
34303
34506
  init_gceIdentity();
34304
34507
  CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
34305
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";
34306
34511
  }
34307
34512
  });
34308
34513
 
@@ -34645,50 +34850,50 @@ function toTestRunResult(result, outputSha256) {
34645
34850
  function toAcceptancePredicate(pair, opts = {}) {
34646
34851
  const { baseline, patched } = pair;
34647
34852
  if (baseline.runId === patched.runId) {
34648
- 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.`);
34649
34854
  }
34650
34855
  if (patched.status === "refused") {
34651
- 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`);
34652
34857
  }
34653
34858
  if (patched.outcome === null) {
34654
- return refuse2("no-outcome-recorded", "the patched run recorded no outcome");
34859
+ return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
34655
34860
  }
34656
34861
  if (baseline.status === "refused") {
34657
- 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`);
34658
34863
  }
34659
34864
  if (baseline.outcome === null) {
34660
- return refuse2("baseline-no-outcome", "the baseline run recorded no outcome");
34865
+ return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
34661
34866
  }
34662
34867
  if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
34663
- 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.`);
34664
34869
  }
34665
34870
  const budget = OUTCOME_TO_BUDGET[patched.outcome];
34666
34871
  if (budget === null) {
34667
- 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.`);
34668
34873
  }
34669
34874
  if (patched.testCommand === null) {
34670
- 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");
34671
34876
  }
34672
34877
  if (baseline.testCommand !== patched.testCommand) {
34673
- 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.`);
34674
34879
  }
34675
34880
  if (baseline.targetSha !== patched.targetSha) {
34676
- 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}`);
34677
34882
  }
34678
34883
  if (patched.treeDigest === null || patched.treeDigest === "") {
34679
- 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");
34680
34885
  }
34681
34886
  if (patched.patchSha256 === null || patched.patchSha256 === "") {
34682
- 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");
34683
34888
  }
34684
34889
  if (baseline.treeDigest === null || baseline.treeDigest === "") {
34685
- 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");
34686
34891
  }
34687
34892
  if (baseline.treeDigest === patched.treeDigest) {
34688
- 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`);
34689
34894
  }
34690
34895
  if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
34691
- 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.`);
34692
34897
  }
34693
34898
  for (const [side, outcome, counts, exitCode] of [
34694
34899
  ["patched", patched.outcome, patched.counts, patched.exitCode],
@@ -34696,7 +34901,7 @@ function toAcceptancePredicate(pair, opts = {}) {
34696
34901
  ]) {
34697
34902
  const why = contradicts(outcome, counts, exitCode);
34698
34903
  if (why !== null) {
34699
- return refuse2("outcome-contradicts-counts", `the ${side} run is ${why}`);
34904
+ return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
34700
34905
  }
34701
34906
  }
34702
34907
  for (const [side, source] of [
@@ -34705,44 +34910,44 @@ function toAcceptancePredicate(pair, opts = {}) {
34705
34910
  ]) {
34706
34911
  const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
34707
34912
  if (why !== null)
34708
- return refuse2("test-command-origin-unattested", `the ${side} run ${why}`);
34913
+ return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
34709
34914
  }
34710
34915
  if (patched.testCommandSource === "developer-declared") {
34711
- 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)");
34712
34917
  }
34713
34918
  if (baseline.testCommandSource !== patched.testCommandSource) {
34714
- 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.`);
34715
34920
  }
34716
34921
  if (baseline.containerImage !== patched.containerImage) {
34717
- 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.`);
34718
34923
  }
34719
34924
  if (patched.containerImage === null || patched.containerImage === "") {
34720
- 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");
34721
34926
  }
34722
34927
  if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
34723
- 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.");
34724
34929
  }
34725
34930
  if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
34726
- 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.`);
34727
34932
  }
34728
34933
  if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
34729
- 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.`);
34730
34935
  }
34731
34936
  if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
34732
- 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.");
34733
34938
  }
34734
34939
  if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
34735
- 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.`);
34736
34941
  }
34737
34942
  if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
34738
- 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.`);
34739
34944
  }
34740
34945
  if (baseline.containerImageDigest !== patched.containerImageDigest) {
34741
- 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.`);
34742
34947
  }
34743
34948
  const claimedDigest = imageDigestOf(patched.containerImage);
34744
34949
  if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
34745
- 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.`);
34746
34951
  }
34747
34952
  return {
34748
34953
  ok: true,
@@ -34769,7 +34974,7 @@ function signRunStatement(predicate, privateKey, keyid) {
34769
34974
  const statement = createAcceptanceStatement(predicate);
34770
34975
  return { statement, envelope: signStatement(statement, privateKey, keyid) };
34771
34976
  }
34772
- 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;
34773
34978
  var init_attestation2 = __esm({
34774
34979
  "../../packages/envrun/dist/attestation.js"() {
34775
34980
  "use strict";
@@ -34873,7 +35078,7 @@ var init_attestation2 = __esm({
34873
35078
  "outcome-contradicts-counts",
34874
35079
  "test-command-origin-unattested"
34875
35080
  ];
34876
- refuse2 = (reason, detail) => ({
35081
+ refuse3 = (reason, detail) => ({
34877
35082
  ok: false,
34878
35083
  reason,
34879
35084
  detail
@@ -35284,6 +35489,22 @@ function bootImageFlags(p) {
35284
35489
  }
35285
35490
  const workloadImage = p.workloadImage;
35286
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
+ ];
35287
35508
  return [
35288
35509
  // The production family. The debug family reports `dbgstat: enabled`, which the
35289
35510
  // intake's verifier refuses, so a debug boot could never produce a run we record.
@@ -35296,7 +35517,15 @@ function bootImageFlags(p) {
35296
35517
  // most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
35297
35518
  // combination is phase 5's measurement.
35298
35519
  "--maintenance-policy=TERMINATE",
35299
- `--metadata=tee-image-reference=${workloadImage},tee-container-log-redirect=true`
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"
35300
35529
  ];
35301
35530
  }
35302
35531
  function assertInstanceIdentity(fn, id) {
@@ -35370,7 +35599,13 @@ function gcpBootArgv(p) {
35370
35599
  // expectation uses (TERM-832): two inline spellings of one value is how
35371
35600
  // the boot path and the verifier drift apart silently.
35372
35601
  `--service-account=${venueIdentityEmail(p.project)}`,
35373
- "--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"],
35374
35609
  "--shielded-secure-boot",
35375
35610
  "--shielded-vtpm",
35376
35611
  "--shielded-integrity-monitoring",
@@ -35403,7 +35638,7 @@ function gcpDeleteArgv(p) {
35403
35638
  "--quiet"
35404
35639
  ];
35405
35640
  }
35406
- var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, CONFIDENTIAL_SPACE_MACHINE_TYPE, WORKLOAD_IMAGE_REFERENCE, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
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;
35407
35642
  var init_gcpPlacement = __esm({
35408
35643
  "../../packages/envrun/dist/gcpPlacement.js"() {
35409
35644
  "use strict";
@@ -35422,6 +35657,8 @@ var init_gcpPlacement = __esm({
35422
35657
  GCP_RUN_LABEL_KEY = "th-run";
35423
35658
  CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
35424
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;
35425
35662
  GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
35426
35663
  GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
35427
35664
  GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -35463,7 +35700,10 @@ var init_emptyGitConfig = __esm({
35463
35700
 
35464
35701
  // ../../packages/envrun/dist/hostedVenue.js
35465
35702
  import { spawn as spawn6, spawnSync as spawnSync6 } from "child_process";
35466
- 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";
35467
35707
  import { join as join40 } from "path";
35468
35708
  import { tmpdir as tmpdir3 } from "os";
35469
35709
  function credentialInGitConfig(text) {
@@ -35665,6 +35905,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
35665
35905
  });
35666
35906
  });
35667
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
+ }
35668
35991
  function quoteForRemoteShell(arg) {
35669
35992
  return `'${arg.replace(/'/g, "'\\''")}'`;
35670
35993
  }
@@ -35999,27 +36322,370 @@ ${res.stderr}`);
35999
36322
  function manualDeleteCommand(vm, project, zone) {
36000
36323
  return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
36001
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
+ }
36002
36654
  function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
36003
36655
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
36004
36656
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
36005
36657
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
36006
36658
  const keyFile = resolveServiceAccountKeyFile(opts);
36007
36659
  const image = venueImageFromEnv(process.env);
36660
+ const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
36008
36661
  const env = venueGcloudEnv(gcloudConfig);
36009
36662
  return {
36010
36663
  kind: "hosted-pool",
36011
36664
  acquire: (runId) => acquireTransactionally(async (allocated) => {
36012
36665
  const vm = venueInstanceName(runId);
36013
36666
  const stageBase = `/tmp/th-stage-${runId}`;
36014
- const bootArgv = gcpBootArgv({
36667
+ const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
36015
36668
  vmName: vm,
36016
36669
  project,
36017
36670
  zone,
36018
36671
  runId,
36019
- machineType: opts.machineType ?? (image.venueImage === "confidential-space" ? CONFIDENTIAL_SPACE_MACHINE_TYPE : DEFAULT_GCP_MACHINE_TYPE),
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,
36020
36682
  ...image
36021
- });
36683
+ };
36684
+ let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
36022
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));
36023
36689
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
36024
36690
  let instanceState = "unknown";
36025
36691
  allocated.onRollback(() => {
@@ -36035,6 +36701,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
36035
36701
  throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
36036
36702
  }
36037
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
+ }
36038
36718
  const deadline = io.now() + SSH_READY_BUDGET_MS;
36039
36719
  let last = { retryable: true, reason: "no probe ran" };
36040
36720
  let identity = null;
@@ -36129,43 +36809,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
36129
36809
  if (!prepared.ok) {
36130
36810
  throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
36131
36811
  }
36132
- const stageVolumes = stageVolumeNames(vm);
36133
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36134
- const created = docker3.sync(volumeCreateArgv(name, runId), {
36135
- timeoutMs: VOLUME_CREATE_TIMEOUT_MS
36136
- });
36137
- if (created.error || created.status !== 0) {
36138
- throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
36139
- }
36140
- }
36141
- for (const name of [stageVolumes.clone, stageVolumes.jail]) {
36142
- const probe = docker3.sync(execProbeArgv(name), {
36143
- timeoutMs: EXEC_PROBE_TIMEOUT_MS
36144
- });
36145
- if (probe.error || probe.status !== 0) {
36146
- 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)}`);
36147
- }
36148
- }
36149
- const after = io.classifyDaemon(docker3);
36150
- if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
36151
- 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.`);
36152
- }
36153
- return makeLease({
36154
- runId,
36155
- vm,
36156
- project,
36157
- zone,
36158
- stageBase,
36159
- stageVolumes,
36812
+ return finishAcquire({
36160
36813
  docker: docker3,
36161
36814
  tunnel,
36162
36815
  socketDir,
36163
36816
  socketPath,
36164
36817
  venueDaemonId,
36165
36818
  venueIdentity,
36166
- env,
36167
- io
36819
+ transport: sshStageTransport(vm, project, zone, env, io)
36168
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
+ }
36169
36862
  })
36170
36863
  };
36171
36864
  }
@@ -36224,28 +36917,7 @@ function makeLease(p) {
36224
36917
  return null;
36225
36918
  };
36226
36919
  let stagedOwner;
36227
- const readStagedOwner = () => {
36228
- const res = p.io.exec(
36229
- "gcloud",
36230
- iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
36231
- OWNER_PROBE_TIMEOUT_MS,
36232
- // `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
36233
- // SERVICE configuration rather than whoever is at the terminal, and a probe that
36234
- // omitted this would ssh under the person's own credential — then report an owner
36235
- // measured against a session the run does not use.
36236
- p.env
36237
- );
36238
- if (!res.ok) {
36239
- 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.`);
36240
- }
36241
- const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
36242
- const ids2 = lines.slice(-2).map((l) => Number(l));
36243
- if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
36244
- 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.`);
36245
- }
36246
- return { uid: ids2[0], gid: ids2[1] };
36247
- };
36248
- stagedOwner = readStagedOwner();
36920
+ stagedOwner = p.transport.readOwner();
36249
36921
  return {
36250
36922
  kind: "hosted-pool",
36251
36923
  runId: p.runId,
@@ -36280,7 +36952,7 @@ function makeLease(p) {
36280
36952
  const dir = venueProxyDir(p.stageBase);
36281
36953
  const buffer = stageProxyCode();
36282
36954
  try {
36283
- 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);
36284
36956
  if (!res.ok) {
36285
36957
  throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
36286
36958
  }
@@ -36290,7 +36962,7 @@ function makeLease(p) {
36290
36962
  return {
36291
36963
  dir,
36292
36964
  cleanup: () => {
36293
- 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);
36294
36966
  }
36295
36967
  };
36296
36968
  },
@@ -36298,7 +36970,7 @@ function makeLease(p) {
36298
36970
  check("staging the tree");
36299
36971
  const paths = venueStagePaths(p.stageBase);
36300
36972
  const push = async (from, to) => {
36301
- 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);
36302
36974
  if (!res.ok) {
36303
36975
  throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
36304
36976
  }
@@ -36456,7 +37128,7 @@ function makeLease(p) {
36456
37128
  }
36457
37129
  };
36458
37130
  }
36459
- 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;
36460
37132
  var init_hostedVenue = __esm({
36461
37133
  "../../packages/envrun/dist/hostedVenue.js"() {
36462
37134
  "use strict";
@@ -36577,7 +37249,55 @@ var init_hostedVenue = __esm({
36577
37249
  throw new Error("JWKS fetch returned a body with no keys array");
36578
37250
  }
36579
37251
  return body;
36580
- }
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 })
36581
37301
  };
36582
37302
  VENUE_SSH_USER = "th-runner";
36583
37303
  GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
@@ -36598,6 +37318,17 @@ var init_hostedVenue = __esm({
36598
37318
  SSH_KEY_NOT_READY = /Permission denied \(publickey/;
36599
37319
  DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
36600
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";
36601
37332
  }
36602
37333
  });
36603
37334
 
@@ -37497,10 +38228,10 @@ var init_references = __esm({
37497
38228
 
37498
38229
  // ../../packages/envspec/dist/repo.js
37499
38230
  import { readdirSync as readdirSync2, readFileSync as readFileSync26, statSync as statSync4 } from "fs";
37500
- 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";
37501
38232
  function createRepoReader(repoPath) {
37502
38233
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join41(repoPath, relativePath);
37503
- const toPosix = (absolute) => relative2(repoPath, absolute).split(sep4).join("/");
38234
+ const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
37504
38235
  const readText = (relativePath) => {
37505
38236
  try {
37506
38237
  return readFileSync26(resolveIn(relativePath), "utf8");
@@ -40330,7 +41061,7 @@ __export(jpi_claim_exports, {
40330
41061
  });
40331
41062
  import {
40332
41063
  readFileSync as readFileSync27,
40333
- writeFileSync as writeFileSync22,
41064
+ writeFileSync as writeFileSync23,
40334
41065
  mkdirSync as mkdirSync9,
40335
41066
  mkdtempSync as mkdtempSync5,
40336
41067
  renameSync as renameSync11,
@@ -40341,7 +41072,7 @@ import {
40341
41072
  readdirSync as readdirSync3
40342
41073
  } from "fs";
40343
41074
  import { join as join43, dirname as dirname10, isAbsolute as isAbsolute5, resolve as pathResolve } from "path";
40344
- import { createHash as createHash10 } from "crypto";
41075
+ import { createHash as createHash11 } from "crypto";
40345
41076
  import { homedir as homedir26, hostname as osHostname } from "os";
40346
41077
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync9 } from "child_process";
40347
41078
  import { promisify as promisify3 } from "util";
@@ -40359,7 +41090,7 @@ function markClaimNudged(id) {
40359
41090
  const ids2 = readNudgedClaimIds();
40360
41091
  ids2.add(id);
40361
41092
  ensureStateDir(TERMINALHIRE_DIR17);
40362
- writeFileSync22(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
41093
+ writeFileSync23(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
40363
41094
  } catch {
40364
41095
  }
40365
41096
  }
@@ -41533,7 +42264,7 @@ async function registerFounderClaim(b) {
41533
42264
  let clearedLocalCredential = false;
41534
42265
  let refusedForPurpose = false;
41535
42266
  let preserveBackgroundToken = false;
41536
- const refuse3 = (reason) => {
42267
+ const refuse4 = (reason) => {
41537
42268
  console.error(
41538
42269
  `
41539
42270
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -41558,7 +42289,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41558
42289
  try {
41559
42290
  expectLogin = await localLoginForPaidBrowserClaim();
41560
42291
  } catch (err) {
41561
- refuse3(
42292
+ refuse4(
41562
42293
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
41563
42294
  Run \`terminalhire login\`, then retry this claim. Browser verification was
41564
42295
  not opened and no claim was sent.`
@@ -41574,16 +42305,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
41574
42305
  claimRef: opportunityShortToken(b.bountyId)
41575
42306
  });
41576
42307
  if (!proofToken) {
41577
- refuse3("could not verify your GitHub identity with terminalhire (see above).");
42308
+ refuse4("could not verify your GitHub identity with terminalhire (see above).");
41578
42309
  }
41579
42310
  const verifiedLogin = proofTokenLogin(proofToken);
41580
42311
  if (!verifiedLogin) {
41581
- refuse3(
42312
+ refuse4(
41582
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."
41583
42314
  );
41584
42315
  }
41585
42316
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
41586
- refuse3(
42317
+ refuse4(
41587
42318
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
41588
42319
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
41589
42320
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -41615,7 +42346,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41615
42346
  try {
41616
42347
  res = await sendRegistration(true);
41617
42348
  } catch (err) {
41618
- refuse3(
42349
+ refuse4(
41619
42350
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41620
42351
  );
41621
42352
  }
@@ -41666,7 +42397,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41666
42397
  try {
41667
42398
  res = await sendRegistration(true);
41668
42399
  } catch (err) {
41669
- refuse3(
42400
+ refuse4(
41670
42401
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41671
42402
  );
41672
42403
  }
@@ -41677,14 +42408,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
41677
42408
  res = await sendRegistration(false);
41678
42409
  refusalBody = await readRefusal(res);
41679
42410
  } catch (err) {
41680
- refuse3(
42411
+ refuse4(
41681
42412
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41682
42413
  );
41683
42414
  }
41684
42415
  }
41685
42416
  if (!res.ok) {
41686
42417
  const detail = refusalBody?.message || refusalBody?.error || "";
41687
- refuse3(
42418
+ refuse4(
41688
42419
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
41689
42420
  );
41690
42421
  }
@@ -41695,7 +42426,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41695
42426
  body = null;
41696
42427
  }
41697
42428
  if (!body || body.ok !== true) {
41698
- refuse3("malformed registration response from the server.");
42429
+ refuse4("malformed registration response from the server.");
41699
42430
  }
41700
42431
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
41701
42432
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -43125,7 +43856,7 @@ function writeSliceFiles(destDir, files) {
43125
43856
  if (typeof f.content === "string") {
43126
43857
  const abs = join43(destDir, f.path);
43127
43858
  mkdirSync9(dirname10(abs), { recursive: true });
43128
- writeFileSync22(abs, f.content, "utf8");
43859
+ writeFileSync23(abs, f.content, "utf8");
43129
43860
  written.push(f.path);
43130
43861
  } else {
43131
43862
  unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
@@ -43174,7 +43905,7 @@ function ensureExcludedPackDir(destDir) {
43174
43905
  const existing = existsSync20(excludeFile) ? readFileSync27(excludeFile, "utf8") : "";
43175
43906
  if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
43176
43907
  mkdirSync9(dirname10(excludeFile), { recursive: true });
43177
- writeFileSync22(
43908
+ writeFileSync23(
43178
43909
  excludeFile,
43179
43910
  `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
43180
43911
  `,
@@ -43190,14 +43921,14 @@ function writePackFile(destDir, relPath, content, what) {
43190
43921
  try {
43191
43922
  const abs = join43(destDir, relPath);
43192
43923
  mkdirSync9(dirname10(abs), { recursive: true });
43193
- writeFileSync22(abs, content, { encoding: "utf8", flag: "wx" });
43924
+ writeFileSync23(abs, content, { encoding: "utf8", flag: "wx" });
43194
43925
  } catch (err) {
43195
43926
  return { written: false, reason: `the ${what} could not be written (${err.message})` };
43196
43927
  }
43197
43928
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
43198
43929
  }
43199
43930
  function sha256OfUtf8(content) {
43200
- return createHash10("sha256").update(content, "utf8").digest("hex");
43931
+ return createHash11("sha256").update(content, "utf8").digest("hex");
43201
43932
  }
43202
43933
  function writeWorkspacePack(destDir, spec, claim, delivery) {
43203
43934
  if (delivery !== "full" && delivery !== "sparse") {
@@ -44554,7 +45285,7 @@ function readClaimPushMarker() {
44554
45285
  }
44555
45286
  function writeClaimPushMarker(marker) {
44556
45287
  ensureStateDir(TERMINALHIRE_DIR17);
44557
- writeFileSync22(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
45288
+ writeFileSync23(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
44558
45289
  }
44559
45290
  function clearClaimPushMarker() {
44560
45291
  try {
@@ -45741,7 +46472,7 @@ import {
45741
46472
  readFileSync as readFileSync28,
45742
46473
  renameSync as renameSync12,
45743
46474
  rmSync as rmSync13,
45744
- writeFileSync as writeFileSync23
46475
+ writeFileSync as writeFileSync24
45745
46476
  } from "fs";
45746
46477
  import { homedir as homedir27 } from "os";
45747
46478
  import { join as join44 } from "path";
@@ -45777,7 +46508,7 @@ function writeFile(file) {
45777
46508
  const path6 = postingDraftFilePath();
45778
46509
  const tmp = `${path6}.${process.pid}.${randomUUID4()}.tmp`;
45779
46510
  try {
45780
- writeFileSync23(tmp, `${JSON.stringify(file, null, 2)}
46511
+ writeFileSync24(tmp, `${JSON.stringify(file, null, 2)}
45781
46512
  `, {
45782
46513
  encoding: "utf8",
45783
46514
  mode: 384,
@@ -46537,7 +47268,7 @@ import {
46537
47268
  renameSync as renameSync13,
46538
47269
  statSync as statSync5,
46539
47270
  unlinkSync as unlinkSync4,
46540
- writeFileSync as writeFileSync24
47271
+ writeFileSync as writeFileSync25
46541
47272
  } from "fs";
46542
47273
  import { homedir as homedir28 } from "os";
46543
47274
  import { basename as basename7, dirname as dirname11, join as join46 } from "path";
@@ -46600,7 +47331,7 @@ function mutateCache(path6, mutate) {
46600
47331
  if (mutate(existing) === false) return false;
46601
47332
  const tmp = `${path6}.${process.pid}.${tmpCounter3 += 1}.tmp`;
46602
47333
  try {
46603
- writeFileSync24(tmp, `${JSON.stringify(existing, null, 2)}
47334
+ writeFileSync25(tmp, `${JSON.stringify(existing, null, 2)}
46604
47335
  `, { mode: 384 });
46605
47336
  renameSync13(tmp, path6);
46606
47337
  } catch {
@@ -47760,7 +48491,7 @@ __export(trajectory_exports, {
47760
48491
  runTrajectory: () => runTrajectory,
47761
48492
  runTrajectoryPush: () => runTrajectoryPush
47762
48493
  });
47763
- 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";
47764
48495
  import { homedir as homedir29 } from "os";
47765
48496
  import { join as join48 } from "path";
47766
48497
  function isRecord4(value) {
@@ -47919,8 +48650,8 @@ function writeExportArtifacts(score, markdown) {
47919
48650
  ensureStateDir(dir);
47920
48651
  const jsonPath = join48(dir, "trajectory-export.json");
47921
48652
  const mdPath = join48(dir, "trajectory-export.md");
47922
- writeFileSync25(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
47923
- writeFileSync25(mdPath, markdown, "utf8");
48653
+ writeFileSync26(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
48654
+ writeFileSync26(mdPath, markdown, "utf8");
47924
48655
  return { jsonPath, mdPath };
47925
48656
  }
47926
48657
  function renderInward(allNodes, view, files) {
@@ -48753,7 +49484,7 @@ var init_jpi_intro = __esm({
48753
49484
  });
48754
49485
 
48755
49486
  // src/chat-keystore.ts
48756
- 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";
48757
49488
  import { randomBytes as randomBytes13 } from "crypto";
48758
49489
  import { homedir as homedir30 } from "os";
48759
49490
  import { join as join49 } from "path";
@@ -48800,7 +49531,7 @@ Recovery: if you intend to reset your chat identity, delete the file yourself an
48800
49531
  function publishIdentityBlob(blob) {
48801
49532
  const tmpFile = `${IDENTITY_FILE}.${process.pid}.${randomBytes13(6).toString("hex")}.tmp`;
48802
49533
  try {
48803
- writeFileSync26(tmpFile, JSON.stringify(blob, null, 2), {
49534
+ writeFileSync27(tmpFile, JSON.stringify(blob, null, 2), {
48804
49535
  encoding: "utf8",
48805
49536
  mode: 384,
48806
49537
  flag: "wx"
@@ -48836,7 +49567,7 @@ var init_chat_keystore = __esm({
48836
49567
  });
48837
49568
 
48838
49569
  // src/chat-client.ts
48839
- 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";
48840
49571
  import { homedir as homedir31 } from "os";
48841
49572
  import { join as join50 } from "path";
48842
49573
  function defaultReadPeerPins() {
@@ -48855,7 +49586,7 @@ function defaultReadPeerPins() {
48855
49586
  }
48856
49587
  function defaultWritePeerPins(pins) {
48857
49588
  ensureStateDir(TERMINALHIRE_DIR19);
48858
- 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" });
48859
49590
  }
48860
49591
  function defaultChatClientDeps() {
48861
49592
  return {
@@ -49484,7 +50215,7 @@ __export(jpi_chat_read_exports, {
49484
50215
  syncUnreadBadge: () => syncUnreadBadge,
49485
50216
  writeReadCursor: () => writeReadCursor
49486
50217
  });
49487
- 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";
49488
50219
  import { homedir as homedir32 } from "os";
49489
50220
  import { join as join51 } from "path";
49490
50221
  async function syncUnreadBadge(deps = {}) {
@@ -49508,7 +50239,7 @@ async function syncUnreadBadge(deps = {}) {
49508
50239
  );
49509
50240
  const entry = JSON.parse(readFileSync35(cacheFile, "utf8"));
49510
50241
  entry.unreadChat = { count: total };
49511
- writeFileSync28(cacheFile, JSON.stringify(entry), "utf8");
50242
+ writeFileSync29(cacheFile, JSON.stringify(entry), "utf8");
49512
50243
  } catch {
49513
50244
  }
49514
50245
  }
@@ -49533,7 +50264,7 @@ function writeReadCursor(login, iso, deps = {}) {
49533
50264
  if (prev && iso <= prev) return;
49534
50265
  cursors[login] = iso;
49535
50266
  ensureStateDir(TERMINALHIRE_DIR20);
49536
- 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" });
49537
50268
  }
49538
50269
  async function postReadCursor(peerLogin, lastReadAt, deps = {}) {
49539
50270
  const readCookie = deps.readCookie ?? readWebSessionCookie;
@@ -52443,7 +53174,7 @@ __export(mcp_config_exports, {
52443
53174
  });
52444
53175
  import { homedir as homedir34 } from "os";
52445
53176
  import { join as join53 } from "path";
52446
- 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";
52447
53178
  import { dirname as dirname12 } from "path";
52448
53179
  function serverEntry() {
52449
53180
  return { command: SERVER_COMMAND, args: [...SERVER_ARGS] };
@@ -52548,7 +53279,7 @@ function writeServerToFile(configPath, serversKey, entry = serverEntry()) {
52548
53279
  } else {
52549
53280
  mkdirSync11(dirname12(configPath), { recursive: true });
52550
53281
  }
52551
- writeFileSync29(configPath, merged.text, "utf8");
53282
+ writeFileSync30(configPath, merged.text, "utf8");
52552
53283
  return { status: "written", backupPath, added: merged.added };
52553
53284
  }
52554
53285
  async function initMcpStep({
@@ -52786,7 +53517,7 @@ __export(util_exports, {
52786
53517
  getSizableOrigin: () => getSizableOrigin,
52787
53518
  hexToUint8Array: () => hexToUint8Array,
52788
53519
  isObject: () => isObject,
52789
- isPlainObject: () => isPlainObject,
53520
+ isPlainObject: () => isPlainObject2,
52790
53521
  issue: () => issue,
52791
53522
  joinValues: () => joinValues,
52792
53523
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -52948,7 +53679,7 @@ function slugify(input) {
52948
53679
  function isObject(data) {
52949
53680
  return typeof data === "object" && data !== null && !Array.isArray(data);
52950
53681
  }
52951
- function isPlainObject(o) {
53682
+ function isPlainObject2(o) {
52952
53683
  if (isObject(o) === false)
52953
53684
  return false;
52954
53685
  const ctor = o.constructor;
@@ -52965,7 +53696,7 @@ function isPlainObject(o) {
52965
53696
  return true;
52966
53697
  }
52967
53698
  function shallowClone(o) {
52968
- if (isPlainObject(o))
53699
+ if (isPlainObject2(o))
52969
53700
  return { ...o };
52970
53701
  if (Array.isArray(o))
52971
53702
  return [...o];
@@ -53105,7 +53836,7 @@ function omit(schema, mask) {
53105
53836
  return clone(schema, def);
53106
53837
  }
53107
53838
  function extend(schema, shape) {
53108
- if (!isPlainObject(shape)) {
53839
+ if (!isPlainObject2(shape)) {
53109
53840
  throw new Error("Invalid input to extend: expected a plain object");
53110
53841
  }
53111
53842
  const checks = schema._zod.def.checks;
@@ -53128,7 +53859,7 @@ function extend(schema, shape) {
53128
53859
  return clone(schema, def);
53129
53860
  }
53130
53861
  function safeExtend(schema, shape) {
53131
- if (!isPlainObject(shape)) {
53862
+ if (!isPlainObject2(shape)) {
53132
53863
  throw new Error("Invalid input to safeExtend: expected a plain object");
53133
53864
  }
53134
53865
  const def = mergeDefs(schema._zod.def, {
@@ -54289,7 +55020,7 @@ function mergeValues(a, b) {
54289
55020
  if (a instanceof Date && b instanceof Date && +a === +b) {
54290
55021
  return { valid: true, data: a };
54291
55022
  }
54292
- if (isPlainObject(a) && isPlainObject(b)) {
55023
+ if (isPlainObject2(a) && isPlainObject2(b)) {
54293
55024
  const bKeys = Object.keys(b);
54294
55025
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
54295
55026
  const newObj = { ...a, ...b };
@@ -55241,7 +55972,7 @@ var init_schemas = __esm({
55241
55972
  $ZodType.init(inst, def);
55242
55973
  inst._zod.parse = (payload, ctx) => {
55243
55974
  const input = payload.value;
55244
- if (!isPlainObject(input)) {
55975
+ if (!isPlainObject2(input)) {
55245
55976
  payload.issues.push({
55246
55977
  expected: "record",
55247
55978
  code: "invalid_type",
@@ -60383,7 +61114,7 @@ var init_zod_json_schema_compat = __esm({
60383
61114
  });
60384
61115
 
60385
61116
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
60386
- function isPlainObject2(value) {
61117
+ function isPlainObject3(value) {
60387
61118
  return value !== null && typeof value === "object" && !Array.isArray(value);
60388
61119
  }
60389
61120
  function mergeCapabilities(base, additional) {
@@ -60394,7 +61125,7 @@ function mergeCapabilities(base, additional) {
60394
61125
  if (addValue === void 0)
60395
61126
  continue;
60396
61127
  const baseValue = result[k];
60397
- if (isPlainObject2(baseValue) && isPlainObject2(addValue)) {
61128
+ if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
60398
61129
  result[k] = { ...baseValue, ...addValue };
60399
61130
  } else {
60400
61131
  result[k] = addValue;
@@ -75693,7 +76424,7 @@ __export(link_exports, {
75693
76424
  runLink: () => runLink,
75694
76425
  runLinkLogout: () => runLinkLogout
75695
76426
  });
75696
- import { createServer } from "http";
76427
+ import { createServer as createServer2 } from "http";
75697
76428
  import { randomBytes as randomBytes14 } from "crypto";
75698
76429
  function resolveLoopbackRequest(rawUrl, expectedNonce) {
75699
76430
  let u;
@@ -75727,7 +76458,7 @@ function defaultStartLoopback(expectedNonce, timeoutMs) {
75727
76458
  }
75728
76459
  });
75729
76460
  };
75730
- const server = createServer((req, res) => {
76461
+ const server = createServer2((req, res) => {
75731
76462
  const outcome = resolveLoopbackRequest(req.url ?? "", expectedNonce);
75732
76463
  res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
75733
76464
  res.end(outcome.ok ? LINKED_HTML : FAILED_HTML);
@@ -76617,7 +77348,7 @@ var jpi_spinner_exports = {};
76617
77348
  __export(jpi_spinner_exports, {
76618
77349
  run: () => run25
76619
77350
  });
76620
- 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";
76621
77352
  import { join as join56 } from "path";
76622
77353
  import { homedir as homedir36 } from "os";
76623
77354
  import { createInterface as createInterface13 } from "readline";
@@ -76631,7 +77362,7 @@ function readConfig3() {
76631
77362
  function writeConfig2(patch) {
76632
77363
  ensureStateDir(TH_DIR);
76633
77364
  const merged = { ...readConfig3(), ...patch };
76634
- writeFileSync30(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
77365
+ writeFileSync31(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
76635
77366
  }
76636
77367
  function backupSettings() {
76637
77368
  if (!existsSync31(SETTINGS_PATH)) return null;
@@ -76877,7 +77608,7 @@ var jpi_sync_exports = {};
76877
77608
  __export(jpi_sync_exports, {
76878
77609
  run: () => run26
76879
77610
  });
76880
- 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";
76881
77612
  import { join as join57 } from "path";
76882
77613
  import { homedir as homedir37, hostname as osHostname2 } from "os";
76883
77614
  import { createInterface as createInterface14 } from "readline";
@@ -76902,7 +77633,7 @@ function readMarker() {
76902
77633
  }
76903
77634
  function writeMarker(marker) {
76904
77635
  ensureStateDir(TH_DIR2);
76905
- writeFileSync31(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
77636
+ writeFileSync32(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
76906
77637
  }
76907
77638
  function clearMarker() {
76908
77639
  try {