terminalhire 0.42.11 → 0.42.12

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.
@@ -28249,6 +28249,177 @@ var init_nonce = __esm({
28249
28249
  }
28250
28250
  });
28251
28251
 
28252
+ // ../../packages/attest/dist/gceIdentity.js
28253
+ import { createHash as createHash6, createPublicKey as createPublicKey2, verify as cryptoVerify2 } from "crypto";
28254
+ function expectedAudience(dispatchId) {
28255
+ if (dispatchId === "")
28256
+ throw new TypeError("expectedAudience: dispatchId is empty");
28257
+ return `${VERIFICATION_RUN_AUDIENCE_BASE}#${dispatchId}`;
28258
+ }
28259
+ function venueIdentityEmail(projectId) {
28260
+ if (projectId === "")
28261
+ throw new TypeError("venueIdentityEmail: projectId is empty");
28262
+ return `venue-identity@${projectId}.iam.gserviceaccount.com`;
28263
+ }
28264
+ function refuseIdentity(reason, detail) {
28265
+ return { ok: false, reason, detail };
28266
+ }
28267
+ function strictBase64UrlDecode(s) {
28268
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1)
28269
+ return null;
28270
+ const decoded = Buffer.from(s, "base64url");
28271
+ if (decoded.toString("base64url") !== s)
28272
+ return null;
28273
+ return decoded;
28274
+ }
28275
+ function decodeJsonSegment(segment) {
28276
+ const bytes = strictBase64UrlDecode(segment);
28277
+ if (bytes === null)
28278
+ return null;
28279
+ let parsed;
28280
+ try {
28281
+ parsed = JSON.parse(bytes.toString("utf8"));
28282
+ } catch {
28283
+ return null;
28284
+ }
28285
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
28286
+ return null;
28287
+ return parsed;
28288
+ }
28289
+ function assertExpectationsComplete(expectations) {
28290
+ const required = [
28291
+ ["audience", expectations.audience],
28292
+ ["serviceAccountEmail", expectations.serviceAccountEmail],
28293
+ ["projectId", expectations.projectId],
28294
+ ["instanceName", expectations.instanceName]
28295
+ ];
28296
+ for (const [name, value] of required) {
28297
+ if (value === "")
28298
+ throw new TypeError(`verifyGceIdentityToken: expectations.${name} is empty`);
28299
+ }
28300
+ if (expectations.instanceId === "") {
28301
+ throw new TypeError("verifyGceIdentityToken: expectations.instanceId is empty");
28302
+ }
28303
+ }
28304
+ async function verifyGceIdentityToken(token, expectations, deps) {
28305
+ assertExpectationsComplete(expectations);
28306
+ if (!Number.isFinite(deps.now)) {
28307
+ throw new TypeError("verifyGceIdentityToken: deps.now is not a finite number");
28308
+ }
28309
+ if (token.length > MAX_TOKEN_LENGTH) {
28310
+ return refuseIdentity("token-malformed", "longer than any GCE identity token");
28311
+ }
28312
+ const parts = token.split(".");
28313
+ if (parts.length !== 3) {
28314
+ return refuseIdentity("token-malformed", "not a three-segment compact JWT");
28315
+ }
28316
+ const [headerB64, payloadB64, signatureB64] = parts;
28317
+ const header = decodeJsonSegment(headerB64);
28318
+ if (header === null) {
28319
+ return refuseIdentity("token-malformed", "header segment is not base64url-encoded JSON");
28320
+ }
28321
+ if (header.alg !== "RS256") {
28322
+ return refuseIdentity("algorithm-not-rs256", "token names an unsupported algorithm");
28323
+ }
28324
+ if (typeof header.kid !== "string" || header.kid === "") {
28325
+ return refuseIdentity("unknown-key", "token header names no key id");
28326
+ }
28327
+ const jwks = await deps.fetchJwks();
28328
+ const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
28329
+ if (jwk === void 0) {
28330
+ return refuseIdentity("unknown-key", "token key id is not in the published key set");
28331
+ }
28332
+ const signature = strictBase64UrlDecode(signatureB64);
28333
+ if (signature === null) {
28334
+ return refuseIdentity("token-malformed", "signature segment is not base64url");
28335
+ }
28336
+ let signatureValid = false;
28337
+ try {
28338
+ const publicKey = createPublicKey2({ key: jwk, format: "jwk" });
28339
+ signatureValid = cryptoVerify2("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
28340
+ } catch {
28341
+ signatureValid = false;
28342
+ }
28343
+ if (!signatureValid) {
28344
+ return refuseIdentity("signature-invalid", "signature does not verify under the named key");
28345
+ }
28346
+ const payload = decodeJsonSegment(payloadB64);
28347
+ if (payload === null) {
28348
+ return refuseIdentity("token-malformed", "payload segment is not base64url-encoded JSON");
28349
+ }
28350
+ if (payload.iss !== "https://accounts.google.com") {
28351
+ return refuseIdentity("issuer-mismatch", "issuer is not https://accounts.google.com");
28352
+ }
28353
+ if (payload.aud !== expectations.audience) {
28354
+ return refuseIdentity("audience-mismatch", "audience is not the one this run expects");
28355
+ }
28356
+ const nowSeconds = deps.now / 1e3;
28357
+ const exp = payload.exp;
28358
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
28359
+ return refuseIdentity("token-expired", "exp is missing, malformed, or in the past");
28360
+ }
28361
+ const iat = payload.iat;
28362
+ if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
28363
+ return refuseIdentity("issued-in-future", "iat is missing, malformed, or too far in the future");
28364
+ }
28365
+ if (payload.email !== expectations.serviceAccountEmail || payload.email_verified !== true) {
28366
+ return refuseIdentity("wrong-account", "token is not from the venue identity account");
28367
+ }
28368
+ const google = payload.google;
28369
+ const computeEngine = typeof google === "object" && google !== null && !Array.isArray(google) ? google.compute_engine : void 0;
28370
+ if (typeof computeEngine !== "object" || computeEngine === null || Array.isArray(computeEngine)) {
28371
+ return refuseIdentity("compute-engine-block-missing", "no google.compute_engine block \u2014 was the token fetched with format=full?");
28372
+ }
28373
+ const ce = computeEngine;
28374
+ if (ce.project_id !== expectations.projectId) {
28375
+ return refuseIdentity("project-mismatch", "token names an instance in a different project");
28376
+ }
28377
+ if (ce.instance_name !== expectations.instanceName) {
28378
+ return refuseIdentity("instance-name-mismatch", "token names a different instance than this run booted");
28379
+ }
28380
+ if (typeof ce.instance_id !== "string" || ce.instance_id === "") {
28381
+ return refuseIdentity("instance-id-missing", "instance id is absent or not a string");
28382
+ }
28383
+ if (expectations.instanceId !== void 0 && ce.instance_id !== expectations.instanceId) {
28384
+ return refuseIdentity("instance-id-mismatch", "token names a different instance than the boot record");
28385
+ }
28386
+ if (typeof ce.zone !== "string" || !expectations.allowedZones.includes(ce.zone)) {
28387
+ return refuseIdentity("zone-not-allowed", "zone is not one the pool is configured to use");
28388
+ }
28389
+ return {
28390
+ ok: true,
28391
+ value: {
28392
+ instanceId: ce.instance_id,
28393
+ instanceName: expectations.instanceName,
28394
+ zone: ce.zone,
28395
+ projectId: expectations.projectId,
28396
+ email: expectations.serviceAccountEmail,
28397
+ iat,
28398
+ exp,
28399
+ tokenSha256: createHash6("sha256").update(token, "utf8").digest("hex")
28400
+ }
28401
+ };
28402
+ }
28403
+ var VERIFICATION_RUN_AUDIENCE_BASE, IAT_FUTURE_SKEW_MS, MAX_TOKEN_LENGTH;
28404
+ var init_gceIdentity = __esm({
28405
+ "../../packages/attest/dist/gceIdentity.js"() {
28406
+ "use strict";
28407
+ VERIFICATION_RUN_AUDIENCE_BASE = "https://terminalhire.com/api/verification-run";
28408
+ IAT_FUTURE_SKEW_MS = 5 * 60 * 1e3;
28409
+ MAX_TOKEN_LENGTH = 8192;
28410
+ }
28411
+ });
28412
+
28413
+ // ../../packages/attest/dist/venueInstanceName.js
28414
+ function venueInstanceName(runId) {
28415
+ return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
28416
+ }
28417
+ var init_venueInstanceName = __esm({
28418
+ "../../packages/attest/dist/venueInstanceName.js"() {
28419
+ "use strict";
28420
+ }
28421
+ });
28422
+
28252
28423
  // ../../packages/attest/dist/verify.js
28253
28424
  var init_verify = __esm({
28254
28425
  "../../packages/attest/dist/verify.js"() {
@@ -28272,6 +28443,8 @@ var init_dist2 = __esm({
28272
28443
  init_manifest();
28273
28444
  init_attestation();
28274
28445
  init_nonce();
28446
+ init_gceIdentity();
28447
+ init_venueInstanceName();
28275
28448
  init_verify();
28276
28449
  }
28277
28450
  });
@@ -28397,7 +28570,7 @@ var init_result = __esm({
28397
28570
  });
28398
28571
 
28399
28572
  // ../../packages/envrun/dist/attestation.js
28400
- import { createHash as createHash6, randomBytes as randomBytes10 } from "crypto";
28573
+ import { createHash as createHash7, randomBytes as randomBytes10 } from "crypto";
28401
28574
  function contradicts(outcome, counts, exitCode) {
28402
28575
  const budget = OUTCOME_TO_BUDGET[outcome];
28403
28576
  if (budget === null)
@@ -28408,7 +28581,7 @@ function localMeasurement(imageReference) {
28408
28581
  return `${LOCAL_MEASUREMENT_PREFIX}${imageReference}`;
28409
28582
  }
28410
28583
  function sha256Hex(data) {
28411
- return createHash6("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
28584
+ return createHash7("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
28412
28585
  }
28413
28586
  function toTestRunResult(result, outputSha256) {
28414
28587
  return {
@@ -29141,10 +29314,32 @@ function gcpBootArgv(p) {
29141
29314
  // A survivor has to be findable. An unlabelled orphan can only be told apart from
29142
29315
  // a legitimate instance by a human who remembers booting it.
29143
29316
  `--labels=${GCP_RUN_LABEL_KEY}=${p.runId},${GCP_MANAGED_LABEL_KEY}=true`,
29144
- // This instance runs untrusted third-party code. With a service account attached,
29145
- // that code reaches the metadata server at 169.254.169.254 and lifts the default
29146
- // account's token — nothing on the VM needs one, so it does not get one.
29147
- "--no-service-account",
29317
+ // This instance runs untrusted third-party code, and it now carries a service
29318
+ // account — the venue-identity binding needs an instance-identity token, and only
29319
+ // an ATTACHED account can mint one (TERM-794/TERM-802). This replaces
29320
+ // `--no-service-account`, which used to mean "nothing on the metadata service to
29321
+ // steal". Attaching an account removes that, so the safety now rests on two things,
29322
+ // and this change MUST land together with proof #2 — never ahead of it:
29323
+ //
29324
+ // 1. `--no-scopes` + ZERO project roles bound the BLAST RADIUS of a lifted token.
29325
+ // No scopes → the OAuth access-token endpoint serves nothing (measured,
29326
+ // TERM-706); zero roles → the token authorizes no GCP API. But it is NOT
29327
+ // valueless: it is the attestation evidence itself, so a lifted token still
29328
+ // authorizes a false claim about WHERE a run happened (the steal-a-true-token
29329
+ // attack in the item-2 spike). `--no-scopes` stays because it removes lateral
29330
+ // GCP access; it does not make losing the identity token cheap.
29331
+ // 2. The untrusted code cannot reach the metadata server to lift the token at all.
29332
+ // The test step runs `--network=none`; the install step runs on an `--internal`
29333
+ // network whose only egress is a CONNECT-only, registry-allowlisted proxy that
29334
+ // denies the metadata host three ways. Measured once on a real VM in
29335
+ // docs/spikes/term-483-item2-metadata-denial.md (only a user bridge, which no
29336
+ // profile here grants, ever reached metadata). The STANDING, per-push proof is
29337
+ // the validate-fence metadata group (TERM-805), which lands in the SAME change
29338
+ // as this SA-attach — that co-landing is the whole safety argument, not a nicety.
29339
+ // The email comes from the SAME speller acquire's serviceAccountEmail
29340
+ // expectation uses (TERM-832): two inline spellings of one value is how
29341
+ // the boot path and the verifier drift apart silently.
29342
+ `--service-account=${venueIdentityEmail(p.project)}`,
29148
29343
  "--no-scopes",
29149
29344
  "--shielded-secure-boot",
29150
29345
  "--shielded-vtpm",
@@ -29197,6 +29392,7 @@ var init_gcpPlacement = __esm({
29197
29392
  "../../packages/envrun/dist/gcpPlacement.js"() {
29198
29393
  "use strict";
29199
29394
  init_dist();
29395
+ init_dist2();
29200
29396
  init_placement();
29201
29397
  init_execute();
29202
29398
  DEFAULT_GCP_PROJECT = "terminalhire-pool";
@@ -29479,6 +29675,26 @@ function iapTunnelArgv(vm, project, zone, socketPath) {
29479
29675
  `${socketPath}:/var/run/docker.sock`
29480
29676
  ];
29481
29677
  }
29678
+ function identityProbeCommand(runId) {
29679
+ const url = `${GCE_METADATA_IDENTITY_URL}?audience=${encodeURIComponent(expectedAudience(runId))}&format=full`;
29680
+ return `curl -sS -f -m 5 -H ${quoteForRemoteShell("Metadata-Flavor: Google")} ${quoteForRemoteShell(url)} && echo && docker info --format {{.ID}}`;
29681
+ }
29682
+ function parseIdentityProbeOutput(stdout) {
29683
+ const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
29684
+ if (lines.length !== 2)
29685
+ return null;
29686
+ const token = lines[0];
29687
+ const daemonId = lines[1];
29688
+ if (token === void 0 || daemonId === void 0)
29689
+ return null;
29690
+ if (token.length > MAX_TOKEN_LENGTH)
29691
+ return null;
29692
+ if (!COMPACT_JWT.test(token))
29693
+ return null;
29694
+ if (!DAEMON_ID.test(daemonId))
29695
+ return null;
29696
+ return { token, daemonId };
29697
+ }
29482
29698
  function iapUntarArgv(vm, project, zone, dir) {
29483
29699
  return iapSshArgv(vm, project, zone, `tar -C ${quoteForRemoteShell(dir)} -xf -`);
29484
29700
  }
@@ -29575,7 +29791,7 @@ ${r.stderr}`;
29575
29791
  }
29576
29792
  function assertServiceCredentials(config, io) {
29577
29793
  const env = venueGcloudEnv(config);
29578
- const active = io.exec("gcloud", ["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], 2e4, env);
29794
+ const active = io.exec("gcloud", ["auth", "list", "--filter=status:ACTIVE", "--format=value(account)"], LOCAL_GCLOUD_TIMEOUT_MS, env);
29579
29795
  if (!active.ok) {
29580
29796
  throw new HostedVenueError(`could not read which account the ${config} gcloud configuration acts as, so booting would run as whoever this terminal is signed in as: ${execDetail(active).slice(0, 300)}`, "ours");
29581
29797
  }
@@ -29588,6 +29804,38 @@ function assertServiceCredentials(config, io) {
29588
29804
  }
29589
29805
  return account;
29590
29806
  }
29807
+ function resolveServiceAccountKeyFile(opts) {
29808
+ const explicit = opts.serviceAccountKeyFile;
29809
+ if (explicit !== void 0 && explicit.trim() !== "")
29810
+ return explicit;
29811
+ const fromEnv = process.env.GOOGLE_APPLICATION_CREDENTIALS;
29812
+ return fromEnv === void 0 || fromEnv.trim() === "" ? null : fromEnv;
29813
+ }
29814
+ function configurationPresent(config, io) {
29815
+ const listed = io.exec("gcloud", ["config", "configurations", "list", "--format=value(name)"], LOCAL_GCLOUD_TIMEOUT_MS, venueGcloudEnv(config));
29816
+ if (!listed.ok) {
29817
+ throw new HostedVenueError(`could not read the gcloud configuration list, so the ${config} configuration could not be provisioned: ${execDetail(listed).slice(0, 300)}`, "ours");
29818
+ }
29819
+ return listed.stdout.split("\n").map((line) => line.trim()).includes(config);
29820
+ }
29821
+ function redactKeyFile(detail, keyFile) {
29822
+ return keyFile === "" ? detail : detail.split(keyFile).join("<key-file>");
29823
+ }
29824
+ function ensureVenueServiceCredentials(config, keyFile, io) {
29825
+ if (keyFile === null)
29826
+ return assertServiceCredentials(config, io);
29827
+ if (!configurationPresent(config, io)) {
29828
+ const created = io.exec("gcloud", ["config", "configurations", "create", config, "--no-activate"], LOCAL_GCLOUD_TIMEOUT_MS, venueGcloudEnv(config));
29829
+ if (!created.ok && !configurationPresent(config, io)) {
29830
+ throw new HostedVenueError(`could not create the ${config} gcloud configuration to hold the service credential: ${execDetail(created).slice(0, 300)}`, "ours");
29831
+ }
29832
+ }
29833
+ const activated = io.exec("gcloud", ["auth", "activate-service-account", `--key-file=${keyFile}`], SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS, venueGcloudEnv(config));
29834
+ if (!activated.ok) {
29835
+ throw new HostedVenueError(`could not activate the runner service credential into the ${config} gcloud configuration: ${redactKeyFile(execDetail(activated), keyFile).slice(0, 300)}`, "ours");
29836
+ }
29837
+ return assertServiceCredentials(config, io);
29838
+ }
29591
29839
  function hostedVenueAvailable(opts = {}, io = defaultHostedVenueIo) {
29592
29840
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
29593
29841
  const config = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
@@ -29677,11 +29925,12 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
29677
29925
  const project = opts.project ?? DEFAULT_GCP_PROJECT;
29678
29926
  const zone = opts.zone ?? DEFAULT_GCP_ZONE;
29679
29927
  const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
29928
+ const keyFile = resolveServiceAccountKeyFile(opts);
29680
29929
  const env = venueGcloudEnv(gcloudConfig);
29681
29930
  return {
29682
29931
  kind: "hosted-pool",
29683
29932
  acquire: (runId) => acquireTransactionally(async (allocated) => {
29684
- const vm = `th-run-${runId}`.toLowerCase().slice(0, 62);
29933
+ const vm = venueInstanceName(runId);
29685
29934
  const stageBase = `/tmp/th-stage-${runId}`;
29686
29935
  const bootArgv = gcpBootArgv({
29687
29936
  vmName: vm,
@@ -29690,7 +29939,7 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
29690
29939
  runId,
29691
29940
  machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE
29692
29941
  });
29693
- assertServiceCredentials(gcloudConfig, io);
29942
+ ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
29694
29943
  const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
29695
29944
  let instanceState = "unknown";
29696
29945
  allocated.onRollback(() => {
@@ -29708,22 +29957,50 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
29708
29957
  instanceState = "exists";
29709
29958
  const deadline = io.now() + SSH_READY_BUDGET_MS;
29710
29959
  let last = { retryable: true, reason: "no probe ran" };
29711
- let ready = false;
29960
+ let identity = null;
29712
29961
  while (io.now() < deadline) {
29713
- const probe = io.exec("gcloud", iapSshArgv(vm, project, zone, "docker info --format {{.ID}}"), SSH_PROBE_TIMEOUT_MS, env);
29714
- if (probe.ok && probe.stdout.trim() !== "") {
29715
- ready = true;
29716
- break;
29717
- }
29718
- last = classifyProbeFailure(probe);
29719
- if (!last.retryable) {
29720
- throw new HostedVenueError(`${vm} cannot be reached over IAP and waiting will not change that \u2014 ${last.reason}`);
29962
+ const probe = io.exec("gcloud", iapSshArgv(vm, project, zone, identityProbeCommand(runId)), SSH_PROBE_TIMEOUT_MS, env);
29963
+ if (probe.ok) {
29964
+ identity = parseIdentityProbeOutput(probe.stdout);
29965
+ if (identity !== null)
29966
+ break;
29967
+ last = { retryable: true, reason: "probe answered without token + daemon id" };
29968
+ } else {
29969
+ last = classifyProbeFailure(probe);
29970
+ if (!last.retryable) {
29971
+ throw new HostedVenueError(`${vm} cannot be reached over IAP and waiting will not change that \u2014 ${last.reason}`);
29972
+ }
29721
29973
  }
29722
29974
  await io.sleep(SSH_PROBE_INTERVAL_MS);
29723
29975
  }
29724
- if (!ready) {
29976
+ if (identity === null) {
29725
29977
  throw new HostedVenueError(`${vm} was not reachable over IAP within ${SSH_READY_BUDGET_MS / 1e3}s \u2014 last reason: ${last.reason}`);
29726
29978
  }
29979
+ let jwks;
29980
+ try {
29981
+ jwks = await io.fetchJwks();
29982
+ } catch (err) {
29983
+ throw new HostedVenueError(`could not reach Google's JWKS to check ${vm}'s identity token \u2014 ${describeErr(err)}`, "ours", { cause: err });
29984
+ }
29985
+ const identityVerdict = await verifyGceIdentityToken(identity.token, {
29986
+ audience: expectedAudience(runId),
29987
+ serviceAccountEmail: venueIdentityEmail(project),
29988
+ projectId: project,
29989
+ instanceName: vm,
29990
+ // The exact boot zone, a singleton — "the booted instance", not
29991
+ // "a pool VM of the right name" (the expectations docblock's own
29992
+ // distinction). No instanceId: this boot path never reads the
29993
+ // create call's output, so there is no token-independent id to
29994
+ // expect; the token's own id is recorded on the lease instead.
29995
+ allowedZones: [zone]
29996
+ }, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
29997
+ if (!identityVerdict.ok) {
29998
+ throw new HostedVenueError(`${vm} presented an identity token this lease refuses (${identityVerdict.reason}): ${identityVerdict.detail}`);
29999
+ }
30000
+ const venueIdentity = {
30001
+ token: identity.token,
30002
+ claims: identityVerdict.value
30003
+ };
29727
30004
  let socketDir;
29728
30005
  try {
29729
30006
  socketDir = io.makePrivateDir();
@@ -29764,6 +30041,9 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
29764
30041
  if (!verdict.distinct) {
29765
30042
  throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(verdict)}`);
29766
30043
  }
30044
+ if (verdict.venueDaemonId !== identity.daemonId) {
30045
+ throw new HostedVenueError(`refusing to hand out a hosted lease: the tunnel answers as daemon ${verdict.venueDaemonId}, but ${vm}'s identity probe reported ${identity.daemonId} from its own socket`);
30046
+ }
29767
30047
  const venueDaemonId = verdict.venueDaemonId;
29768
30048
  const prepared = io.exec("gcloud", stageMkdirArgv(vm, project, zone, stageBase), MKDIR_TIMEOUT_MS, env);
29769
30049
  if (!prepared.ok) {
@@ -29780,6 +30060,7 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
29780
30060
  socketDir,
29781
30061
  socketPath,
29782
30062
  venueDaemonId,
30063
+ venueIdentity,
29783
30064
  env,
29784
30065
  io
29785
30066
  });
@@ -29881,6 +30162,11 @@ function makeLease(p) {
29881
30162
  get guestUser() {
29882
30163
  return stagedOwner;
29883
30164
  },
30165
+ // Verified at acquire, before anything was staged; carried so the caller
30166
+ // can hand PR 5's intake the evidence. The lease is the only holder of
30167
+ // the raw token — `VenueLease.venueIdentity` says why it is not a result
30168
+ // field.
30169
+ venueIdentity: p.venueIdentity,
29884
30170
  get released() {
29885
30171
  return released;
29886
30172
  },
@@ -30017,11 +30303,12 @@ function makeLease(p) {
30017
30303
  }
30018
30304
  };
30019
30305
  }
30020
- var SSH_READY_BUDGET_MS, SSH_PROBE_INTERVAL_MS, SSH_PROBE_TIMEOUT_MS, TUNNEL_BUDGET_MS, TUNNEL_POLL_INTERVAL_MS, CREDENTIAL_QUERY_PARAM, UNDECODABLE, STAGE_PUSH_TIMEOUT_MS, PROXY_CLEANUP_TIMEOUT_MS, OWNER_PROBE_TIMEOUT_MS, BOOT_TIMEOUT_MS, MKDIR_TIMEOUT_MS, DELETE_TIMEOUT_MS, SOCKET_DIR_PREFIX, VENUE_SOCKET_NAME, HostedVenueError, VENUE_GCLOUD_CONFIG, SERVICE_ACCOUNT_SUFFIX, GCLOUD_PRINCIPAL_OVERRIDES, defaultHostedVenueIo, 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;
30306
+ 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, 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, GCE_METADATA_IDENTITY_URL, COMPACT_JWT, 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;
30021
30307
  var init_hostedVenue = __esm({
30022
30308
  "../../packages/envrun/dist/hostedVenue.js"() {
30023
30309
  "use strict";
30024
30310
  init_dist();
30311
+ init_dist2();
30025
30312
  init_gcpPlacement();
30026
30313
  init_execute();
30027
30314
  init_labels();
@@ -30032,6 +30319,8 @@ var init_hostedVenue = __esm({
30032
30319
  SSH_PROBE_TIMEOUT_MS = 25e3;
30033
30320
  TUNNEL_BUDGET_MS = 6e4;
30034
30321
  TUNNEL_POLL_INTERVAL_MS = 500;
30322
+ GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
30323
+ JWKS_FETCH_TIMEOUT_MS = 1e4;
30035
30324
  CREDENTIAL_QUERY_PARAM = /[?&][^=&\s]*(token|secret|password|passwd|api[-_]?key|signature|sig|auth|credential)[^=&\s]*=[^\s&]/i;
30036
30325
  UNDECODABLE = "?token=this:url-could-not-be-decoded-so-it-is-refused";
30037
30326
  STAGE_PUSH_TIMEOUT_MS = 3e5;
@@ -30040,6 +30329,8 @@ var init_hostedVenue = __esm({
30040
30329
  BOOT_TIMEOUT_MS = 18e4;
30041
30330
  MKDIR_TIMEOUT_MS = 6e4;
30042
30331
  DELETE_TIMEOUT_MS = 3e5;
30332
+ LOCAL_GCLOUD_TIMEOUT_MS = 2e4;
30333
+ SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS = 3e4;
30043
30334
  SOCKET_DIR_PREFIX = "th-venue-";
30044
30335
  VENUE_SOCKET_NAME = "docker.sock";
30045
30336
  HostedVenueError = class extends RunRefusalError {
@@ -30105,8 +30396,23 @@ var init_hostedVenue = __esm({
30105
30396
  },
30106
30397
  dockerFor: (socketPath) => remoteDockerClient(`unix://${socketPath}`),
30107
30398
  classifyDaemon: (docker3) => classifyVenueDaemon(docker3),
30108
- containmentOn: (docker3) => containerContainmentOn(docker3)
30399
+ containmentOn: (docker3) => containerContainmentOn(docker3),
30400
+ fetchJwks: async () => {
30401
+ const res = await globalThis.fetch(GOOGLE_JWKS_URL, {
30402
+ signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
30403
+ });
30404
+ if (!res.ok) {
30405
+ throw new Error(`JWKS fetch returned HTTP ${String(res.status)}`);
30406
+ }
30407
+ const body = await res.json();
30408
+ if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
30409
+ throw new Error("JWKS fetch returned a body with no keys array");
30410
+ }
30411
+ return body;
30412
+ }
30109
30413
  };
30414
+ GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
30415
+ COMPACT_JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
30110
30416
  IAP_NOT_READY = /\b4047\s*[:\]]/;
30111
30417
  IAP_BACKEND_UNREACHABLE = /\b4003\s*[:\]]/;
30112
30418
  IAP_DENIED = /PERMISSION_DENIED|Required '[^']+' permission/;
@@ -32038,7 +32344,8 @@ async function verifyWorkingDiff(req) {
32038
32344
  // The whole point of TERM-738. `refusal.message` above is the sentence we show; this
32039
32345
  // is what actually went wrong, and without it the cause we attached at the throw site
32040
32346
  // is discarded here — a returned refusal never reaches a caller's `catch`.
32041
- diagnostic: describeCause(err)
32347
+ diagnostic: describeCause(err),
32348
+ venueIdentity: null
32042
32349
  };
32043
32350
  }
32044
32351
  }
@@ -32066,7 +32373,8 @@ async function runVerification(req, ctx) {
32066
32373
  verdict: null,
32067
32374
  // Null, and not an oversight: this refusal is our own check failing, so the sentence
32068
32375
  // above IS the whole cause and there is no error underneath it to carry.
32069
- diagnostic: null
32376
+ diagnostic: null,
32377
+ venueIdentity: null
32070
32378
  };
32071
32379
  }
32072
32380
  const placement = req.placement ?? localDockerPlacement();
@@ -32091,7 +32399,8 @@ async function runVerification(req, ctx) {
32091
32399
  verdict: null,
32092
32400
  // Null, and not an oversight: this refusal is our own check failing, so the sentence
32093
32401
  // above IS the whole cause and there is no error underneath it to carry.
32094
- diagnostic: null
32402
+ diagnostic: null,
32403
+ venueIdentity: null
32095
32404
  };
32096
32405
  }
32097
32406
  const source = req.source;
@@ -32127,7 +32436,8 @@ async function runVerification(req, ctx) {
32127
32436
  verdict: null,
32128
32437
  // Null, and not an oversight: this refusal is our own check failing, so the sentence
32129
32438
  // above IS the whole cause and there is no error underneath it to carry.
32130
- diagnostic: null
32439
+ diagnostic: null,
32440
+ venueIdentity: null
32131
32441
  };
32132
32442
  }
32133
32443
  const stage = join23(req.scratchRoot, runId);
@@ -32188,7 +32498,8 @@ async function runVerification(req, ctx) {
32188
32498
  // a refusal, and a caller reading it as "this much of the run happened"
32189
32499
  // would be reading a run that did not.
32190
32500
  spec: null,
32191
- verdict: null
32501
+ verdict: null,
32502
+ venueIdentity: null
32192
32503
  };
32193
32504
  }
32194
32505
  const lease = resolved.lease;
@@ -32246,7 +32557,14 @@ async function runVerification(req, ctx) {
32246
32557
  leaksClean: verdict.leaks.clean
32247
32558
  };
32248
32559
  if (req.preview === false)
32249
- return { result: base, preview: null, spec, verdict, diagnostic: null };
32560
+ return {
32561
+ result: base,
32562
+ preview: null,
32563
+ spec,
32564
+ verdict,
32565
+ diagnostic: null,
32566
+ venueIdentity: lease.venueIdentity ?? null
32567
+ };
32250
32568
  progress("preview", "starting one instance both parties can open");
32251
32569
  const instance = await lease.publishPreview({
32252
32570
  labels,
@@ -32270,7 +32588,8 @@ async function runVerification(req, ctx) {
32270
32588
  preview: instance,
32271
32589
  spec,
32272
32590
  verdict,
32273
- diagnostic: null
32591
+ diagnostic: null,
32592
+ venueIdentity: lease.venueIdentity ?? null
32274
32593
  };
32275
32594
  } finally {
32276
32595
  await releaseWithoutThrowing(lease, progress);
@@ -33158,11 +33477,14 @@ __export(dist_exports, {
33158
33477
  DbStackError: () => DbStackError,
33159
33478
  EGRESS_PROBE: () => EGRESS_PROBE,
33160
33479
  EnvRunError: () => EnvRunError,
33480
+ GCE_METADATA_IDENTITY_URL: () => GCE_METADATA_IDENTITY_URL,
33161
33481
  GCP_MANAGED_LABEL_KEY: () => GCP_MANAGED_LABEL_KEY,
33162
33482
  GCP_MAX_RUN_DURATION_SECONDS: () => GCP_MAX_RUN_DURATION_SECONDS,
33163
33483
  GCP_RUN_LABEL_KEY: () => GCP_RUN_LABEL_KEY,
33484
+ GOOGLE_JWKS_URL: () => GOOGLE_JWKS_URL,
33164
33485
  HOSTED_POOL_REFUSAL: () => HOSTED_POOL_REFUSAL,
33165
33486
  HostedVenueError: () => HostedVenueError,
33487
+ JWKS_FETCH_TIMEOUT_MS: () => JWKS_FETCH_TIMEOUT_MS,
33166
33488
  LOCAL_MEASUREMENT_PREFIX: () => LOCAL_MEASUREMENT_PREFIX,
33167
33489
  LabelWatch: () => LabelWatch,
33168
33490
  MIN_GIT_VERSION_FOR_END_OF_OPTIONS: () => MIN_GIT_VERSION_FOR_END_OF_OPTIONS,
@@ -33223,6 +33545,7 @@ __export(dist_exports, {
33223
33545
  describeVenueDaemon: () => describeVenueDaemon,
33224
33546
  detectRunner: () => detectRunner,
33225
33547
  endOfOptionsUnsupported: () => endOfOptionsUnsupported,
33548
+ ensureVenueServiceCredentials: () => ensureVenueServiceCredentials,
33226
33549
  failureSourceOf: () => failureSourceOf,
33227
33550
  findRunRefusal: () => findRunRefusal,
33228
33551
  gcpBootArgv: () => gcpBootArgv,
@@ -33235,6 +33558,7 @@ __export(dist_exports, {
33235
33558
  iapSshArgv: () => iapSshArgv,
33236
33559
  iapTunnelArgv: () => iapTunnelArgv,
33237
33560
  iapUntarArgv: () => iapUntarArgv,
33561
+ identityProbeCommand: () => identityProbeCommand,
33238
33562
  imageForRuntime: () => imageForRuntime,
33239
33563
  installCommandFor: () => installCommandFor,
33240
33564
  installLocalMigrationTooling: () => installLocalMigrationTooling,
@@ -33251,6 +33575,7 @@ __export(dist_exports, {
33251
33575
  manualDeleteCommand: () => manualDeleteCommand,
33252
33576
  migrationUnits: () => migrationUnits,
33253
33577
  parseAlembicRevision: () => parseAlembicRevision,
33578
+ parseIdentityProbeOutput: () => parseIdentityProbeOutput,
33254
33579
  parsePlacementKind: () => parsePlacementKind,
33255
33580
  placementFor: () => placementFor,
33256
33581
  planDatabase: () => planDatabase,
@@ -33579,7 +33904,15 @@ var init_audit2 = __esm({
33579
33904
 
33580
33905
  // bin/jpi-claim.js
33581
33906
  init_src();
33582
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, mkdirSync as mkdirSync8, existsSync as existsSync13, rmSync as rmSync9, readdirSync as readdirSync3 } from "fs";
33907
+ import {
33908
+ readFileSync as readFileSync13,
33909
+ writeFileSync as writeFileSync12,
33910
+ mkdirSync as mkdirSync8,
33911
+ existsSync as existsSync13,
33912
+ lstatSync as lstatSync3,
33913
+ rmSync as rmSync9,
33914
+ readdirSync as readdirSync3
33915
+ } from "fs";
33583
33916
  import { join as join25, dirname as dirname8, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
33584
33917
  import { homedir as homedir12, hostname as osHostname } from "os";
33585
33918
  import { execFile as execFile3, execFileSync as execFileSync2 } from "child_process";
@@ -36284,6 +36617,59 @@ function writeSliceFiles(destDir, files) {
36284
36617
  }
36285
36618
  return { written, unavailable };
36286
36619
  }
36620
+ var BRIEF_DIR = ".terminalhire";
36621
+ var BRIEF_REL_PATH = `${BRIEF_DIR}/BRIEF.md`;
36622
+ var BRIEF_EXCLUDE_LINE = `/${BRIEF_DIR}/`;
36623
+ function writeDeliveredBrief(destDir, spec) {
36624
+ if (typeof spec !== "string" || spec.trim() === "") {
36625
+ return { written: false, reason: "the server sent no brief for this posting" };
36626
+ }
36627
+ const abs = join25(destDir, BRIEF_REL_PATH);
36628
+ const briefDir = join25(destDir, BRIEF_DIR);
36629
+ let occupant = null;
36630
+ try {
36631
+ occupant = lstatSync3(briefDir);
36632
+ } catch {
36633
+ }
36634
+ if (occupant) {
36635
+ return {
36636
+ written: false,
36637
+ reason: `${BRIEF_DIR}/ already exists in the delivered tree, and excluding it would hide that content from your patch`
36638
+ };
36639
+ }
36640
+ const excludeFile = join25(destDir, ".git", "info", "exclude");
36641
+ try {
36642
+ const existing = existsSync13(excludeFile) ? readFileSync13(excludeFile, "utf8") : "";
36643
+ if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
36644
+ mkdirSync8(dirname8(excludeFile), { recursive: true });
36645
+ writeFileSync12(
36646
+ excludeFile,
36647
+ `${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
36648
+ `,
36649
+ "utf8"
36650
+ );
36651
+ }
36652
+ } catch (err) {
36653
+ return { written: false, reason: `the git exclude could not be written (${err.message})` };
36654
+ }
36655
+ try {
36656
+ mkdirSync8(dirname8(abs), { recursive: true });
36657
+ writeFileSync12(abs, spec, { encoding: "utf8", flag: "wx" });
36658
+ } catch (err) {
36659
+ return { written: false, reason: `the brief could not be written (${err.message})` };
36660
+ }
36661
+ return { written: true, reason: null };
36662
+ }
36663
+ function printDeliveredBrief(result) {
36664
+ if (!result) return;
36665
+ if (result.written) {
36666
+ console.log(
36667
+ ` brief: ${BRIEF_REL_PATH} \u2014 the founder's write-up, excluded from your commits`
36668
+ );
36669
+ } else {
36670
+ console.log(` brief: not delivered \u2014 ${result.reason}`);
36671
+ }
36672
+ }
36287
36673
  function buildPatchSubmission({ bountyId, claimId, patch, authorName, authorEmail, auth }) {
36288
36674
  if (auth && "pushToken" in auth) {
36289
36675
  throw new Error(
@@ -36599,6 +36985,7 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
36599
36985
  );
36600
36986
  process.exit(1);
36601
36987
  }
36988
+ const brief = writeDeliveredBrief(dest, body.spec);
36602
36989
  const working = claims.updateClaim(claim.id, { worktreePath: dest, branch, state: "working" });
36603
36990
  console.log(`
36604
36991
  \u2713 Repository received: ${mintedRepoFullName} at ${baseSha.slice(0, 12)}`);
@@ -36614,6 +37001,7 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
36614
37001
  console.log(`
36615
37002
  worktree: ${dest}`);
36616
37003
  console.log(` branch: ${branch}`);
37004
+ printDeliveredBrief(brief);
36617
37005
  console.log(
36618
37006
  `
36619
37007
  Author your change there (commit as you go), then: terminalhire claim submit ${claim.id}`
@@ -36744,8 +37132,10 @@ async function cmdSlice(id, flags = {}) {
36744
37132
  for (const l of String(body.spec).split("\n")) console.log(` ${l}`);
36745
37133
  }
36746
37134
  const branch = `claim/${String(claim.id).replace(/[^A-Za-z0-9._-]/g, "-")}`;
37135
+ let brief;
36747
37136
  try {
36748
37137
  await sh("git", ["-C", dest, "init"]);
37138
+ brief = writeDeliveredBrief(dest, body.spec);
36749
37139
  await sh("git", ["-C", dest, "add", "-A"]);
36750
37140
  await sh("git", [
36751
37141
  "-C",
@@ -36771,6 +37161,7 @@ async function cmdSlice(id, flags = {}) {
36771
37161
  console.log(`
36772
37162
  worktree: ${dest}`);
36773
37163
  console.log(` branch: ${branch}`);
37164
+ printDeliveredBrief(brief);
36774
37165
  console.log(
36775
37166
  `
36776
37167
  Author your change there (commit as you go), then: terminalhire claim submit ${claim.id}`
@@ -38042,6 +38433,8 @@ async function run() {
38042
38433
  }
38043
38434
  export {
38044
38435
  AI_DISCLOSURE_NOTE,
38436
+ BRIEF_DIR,
38437
+ BRIEF_REL_PATH,
38045
38438
  CLAIM_CONSENT_VERSION,
38046
38439
  PUSH_TOKEN_REFUSAL,
38047
38440
  REVISE_RECOVERY_STATES,
@@ -38111,6 +38504,7 @@ export {
38111
38504
  terminalSafeLines,
38112
38505
  watchRunsLoop,
38113
38506
  workDirFor,
38507
+ writeDeliveredBrief,
38114
38508
  writeSliceFiles
38115
38509
  };
38116
38510
  /*! Bundled license information: