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.
- package/dist/bin/jpi-claim.js +423 -29
- package/dist/bin/jpi-dispatch.js +424 -30
- package/dist/bin/jpi-mcp.js +424 -30
- package/dist/bin/jpi-run.js +353 -28
- package/package.json +1 -1
package/dist/bin/jpi-claim.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
29145
|
-
//
|
|
29146
|
-
//
|
|
29147
|
-
|
|
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)"],
|
|
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 =
|
|
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
|
-
|
|
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
|
|
29960
|
+
let identity = null;
|
|
29712
29961
|
while (io.now() < deadline) {
|
|
29713
|
-
const probe = io.exec("gcloud", iapSshArgv(vm, project, zone,
|
|
29714
|
-
if (probe.ok
|
|
29715
|
-
|
|
29716
|
-
|
|
29717
|
-
|
|
29718
|
-
|
|
29719
|
-
|
|
29720
|
-
|
|
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 (
|
|
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 {
|
|
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 {
|
|
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:
|