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