terminalhire 0.42.20 → 0.42.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +854 -123
- package/dist/bin/jpi-contribute.js +6 -1
- package/dist/bin/jpi-dispatch.js +891 -160
- 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 +863 -132
- package/dist/bin/jpi-refresh.js +1 -1
- package/dist/bin/jpi-run.js +826 -100
- 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
|
},
|
|
@@ -34296,13 +34322,192 @@ var init_gceIdentity = __esm({
|
|
|
34296
34322
|
|
|
34297
34323
|
// ../../packages/attest/dist/confidentialSpace.js
|
|
34298
34324
|
import { createHash as createHash8, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
|
|
34299
|
-
|
|
34325
|
+
function refuse2(reason, detail) {
|
|
34326
|
+
return { ok: false, reason, detail };
|
|
34327
|
+
}
|
|
34328
|
+
function isPlainObject(v) {
|
|
34329
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34330
|
+
}
|
|
34331
|
+
function assertExpectationsComplete2(expectations) {
|
|
34332
|
+
const required2 = [
|
|
34333
|
+
["audience", expectations.audience],
|
|
34334
|
+
["nonce", expectations.nonce],
|
|
34335
|
+
["instanceName", expectations.instanceName],
|
|
34336
|
+
["projectId", expectations.projectId],
|
|
34337
|
+
...(expectations.additionalNonces ?? []).map((n) => [
|
|
34338
|
+
"additionalNonces[]",
|
|
34339
|
+
n
|
|
34340
|
+
]),
|
|
34341
|
+
...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
|
|
34342
|
+
["containerEnv name", name],
|
|
34343
|
+
[`containerEnv.${name}`, value]
|
|
34344
|
+
])
|
|
34345
|
+
];
|
|
34346
|
+
for (const [name, value] of required2) {
|
|
34347
|
+
if (value === "") {
|
|
34348
|
+
throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
|
|
34349
|
+
}
|
|
34350
|
+
}
|
|
34351
|
+
}
|
|
34352
|
+
function nonceList(v) {
|
|
34353
|
+
if (typeof v === "string")
|
|
34354
|
+
return [v];
|
|
34355
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
34356
|
+
return null;
|
|
34357
|
+
if (!v.every((n) => typeof n === "string"))
|
|
34358
|
+
return null;
|
|
34359
|
+
return v;
|
|
34360
|
+
}
|
|
34361
|
+
async function verifyConfidentialSpaceToken(token, expectations, deps) {
|
|
34362
|
+
assertExpectationsComplete2(expectations);
|
|
34363
|
+
if (!Number.isFinite(deps.now)) {
|
|
34364
|
+
throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
|
|
34365
|
+
}
|
|
34366
|
+
if (expectations.allowedImageDigests.length === 0) {
|
|
34367
|
+
return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
|
|
34368
|
+
}
|
|
34369
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
34370
|
+
return refuse2("token-malformed", "longer than any attestation token");
|
|
34371
|
+
}
|
|
34372
|
+
const parts = token.split(".");
|
|
34373
|
+
if (parts.length !== 3) {
|
|
34374
|
+
return refuse2("token-malformed", "not a three-segment compact JWT");
|
|
34375
|
+
}
|
|
34376
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
34377
|
+
const header = decodeJsonSegment(headerB64);
|
|
34378
|
+
if (header === null) {
|
|
34379
|
+
return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
|
|
34380
|
+
}
|
|
34381
|
+
if (header.alg !== "RS256") {
|
|
34382
|
+
return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
|
|
34383
|
+
}
|
|
34384
|
+
if (typeof header.kid !== "string" || header.kid === "") {
|
|
34385
|
+
return refuse2("unknown-key", "token header names no key id");
|
|
34386
|
+
}
|
|
34387
|
+
const jwks = await deps.fetchJwks();
|
|
34388
|
+
const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
|
|
34389
|
+
if (jwk === void 0) {
|
|
34390
|
+
return refuse2("unknown-key", "token key id is not in the published key set");
|
|
34391
|
+
}
|
|
34392
|
+
const signature = strictBase64UrlDecode(signatureB64);
|
|
34393
|
+
if (signature === null) {
|
|
34394
|
+
return refuse2("token-malformed", "signature segment is not base64url");
|
|
34395
|
+
}
|
|
34396
|
+
let signatureValid = false;
|
|
34397
|
+
try {
|
|
34398
|
+
const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
|
|
34399
|
+
signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
34400
|
+
} catch {
|
|
34401
|
+
signatureValid = false;
|
|
34402
|
+
}
|
|
34403
|
+
if (!signatureValid) {
|
|
34404
|
+
return refuse2("signature-invalid", "signature does not verify under the named key");
|
|
34405
|
+
}
|
|
34406
|
+
const payload = decodeJsonSegment(payloadB64);
|
|
34407
|
+
if (payload === null) {
|
|
34408
|
+
return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
|
|
34409
|
+
}
|
|
34410
|
+
if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
|
|
34411
|
+
return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
|
|
34412
|
+
}
|
|
34413
|
+
if (payload.aud !== expectations.audience) {
|
|
34414
|
+
return refuse2("wrong-audience", "audience is not the one this run expects");
|
|
34415
|
+
}
|
|
34416
|
+
const nowSeconds = deps.now / 1e3;
|
|
34417
|
+
const exp = payload.exp;
|
|
34418
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
|
|
34419
|
+
return refuse2("expired", "exp is missing, malformed, or in the past");
|
|
34420
|
+
}
|
|
34421
|
+
const iat = payload.iat;
|
|
34422
|
+
if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
|
|
34423
|
+
return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
|
|
34424
|
+
}
|
|
34425
|
+
const nonces = nonceList(payload.eat_nonce);
|
|
34426
|
+
if (nonces === null) {
|
|
34427
|
+
return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
|
|
34428
|
+
}
|
|
34429
|
+
if (!nonces.includes(expectations.nonce)) {
|
|
34430
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
|
|
34431
|
+
}
|
|
34432
|
+
for (const extra of expectations.additionalNonces ?? []) {
|
|
34433
|
+
if (!nonces.includes(extra)) {
|
|
34434
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
|
|
34435
|
+
}
|
|
34436
|
+
}
|
|
34437
|
+
if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
|
|
34438
|
+
return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
|
|
34439
|
+
}
|
|
34440
|
+
if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
|
|
34441
|
+
return refuse2("debug-enabled", "the image is not a production image with debug disabled");
|
|
34442
|
+
}
|
|
34443
|
+
if (payload.secboot !== true) {
|
|
34444
|
+
return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
|
|
34445
|
+
}
|
|
34446
|
+
if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
|
|
34447
|
+
return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
|
|
34448
|
+
}
|
|
34449
|
+
const hwmodel = payload.hwmodel;
|
|
34450
|
+
const submods = payload.submods;
|
|
34451
|
+
const gce = isPlainObject(submods) ? submods.gce : void 0;
|
|
34452
|
+
if (!isPlainObject(gce)) {
|
|
34453
|
+
return refuse2("submods-gce-missing", "no submods.gce block names the instance");
|
|
34454
|
+
}
|
|
34455
|
+
if (gce.project_id !== expectations.projectId) {
|
|
34456
|
+
return refuse2("project-mismatch", "token names an instance in a different project");
|
|
34457
|
+
}
|
|
34458
|
+
if (gce.instance_name !== expectations.instanceName) {
|
|
34459
|
+
return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
|
|
34460
|
+
}
|
|
34461
|
+
if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
|
|
34462
|
+
return refuse2("instance-id-missing", "instance id is absent or not a string");
|
|
34463
|
+
}
|
|
34464
|
+
const instanceId = gce.instance_id;
|
|
34465
|
+
if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
|
|
34466
|
+
return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
|
|
34467
|
+
}
|
|
34468
|
+
const zone = gce.zone;
|
|
34469
|
+
const container = isPlainObject(submods) ? submods.container : void 0;
|
|
34470
|
+
if (!isPlainObject(container)) {
|
|
34471
|
+
return refuse2("submods-container-missing", "no submods.container block names the workload");
|
|
34472
|
+
}
|
|
34473
|
+
if (typeof container.image_reference !== "string" || container.image_reference === "") {
|
|
34474
|
+
return refuse2("image-reference-missing", "the workload image reference is absent");
|
|
34475
|
+
}
|
|
34476
|
+
const imageReference = container.image_reference;
|
|
34477
|
+
if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
|
|
34478
|
+
return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
|
|
34479
|
+
}
|
|
34480
|
+
const env = container.env;
|
|
34481
|
+
for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
|
|
34482
|
+
if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
|
|
34483
|
+
return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
|
|
34484
|
+
}
|
|
34485
|
+
}
|
|
34486
|
+
return {
|
|
34487
|
+
ok: true,
|
|
34488
|
+
value: {
|
|
34489
|
+
instanceId,
|
|
34490
|
+
instanceName: expectations.instanceName,
|
|
34491
|
+
zone,
|
|
34492
|
+
projectId: expectations.projectId,
|
|
34493
|
+
imageDigest: container.image_digest,
|
|
34494
|
+
imageReference,
|
|
34495
|
+
hwmodel,
|
|
34496
|
+
iat,
|
|
34497
|
+
exp,
|
|
34498
|
+
tokenSha256: createHash8("sha256").update(token, "utf8").digest("hex")
|
|
34499
|
+
}
|
|
34500
|
+
};
|
|
34501
|
+
}
|
|
34502
|
+
var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
|
|
34300
34503
|
var init_confidentialSpace = __esm({
|
|
34301
34504
|
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
34302
34505
|
"use strict";
|
|
34303
34506
|
init_gceIdentity();
|
|
34304
34507
|
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
34305
34508
|
CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
|
|
34509
|
+
CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
|
|
34510
|
+
CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
|
|
34306
34511
|
}
|
|
34307
34512
|
});
|
|
34308
34513
|
|
|
@@ -34645,50 +34850,50 @@ function toTestRunResult(result, outputSha256) {
|
|
|
34645
34850
|
function toAcceptancePredicate(pair, opts = {}) {
|
|
34646
34851
|
const { baseline, patched } = pair;
|
|
34647
34852
|
if (baseline.runId === patched.runId) {
|
|
34648
|
-
return
|
|
34853
|
+
return refuse3("pair-is-the-same-run", `both sides are run ${patched.runId}. A baseline and a patched result that are the same execution prove nothing about the patch, and every field of the pair would be equal by construction.`);
|
|
34649
34854
|
}
|
|
34650
34855
|
if (patched.status === "refused") {
|
|
34651
|
-
return
|
|
34856
|
+
return refuse3("run-was-refused", `the patched run was refused before executing (${patched.boundaryRefusals.length} boundary refusal(s)), so there is no result to attest`);
|
|
34652
34857
|
}
|
|
34653
34858
|
if (patched.outcome === null) {
|
|
34654
|
-
return
|
|
34859
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
34655
34860
|
}
|
|
34656
34861
|
if (baseline.status === "refused") {
|
|
34657
|
-
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`);
|
|
34658
34863
|
}
|
|
34659
34864
|
if (baseline.outcome === null) {
|
|
34660
|
-
return
|
|
34865
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
34661
34866
|
}
|
|
34662
34867
|
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
34663
|
-
return
|
|
34868
|
+
return refuse3("baseline-environment-failed", `the baseline ended ${baseline.outcome}, which is not an observed verdict about the unpatched tree. Signing it would let a baseline we could not read stand in for one that failed, and a broken baseline flatters every patch.`);
|
|
34664
34869
|
}
|
|
34665
34870
|
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
34666
34871
|
if (budget === null) {
|
|
34667
|
-
return
|
|
34872
|
+
return refuse3("our-environment-failed", `the patched run ended ${patched.outcome}: the test command could not be invoked, which is our environment failing and not a verdict about the work. No statement is emitted, because every possible value would be a claim we cannot support.`);
|
|
34668
34873
|
}
|
|
34669
34874
|
if (patched.testCommand === null) {
|
|
34670
|
-
return
|
|
34875
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
34671
34876
|
}
|
|
34672
34877
|
if (baseline.testCommand !== patched.testCommand) {
|
|
34673
|
-
return
|
|
34878
|
+
return refuse3("pair-disagrees-on-harness", `baseline ran ${JSON.stringify(baseline.testCommand)} and patched ran ${JSON.stringify(patched.testCommand)}. Opposite verdicts from different harnesses say nothing about the patch.`);
|
|
34674
34879
|
}
|
|
34675
34880
|
if (baseline.targetSha !== patched.targetSha) {
|
|
34676
|
-
return
|
|
34881
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
34677
34882
|
}
|
|
34678
34883
|
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
34679
|
-
return
|
|
34884
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
34680
34885
|
}
|
|
34681
34886
|
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
34682
|
-
return
|
|
34887
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
34683
34888
|
}
|
|
34684
34889
|
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
34685
|
-
return
|
|
34890
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
34686
34891
|
}
|
|
34687
34892
|
if (baseline.treeDigest === patched.treeDigest) {
|
|
34688
|
-
return
|
|
34893
|
+
return refuse3("pair-has-identical-tree", `both sides are tree ${String(patched.treeDigest).slice(0, 12)}\u2026, so whatever the two runs disagree about, it is not the patch`);
|
|
34689
34894
|
}
|
|
34690
34895
|
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
34691
|
-
return
|
|
34896
|
+
return refuse3("patch-binding-incomplete", `a baseline patch shaped the ${patched.baselinePatchSha256 !== null ? "patched" : "baseline"} tree, so it cannot be reproduced from (base_commit_oid, patch_sha256) alone. Fold it into the binding before signing; do not sign a triple a verifier cannot follow.`);
|
|
34692
34897
|
}
|
|
34693
34898
|
for (const [side, outcome, counts, exitCode] of [
|
|
34694
34899
|
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
@@ -34696,7 +34901,7 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
34696
34901
|
]) {
|
|
34697
34902
|
const why = contradicts(outcome, counts, exitCode);
|
|
34698
34903
|
if (why !== null) {
|
|
34699
|
-
return
|
|
34904
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
34700
34905
|
}
|
|
34701
34906
|
}
|
|
34702
34907
|
for (const [side, source] of [
|
|
@@ -34705,44 +34910,44 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
34705
34910
|
]) {
|
|
34706
34911
|
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
34707
34912
|
if (why !== null)
|
|
34708
|
-
return
|
|
34913
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
34709
34914
|
}
|
|
34710
34915
|
if (patched.testCommandSource === "developer-declared") {
|
|
34711
|
-
return
|
|
34916
|
+
return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
|
|
34712
34917
|
}
|
|
34713
34918
|
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
34714
|
-
return
|
|
34919
|
+
return refuse3("test-command-origin-unattested", `the pair disagrees on where the command came from: baseline ${baseline.testCommandSource}, patched ${patched.testCommandSource}. One signed value cannot describe both.`);
|
|
34715
34920
|
}
|
|
34716
34921
|
if (baseline.containerImage !== patched.containerImage) {
|
|
34717
|
-
return
|
|
34922
|
+
return refuse3("pair-disagrees-on-image", `baseline ran in ${String(baseline.containerImage)} and patched in ${String(patched.containerImage)}. The signed measurement names one image, so a pair from two environments would attribute to the patch whatever the image changed.`);
|
|
34718
34923
|
}
|
|
34719
34924
|
if (patched.containerImage === null || patched.containerImage === "") {
|
|
34720
|
-
return
|
|
34925
|
+
return refuse3("missing-container-image", "no container image was recorded, so the measurement would name a stand-in rather than the environment the suite actually ran in");
|
|
34721
34926
|
}
|
|
34722
34927
|
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
34723
|
-
return
|
|
34928
|
+
return refuse3("missing-image-digest", "no image digest was recorded for the patched half, so the measurement would name the mutable tag rather than the bytes the suite actually ran in. A run without the digest is recordable but not attestable \u2014 the `missing-patch-digest` split.");
|
|
34724
34929
|
}
|
|
34725
34930
|
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
34726
|
-
return
|
|
34931
|
+
return refuse3("malformed-image-digest", `the patched half carries ${JSON.stringify(patched.containerImageDigest).slice(0, 120)} where a RepoDigest (\`repo@sha256:<64 hex>\`) belongs. A tag or a stand-in string here would be signed as if it were content-addressed, which is the lie the digest axis exists to refuse.`);
|
|
34727
34932
|
}
|
|
34728
34933
|
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
34729
|
-
return
|
|
34934
|
+
return refuse3("image-digest-repo-mismatch", `the patched half's digest names repository ${imageRepo(patched.containerImageDigest)} but the pair ran ${patched.containerImage}. An image can carry digests for several repositories; signing one the pair did not name would attribute these bytes to a different name.`);
|
|
34730
34935
|
}
|
|
34731
34936
|
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
34732
|
-
return
|
|
34937
|
+
return refuse3("missing-image-digest", "no image digest was recorded for the baseline half, so the measurement would name the mutable tag rather than the bytes the suite actually ran in. A run without the digest is recordable but not attestable \u2014 the `missing-patch-digest` split.");
|
|
34733
34938
|
}
|
|
34734
34939
|
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
34735
|
-
return
|
|
34940
|
+
return refuse3("malformed-image-digest", `the baseline half carries ${JSON.stringify(baseline.containerImageDigest).slice(0, 120)} where a RepoDigest (\`repo@sha256:<64 hex>\`) belongs. A tag or a stand-in string here would be signed as if it were content-addressed, which is the lie the digest axis exists to refuse.`);
|
|
34736
34941
|
}
|
|
34737
34942
|
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
34738
|
-
return
|
|
34943
|
+
return refuse3("image-digest-repo-mismatch", `the baseline half's digest names repository ${imageRepo(baseline.containerImageDigest)} but the pair ran ${patched.containerImage}. An image can carry digests for several repositories; signing one the pair did not name would attribute these bytes to a different name.`);
|
|
34739
34944
|
}
|
|
34740
34945
|
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
34741
|
-
return
|
|
34946
|
+
return refuse3("pair-disagrees-on-image-digest", `baseline ran image digest ${String(baseline.containerImageDigest)} and patched ${String(patched.containerImageDigest)} under one tag. The tag agreeing is the trap: a re-pointed tag is two environments wearing one name.`);
|
|
34742
34947
|
}
|
|
34743
34948
|
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
34744
34949
|
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
34745
|
-
return
|
|
34950
|
+
return refuse3("image-digest-contradicts-image", `the pair ran ${patched.containerImage}, whose reference pins digest ${claimedDigest}, but the digest field says ${String(imageDigestOf(patched.containerImageDigest))}. A measurement must not sign one digest while the record names another.`);
|
|
34746
34951
|
}
|
|
34747
34952
|
return {
|
|
34748
34953
|
ok: true,
|
|
@@ -34769,7 +34974,7 @@ function signRunStatement(predicate, privateKey, keyid) {
|
|
|
34769
34974
|
const statement = createAcceptanceStatement(predicate);
|
|
34770
34975
|
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
34771
34976
|
}
|
|
34772
|
-
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS,
|
|
34977
|
+
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS, refuse3, LOCAL_MEASUREMENT_PREFIX, REFERENCE_DOMAIN_COMPONENT, REFERENCE_DOMAIN_NAME, REFERENCE_IPV6, REFERENCE_DOMAIN, REFERENCE_PATH_COMPONENT, REPO_DIGEST_RE;
|
|
34773
34978
|
var init_attestation2 = __esm({
|
|
34774
34979
|
"../../packages/envrun/dist/attestation.js"() {
|
|
34775
34980
|
"use strict";
|
|
@@ -34873,7 +35078,7 @@ var init_attestation2 = __esm({
|
|
|
34873
35078
|
"outcome-contradicts-counts",
|
|
34874
35079
|
"test-command-origin-unattested"
|
|
34875
35080
|
];
|
|
34876
|
-
|
|
35081
|
+
refuse3 = (reason, detail) => ({
|
|
34877
35082
|
ok: false,
|
|
34878
35083
|
reason,
|
|
34879
35084
|
detail
|
|
@@ -35284,6 +35489,22 @@ function bootImageFlags(p) {
|
|
|
35284
35489
|
}
|
|
35285
35490
|
const workloadImage = p.workloadImage;
|
|
35286
35491
|
assertWorkloadImage("gcpBootArgv", workloadImage);
|
|
35492
|
+
const clientCert = p.clientCert;
|
|
35493
|
+
if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
|
|
35494
|
+
throw new GcpPlacementError(`gcpBootArgv: the client certificate is missing or is not one base64url value (${String(CLIENT_CERT_BASE64URL)}). It lands inside the --metadata value, so anything that could start a second key is refused.`);
|
|
35495
|
+
}
|
|
35496
|
+
if (p.runId.length < MIN_NONCE_BYTES) {
|
|
35497
|
+
throw new GcpPlacementError(`gcpBootArgv: a Confidential Space run id must be at least ${String(MIN_NONCE_BYTES)} characters, because it is the attestation token's nonce and the launcher refuses a shorter one. Refused here, before a VM is billed for a token it cannot mint.`);
|
|
35498
|
+
}
|
|
35499
|
+
const metadata = [
|
|
35500
|
+
`tee-image-reference=${workloadImage}`,
|
|
35501
|
+
"tee-container-log-redirect=true",
|
|
35502
|
+
'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
|
|
35503
|
+
"tee-cgroup-ns=true",
|
|
35504
|
+
`tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
|
|
35505
|
+
`tee-env-TH_NONCE=${p.runId}`,
|
|
35506
|
+
`tee-env-TH_CLIENT_CERT=${clientCert}`
|
|
35507
|
+
];
|
|
35287
35508
|
return [
|
|
35288
35509
|
// The production family. The debug family reports `dbgstat: enabled`, which the
|
|
35289
35510
|
// intake's verifier refuses, so a debug boot could never produce a run we record.
|
|
@@ -35296,7 +35517,15 @@ function bootImageFlags(p) {
|
|
|
35296
35517
|
// most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
|
|
35297
35518
|
// combination is phase 5's measurement.
|
|
35298
35519
|
"--maintenance-policy=TERMINATE",
|
|
35299
|
-
`--metadata
|
|
35520
|
+
`--metadata=^;^${metadata.join(";")}`,
|
|
35521
|
+
// What the standing firewall rule for IAP's range matches, so the tunnels to the
|
|
35522
|
+
// workload's dockerd and attest ports open on these VMs and no others.
|
|
35523
|
+
"--tags=th-cs-venue",
|
|
35524
|
+
// The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
|
|
35525
|
+
// runs vfs, which copies every layer and every container's rootfs in full. Pulling
|
|
35526
|
+
// node:20-bookworm left 992 MB, and the next container create failed ENOSPC
|
|
35527
|
+
// (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
|
|
35528
|
+
"--boot-disk-size=50GB"
|
|
35300
35529
|
];
|
|
35301
35530
|
}
|
|
35302
35531
|
function assertInstanceIdentity(fn, id) {
|
|
@@ -35370,7 +35599,13 @@ function gcpBootArgv(p) {
|
|
|
35370
35599
|
// expectation uses (TERM-832): two inline spellings of one value is how
|
|
35371
35600
|
// the boot path and the verifier drift apart silently.
|
|
35372
35601
|
`--service-account=${venueIdentityEmail(p.project)}`,
|
|
35373
|
-
|
|
35602
|
+
// A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
|
|
35603
|
+
// pulls the workload image and mints the attestation token with this account, and
|
|
35604
|
+
// under `--no-scopes` it can do neither. What bounds a lifted token there is the
|
|
35605
|
+
// account's two grants — `confidentialcomputing.workloadUser` and a reader on the
|
|
35606
|
+
// one venue image repository — plus point 2 above, which is unchanged: the fence
|
|
35607
|
+
// still denies untrusted code the metadata server.
|
|
35608
|
+
...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
|
|
35374
35609
|
"--shielded-secure-boot",
|
|
35375
35610
|
"--shielded-vtpm",
|
|
35376
35611
|
"--shielded-integrity-monitoring",
|
|
@@ -35403,7 +35638,7 @@ function gcpDeleteArgv(p) {
|
|
|
35403
35638
|
"--quiet"
|
|
35404
35639
|
];
|
|
35405
35640
|
}
|
|
35406
|
-
var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, CONFIDENTIAL_SPACE_MACHINE_TYPE, WORKLOAD_IMAGE_REFERENCE, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
|
|
35641
|
+
var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, CONFIDENTIAL_SPACE_MACHINE_TYPE, WORKLOAD_IMAGE_REFERENCE, CLIENT_CERT_BASE64URL, MIN_NONCE_BYTES, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
|
|
35407
35642
|
var init_gcpPlacement = __esm({
|
|
35408
35643
|
"../../packages/envrun/dist/gcpPlacement.js"() {
|
|
35409
35644
|
"use strict";
|
|
@@ -35422,6 +35657,8 @@ var init_gcpPlacement = __esm({
|
|
|
35422
35657
|
GCP_RUN_LABEL_KEY = "th-run";
|
|
35423
35658
|
CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
|
|
35424
35659
|
WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
|
|
35660
|
+
CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
|
|
35661
|
+
MIN_NONCE_BYTES = 10;
|
|
35425
35662
|
GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
|
|
35426
35663
|
GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
|
35427
35664
|
GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -35463,7 +35700,10 @@ var init_emptyGitConfig = __esm({
|
|
|
35463
35700
|
|
|
35464
35701
|
// ../../packages/envrun/dist/hostedVenue.js
|
|
35465
35702
|
import { spawn as spawn6, spawnSync as spawnSync6 } from "child_process";
|
|
35466
|
-
import {
|
|
35703
|
+
import { createHash as createHash10, X509Certificate } from "crypto";
|
|
35704
|
+
import { chmodSync as chmodSync3, existsSync as existsSync18, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, rmSync as rmSync10, writeFileSync as writeFileSync22 } from "fs";
|
|
35705
|
+
import { request as httpsRequest } from "https";
|
|
35706
|
+
import { createServer } from "net";
|
|
35467
35707
|
import { join as join40 } from "path";
|
|
35468
35708
|
import { tmpdir as tmpdir3 } from "os";
|
|
35469
35709
|
function credentialInGitConfig(text) {
|
|
@@ -35665,6 +35905,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
|
|
|
35665
35905
|
});
|
|
35666
35906
|
});
|
|
35667
35907
|
}
|
|
35908
|
+
function socketErrorReason(err) {
|
|
35909
|
+
if (err.message !== "")
|
|
35910
|
+
return err.message;
|
|
35911
|
+
const code = err.code;
|
|
35912
|
+
if (code !== void 0)
|
|
35913
|
+
return code;
|
|
35914
|
+
const inner = err.errors;
|
|
35915
|
+
if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
|
|
35916
|
+
return inner[0].message;
|
|
35917
|
+
}
|
|
35918
|
+
return err.name;
|
|
35919
|
+
}
|
|
35920
|
+
function fetchAttestationOverTls(req) {
|
|
35921
|
+
return new Promise((resolve7) => {
|
|
35922
|
+
let whole;
|
|
35923
|
+
const settle = (reading) => {
|
|
35924
|
+
if (whole !== void 0)
|
|
35925
|
+
clearTimeout(whole);
|
|
35926
|
+
resolve7(reading);
|
|
35927
|
+
};
|
|
35928
|
+
let cert;
|
|
35929
|
+
let key;
|
|
35930
|
+
try {
|
|
35931
|
+
cert = readFileSync25(req.certPath);
|
|
35932
|
+
key = readFileSync25(req.keyPath);
|
|
35933
|
+
} catch (err) {
|
|
35934
|
+
settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
|
|
35935
|
+
return;
|
|
35936
|
+
}
|
|
35937
|
+
const call = httpsRequest({
|
|
35938
|
+
host: "localhost",
|
|
35939
|
+
port: req.port,
|
|
35940
|
+
path: "/attest",
|
|
35941
|
+
method: "GET",
|
|
35942
|
+
servername: "localhost",
|
|
35943
|
+
cert,
|
|
35944
|
+
key,
|
|
35945
|
+
rejectUnauthorized: false,
|
|
35946
|
+
agent: false,
|
|
35947
|
+
timeout: req.timeoutMs
|
|
35948
|
+
}, (res) => {
|
|
35949
|
+
const peer = res.socket.getPeerCertificate();
|
|
35950
|
+
let body = "";
|
|
35951
|
+
res.setEncoding("utf8");
|
|
35952
|
+
res.on("data", (chunk) => {
|
|
35953
|
+
body += chunk;
|
|
35954
|
+
if (body.length > MAX_TOKEN_LENGTH) {
|
|
35955
|
+
settle({ ok: false, reason: "the answer is longer than any attestation token" });
|
|
35956
|
+
res.destroy();
|
|
35957
|
+
}
|
|
35958
|
+
});
|
|
35959
|
+
res.on("end", () => {
|
|
35960
|
+
if (res.statusCode !== 200) {
|
|
35961
|
+
settle({
|
|
35962
|
+
ok: false,
|
|
35963
|
+
reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
|
|
35964
|
+
});
|
|
35965
|
+
return;
|
|
35966
|
+
}
|
|
35967
|
+
const raw = peer.raw;
|
|
35968
|
+
if (raw === void 0 || raw.length === 0) {
|
|
35969
|
+
settle({ ok: false, reason: "the attest endpoint presented no certificate" });
|
|
35970
|
+
return;
|
|
35971
|
+
}
|
|
35972
|
+
settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
|
|
35973
|
+
});
|
|
35974
|
+
res.on("error", (err) => {
|
|
35975
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
35976
|
+
});
|
|
35977
|
+
});
|
|
35978
|
+
whole = setTimeout(() => {
|
|
35979
|
+
settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
|
|
35980
|
+
call.destroy();
|
|
35981
|
+
}, req.timeoutMs);
|
|
35982
|
+
call.on("timeout", () => {
|
|
35983
|
+
call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
|
|
35984
|
+
});
|
|
35985
|
+
call.on("error", (err) => {
|
|
35986
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
35987
|
+
});
|
|
35988
|
+
call.end();
|
|
35989
|
+
});
|
|
35990
|
+
}
|
|
35668
35991
|
function quoteForRemoteShell(arg) {
|
|
35669
35992
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
35670
35993
|
}
|
|
@@ -35999,27 +36322,370 @@ ${res.stderr}`);
|
|
|
35999
36322
|
function manualDeleteCommand(vm, project, zone) {
|
|
36000
36323
|
return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
|
|
36001
36324
|
}
|
|
36325
|
+
function clientCertArgv(keyPath, certPath) {
|
|
36326
|
+
return [
|
|
36327
|
+
"req",
|
|
36328
|
+
"-x509",
|
|
36329
|
+
"-newkey",
|
|
36330
|
+
"ec",
|
|
36331
|
+
"-pkeyopt",
|
|
36332
|
+
"ec_paramgen_curve:P-256",
|
|
36333
|
+
"-nodes",
|
|
36334
|
+
"-keyout",
|
|
36335
|
+
keyPath,
|
|
36336
|
+
"-out",
|
|
36337
|
+
certPath,
|
|
36338
|
+
"-subj",
|
|
36339
|
+
"/CN=terminalhire-worker",
|
|
36340
|
+
"-days",
|
|
36341
|
+
"1",
|
|
36342
|
+
"-addext",
|
|
36343
|
+
"extendedKeyUsage=clientAuth"
|
|
36344
|
+
];
|
|
36345
|
+
}
|
|
36346
|
+
function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
|
|
36347
|
+
return [
|
|
36348
|
+
"compute",
|
|
36349
|
+
"start-iap-tunnel",
|
|
36350
|
+
vm,
|
|
36351
|
+
String(remotePort),
|
|
36352
|
+
`--local-host-port=localhost:${String(localPort)}`,
|
|
36353
|
+
`--project=${project}`,
|
|
36354
|
+
`--zone=${zone}`,
|
|
36355
|
+
"--iap-tunnel-disable-connection-check"
|
|
36356
|
+
];
|
|
36357
|
+
}
|
|
36358
|
+
function spkiSha256Hex(certDer) {
|
|
36359
|
+
const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
|
|
36360
|
+
return createHash10("sha256").update(spki).digest("hex");
|
|
36361
|
+
}
|
|
36362
|
+
function certMetadataValue(pem) {
|
|
36363
|
+
return new X509Certificate(pem).raw.toString("base64url");
|
|
36364
|
+
}
|
|
36365
|
+
function requireWorkloadDigest(reference) {
|
|
36366
|
+
const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
|
|
36367
|
+
if (named === null) {
|
|
36368
|
+
throw new GcpPlacementError("VENUE_WORKLOAD_IMAGE must name its image by digest (\u2026@sha256:<64 hex>) for a Confidential Space venue: the attestation is checked against that digest, and a tag names whatever was pushed last.");
|
|
36369
|
+
}
|
|
36370
|
+
return named[1];
|
|
36371
|
+
}
|
|
36372
|
+
function underStageRoot(path6) {
|
|
36373
|
+
if (!path6.startsWith(`${CS_STAGE_ROOT}/`)) {
|
|
36374
|
+
throw new Error(`${path6} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
|
|
36375
|
+
}
|
|
36376
|
+
return path6;
|
|
36377
|
+
}
|
|
36378
|
+
function userFlag(owner) {
|
|
36379
|
+
return `--user=${String(owner.uid)}:${String(owner.gid)}`;
|
|
36380
|
+
}
|
|
36381
|
+
function csStagePrepareArgv(stageBase, owner) {
|
|
36382
|
+
const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
|
|
36383
|
+
return [
|
|
36384
|
+
"run",
|
|
36385
|
+
"--rm",
|
|
36386
|
+
"--network=none",
|
|
36387
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
36388
|
+
"--",
|
|
36389
|
+
STAGE_HELPER_IMAGE,
|
|
36390
|
+
"sh",
|
|
36391
|
+
"-c",
|
|
36392
|
+
`mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
|
|
36393
|
+
];
|
|
36394
|
+
}
|
|
36395
|
+
function csUntarArgv(dir, owner) {
|
|
36396
|
+
return [
|
|
36397
|
+
"run",
|
|
36398
|
+
"-i",
|
|
36399
|
+
"--rm",
|
|
36400
|
+
"--network=none",
|
|
36401
|
+
userFlag(owner),
|
|
36402
|
+
`--volume=${underStageRoot(dir)}:/dst:rw`,
|
|
36403
|
+
"--",
|
|
36404
|
+
STAGE_HELPER_IMAGE,
|
|
36405
|
+
"tar",
|
|
36406
|
+
"-xf",
|
|
36407
|
+
"-",
|
|
36408
|
+
"-C",
|
|
36409
|
+
"/dst"
|
|
36410
|
+
];
|
|
36411
|
+
}
|
|
36412
|
+
function csUntarProxyArgv(dir, owner) {
|
|
36413
|
+
const d = quoteForRemoteShell(underStageRoot(dir));
|
|
36414
|
+
return [
|
|
36415
|
+
"run",
|
|
36416
|
+
"-i",
|
|
36417
|
+
"--rm",
|
|
36418
|
+
"--network=none",
|
|
36419
|
+
userFlag(owner),
|
|
36420
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
36421
|
+
"--",
|
|
36422
|
+
STAGE_HELPER_IMAGE,
|
|
36423
|
+
"sh",
|
|
36424
|
+
"-c",
|
|
36425
|
+
`mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
|
|
36426
|
+
];
|
|
36427
|
+
}
|
|
36428
|
+
function csRemoveArgv(dir, owner) {
|
|
36429
|
+
return [
|
|
36430
|
+
"run",
|
|
36431
|
+
"--rm",
|
|
36432
|
+
"--network=none",
|
|
36433
|
+
userFlag(owner),
|
|
36434
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
36435
|
+
"--",
|
|
36436
|
+
STAGE_HELPER_IMAGE,
|
|
36437
|
+
"rm",
|
|
36438
|
+
"-rf",
|
|
36439
|
+
"--",
|
|
36440
|
+
underStageRoot(dir)
|
|
36441
|
+
];
|
|
36442
|
+
}
|
|
36443
|
+
function sshStageTransport(vm, project, zone, env, io) {
|
|
36444
|
+
return {
|
|
36445
|
+
/**
|
|
36446
|
+
* `id -u` / `id -g` on the venue, as the account that untarred the tree.
|
|
36447
|
+
*
|
|
36448
|
+
* That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
|
|
36449
|
+
* so asking it who it is answers exactly the question the mount needs, without
|
|
36450
|
+
* a chown and without assuming a uid. One ssh round trip, once per run.
|
|
36451
|
+
*
|
|
36452
|
+
* Both ids come from ONE invocation. Two calls could straddle a reconnect and
|
|
36453
|
+
* pair a uid with another session's gid — a third wrong answer rather than a
|
|
36454
|
+
* half-right one, which is the reason `GuestUser` is a pair in the first place.
|
|
36455
|
+
*/
|
|
36456
|
+
readOwner: () => {
|
|
36457
|
+
const res = io.exec(
|
|
36458
|
+
"gcloud",
|
|
36459
|
+
iapSshArgv(vm, project, zone, "id -u && id -g"),
|
|
36460
|
+
OWNER_PROBE_TIMEOUT_MS,
|
|
36461
|
+
// `env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
36462
|
+
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
36463
|
+
// omitted this would ssh under the person's own credential — then report an owner
|
|
36464
|
+
// measured against a session the run does not use.
|
|
36465
|
+
env
|
|
36466
|
+
);
|
|
36467
|
+
if (!res.ok) {
|
|
36468
|
+
throw new HostedVenueError(`could not read the staged tree's owner on ${vm}: ${execDetail(res).slice(0, 300)}. Refusing rather than falling back to this machine\u2019s uid, which would hand the guest an id with no write access to its own clone.`);
|
|
36469
|
+
}
|
|
36470
|
+
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
36471
|
+
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
36472
|
+
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
36473
|
+
throw new HostedVenueError(`the venue ${vm} answered 'id -u && id -g' with ${JSON.stringify(res.stdout.slice(0, 120))}, which is not a uid/gid pair. Refusing rather than guessing an owner.`);
|
|
36474
|
+
}
|
|
36475
|
+
return { uid: ids2[0], gid: ids2[1] };
|
|
36476
|
+
},
|
|
36477
|
+
push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
|
|
36478
|
+
pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
|
|
36479
|
+
removeProxy: (dir) => {
|
|
36480
|
+
io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
|
|
36481
|
+
}
|
|
36482
|
+
};
|
|
36483
|
+
}
|
|
36484
|
+
function dockerStageTransport(docker3, env, io) {
|
|
36485
|
+
const pushVia = (from, args5) => {
|
|
36486
|
+
const [file, ...rest] = docker3.commandLine(args5);
|
|
36487
|
+
if (file === void 0)
|
|
36488
|
+
throw new Error("the docker client returned an empty command line");
|
|
36489
|
+
return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
|
|
36490
|
+
};
|
|
36491
|
+
return {
|
|
36492
|
+
// Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
|
|
36493
|
+
readOwner: () => CS_GUEST_USER,
|
|
36494
|
+
push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
|
|
36495
|
+
pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
|
|
36496
|
+
removeProxy: (dir) => {
|
|
36497
|
+
docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
|
|
36498
|
+
}
|
|
36499
|
+
};
|
|
36500
|
+
}
|
|
36501
|
+
function bothTunnels(a, b) {
|
|
36502
|
+
return {
|
|
36503
|
+
kill: (signal) => {
|
|
36504
|
+
let failed = false;
|
|
36505
|
+
let error2;
|
|
36506
|
+
for (const t of [a, b]) {
|
|
36507
|
+
try {
|
|
36508
|
+
t.kill(signal);
|
|
36509
|
+
} catch (err) {
|
|
36510
|
+
if (!failed)
|
|
36511
|
+
error2 = err;
|
|
36512
|
+
failed = true;
|
|
36513
|
+
}
|
|
36514
|
+
}
|
|
36515
|
+
if (failed)
|
|
36516
|
+
throw error2;
|
|
36517
|
+
},
|
|
36518
|
+
failure: () => a.failure() ?? b.failure()
|
|
36519
|
+
};
|
|
36520
|
+
}
|
|
36521
|
+
function makeClientTls(io, allocated, vm) {
|
|
36522
|
+
let dir;
|
|
36523
|
+
try {
|
|
36524
|
+
dir = io.makePrivateDir();
|
|
36525
|
+
} catch (err) {
|
|
36526
|
+
throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
|
|
36527
|
+
}
|
|
36528
|
+
allocated.onRollback(() => {
|
|
36529
|
+
try {
|
|
36530
|
+
io.removeTree(dir);
|
|
36531
|
+
} catch (err) {
|
|
36532
|
+
throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
|
|
36533
|
+
}
|
|
36534
|
+
});
|
|
36535
|
+
const certPath = join40(dir, "client.pem");
|
|
36536
|
+
const keyPath = join40(dir, "client-key.pem");
|
|
36537
|
+
const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
|
|
36538
|
+
if (!made.ok) {
|
|
36539
|
+
throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
|
|
36540
|
+
}
|
|
36541
|
+
let metadataValue;
|
|
36542
|
+
try {
|
|
36543
|
+
const pem = io.readTextIfPresent(certPath);
|
|
36544
|
+
if (pem === null)
|
|
36545
|
+
throw new Error(`openssl wrote nothing to ${certPath}`);
|
|
36546
|
+
metadataValue = certMetadataValue(pem);
|
|
36547
|
+
} catch (err) {
|
|
36548
|
+
throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
|
|
36549
|
+
}
|
|
36550
|
+
return { dir, certPath, keyPath, metadataValue };
|
|
36551
|
+
}
|
|
36552
|
+
async function reachConfidentialSpace(a) {
|
|
36553
|
+
const { io, vm } = a;
|
|
36554
|
+
let ports;
|
|
36555
|
+
try {
|
|
36556
|
+
ports = await io.freeLocalPorts(2);
|
|
36557
|
+
} catch (err) {
|
|
36558
|
+
throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
|
|
36559
|
+
}
|
|
36560
|
+
const [attestPort, dockerPort] = ports;
|
|
36561
|
+
if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
|
|
36562
|
+
throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
|
|
36563
|
+
}
|
|
36564
|
+
const forward = (remote, local) => {
|
|
36565
|
+
const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
|
|
36566
|
+
a.allocated.onRollback(() => {
|
|
36567
|
+
try {
|
|
36568
|
+
t.kill("SIGTERM");
|
|
36569
|
+
} catch {
|
|
36570
|
+
}
|
|
36571
|
+
});
|
|
36572
|
+
return t;
|
|
36573
|
+
};
|
|
36574
|
+
const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
|
|
36575
|
+
const deadline = io.now() + CS_READY_BUDGET_MS;
|
|
36576
|
+
let reading = { ok: false, reason: "no request was made" };
|
|
36577
|
+
while (io.now() < deadline) {
|
|
36578
|
+
const dead = tunnel.failure();
|
|
36579
|
+
if (dead !== null) {
|
|
36580
|
+
throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
|
|
36581
|
+
}
|
|
36582
|
+
reading = await io.fetchAttestation({
|
|
36583
|
+
port: attestPort,
|
|
36584
|
+
certPath: a.tls.certPath,
|
|
36585
|
+
keyPath: a.tls.keyPath,
|
|
36586
|
+
timeoutMs: CS_ATTEST_TIMEOUT_MS
|
|
36587
|
+
});
|
|
36588
|
+
if (reading.ok)
|
|
36589
|
+
break;
|
|
36590
|
+
await io.sleep(CS_ATTEST_INTERVAL_MS);
|
|
36591
|
+
}
|
|
36592
|
+
if (!reading.ok) {
|
|
36593
|
+
throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
|
|
36594
|
+
}
|
|
36595
|
+
let spki;
|
|
36596
|
+
let venuePem;
|
|
36597
|
+
try {
|
|
36598
|
+
spki = spkiSha256Hex(reading.peerCertDer);
|
|
36599
|
+
venuePem = new X509Certificate(reading.peerCertDer).toString();
|
|
36600
|
+
} catch (err) {
|
|
36601
|
+
throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
|
|
36602
|
+
}
|
|
36603
|
+
let jwks;
|
|
36604
|
+
try {
|
|
36605
|
+
jwks = await io.fetchConfidentialSpaceJwks();
|
|
36606
|
+
} catch (err) {
|
|
36607
|
+
throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
|
|
36608
|
+
}
|
|
36609
|
+
const verdict = await verifyConfidentialSpaceToken(reading.token, {
|
|
36610
|
+
audience: expectedAudience(a.runId),
|
|
36611
|
+
nonce: a.runId,
|
|
36612
|
+
additionalNonces: [spki],
|
|
36613
|
+
instanceName: vm,
|
|
36614
|
+
allowedZones: [a.zone],
|
|
36615
|
+
projectId: a.project,
|
|
36616
|
+
allowedImageDigests: [a.imageDigest],
|
|
36617
|
+
containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
|
|
36618
|
+
}, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
|
|
36619
|
+
if (!verdict.ok) {
|
|
36620
|
+
throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
|
|
36621
|
+
}
|
|
36622
|
+
const caPath = join40(a.tls.dir, "venue.pem");
|
|
36623
|
+
try {
|
|
36624
|
+
io.writeFile(caPath, venuePem);
|
|
36625
|
+
} catch (err) {
|
|
36626
|
+
throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
|
|
36627
|
+
}
|
|
36628
|
+
const dockerAt = `tcp://localhost:${String(dockerPort)}`;
|
|
36629
|
+
const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
|
|
36630
|
+
const daemon = io.classifyDaemon(docker3);
|
|
36631
|
+
if (!daemon.distinct) {
|
|
36632
|
+
throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
|
|
36633
|
+
}
|
|
36634
|
+
const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
|
|
36635
|
+
timeoutMs: MKDIR_TIMEOUT_MS
|
|
36636
|
+
});
|
|
36637
|
+
if (prepared.error || prepared.status !== 0) {
|
|
36638
|
+
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
|
|
36639
|
+
}
|
|
36640
|
+
const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
|
|
36641
|
+
if (pulled.error || pulled.status !== 0) {
|
|
36642
|
+
throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
|
|
36643
|
+
}
|
|
36644
|
+
return {
|
|
36645
|
+
docker: docker3,
|
|
36646
|
+
tunnel,
|
|
36647
|
+
socketDir: a.tls.dir,
|
|
36648
|
+
socketPath: dockerAt,
|
|
36649
|
+
venueDaemonId: daemon.venueDaemonId,
|
|
36650
|
+
venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
|
|
36651
|
+
transport: dockerStageTransport(docker3, a.env, io)
|
|
36652
|
+
};
|
|
36653
|
+
}
|
|
36002
36654
|
function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
36003
36655
|
const project = opts.project ?? DEFAULT_GCP_PROJECT;
|
|
36004
36656
|
const zone = opts.zone ?? DEFAULT_GCP_ZONE;
|
|
36005
36657
|
const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
|
|
36006
36658
|
const keyFile = resolveServiceAccountKeyFile(opts);
|
|
36007
36659
|
const image = venueImageFromEnv(process.env);
|
|
36660
|
+
const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
|
|
36008
36661
|
const env = venueGcloudEnv(gcloudConfig);
|
|
36009
36662
|
return {
|
|
36010
36663
|
kind: "hosted-pool",
|
|
36011
36664
|
acquire: (runId) => acquireTransactionally(async (allocated) => {
|
|
36012
36665
|
const vm = venueInstanceName(runId);
|
|
36013
36666
|
const stageBase = `/tmp/th-stage-${runId}`;
|
|
36014
|
-
const
|
|
36667
|
+
const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
|
|
36015
36668
|
vmName: vm,
|
|
36016
36669
|
project,
|
|
36017
36670
|
zone,
|
|
36018
36671
|
runId,
|
|
36019
|
-
machineType: opts.machineType ??
|
|
36672
|
+
machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
|
|
36673
|
+
venueImage: "confidential-space",
|
|
36674
|
+
workloadImage: image.workloadImage,
|
|
36675
|
+
clientCert
|
|
36676
|
+
} : {
|
|
36677
|
+
vmName: vm,
|
|
36678
|
+
project,
|
|
36679
|
+
zone,
|
|
36680
|
+
runId,
|
|
36681
|
+
machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
|
|
36020
36682
|
...image
|
|
36021
|
-
}
|
|
36683
|
+
};
|
|
36684
|
+
let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
|
|
36022
36685
|
ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
|
|
36686
|
+
const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
|
|
36687
|
+
if (clientTls !== null)
|
|
36688
|
+
bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
|
|
36023
36689
|
const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
|
|
36024
36690
|
let instanceState = "unknown";
|
|
36025
36691
|
allocated.onRollback(() => {
|
|
@@ -36035,6 +36701,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
36035
36701
|
throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
|
|
36036
36702
|
}
|
|
36037
36703
|
instanceState = "exists";
|
|
36704
|
+
if (clientTls !== null) {
|
|
36705
|
+
return finishAcquire(await reachConfidentialSpace({
|
|
36706
|
+
vm,
|
|
36707
|
+
project,
|
|
36708
|
+
zone,
|
|
36709
|
+
runId,
|
|
36710
|
+
stageBase,
|
|
36711
|
+
env,
|
|
36712
|
+
io,
|
|
36713
|
+
allocated,
|
|
36714
|
+
tls: clientTls,
|
|
36715
|
+
imageDigest: clientTls.imageDigest
|
|
36716
|
+
}));
|
|
36717
|
+
}
|
|
36038
36718
|
const deadline = io.now() + SSH_READY_BUDGET_MS;
|
|
36039
36719
|
let last = { retryable: true, reason: "no probe ran" };
|
|
36040
36720
|
let identity = null;
|
|
@@ -36129,43 +36809,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
36129
36809
|
if (!prepared.ok) {
|
|
36130
36810
|
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
|
|
36131
36811
|
}
|
|
36132
|
-
|
|
36133
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
36134
|
-
const created = docker3.sync(volumeCreateArgv(name, runId), {
|
|
36135
|
-
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
36136
|
-
});
|
|
36137
|
-
if (created.error || created.status !== 0) {
|
|
36138
|
-
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
36139
|
-
}
|
|
36140
|
-
}
|
|
36141
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
36142
|
-
const probe = docker3.sync(execProbeArgv(name), {
|
|
36143
|
-
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
36144
|
-
});
|
|
36145
|
-
if (probe.error || probe.status !== 0) {
|
|
36146
|
-
throw new HostedVenueError(`${vm} cannot execute a file written to its own Docker volume ${name}, so no install step with a native postinstall could run there. Refusing before staging anything: ${dockerDetail(probe).slice(0, 300)}`);
|
|
36147
|
-
}
|
|
36148
|
-
}
|
|
36149
|
-
const after = io.classifyDaemon(docker3);
|
|
36150
|
-
if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
|
|
36151
|
-
throw new HostedVenueError(`the daemon at ${socketPath} changed while the stage volumes were being created and proved: it now answers as ${after.distinct ? `daemon ${after.venueDaemonId}` : describeVenueDaemon(after)}, not ${venueDaemonId}. Refusing to hand out a lease whose proof was made elsewhere.`);
|
|
36152
|
-
}
|
|
36153
|
-
return makeLease({
|
|
36154
|
-
runId,
|
|
36155
|
-
vm,
|
|
36156
|
-
project,
|
|
36157
|
-
zone,
|
|
36158
|
-
stageBase,
|
|
36159
|
-
stageVolumes,
|
|
36812
|
+
return finishAcquire({
|
|
36160
36813
|
docker: docker3,
|
|
36161
36814
|
tunnel,
|
|
36162
36815
|
socketDir,
|
|
36163
36816
|
socketPath,
|
|
36164
36817
|
venueDaemonId,
|
|
36165
36818
|
venueIdentity,
|
|
36166
|
-
env,
|
|
36167
|
-
io
|
|
36819
|
+
transport: sshStageTransport(vm, project, zone, env, io)
|
|
36168
36820
|
});
|
|
36821
|
+
function finishAcquire(r) {
|
|
36822
|
+
const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
|
|
36823
|
+
const stageVolumes = stageVolumeNames(vm);
|
|
36824
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
36825
|
+
const created = docker4.sync(volumeCreateArgv(name, runId), {
|
|
36826
|
+
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
36827
|
+
});
|
|
36828
|
+
if (created.error || created.status !== 0) {
|
|
36829
|
+
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
36830
|
+
}
|
|
36831
|
+
}
|
|
36832
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
36833
|
+
const probe = docker4.sync(execProbeArgv(name), {
|
|
36834
|
+
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
36835
|
+
});
|
|
36836
|
+
if (probe.error || probe.status !== 0) {
|
|
36837
|
+
throw new HostedVenueError(`${vm} cannot execute a file written to its own Docker volume ${name}, so no install step with a native postinstall could run there. Refusing before staging anything: ${dockerDetail(probe).slice(0, 300)}`);
|
|
36838
|
+
}
|
|
36839
|
+
}
|
|
36840
|
+
const after = io.classifyDaemon(docker4);
|
|
36841
|
+
if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
|
|
36842
|
+
throw new HostedVenueError(`the daemon at ${socketPath2} changed while the stage volumes were being created and proved: it now answers as ${after.distinct ? `daemon ${after.venueDaemonId}` : describeVenueDaemon(after)}, not ${venueDaemonId2}. Refusing to hand out a lease whose proof was made elsewhere.`);
|
|
36843
|
+
}
|
|
36844
|
+
return makeLease({
|
|
36845
|
+
runId,
|
|
36846
|
+
vm,
|
|
36847
|
+
project,
|
|
36848
|
+
zone,
|
|
36849
|
+
stageBase,
|
|
36850
|
+
stageVolumes,
|
|
36851
|
+
docker: docker4,
|
|
36852
|
+
tunnel: r.tunnel,
|
|
36853
|
+
socketDir: r.socketDir,
|
|
36854
|
+
socketPath: socketPath2,
|
|
36855
|
+
venueDaemonId: venueDaemonId2,
|
|
36856
|
+
venueIdentity: r.venueIdentity,
|
|
36857
|
+
transport: r.transport,
|
|
36858
|
+
env,
|
|
36859
|
+
io
|
|
36860
|
+
});
|
|
36861
|
+
}
|
|
36169
36862
|
})
|
|
36170
36863
|
};
|
|
36171
36864
|
}
|
|
@@ -36224,28 +36917,7 @@ function makeLease(p) {
|
|
|
36224
36917
|
return null;
|
|
36225
36918
|
};
|
|
36226
36919
|
let stagedOwner;
|
|
36227
|
-
|
|
36228
|
-
const res = p.io.exec(
|
|
36229
|
-
"gcloud",
|
|
36230
|
-
iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
|
|
36231
|
-
OWNER_PROBE_TIMEOUT_MS,
|
|
36232
|
-
// `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
36233
|
-
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
36234
|
-
// omitted this would ssh under the person's own credential — then report an owner
|
|
36235
|
-
// measured against a session the run does not use.
|
|
36236
|
-
p.env
|
|
36237
|
-
);
|
|
36238
|
-
if (!res.ok) {
|
|
36239
|
-
throw new HostedVenueError(`could not read the staged tree's owner on ${p.vm}: ${execDetail(res).slice(0, 300)}. Refusing rather than falling back to this machine\u2019s uid, which would hand the guest an id with no write access to its own clone.`);
|
|
36240
|
-
}
|
|
36241
|
-
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
36242
|
-
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
36243
|
-
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
36244
|
-
throw new HostedVenueError(`the venue ${p.vm} answered 'id -u && id -g' with ${JSON.stringify(res.stdout.slice(0, 120))}, which is not a uid/gid pair. Refusing rather than guessing an owner.`);
|
|
36245
|
-
}
|
|
36246
|
-
return { uid: ids2[0], gid: ids2[1] };
|
|
36247
|
-
};
|
|
36248
|
-
stagedOwner = readStagedOwner();
|
|
36920
|
+
stagedOwner = p.transport.readOwner();
|
|
36249
36921
|
return {
|
|
36250
36922
|
kind: "hosted-pool",
|
|
36251
36923
|
runId: p.runId,
|
|
@@ -36280,7 +36952,7 @@ function makeLease(p) {
|
|
|
36280
36952
|
const dir = venueProxyDir(p.stageBase);
|
|
36281
36953
|
const buffer = stageProxyCode();
|
|
36282
36954
|
try {
|
|
36283
|
-
const res = await p.
|
|
36955
|
+
const res = await p.transport.pushProxy(buffer.dir, dir);
|
|
36284
36956
|
if (!res.ok) {
|
|
36285
36957
|
throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
36286
36958
|
}
|
|
@@ -36290,7 +36962,7 @@ function makeLease(p) {
|
|
|
36290
36962
|
return {
|
|
36291
36963
|
dir,
|
|
36292
36964
|
cleanup: () => {
|
|
36293
|
-
p.
|
|
36965
|
+
p.transport.removeProxy(dir);
|
|
36294
36966
|
}
|
|
36295
36967
|
};
|
|
36296
36968
|
},
|
|
@@ -36298,7 +36970,7 @@ function makeLease(p) {
|
|
|
36298
36970
|
check("staging the tree");
|
|
36299
36971
|
const paths = venueStagePaths(p.stageBase);
|
|
36300
36972
|
const push = async (from, to) => {
|
|
36301
|
-
const res = await p.
|
|
36973
|
+
const res = await p.transport.push(from, to);
|
|
36302
36974
|
if (!res.ok) {
|
|
36303
36975
|
throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
36304
36976
|
}
|
|
@@ -36456,7 +37128,7 @@ function makeLease(p) {
|
|
|
36456
37128
|
}
|
|
36457
37129
|
};
|
|
36458
37130
|
}
|
|
36459
|
-
var SSH_READY_BUDGET_MS, SSH_PROBE_INTERVAL_MS, SSH_PROBE_TIMEOUT_MS, TUNNEL_BUDGET_MS, TUNNEL_POLL_INTERVAL_MS, GOOGLE_JWKS_URL, JWKS_FETCH_TIMEOUT_MS, CREDENTIAL_QUERY_PARAM, UNDECODABLE, STAGE_PUSH_TIMEOUT_MS, DISPATCHED_PROBE_TIMEOUT_MS, DISPATCHED_STATUS_ARGV, DISPATCHED_GIT_CANDIDATES, PROXY_CLEANUP_TIMEOUT_MS, OWNER_PROBE_TIMEOUT_MS, BOOT_TIMEOUT_MS, MKDIR_TIMEOUT_MS, DELETE_TIMEOUT_MS, LOCAL_GCLOUD_TIMEOUT_MS, SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS, SOCKET_DIR_PREFIX, VENUE_SOCKET_NAME, HostedVenueError, VENUE_GCLOUD_CONFIG, SERVICE_ACCOUNT_SUFFIX, GCLOUD_PRINCIPAL_OVERRIDES, defaultHostedVenueIo, VENUE_SSH_USER, GCE_METADATA_IDENTITY_URL, COMPACT_JWT, STAGE_HELPER_IMAGE, VOLUME_CREATE_TIMEOUT_MS, EXEC_PROBE_TIMEOUT_MS, POPULATE_TIMEOUT_MS, STAGE_VOLUME_LABEL_KEY, STAGE_PROOF_PREFIX, IAP_NOT_READY, IAP_BACKEND_UNREACHABLE, IAP_DENIED, TERMINAL_GCP, INSTANCE_NOT_RUNNING, PREEMPTED, HOST_KEY_MISMATCH, SSH_KEY_NOT_READY, DAEMON_NOT_READY, SSH_NOT_ANSWERING;
|
|
37131
|
+
var SSH_READY_BUDGET_MS, SSH_PROBE_INTERVAL_MS, SSH_PROBE_TIMEOUT_MS, TUNNEL_BUDGET_MS, TUNNEL_POLL_INTERVAL_MS, GOOGLE_JWKS_URL, JWKS_FETCH_TIMEOUT_MS, CREDENTIAL_QUERY_PARAM, UNDECODABLE, STAGE_PUSH_TIMEOUT_MS, DISPATCHED_PROBE_TIMEOUT_MS, DISPATCHED_STATUS_ARGV, DISPATCHED_GIT_CANDIDATES, PROXY_CLEANUP_TIMEOUT_MS, OWNER_PROBE_TIMEOUT_MS, BOOT_TIMEOUT_MS, MKDIR_TIMEOUT_MS, DELETE_TIMEOUT_MS, LOCAL_GCLOUD_TIMEOUT_MS, SERVICE_ACCOUNT_ACTIVATE_TIMEOUT_MS, SOCKET_DIR_PREFIX, VENUE_SOCKET_NAME, HostedVenueError, VENUE_GCLOUD_CONFIG, SERVICE_ACCOUNT_SUFFIX, GCLOUD_PRINCIPAL_OVERRIDES, defaultHostedVenueIo, VENUE_SSH_USER, GCE_METADATA_IDENTITY_URL, COMPACT_JWT, STAGE_HELPER_IMAGE, VOLUME_CREATE_TIMEOUT_MS, EXEC_PROBE_TIMEOUT_MS, POPULATE_TIMEOUT_MS, STAGE_VOLUME_LABEL_KEY, STAGE_PROOF_PREFIX, IAP_NOT_READY, IAP_BACKEND_UNREACHABLE, IAP_DENIED, TERMINAL_GCP, INSTANCE_NOT_RUNNING, PREEMPTED, HOST_KEY_MISMATCH, SSH_KEY_NOT_READY, DAEMON_NOT_READY, SSH_NOT_ANSWERING, CS_ATTEST_PORT, CS_DOCKER_PORT, CS_READY_BUDGET_MS, CS_ATTEST_TIMEOUT_MS, CS_PULL_TIMEOUT_MS, CS_ATTEST_INTERVAL_MS, OPENSSL_TIMEOUT_MS, CS_RUN_ARGS, CS_GUEST_USER, CS_STAGE_ROOT, CLIENT_CERT_STAND_IN;
|
|
36460
37132
|
var init_hostedVenue = __esm({
|
|
36461
37133
|
"../../packages/envrun/dist/hostedVenue.js"() {
|
|
36462
37134
|
"use strict";
|
|
@@ -36577,7 +37249,55 @@ var init_hostedVenue = __esm({
|
|
|
36577
37249
|
throw new Error("JWKS fetch returned a body with no keys array");
|
|
36578
37250
|
}
|
|
36579
37251
|
return body;
|
|
36580
|
-
}
|
|
37252
|
+
},
|
|
37253
|
+
fetchConfidentialSpaceJwks: async () => {
|
|
37254
|
+
const getJson = async (url, what) => {
|
|
37255
|
+
const res = await globalThis.fetch(url, {
|
|
37256
|
+
signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
|
|
37257
|
+
});
|
|
37258
|
+
if (!res.ok) {
|
|
37259
|
+
throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
|
|
37260
|
+
}
|
|
37261
|
+
return res.json();
|
|
37262
|
+
};
|
|
37263
|
+
const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
|
|
37264
|
+
const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
|
|
37265
|
+
if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
|
|
37266
|
+
throw new Error("Confidential Space discovery names no https jwks_uri");
|
|
37267
|
+
}
|
|
37268
|
+
const body = await getJson(jwksUri, "key set");
|
|
37269
|
+
if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
|
|
37270
|
+
throw new Error("Confidential Space key set has no keys array");
|
|
37271
|
+
}
|
|
37272
|
+
return body;
|
|
37273
|
+
},
|
|
37274
|
+
freeLocalPorts: async (n) => {
|
|
37275
|
+
const servers = [];
|
|
37276
|
+
try {
|
|
37277
|
+
for (let i = 0; i < n; i += 1) {
|
|
37278
|
+
const server = createServer();
|
|
37279
|
+
servers.push(server);
|
|
37280
|
+
await new Promise((resolve7, reject) => {
|
|
37281
|
+
server.once("error", reject);
|
|
37282
|
+
server.listen(0, "localhost", () => {
|
|
37283
|
+
resolve7();
|
|
37284
|
+
});
|
|
37285
|
+
});
|
|
37286
|
+
}
|
|
37287
|
+
return servers.map((s) => s.address().port);
|
|
37288
|
+
} finally {
|
|
37289
|
+
await Promise.all(servers.map((s) => new Promise((resolve7) => {
|
|
37290
|
+
s.close(() => {
|
|
37291
|
+
resolve7();
|
|
37292
|
+
});
|
|
37293
|
+
})));
|
|
37294
|
+
}
|
|
37295
|
+
},
|
|
37296
|
+
writeFile: (path6, text) => {
|
|
37297
|
+
writeFileSync22(path6, text, { mode: 384, flag: "wx" });
|
|
37298
|
+
},
|
|
37299
|
+
fetchAttestation: (req) => fetchAttestationOverTls(req),
|
|
37300
|
+
dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
|
|
36581
37301
|
};
|
|
36582
37302
|
VENUE_SSH_USER = "th-runner";
|
|
36583
37303
|
GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
|
|
@@ -36598,6 +37318,17 @@ var init_hostedVenue = __esm({
|
|
|
36598
37318
|
SSH_KEY_NOT_READY = /Permission denied \(publickey/;
|
|
36599
37319
|
DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
|
|
36600
37320
|
SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
|
|
37321
|
+
CS_ATTEST_PORT = 8443;
|
|
37322
|
+
CS_DOCKER_PORT = 2376;
|
|
37323
|
+
CS_READY_BUDGET_MS = 48e4;
|
|
37324
|
+
CS_ATTEST_TIMEOUT_MS = 15e3;
|
|
37325
|
+
CS_PULL_TIMEOUT_MS = 6e5;
|
|
37326
|
+
CS_ATTEST_INTERVAL_MS = 5e3;
|
|
37327
|
+
OPENSSL_TIMEOUT_MS = 3e4;
|
|
37328
|
+
CS_RUN_ARGS = ["--oom-score-adj", "1000"];
|
|
37329
|
+
CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
|
|
37330
|
+
CS_STAGE_ROOT = "/tmp";
|
|
37331
|
+
CLIENT_CERT_STAND_IN = "stand-in-certificate";
|
|
36601
37332
|
}
|
|
36602
37333
|
});
|
|
36603
37334
|
|
|
@@ -37497,10 +38228,10 @@ var init_references = __esm({
|
|
|
37497
38228
|
|
|
37498
38229
|
// ../../packages/envspec/dist/repo.js
|
|
37499
38230
|
import { readdirSync as readdirSync2, readFileSync as readFileSync26, statSync as statSync4 } from "fs";
|
|
37500
|
-
import { join as join41, relative as relative2, sep as
|
|
38231
|
+
import { join as join41, relative as relative2, sep as sep5 } from "path";
|
|
37501
38232
|
function createRepoReader(repoPath) {
|
|
37502
38233
|
const resolveIn = (relativePath) => relativePath === "" ? repoPath : join41(repoPath, relativePath);
|
|
37503
|
-
const toPosix = (absolute) => relative2(repoPath, absolute).split(
|
|
38234
|
+
const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
|
|
37504
38235
|
const readText = (relativePath) => {
|
|
37505
38236
|
try {
|
|
37506
38237
|
return readFileSync26(resolveIn(relativePath), "utf8");
|
|
@@ -40330,7 +41061,7 @@ __export(jpi_claim_exports, {
|
|
|
40330
41061
|
});
|
|
40331
41062
|
import {
|
|
40332
41063
|
readFileSync as readFileSync27,
|
|
40333
|
-
writeFileSync as
|
|
41064
|
+
writeFileSync as writeFileSync23,
|
|
40334
41065
|
mkdirSync as mkdirSync9,
|
|
40335
41066
|
mkdtempSync as mkdtempSync5,
|
|
40336
41067
|
renameSync as renameSync11,
|
|
@@ -40341,7 +41072,7 @@ import {
|
|
|
40341
41072
|
readdirSync as readdirSync3
|
|
40342
41073
|
} from "fs";
|
|
40343
41074
|
import { join as join43, dirname as dirname10, isAbsolute as isAbsolute5, resolve as pathResolve } from "path";
|
|
40344
|
-
import { createHash as
|
|
41075
|
+
import { createHash as createHash11 } from "crypto";
|
|
40345
41076
|
import { homedir as homedir26, hostname as osHostname } from "os";
|
|
40346
41077
|
import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync9 } from "child_process";
|
|
40347
41078
|
import { promisify as promisify3 } from "util";
|
|
@@ -40359,7 +41090,7 @@ function markClaimNudged(id) {
|
|
|
40359
41090
|
const ids2 = readNudgedClaimIds();
|
|
40360
41091
|
ids2.add(id);
|
|
40361
41092
|
ensureStateDir(TERMINALHIRE_DIR17);
|
|
40362
|
-
|
|
41093
|
+
writeFileSync23(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
|
|
40363
41094
|
} catch {
|
|
40364
41095
|
}
|
|
40365
41096
|
}
|
|
@@ -41533,7 +42264,7 @@ async function registerFounderClaim(b) {
|
|
|
41533
42264
|
let clearedLocalCredential = false;
|
|
41534
42265
|
let refusedForPurpose = false;
|
|
41535
42266
|
let preserveBackgroundToken = false;
|
|
41536
|
-
const
|
|
42267
|
+
const refuse4 = (reason) => {
|
|
41537
42268
|
console.error(
|
|
41538
42269
|
`
|
|
41539
42270
|
terminalhire claim: refusing to record \u2014 ${reason}
|
|
@@ -41558,7 +42289,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41558
42289
|
try {
|
|
41559
42290
|
expectLogin = await localLoginForPaidBrowserClaim();
|
|
41560
42291
|
} catch (err) {
|
|
41561
|
-
|
|
42292
|
+
refuse4(
|
|
41562
42293
|
`could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
|
|
41563
42294
|
Run \`terminalhire login\`, then retry this claim. Browser verification was
|
|
41564
42295
|
not opened and no claim was sent.`
|
|
@@ -41574,16 +42305,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41574
42305
|
claimRef: opportunityShortToken(b.bountyId)
|
|
41575
42306
|
});
|
|
41576
42307
|
if (!proofToken) {
|
|
41577
|
-
|
|
42308
|
+
refuse4("could not verify your GitHub identity with terminalhire (see above).");
|
|
41578
42309
|
}
|
|
41579
42310
|
const verifiedLogin = proofTokenLogin(proofToken);
|
|
41580
42311
|
if (!verifiedLogin) {
|
|
41581
|
-
|
|
42312
|
+
refuse4(
|
|
41582
42313
|
"the browser verification response did not name a GitHub account, so it could not be compared with this machine\u2019s sign-in. Nothing was sent; retry the claim."
|
|
41583
42314
|
);
|
|
41584
42315
|
}
|
|
41585
42316
|
if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
|
|
41586
|
-
|
|
42317
|
+
refuse4(
|
|
41587
42318
|
`you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
|
|
41588
42319
|
Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
|
|
41589
42320
|
run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
|
|
@@ -41615,7 +42346,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41615
42346
|
try {
|
|
41616
42347
|
res = await sendRegistration(true);
|
|
41617
42348
|
} catch (err) {
|
|
41618
|
-
|
|
42349
|
+
refuse4(
|
|
41619
42350
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
41620
42351
|
);
|
|
41621
42352
|
}
|
|
@@ -41666,7 +42397,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41666
42397
|
try {
|
|
41667
42398
|
res = await sendRegistration(true);
|
|
41668
42399
|
} catch (err) {
|
|
41669
|
-
|
|
42400
|
+
refuse4(
|
|
41670
42401
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
41671
42402
|
);
|
|
41672
42403
|
}
|
|
@@ -41677,14 +42408,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41677
42408
|
res = await sendRegistration(false);
|
|
41678
42409
|
refusalBody = await readRefusal(res);
|
|
41679
42410
|
} catch (err) {
|
|
41680
|
-
|
|
42411
|
+
refuse4(
|
|
41681
42412
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
41682
42413
|
);
|
|
41683
42414
|
}
|
|
41684
42415
|
}
|
|
41685
42416
|
if (!res.ok) {
|
|
41686
42417
|
const detail = refusalBody?.message || refusalBody?.error || "";
|
|
41687
|
-
|
|
42418
|
+
refuse4(
|
|
41688
42419
|
detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
|
|
41689
42420
|
);
|
|
41690
42421
|
}
|
|
@@ -41695,7 +42426,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
41695
42426
|
body = null;
|
|
41696
42427
|
}
|
|
41697
42428
|
if (!body || body.ok !== true) {
|
|
41698
|
-
|
|
42429
|
+
refuse4("malformed registration response from the server.");
|
|
41699
42430
|
}
|
|
41700
42431
|
const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
|
|
41701
42432
|
if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
|
|
@@ -43125,7 +43856,7 @@ function writeSliceFiles(destDir, files) {
|
|
|
43125
43856
|
if (typeof f.content === "string") {
|
|
43126
43857
|
const abs = join43(destDir, f.path);
|
|
43127
43858
|
mkdirSync9(dirname10(abs), { recursive: true });
|
|
43128
|
-
|
|
43859
|
+
writeFileSync23(abs, f.content, "utf8");
|
|
43129
43860
|
written.push(f.path);
|
|
43130
43861
|
} else {
|
|
43131
43862
|
unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
|
|
@@ -43174,7 +43905,7 @@ function ensureExcludedPackDir(destDir) {
|
|
|
43174
43905
|
const existing = existsSync20(excludeFile) ? readFileSync27(excludeFile, "utf8") : "";
|
|
43175
43906
|
if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
|
|
43176
43907
|
mkdirSync9(dirname10(excludeFile), { recursive: true });
|
|
43177
|
-
|
|
43908
|
+
writeFileSync23(
|
|
43178
43909
|
excludeFile,
|
|
43179
43910
|
`${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
|
|
43180
43911
|
`,
|
|
@@ -43190,14 +43921,14 @@ function writePackFile(destDir, relPath, content, what) {
|
|
|
43190
43921
|
try {
|
|
43191
43922
|
const abs = join43(destDir, relPath);
|
|
43192
43923
|
mkdirSync9(dirname10(abs), { recursive: true });
|
|
43193
|
-
|
|
43924
|
+
writeFileSync23(abs, content, { encoding: "utf8", flag: "wx" });
|
|
43194
43925
|
} catch (err) {
|
|
43195
43926
|
return { written: false, reason: `the ${what} could not be written (${err.message})` };
|
|
43196
43927
|
}
|
|
43197
43928
|
return { written: true, reason: null, sha256: sha256OfUtf8(content) };
|
|
43198
43929
|
}
|
|
43199
43930
|
function sha256OfUtf8(content) {
|
|
43200
|
-
return
|
|
43931
|
+
return createHash11("sha256").update(content, "utf8").digest("hex");
|
|
43201
43932
|
}
|
|
43202
43933
|
function writeWorkspacePack(destDir, spec, claim, delivery) {
|
|
43203
43934
|
if (delivery !== "full" && delivery !== "sparse") {
|
|
@@ -44554,7 +45285,7 @@ function readClaimPushMarker() {
|
|
|
44554
45285
|
}
|
|
44555
45286
|
function writeClaimPushMarker(marker) {
|
|
44556
45287
|
ensureStateDir(TERMINALHIRE_DIR17);
|
|
44557
|
-
|
|
45288
|
+
writeFileSync23(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
44558
45289
|
}
|
|
44559
45290
|
function clearClaimPushMarker() {
|
|
44560
45291
|
try {
|
|
@@ -45741,7 +46472,7 @@ import {
|
|
|
45741
46472
|
readFileSync as readFileSync28,
|
|
45742
46473
|
renameSync as renameSync12,
|
|
45743
46474
|
rmSync as rmSync13,
|
|
45744
|
-
writeFileSync as
|
|
46475
|
+
writeFileSync as writeFileSync24
|
|
45745
46476
|
} from "fs";
|
|
45746
46477
|
import { homedir as homedir27 } from "os";
|
|
45747
46478
|
import { join as join44 } from "path";
|
|
@@ -45777,7 +46508,7 @@ function writeFile(file) {
|
|
|
45777
46508
|
const path6 = postingDraftFilePath();
|
|
45778
46509
|
const tmp = `${path6}.${process.pid}.${randomUUID4()}.tmp`;
|
|
45779
46510
|
try {
|
|
45780
|
-
|
|
46511
|
+
writeFileSync24(tmp, `${JSON.stringify(file, null, 2)}
|
|
45781
46512
|
`, {
|
|
45782
46513
|
encoding: "utf8",
|
|
45783
46514
|
mode: 384,
|
|
@@ -46537,7 +47268,7 @@ import {
|
|
|
46537
47268
|
renameSync as renameSync13,
|
|
46538
47269
|
statSync as statSync5,
|
|
46539
47270
|
unlinkSync as unlinkSync4,
|
|
46540
|
-
writeFileSync as
|
|
47271
|
+
writeFileSync as writeFileSync25
|
|
46541
47272
|
} from "fs";
|
|
46542
47273
|
import { homedir as homedir28 } from "os";
|
|
46543
47274
|
import { basename as basename7, dirname as dirname11, join as join46 } from "path";
|
|
@@ -46600,7 +47331,7 @@ function mutateCache(path6, mutate) {
|
|
|
46600
47331
|
if (mutate(existing) === false) return false;
|
|
46601
47332
|
const tmp = `${path6}.${process.pid}.${tmpCounter3 += 1}.tmp`;
|
|
46602
47333
|
try {
|
|
46603
|
-
|
|
47334
|
+
writeFileSync25(tmp, `${JSON.stringify(existing, null, 2)}
|
|
46604
47335
|
`, { mode: 384 });
|
|
46605
47336
|
renameSync13(tmp, path6);
|
|
46606
47337
|
} catch {
|
|
@@ -47760,7 +48491,7 @@ __export(trajectory_exports, {
|
|
|
47760
48491
|
runTrajectory: () => runTrajectory,
|
|
47761
48492
|
runTrajectoryPush: () => runTrajectoryPush
|
|
47762
48493
|
});
|
|
47763
|
-
import { existsSync as existsSync25, readFileSync as readFileSync32, readdirSync as readdirSync5, writeFileSync as
|
|
48494
|
+
import { existsSync as existsSync25, readFileSync as readFileSync32, readdirSync as readdirSync5, writeFileSync as writeFileSync26 } from "fs";
|
|
47764
48495
|
import { homedir as homedir29 } from "os";
|
|
47765
48496
|
import { join as join48 } from "path";
|
|
47766
48497
|
function isRecord4(value) {
|
|
@@ -47919,8 +48650,8 @@ function writeExportArtifacts(score, markdown) {
|
|
|
47919
48650
|
ensureStateDir(dir);
|
|
47920
48651
|
const jsonPath = join48(dir, "trajectory-export.json");
|
|
47921
48652
|
const mdPath = join48(dir, "trajectory-export.md");
|
|
47922
|
-
|
|
47923
|
-
|
|
48653
|
+
writeFileSync26(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
|
|
48654
|
+
writeFileSync26(mdPath, markdown, "utf8");
|
|
47924
48655
|
return { jsonPath, mdPath };
|
|
47925
48656
|
}
|
|
47926
48657
|
function renderInward(allNodes, view, files) {
|
|
@@ -48753,7 +49484,7 @@ var init_jpi_intro = __esm({
|
|
|
48753
49484
|
});
|
|
48754
49485
|
|
|
48755
49486
|
// src/chat-keystore.ts
|
|
48756
|
-
import { existsSync as existsSync26, linkSync as linkSync2, readFileSync as readFileSync33, rmSync as rmSync15, unlinkSync as unlinkSync5, writeFileSync as
|
|
49487
|
+
import { existsSync as existsSync26, linkSync as linkSync2, readFileSync as readFileSync33, rmSync as rmSync15, unlinkSync as unlinkSync5, writeFileSync as writeFileSync27 } from "fs";
|
|
48757
49488
|
import { randomBytes as randomBytes13 } from "crypto";
|
|
48758
49489
|
import { homedir as homedir30 } from "os";
|
|
48759
49490
|
import { join as join49 } from "path";
|
|
@@ -48800,7 +49531,7 @@ Recovery: if you intend to reset your chat identity, delete the file yourself an
|
|
|
48800
49531
|
function publishIdentityBlob(blob) {
|
|
48801
49532
|
const tmpFile = `${IDENTITY_FILE}.${process.pid}.${randomBytes13(6).toString("hex")}.tmp`;
|
|
48802
49533
|
try {
|
|
48803
|
-
|
|
49534
|
+
writeFileSync27(tmpFile, JSON.stringify(blob, null, 2), {
|
|
48804
49535
|
encoding: "utf8",
|
|
48805
49536
|
mode: 384,
|
|
48806
49537
|
flag: "wx"
|
|
@@ -48836,7 +49567,7 @@ var init_chat_keystore = __esm({
|
|
|
48836
49567
|
});
|
|
48837
49568
|
|
|
48838
49569
|
// src/chat-client.ts
|
|
48839
|
-
import { existsSync as existsSync27, readFileSync as readFileSync34, writeFileSync as
|
|
49570
|
+
import { existsSync as existsSync27, readFileSync as readFileSync34, writeFileSync as writeFileSync28 } from "fs";
|
|
48840
49571
|
import { homedir as homedir31 } from "os";
|
|
48841
49572
|
import { join as join50 } from "path";
|
|
48842
49573
|
function defaultReadPeerPins() {
|
|
@@ -48855,7 +49586,7 @@ function defaultReadPeerPins() {
|
|
|
48855
49586
|
}
|
|
48856
49587
|
function defaultWritePeerPins(pins) {
|
|
48857
49588
|
ensureStateDir(TERMINALHIRE_DIR19);
|
|
48858
|
-
|
|
49589
|
+
writeFileSync28(PEERS_FILE, JSON.stringify(pins, null, 2), { mode: 384, encoding: "utf8" });
|
|
48859
49590
|
}
|
|
48860
49591
|
function defaultChatClientDeps() {
|
|
48861
49592
|
return {
|
|
@@ -49484,7 +50215,7 @@ __export(jpi_chat_read_exports, {
|
|
|
49484
50215
|
syncUnreadBadge: () => syncUnreadBadge,
|
|
49485
50216
|
writeReadCursor: () => writeReadCursor
|
|
49486
50217
|
});
|
|
49487
|
-
import { existsSync as existsSync28, readFileSync as readFileSync35, writeFileSync as
|
|
50218
|
+
import { existsSync as existsSync28, readFileSync as readFileSync35, writeFileSync as writeFileSync29 } from "fs";
|
|
49488
50219
|
import { homedir as homedir32 } from "os";
|
|
49489
50220
|
import { join as join51 } from "path";
|
|
49490
50221
|
async function syncUnreadBadge(deps = {}) {
|
|
@@ -49508,7 +50239,7 @@ async function syncUnreadBadge(deps = {}) {
|
|
|
49508
50239
|
);
|
|
49509
50240
|
const entry = JSON.parse(readFileSync35(cacheFile, "utf8"));
|
|
49510
50241
|
entry.unreadChat = { count: total };
|
|
49511
|
-
|
|
50242
|
+
writeFileSync29(cacheFile, JSON.stringify(entry), "utf8");
|
|
49512
50243
|
} catch {
|
|
49513
50244
|
}
|
|
49514
50245
|
}
|
|
@@ -49533,7 +50264,7 @@ function writeReadCursor(login, iso, deps = {}) {
|
|
|
49533
50264
|
if (prev && iso <= prev) return;
|
|
49534
50265
|
cursors[login] = iso;
|
|
49535
50266
|
ensureStateDir(TERMINALHIRE_DIR20);
|
|
49536
|
-
|
|
50267
|
+
writeFileSync29(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
|
|
49537
50268
|
}
|
|
49538
50269
|
async function postReadCursor(peerLogin, lastReadAt, deps = {}) {
|
|
49539
50270
|
const readCookie = deps.readCookie ?? readWebSessionCookie;
|
|
@@ -52443,7 +53174,7 @@ __export(mcp_config_exports, {
|
|
|
52443
53174
|
});
|
|
52444
53175
|
import { homedir as homedir34 } from "os";
|
|
52445
53176
|
import { join as join53 } from "path";
|
|
52446
|
-
import { existsSync as existsSync30, readFileSync as readFileSync37, copyFileSync as copyFileSync3, writeFileSync as
|
|
53177
|
+
import { existsSync as existsSync30, readFileSync as readFileSync37, copyFileSync as copyFileSync3, writeFileSync as writeFileSync30, mkdirSync as mkdirSync11 } from "fs";
|
|
52447
53178
|
import { dirname as dirname12 } from "path";
|
|
52448
53179
|
function serverEntry() {
|
|
52449
53180
|
return { command: SERVER_COMMAND, args: [...SERVER_ARGS] };
|
|
@@ -52548,7 +53279,7 @@ function writeServerToFile(configPath, serversKey, entry = serverEntry()) {
|
|
|
52548
53279
|
} else {
|
|
52549
53280
|
mkdirSync11(dirname12(configPath), { recursive: true });
|
|
52550
53281
|
}
|
|
52551
|
-
|
|
53282
|
+
writeFileSync30(configPath, merged.text, "utf8");
|
|
52552
53283
|
return { status: "written", backupPath, added: merged.added };
|
|
52553
53284
|
}
|
|
52554
53285
|
async function initMcpStep({
|
|
@@ -52786,7 +53517,7 @@ __export(util_exports, {
|
|
|
52786
53517
|
getSizableOrigin: () => getSizableOrigin,
|
|
52787
53518
|
hexToUint8Array: () => hexToUint8Array,
|
|
52788
53519
|
isObject: () => isObject,
|
|
52789
|
-
isPlainObject: () =>
|
|
53520
|
+
isPlainObject: () => isPlainObject2,
|
|
52790
53521
|
issue: () => issue,
|
|
52791
53522
|
joinValues: () => joinValues,
|
|
52792
53523
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
@@ -52948,7 +53679,7 @@ function slugify(input) {
|
|
|
52948
53679
|
function isObject(data) {
|
|
52949
53680
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
52950
53681
|
}
|
|
52951
|
-
function
|
|
53682
|
+
function isPlainObject2(o) {
|
|
52952
53683
|
if (isObject(o) === false)
|
|
52953
53684
|
return false;
|
|
52954
53685
|
const ctor = o.constructor;
|
|
@@ -52965,7 +53696,7 @@ function isPlainObject(o) {
|
|
|
52965
53696
|
return true;
|
|
52966
53697
|
}
|
|
52967
53698
|
function shallowClone(o) {
|
|
52968
|
-
if (
|
|
53699
|
+
if (isPlainObject2(o))
|
|
52969
53700
|
return { ...o };
|
|
52970
53701
|
if (Array.isArray(o))
|
|
52971
53702
|
return [...o];
|
|
@@ -53105,7 +53836,7 @@ function omit(schema, mask) {
|
|
|
53105
53836
|
return clone(schema, def);
|
|
53106
53837
|
}
|
|
53107
53838
|
function extend(schema, shape) {
|
|
53108
|
-
if (!
|
|
53839
|
+
if (!isPlainObject2(shape)) {
|
|
53109
53840
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
53110
53841
|
}
|
|
53111
53842
|
const checks = schema._zod.def.checks;
|
|
@@ -53128,7 +53859,7 @@ function extend(schema, shape) {
|
|
|
53128
53859
|
return clone(schema, def);
|
|
53129
53860
|
}
|
|
53130
53861
|
function safeExtend(schema, shape) {
|
|
53131
|
-
if (!
|
|
53862
|
+
if (!isPlainObject2(shape)) {
|
|
53132
53863
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
53133
53864
|
}
|
|
53134
53865
|
const def = mergeDefs(schema._zod.def, {
|
|
@@ -54289,7 +55020,7 @@ function mergeValues(a, b) {
|
|
|
54289
55020
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
54290
55021
|
return { valid: true, data: a };
|
|
54291
55022
|
}
|
|
54292
|
-
if (
|
|
55023
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
54293
55024
|
const bKeys = Object.keys(b);
|
|
54294
55025
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
54295
55026
|
const newObj = { ...a, ...b };
|
|
@@ -55241,7 +55972,7 @@ var init_schemas = __esm({
|
|
|
55241
55972
|
$ZodType.init(inst, def);
|
|
55242
55973
|
inst._zod.parse = (payload, ctx) => {
|
|
55243
55974
|
const input = payload.value;
|
|
55244
|
-
if (!
|
|
55975
|
+
if (!isPlainObject2(input)) {
|
|
55245
55976
|
payload.issues.push({
|
|
55246
55977
|
expected: "record",
|
|
55247
55978
|
code: "invalid_type",
|
|
@@ -60383,7 +61114,7 @@ var init_zod_json_schema_compat = __esm({
|
|
|
60383
61114
|
});
|
|
60384
61115
|
|
|
60385
61116
|
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
|
|
60386
|
-
function
|
|
61117
|
+
function isPlainObject3(value) {
|
|
60387
61118
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
60388
61119
|
}
|
|
60389
61120
|
function mergeCapabilities(base, additional) {
|
|
@@ -60394,7 +61125,7 @@ function mergeCapabilities(base, additional) {
|
|
|
60394
61125
|
if (addValue === void 0)
|
|
60395
61126
|
continue;
|
|
60396
61127
|
const baseValue = result[k];
|
|
60397
|
-
if (
|
|
61128
|
+
if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
|
|
60398
61129
|
result[k] = { ...baseValue, ...addValue };
|
|
60399
61130
|
} else {
|
|
60400
61131
|
result[k] = addValue;
|
|
@@ -75693,7 +76424,7 @@ __export(link_exports, {
|
|
|
75693
76424
|
runLink: () => runLink,
|
|
75694
76425
|
runLinkLogout: () => runLinkLogout
|
|
75695
76426
|
});
|
|
75696
|
-
import { createServer } from "http";
|
|
76427
|
+
import { createServer as createServer2 } from "http";
|
|
75697
76428
|
import { randomBytes as randomBytes14 } from "crypto";
|
|
75698
76429
|
function resolveLoopbackRequest(rawUrl, expectedNonce) {
|
|
75699
76430
|
let u;
|
|
@@ -75727,7 +76458,7 @@ function defaultStartLoopback(expectedNonce, timeoutMs) {
|
|
|
75727
76458
|
}
|
|
75728
76459
|
});
|
|
75729
76460
|
};
|
|
75730
|
-
const server =
|
|
76461
|
+
const server = createServer2((req, res) => {
|
|
75731
76462
|
const outcome = resolveLoopbackRequest(req.url ?? "", expectedNonce);
|
|
75732
76463
|
res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
|
|
75733
76464
|
res.end(outcome.ok ? LINKED_HTML : FAILED_HTML);
|
|
@@ -76617,7 +77348,7 @@ var jpi_spinner_exports = {};
|
|
|
76617
77348
|
__export(jpi_spinner_exports, {
|
|
76618
77349
|
run: () => run25
|
|
76619
77350
|
});
|
|
76620
|
-
import { readFileSync as readFileSync39, writeFileSync as
|
|
77351
|
+
import { readFileSync as readFileSync39, writeFileSync as writeFileSync31, copyFileSync as copyFileSync4, existsSync as existsSync31 } from "fs";
|
|
76621
77352
|
import { join as join56 } from "path";
|
|
76622
77353
|
import { homedir as homedir36 } from "os";
|
|
76623
77354
|
import { createInterface as createInterface13 } from "readline";
|
|
@@ -76631,7 +77362,7 @@ function readConfig3() {
|
|
|
76631
77362
|
function writeConfig2(patch) {
|
|
76632
77363
|
ensureStateDir(TH_DIR);
|
|
76633
77364
|
const merged = { ...readConfig3(), ...patch };
|
|
76634
|
-
|
|
77365
|
+
writeFileSync31(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
76635
77366
|
}
|
|
76636
77367
|
function backupSettings() {
|
|
76637
77368
|
if (!existsSync31(SETTINGS_PATH)) return null;
|
|
@@ -76877,7 +77608,7 @@ var jpi_sync_exports = {};
|
|
|
76877
77608
|
__export(jpi_sync_exports, {
|
|
76878
77609
|
run: () => run26
|
|
76879
77610
|
});
|
|
76880
|
-
import { readFileSync as readFileSync40, writeFileSync as
|
|
77611
|
+
import { readFileSync as readFileSync40, writeFileSync as writeFileSync32, existsSync as existsSync32, rmSync as rmSync16 } from "fs";
|
|
76881
77612
|
import { join as join57 } from "path";
|
|
76882
77613
|
import { homedir as homedir37, hostname as osHostname2 } from "os";
|
|
76883
77614
|
import { createInterface as createInterface14 } from "readline";
|
|
@@ -76902,7 +77633,7 @@ function readMarker() {
|
|
|
76902
77633
|
}
|
|
76903
77634
|
function writeMarker(marker) {
|
|
76904
77635
|
ensureStateDir(TH_DIR2);
|
|
76905
|
-
|
|
77636
|
+
writeFileSync32(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
76906
77637
|
}
|
|
76907
77638
|
function clearMarker() {
|
|
76908
77639
|
try {
|