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-mcp.js
CHANGED
|
@@ -482,6 +482,7 @@ __export(github_auth_exports, {
|
|
|
482
482
|
decrypt: () => decrypt,
|
|
483
483
|
deleteGitHubToken: () => deleteGitHubToken,
|
|
484
484
|
encrypt: () => encrypt,
|
|
485
|
+
githubTokenDisplayPath: () => githubTokenDisplayPath,
|
|
485
486
|
hasGitHubToken: () => hasGitHubToken,
|
|
486
487
|
loadKey: () => loadKey,
|
|
487
488
|
readGitHubToken: () => readGitHubToken,
|
|
@@ -492,8 +493,12 @@ __export(github_auth_exports, {
|
|
|
492
493
|
});
|
|
493
494
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
|
|
494
495
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync4, rmSync as rmSync2, renameSync as renameSync3 } from "fs";
|
|
495
|
-
import { join as join7 } from "path";
|
|
496
|
+
import { join as join7, sep } from "path";
|
|
496
497
|
import { homedir as homedir6 } from "os";
|
|
498
|
+
function githubTokenDisplayPath() {
|
|
499
|
+
const home = homedir6();
|
|
500
|
+
return TOKEN_FILE.startsWith(home + sep) ? `~${TOKEN_FILE.slice(home.length)}` : TOKEN_FILE;
|
|
501
|
+
}
|
|
497
502
|
async function loadKey() {
|
|
498
503
|
return loadOrCreateSharedKey();
|
|
499
504
|
}
|
|
@@ -27069,12 +27074,33 @@ function scrubEndpointEnv(env) {
|
|
|
27069
27074
|
}
|
|
27070
27075
|
return out;
|
|
27071
27076
|
}
|
|
27072
|
-
function
|
|
27077
|
+
function containerFlagsAt(args) {
|
|
27078
|
+
if (args[0] === "run" || args[0] === "create")
|
|
27079
|
+
return 1;
|
|
27080
|
+
if (args[0] === "container" && (args[1] === "run" || args[1] === "create"))
|
|
27081
|
+
return 2;
|
|
27082
|
+
return -1;
|
|
27083
|
+
}
|
|
27084
|
+
function remoteDockerClient(endpoint, opts = {}) {
|
|
27073
27085
|
if (endpoint.trim().length === 0) {
|
|
27074
27086
|
throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
|
|
27075
27087
|
}
|
|
27076
27088
|
const target = endpoint.trim();
|
|
27077
|
-
const
|
|
27089
|
+
const tlsFlags = [];
|
|
27090
|
+
if (opts.tls) {
|
|
27091
|
+
for (const key of ["caPath", "certPath", "keyPath"]) {
|
|
27092
|
+
if (opts.tls[key].trim().length === 0) {
|
|
27093
|
+
throw new Error(`remoteDockerClient: tls.${key} must name a file`);
|
|
27094
|
+
}
|
|
27095
|
+
}
|
|
27096
|
+
tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
|
|
27097
|
+
}
|
|
27098
|
+
const runArgs = opts.runArgs ?? [];
|
|
27099
|
+
const argv = (args) => {
|
|
27100
|
+
const at = runArgs.length > 0 ? containerFlagsAt(args) : -1;
|
|
27101
|
+
const sub = at < 0 ? [...args] : [...args.slice(0, at), ...runArgs, ...args.slice(at)];
|
|
27102
|
+
return ["--host", target, ...tlsFlags, ...sub];
|
|
27103
|
+
};
|
|
27078
27104
|
return {
|
|
27079
27105
|
endpoint: target,
|
|
27080
27106
|
// Always empty, and it is a fact about this client rather than about the
|
|
@@ -27091,10 +27117,10 @@ function remoteDockerClient(endpoint) {
|
|
|
27091
27117
|
env: scrubEndpointEnv(process.env)
|
|
27092
27118
|
});
|
|
27093
27119
|
},
|
|
27094
|
-
sync(args,
|
|
27120
|
+
sync(args, opts2) {
|
|
27095
27121
|
return syncResult(spawnSync3(DOCKER_BIN, argv(args), {
|
|
27096
27122
|
encoding: "utf8",
|
|
27097
|
-
timeout:
|
|
27123
|
+
timeout: opts2?.timeoutMs,
|
|
27098
27124
|
env: scrubEndpointEnv(process.env)
|
|
27099
27125
|
}));
|
|
27100
27126
|
},
|
|
@@ -29077,13 +29103,192 @@ var init_gceIdentity = __esm({
|
|
|
29077
29103
|
|
|
29078
29104
|
// ../../packages/attest/dist/confidentialSpace.js
|
|
29079
29105
|
import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
|
|
29080
|
-
|
|
29106
|
+
function refuse2(reason, detail) {
|
|
29107
|
+
return { ok: false, reason, detail };
|
|
29108
|
+
}
|
|
29109
|
+
function isPlainObject(v) {
|
|
29110
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29111
|
+
}
|
|
29112
|
+
function assertExpectationsComplete2(expectations) {
|
|
29113
|
+
const required2 = [
|
|
29114
|
+
["audience", expectations.audience],
|
|
29115
|
+
["nonce", expectations.nonce],
|
|
29116
|
+
["instanceName", expectations.instanceName],
|
|
29117
|
+
["projectId", expectations.projectId],
|
|
29118
|
+
...(expectations.additionalNonces ?? []).map((n) => [
|
|
29119
|
+
"additionalNonces[]",
|
|
29120
|
+
n
|
|
29121
|
+
]),
|
|
29122
|
+
...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
|
|
29123
|
+
["containerEnv name", name],
|
|
29124
|
+
[`containerEnv.${name}`, value]
|
|
29125
|
+
])
|
|
29126
|
+
];
|
|
29127
|
+
for (const [name, value] of required2) {
|
|
29128
|
+
if (value === "") {
|
|
29129
|
+
throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
|
|
29130
|
+
}
|
|
29131
|
+
}
|
|
29132
|
+
}
|
|
29133
|
+
function nonceList(v) {
|
|
29134
|
+
if (typeof v === "string")
|
|
29135
|
+
return [v];
|
|
29136
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
29137
|
+
return null;
|
|
29138
|
+
if (!v.every((n) => typeof n === "string"))
|
|
29139
|
+
return null;
|
|
29140
|
+
return v;
|
|
29141
|
+
}
|
|
29142
|
+
async function verifyConfidentialSpaceToken(token, expectations, deps) {
|
|
29143
|
+
assertExpectationsComplete2(expectations);
|
|
29144
|
+
if (!Number.isFinite(deps.now)) {
|
|
29145
|
+
throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
|
|
29146
|
+
}
|
|
29147
|
+
if (expectations.allowedImageDigests.length === 0) {
|
|
29148
|
+
return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
|
|
29149
|
+
}
|
|
29150
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
29151
|
+
return refuse2("token-malformed", "longer than any attestation token");
|
|
29152
|
+
}
|
|
29153
|
+
const parts = token.split(".");
|
|
29154
|
+
if (parts.length !== 3) {
|
|
29155
|
+
return refuse2("token-malformed", "not a three-segment compact JWT");
|
|
29156
|
+
}
|
|
29157
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
29158
|
+
const header = decodeJsonSegment(headerB64);
|
|
29159
|
+
if (header === null) {
|
|
29160
|
+
return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
|
|
29161
|
+
}
|
|
29162
|
+
if (header.alg !== "RS256") {
|
|
29163
|
+
return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
|
|
29164
|
+
}
|
|
29165
|
+
if (typeof header.kid !== "string" || header.kid === "") {
|
|
29166
|
+
return refuse2("unknown-key", "token header names no key id");
|
|
29167
|
+
}
|
|
29168
|
+
const jwks = await deps.fetchJwks();
|
|
29169
|
+
const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
|
|
29170
|
+
if (jwk === void 0) {
|
|
29171
|
+
return refuse2("unknown-key", "token key id is not in the published key set");
|
|
29172
|
+
}
|
|
29173
|
+
const signature = strictBase64UrlDecode(signatureB64);
|
|
29174
|
+
if (signature === null) {
|
|
29175
|
+
return refuse2("token-malformed", "signature segment is not base64url");
|
|
29176
|
+
}
|
|
29177
|
+
let signatureValid = false;
|
|
29178
|
+
try {
|
|
29179
|
+
const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
|
|
29180
|
+
signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
29181
|
+
} catch {
|
|
29182
|
+
signatureValid = false;
|
|
29183
|
+
}
|
|
29184
|
+
if (!signatureValid) {
|
|
29185
|
+
return refuse2("signature-invalid", "signature does not verify under the named key");
|
|
29186
|
+
}
|
|
29187
|
+
const payload = decodeJsonSegment(payloadB64);
|
|
29188
|
+
if (payload === null) {
|
|
29189
|
+
return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
|
|
29190
|
+
}
|
|
29191
|
+
if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
|
|
29192
|
+
return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
|
|
29193
|
+
}
|
|
29194
|
+
if (payload.aud !== expectations.audience) {
|
|
29195
|
+
return refuse2("wrong-audience", "audience is not the one this run expects");
|
|
29196
|
+
}
|
|
29197
|
+
const nowSeconds = deps.now / 1e3;
|
|
29198
|
+
const exp = payload.exp;
|
|
29199
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
|
|
29200
|
+
return refuse2("expired", "exp is missing, malformed, or in the past");
|
|
29201
|
+
}
|
|
29202
|
+
const iat = payload.iat;
|
|
29203
|
+
if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
|
|
29204
|
+
return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
|
|
29205
|
+
}
|
|
29206
|
+
const nonces = nonceList(payload.eat_nonce);
|
|
29207
|
+
if (nonces === null) {
|
|
29208
|
+
return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
|
|
29209
|
+
}
|
|
29210
|
+
if (!nonces.includes(expectations.nonce)) {
|
|
29211
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
|
|
29212
|
+
}
|
|
29213
|
+
for (const extra of expectations.additionalNonces ?? []) {
|
|
29214
|
+
if (!nonces.includes(extra)) {
|
|
29215
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
|
|
29216
|
+
}
|
|
29217
|
+
}
|
|
29218
|
+
if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
|
|
29219
|
+
return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
|
|
29220
|
+
}
|
|
29221
|
+
if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
|
|
29222
|
+
return refuse2("debug-enabled", "the image is not a production image with debug disabled");
|
|
29223
|
+
}
|
|
29224
|
+
if (payload.secboot !== true) {
|
|
29225
|
+
return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
|
|
29226
|
+
}
|
|
29227
|
+
if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
|
|
29228
|
+
return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
|
|
29229
|
+
}
|
|
29230
|
+
const hwmodel = payload.hwmodel;
|
|
29231
|
+
const submods = payload.submods;
|
|
29232
|
+
const gce = isPlainObject(submods) ? submods.gce : void 0;
|
|
29233
|
+
if (!isPlainObject(gce)) {
|
|
29234
|
+
return refuse2("submods-gce-missing", "no submods.gce block names the instance");
|
|
29235
|
+
}
|
|
29236
|
+
if (gce.project_id !== expectations.projectId) {
|
|
29237
|
+
return refuse2("project-mismatch", "token names an instance in a different project");
|
|
29238
|
+
}
|
|
29239
|
+
if (gce.instance_name !== expectations.instanceName) {
|
|
29240
|
+
return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
|
|
29241
|
+
}
|
|
29242
|
+
if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
|
|
29243
|
+
return refuse2("instance-id-missing", "instance id is absent or not a string");
|
|
29244
|
+
}
|
|
29245
|
+
const instanceId = gce.instance_id;
|
|
29246
|
+
if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
|
|
29247
|
+
return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
|
|
29248
|
+
}
|
|
29249
|
+
const zone = gce.zone;
|
|
29250
|
+
const container = isPlainObject(submods) ? submods.container : void 0;
|
|
29251
|
+
if (!isPlainObject(container)) {
|
|
29252
|
+
return refuse2("submods-container-missing", "no submods.container block names the workload");
|
|
29253
|
+
}
|
|
29254
|
+
if (typeof container.image_reference !== "string" || container.image_reference === "") {
|
|
29255
|
+
return refuse2("image-reference-missing", "the workload image reference is absent");
|
|
29256
|
+
}
|
|
29257
|
+
const imageReference = container.image_reference;
|
|
29258
|
+
if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
|
|
29259
|
+
return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
|
|
29260
|
+
}
|
|
29261
|
+
const env = container.env;
|
|
29262
|
+
for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
|
|
29263
|
+
if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
|
|
29264
|
+
return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
|
|
29265
|
+
}
|
|
29266
|
+
}
|
|
29267
|
+
return {
|
|
29268
|
+
ok: true,
|
|
29269
|
+
value: {
|
|
29270
|
+
instanceId,
|
|
29271
|
+
instanceName: expectations.instanceName,
|
|
29272
|
+
zone,
|
|
29273
|
+
projectId: expectations.projectId,
|
|
29274
|
+
imageDigest: container.image_digest,
|
|
29275
|
+
imageReference,
|
|
29276
|
+
hwmodel,
|
|
29277
|
+
iat,
|
|
29278
|
+
exp,
|
|
29279
|
+
tokenSha256: createHash7("sha256").update(token, "utf8").digest("hex")
|
|
29280
|
+
}
|
|
29281
|
+
};
|
|
29282
|
+
}
|
|
29283
|
+
var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
|
|
29081
29284
|
var init_confidentialSpace = __esm({
|
|
29082
29285
|
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
29083
29286
|
"use strict";
|
|
29084
29287
|
init_gceIdentity();
|
|
29085
29288
|
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
29086
29289
|
CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
|
|
29290
|
+
CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
|
|
29291
|
+
CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
|
|
29087
29292
|
}
|
|
29088
29293
|
});
|
|
29089
29294
|
|
|
@@ -29426,50 +29631,50 @@ function toTestRunResult(result, outputSha256) {
|
|
|
29426
29631
|
function toAcceptancePredicate(pair, opts = {}) {
|
|
29427
29632
|
const { baseline, patched } = pair;
|
|
29428
29633
|
if (baseline.runId === patched.runId) {
|
|
29429
|
-
return
|
|
29634
|
+
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.`);
|
|
29430
29635
|
}
|
|
29431
29636
|
if (patched.status === "refused") {
|
|
29432
|
-
return
|
|
29637
|
+
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`);
|
|
29433
29638
|
}
|
|
29434
29639
|
if (patched.outcome === null) {
|
|
29435
|
-
return
|
|
29640
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
29436
29641
|
}
|
|
29437
29642
|
if (baseline.status === "refused") {
|
|
29438
|
-
return
|
|
29643
|
+
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`);
|
|
29439
29644
|
}
|
|
29440
29645
|
if (baseline.outcome === null) {
|
|
29441
|
-
return
|
|
29646
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
29442
29647
|
}
|
|
29443
29648
|
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
29444
|
-
return
|
|
29649
|
+
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.`);
|
|
29445
29650
|
}
|
|
29446
29651
|
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
29447
29652
|
if (budget === null) {
|
|
29448
|
-
return
|
|
29653
|
+
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.`);
|
|
29449
29654
|
}
|
|
29450
29655
|
if (patched.testCommand === null) {
|
|
29451
|
-
return
|
|
29656
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
29452
29657
|
}
|
|
29453
29658
|
if (baseline.testCommand !== patched.testCommand) {
|
|
29454
|
-
return
|
|
29659
|
+
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.`);
|
|
29455
29660
|
}
|
|
29456
29661
|
if (baseline.targetSha !== patched.targetSha) {
|
|
29457
|
-
return
|
|
29662
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
29458
29663
|
}
|
|
29459
29664
|
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
29460
|
-
return
|
|
29665
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
29461
29666
|
}
|
|
29462
29667
|
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
29463
|
-
return
|
|
29668
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
29464
29669
|
}
|
|
29465
29670
|
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
29466
|
-
return
|
|
29671
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
29467
29672
|
}
|
|
29468
29673
|
if (baseline.treeDigest === patched.treeDigest) {
|
|
29469
|
-
return
|
|
29674
|
+
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`);
|
|
29470
29675
|
}
|
|
29471
29676
|
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
29472
|
-
return
|
|
29677
|
+
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.`);
|
|
29473
29678
|
}
|
|
29474
29679
|
for (const [side, outcome, counts, exitCode] of [
|
|
29475
29680
|
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
@@ -29477,7 +29682,7 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
29477
29682
|
]) {
|
|
29478
29683
|
const why = contradicts(outcome, counts, exitCode);
|
|
29479
29684
|
if (why !== null) {
|
|
29480
|
-
return
|
|
29685
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
29481
29686
|
}
|
|
29482
29687
|
}
|
|
29483
29688
|
for (const [side, source] of [
|
|
@@ -29486,44 +29691,44 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
29486
29691
|
]) {
|
|
29487
29692
|
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
29488
29693
|
if (why !== null)
|
|
29489
|
-
return
|
|
29694
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
29490
29695
|
}
|
|
29491
29696
|
if (patched.testCommandSource === "developer-declared") {
|
|
29492
|
-
return
|
|
29697
|
+
return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
|
|
29493
29698
|
}
|
|
29494
29699
|
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
29495
|
-
return
|
|
29700
|
+
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.`);
|
|
29496
29701
|
}
|
|
29497
29702
|
if (baseline.containerImage !== patched.containerImage) {
|
|
29498
|
-
return
|
|
29703
|
+
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.`);
|
|
29499
29704
|
}
|
|
29500
29705
|
if (patched.containerImage === null || patched.containerImage === "") {
|
|
29501
|
-
return
|
|
29706
|
+
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");
|
|
29502
29707
|
}
|
|
29503
29708
|
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
29504
|
-
return
|
|
29709
|
+
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.");
|
|
29505
29710
|
}
|
|
29506
29711
|
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
29507
|
-
return
|
|
29712
|
+
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.`);
|
|
29508
29713
|
}
|
|
29509
29714
|
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
29510
|
-
return
|
|
29715
|
+
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.`);
|
|
29511
29716
|
}
|
|
29512
29717
|
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
29513
|
-
return
|
|
29718
|
+
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.");
|
|
29514
29719
|
}
|
|
29515
29720
|
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
29516
|
-
return
|
|
29721
|
+
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.`);
|
|
29517
29722
|
}
|
|
29518
29723
|
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
29519
|
-
return
|
|
29724
|
+
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.`);
|
|
29520
29725
|
}
|
|
29521
29726
|
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
29522
|
-
return
|
|
29727
|
+
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.`);
|
|
29523
29728
|
}
|
|
29524
29729
|
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
29525
29730
|
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
29526
|
-
return
|
|
29731
|
+
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.`);
|
|
29527
29732
|
}
|
|
29528
29733
|
return {
|
|
29529
29734
|
ok: true,
|
|
@@ -29550,7 +29755,7 @@ function signRunStatement(predicate, privateKey, keyid) {
|
|
|
29550
29755
|
const statement = createAcceptanceStatement(predicate);
|
|
29551
29756
|
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
29552
29757
|
}
|
|
29553
|
-
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS,
|
|
29758
|
+
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;
|
|
29554
29759
|
var init_attestation2 = __esm({
|
|
29555
29760
|
"../../packages/envrun/dist/attestation.js"() {
|
|
29556
29761
|
"use strict";
|
|
@@ -29654,7 +29859,7 @@ var init_attestation2 = __esm({
|
|
|
29654
29859
|
"outcome-contradicts-counts",
|
|
29655
29860
|
"test-command-origin-unattested"
|
|
29656
29861
|
];
|
|
29657
|
-
|
|
29862
|
+
refuse3 = (reason, detail) => ({
|
|
29658
29863
|
ok: false,
|
|
29659
29864
|
reason,
|
|
29660
29865
|
detail
|
|
@@ -30065,6 +30270,22 @@ function bootImageFlags(p) {
|
|
|
30065
30270
|
}
|
|
30066
30271
|
const workloadImage = p.workloadImage;
|
|
30067
30272
|
assertWorkloadImage("gcpBootArgv", workloadImage);
|
|
30273
|
+
const clientCert = p.clientCert;
|
|
30274
|
+
if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
|
|
30275
|
+
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.`);
|
|
30276
|
+
}
|
|
30277
|
+
if (p.runId.length < MIN_NONCE_BYTES) {
|
|
30278
|
+
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.`);
|
|
30279
|
+
}
|
|
30280
|
+
const metadata = [
|
|
30281
|
+
`tee-image-reference=${workloadImage}`,
|
|
30282
|
+
"tee-container-log-redirect=true",
|
|
30283
|
+
'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
|
|
30284
|
+
"tee-cgroup-ns=true",
|
|
30285
|
+
`tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
|
|
30286
|
+
`tee-env-TH_NONCE=${p.runId}`,
|
|
30287
|
+
`tee-env-TH_CLIENT_CERT=${clientCert}`
|
|
30288
|
+
];
|
|
30068
30289
|
return [
|
|
30069
30290
|
// The production family. The debug family reports `dbgstat: enabled`, which the
|
|
30070
30291
|
// intake's verifier refuses, so a debug boot could never produce a run we record.
|
|
@@ -30077,7 +30298,15 @@ function bootImageFlags(p) {
|
|
|
30077
30298
|
// most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
|
|
30078
30299
|
// combination is phase 5's measurement.
|
|
30079
30300
|
"--maintenance-policy=TERMINATE",
|
|
30080
|
-
`--metadata
|
|
30301
|
+
`--metadata=^;^${metadata.join(";")}`,
|
|
30302
|
+
// What the standing firewall rule for IAP's range matches, so the tunnels to the
|
|
30303
|
+
// workload's dockerd and attest ports open on these VMs and no others.
|
|
30304
|
+
"--tags=th-cs-venue",
|
|
30305
|
+
// The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
|
|
30306
|
+
// runs vfs, which copies every layer and every container's rootfs in full. Pulling
|
|
30307
|
+
// node:20-bookworm left 992 MB, and the next container create failed ENOSPC
|
|
30308
|
+
// (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
|
|
30309
|
+
"--boot-disk-size=50GB"
|
|
30081
30310
|
];
|
|
30082
30311
|
}
|
|
30083
30312
|
function assertInstanceIdentity(fn, id) {
|
|
@@ -30151,7 +30380,13 @@ function gcpBootArgv(p) {
|
|
|
30151
30380
|
// expectation uses (TERM-832): two inline spellings of one value is how
|
|
30152
30381
|
// the boot path and the verifier drift apart silently.
|
|
30153
30382
|
`--service-account=${venueIdentityEmail(p.project)}`,
|
|
30154
|
-
|
|
30383
|
+
// A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
|
|
30384
|
+
// pulls the workload image and mints the attestation token with this account, and
|
|
30385
|
+
// under `--no-scopes` it can do neither. What bounds a lifted token there is the
|
|
30386
|
+
// account's two grants — `confidentialcomputing.workloadUser` and a reader on the
|
|
30387
|
+
// one venue image repository — plus point 2 above, which is unchanged: the fence
|
|
30388
|
+
// still denies untrusted code the metadata server.
|
|
30389
|
+
...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
|
|
30155
30390
|
"--shielded-secure-boot",
|
|
30156
30391
|
"--shielded-vtpm",
|
|
30157
30392
|
"--shielded-integrity-monitoring",
|
|
@@ -30184,7 +30419,7 @@ function gcpDeleteArgv(p) {
|
|
|
30184
30419
|
"--quiet"
|
|
30185
30420
|
];
|
|
30186
30421
|
}
|
|
30187
|
-
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;
|
|
30422
|
+
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;
|
|
30188
30423
|
var init_gcpPlacement = __esm({
|
|
30189
30424
|
"../../packages/envrun/dist/gcpPlacement.js"() {
|
|
30190
30425
|
"use strict";
|
|
@@ -30203,6 +30438,8 @@ var init_gcpPlacement = __esm({
|
|
|
30203
30438
|
GCP_RUN_LABEL_KEY = "th-run";
|
|
30204
30439
|
CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
|
|
30205
30440
|
WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
|
|
30441
|
+
CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
|
|
30442
|
+
MIN_NONCE_BYTES = 10;
|
|
30206
30443
|
GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
|
|
30207
30444
|
GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
|
30208
30445
|
GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -30244,7 +30481,10 @@ var init_emptyGitConfig = __esm({
|
|
|
30244
30481
|
|
|
30245
30482
|
// ../../packages/envrun/dist/hostedVenue.js
|
|
30246
30483
|
import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
|
|
30247
|
-
import {
|
|
30484
|
+
import { createHash as createHash9, X509Certificate } from "crypto";
|
|
30485
|
+
import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "fs";
|
|
30486
|
+
import { request as httpsRequest } from "https";
|
|
30487
|
+
import { createServer } from "net";
|
|
30248
30488
|
import { join as join26 } from "path";
|
|
30249
30489
|
import { tmpdir as tmpdir3 } from "os";
|
|
30250
30490
|
function credentialInGitConfig(text) {
|
|
@@ -30446,6 +30686,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
|
|
|
30446
30686
|
});
|
|
30447
30687
|
});
|
|
30448
30688
|
}
|
|
30689
|
+
function socketErrorReason(err) {
|
|
30690
|
+
if (err.message !== "")
|
|
30691
|
+
return err.message;
|
|
30692
|
+
const code = err.code;
|
|
30693
|
+
if (code !== void 0)
|
|
30694
|
+
return code;
|
|
30695
|
+
const inner = err.errors;
|
|
30696
|
+
if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
|
|
30697
|
+
return inner[0].message;
|
|
30698
|
+
}
|
|
30699
|
+
return err.name;
|
|
30700
|
+
}
|
|
30701
|
+
function fetchAttestationOverTls(req) {
|
|
30702
|
+
return new Promise((resolve4) => {
|
|
30703
|
+
let whole;
|
|
30704
|
+
const settle = (reading) => {
|
|
30705
|
+
if (whole !== void 0)
|
|
30706
|
+
clearTimeout(whole);
|
|
30707
|
+
resolve4(reading);
|
|
30708
|
+
};
|
|
30709
|
+
let cert;
|
|
30710
|
+
let key;
|
|
30711
|
+
try {
|
|
30712
|
+
cert = readFileSync13(req.certPath);
|
|
30713
|
+
key = readFileSync13(req.keyPath);
|
|
30714
|
+
} catch (err) {
|
|
30715
|
+
settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
|
|
30716
|
+
return;
|
|
30717
|
+
}
|
|
30718
|
+
const call = httpsRequest({
|
|
30719
|
+
host: "localhost",
|
|
30720
|
+
port: req.port,
|
|
30721
|
+
path: "/attest",
|
|
30722
|
+
method: "GET",
|
|
30723
|
+
servername: "localhost",
|
|
30724
|
+
cert,
|
|
30725
|
+
key,
|
|
30726
|
+
rejectUnauthorized: false,
|
|
30727
|
+
agent: false,
|
|
30728
|
+
timeout: req.timeoutMs
|
|
30729
|
+
}, (res) => {
|
|
30730
|
+
const peer = res.socket.getPeerCertificate();
|
|
30731
|
+
let body = "";
|
|
30732
|
+
res.setEncoding("utf8");
|
|
30733
|
+
res.on("data", (chunk) => {
|
|
30734
|
+
body += chunk;
|
|
30735
|
+
if (body.length > MAX_TOKEN_LENGTH) {
|
|
30736
|
+
settle({ ok: false, reason: "the answer is longer than any attestation token" });
|
|
30737
|
+
res.destroy();
|
|
30738
|
+
}
|
|
30739
|
+
});
|
|
30740
|
+
res.on("end", () => {
|
|
30741
|
+
if (res.statusCode !== 200) {
|
|
30742
|
+
settle({
|
|
30743
|
+
ok: false,
|
|
30744
|
+
reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
|
|
30745
|
+
});
|
|
30746
|
+
return;
|
|
30747
|
+
}
|
|
30748
|
+
const raw = peer.raw;
|
|
30749
|
+
if (raw === void 0 || raw.length === 0) {
|
|
30750
|
+
settle({ ok: false, reason: "the attest endpoint presented no certificate" });
|
|
30751
|
+
return;
|
|
30752
|
+
}
|
|
30753
|
+
settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
|
|
30754
|
+
});
|
|
30755
|
+
res.on("error", (err) => {
|
|
30756
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
30757
|
+
});
|
|
30758
|
+
});
|
|
30759
|
+
whole = setTimeout(() => {
|
|
30760
|
+
settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
|
|
30761
|
+
call.destroy();
|
|
30762
|
+
}, req.timeoutMs);
|
|
30763
|
+
call.on("timeout", () => {
|
|
30764
|
+
call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
|
|
30765
|
+
});
|
|
30766
|
+
call.on("error", (err) => {
|
|
30767
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
30768
|
+
});
|
|
30769
|
+
call.end();
|
|
30770
|
+
});
|
|
30771
|
+
}
|
|
30449
30772
|
function quoteForRemoteShell(arg) {
|
|
30450
30773
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
30451
30774
|
}
|
|
@@ -30780,27 +31103,370 @@ ${res.stderr}`);
|
|
|
30780
31103
|
function manualDeleteCommand(vm, project, zone) {
|
|
30781
31104
|
return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
|
|
30782
31105
|
}
|
|
31106
|
+
function clientCertArgv(keyPath, certPath) {
|
|
31107
|
+
return [
|
|
31108
|
+
"req",
|
|
31109
|
+
"-x509",
|
|
31110
|
+
"-newkey",
|
|
31111
|
+
"ec",
|
|
31112
|
+
"-pkeyopt",
|
|
31113
|
+
"ec_paramgen_curve:P-256",
|
|
31114
|
+
"-nodes",
|
|
31115
|
+
"-keyout",
|
|
31116
|
+
keyPath,
|
|
31117
|
+
"-out",
|
|
31118
|
+
certPath,
|
|
31119
|
+
"-subj",
|
|
31120
|
+
"/CN=terminalhire-worker",
|
|
31121
|
+
"-days",
|
|
31122
|
+
"1",
|
|
31123
|
+
"-addext",
|
|
31124
|
+
"extendedKeyUsage=clientAuth"
|
|
31125
|
+
];
|
|
31126
|
+
}
|
|
31127
|
+
function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
|
|
31128
|
+
return [
|
|
31129
|
+
"compute",
|
|
31130
|
+
"start-iap-tunnel",
|
|
31131
|
+
vm,
|
|
31132
|
+
String(remotePort),
|
|
31133
|
+
`--local-host-port=localhost:${String(localPort)}`,
|
|
31134
|
+
`--project=${project}`,
|
|
31135
|
+
`--zone=${zone}`,
|
|
31136
|
+
"--iap-tunnel-disable-connection-check"
|
|
31137
|
+
];
|
|
31138
|
+
}
|
|
31139
|
+
function spkiSha256Hex(certDer) {
|
|
31140
|
+
const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
|
|
31141
|
+
return createHash9("sha256").update(spki).digest("hex");
|
|
31142
|
+
}
|
|
31143
|
+
function certMetadataValue(pem) {
|
|
31144
|
+
return new X509Certificate(pem).raw.toString("base64url");
|
|
31145
|
+
}
|
|
31146
|
+
function requireWorkloadDigest(reference) {
|
|
31147
|
+
const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
|
|
31148
|
+
if (named === null) {
|
|
31149
|
+
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.");
|
|
31150
|
+
}
|
|
31151
|
+
return named[1];
|
|
31152
|
+
}
|
|
31153
|
+
function underStageRoot(path5) {
|
|
31154
|
+
if (!path5.startsWith(`${CS_STAGE_ROOT}/`)) {
|
|
31155
|
+
throw new Error(`${path5} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
|
|
31156
|
+
}
|
|
31157
|
+
return path5;
|
|
31158
|
+
}
|
|
31159
|
+
function userFlag(owner) {
|
|
31160
|
+
return `--user=${String(owner.uid)}:${String(owner.gid)}`;
|
|
31161
|
+
}
|
|
31162
|
+
function csStagePrepareArgv(stageBase, owner) {
|
|
31163
|
+
const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
|
|
31164
|
+
return [
|
|
31165
|
+
"run",
|
|
31166
|
+
"--rm",
|
|
31167
|
+
"--network=none",
|
|
31168
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
31169
|
+
"--",
|
|
31170
|
+
STAGE_HELPER_IMAGE,
|
|
31171
|
+
"sh",
|
|
31172
|
+
"-c",
|
|
31173
|
+
`mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
|
|
31174
|
+
];
|
|
31175
|
+
}
|
|
31176
|
+
function csUntarArgv(dir, owner) {
|
|
31177
|
+
return [
|
|
31178
|
+
"run",
|
|
31179
|
+
"-i",
|
|
31180
|
+
"--rm",
|
|
31181
|
+
"--network=none",
|
|
31182
|
+
userFlag(owner),
|
|
31183
|
+
`--volume=${underStageRoot(dir)}:/dst:rw`,
|
|
31184
|
+
"--",
|
|
31185
|
+
STAGE_HELPER_IMAGE,
|
|
31186
|
+
"tar",
|
|
31187
|
+
"-xf",
|
|
31188
|
+
"-",
|
|
31189
|
+
"-C",
|
|
31190
|
+
"/dst"
|
|
31191
|
+
];
|
|
31192
|
+
}
|
|
31193
|
+
function csUntarProxyArgv(dir, owner) {
|
|
31194
|
+
const d = quoteForRemoteShell(underStageRoot(dir));
|
|
31195
|
+
return [
|
|
31196
|
+
"run",
|
|
31197
|
+
"-i",
|
|
31198
|
+
"--rm",
|
|
31199
|
+
"--network=none",
|
|
31200
|
+
userFlag(owner),
|
|
31201
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
31202
|
+
"--",
|
|
31203
|
+
STAGE_HELPER_IMAGE,
|
|
31204
|
+
"sh",
|
|
31205
|
+
"-c",
|
|
31206
|
+
`mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
|
|
31207
|
+
];
|
|
31208
|
+
}
|
|
31209
|
+
function csRemoveArgv(dir, owner) {
|
|
31210
|
+
return [
|
|
31211
|
+
"run",
|
|
31212
|
+
"--rm",
|
|
31213
|
+
"--network=none",
|
|
31214
|
+
userFlag(owner),
|
|
31215
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
31216
|
+
"--",
|
|
31217
|
+
STAGE_HELPER_IMAGE,
|
|
31218
|
+
"rm",
|
|
31219
|
+
"-rf",
|
|
31220
|
+
"--",
|
|
31221
|
+
underStageRoot(dir)
|
|
31222
|
+
];
|
|
31223
|
+
}
|
|
31224
|
+
function sshStageTransport(vm, project, zone, env, io) {
|
|
31225
|
+
return {
|
|
31226
|
+
/**
|
|
31227
|
+
* `id -u` / `id -g` on the venue, as the account that untarred the tree.
|
|
31228
|
+
*
|
|
31229
|
+
* That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
|
|
31230
|
+
* so asking it who it is answers exactly the question the mount needs, without
|
|
31231
|
+
* a chown and without assuming a uid. One ssh round trip, once per run.
|
|
31232
|
+
*
|
|
31233
|
+
* Both ids come from ONE invocation. Two calls could straddle a reconnect and
|
|
31234
|
+
* pair a uid with another session's gid — a third wrong answer rather than a
|
|
31235
|
+
* half-right one, which is the reason `GuestUser` is a pair in the first place.
|
|
31236
|
+
*/
|
|
31237
|
+
readOwner: () => {
|
|
31238
|
+
const res = io.exec(
|
|
31239
|
+
"gcloud",
|
|
31240
|
+
iapSshArgv(vm, project, zone, "id -u && id -g"),
|
|
31241
|
+
OWNER_PROBE_TIMEOUT_MS,
|
|
31242
|
+
// `env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
31243
|
+
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
31244
|
+
// omitted this would ssh under the person's own credential — then report an owner
|
|
31245
|
+
// measured against a session the run does not use.
|
|
31246
|
+
env
|
|
31247
|
+
);
|
|
31248
|
+
if (!res.ok) {
|
|
31249
|
+
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.`);
|
|
31250
|
+
}
|
|
31251
|
+
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
31252
|
+
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
31253
|
+
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
31254
|
+
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.`);
|
|
31255
|
+
}
|
|
31256
|
+
return { uid: ids2[0], gid: ids2[1] };
|
|
31257
|
+
},
|
|
31258
|
+
push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
|
|
31259
|
+
pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
|
|
31260
|
+
removeProxy: (dir) => {
|
|
31261
|
+
io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
|
|
31262
|
+
}
|
|
31263
|
+
};
|
|
31264
|
+
}
|
|
31265
|
+
function dockerStageTransport(docker3, env, io) {
|
|
31266
|
+
const pushVia = (from, args) => {
|
|
31267
|
+
const [file, ...rest] = docker3.commandLine(args);
|
|
31268
|
+
if (file === void 0)
|
|
31269
|
+
throw new Error("the docker client returned an empty command line");
|
|
31270
|
+
return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
|
|
31271
|
+
};
|
|
31272
|
+
return {
|
|
31273
|
+
// Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
|
|
31274
|
+
readOwner: () => CS_GUEST_USER,
|
|
31275
|
+
push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
|
|
31276
|
+
pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
|
|
31277
|
+
removeProxy: (dir) => {
|
|
31278
|
+
docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
|
|
31279
|
+
}
|
|
31280
|
+
};
|
|
31281
|
+
}
|
|
31282
|
+
function bothTunnels(a, b) {
|
|
31283
|
+
return {
|
|
31284
|
+
kill: (signal) => {
|
|
31285
|
+
let failed = false;
|
|
31286
|
+
let error2;
|
|
31287
|
+
for (const t of [a, b]) {
|
|
31288
|
+
try {
|
|
31289
|
+
t.kill(signal);
|
|
31290
|
+
} catch (err) {
|
|
31291
|
+
if (!failed)
|
|
31292
|
+
error2 = err;
|
|
31293
|
+
failed = true;
|
|
31294
|
+
}
|
|
31295
|
+
}
|
|
31296
|
+
if (failed)
|
|
31297
|
+
throw error2;
|
|
31298
|
+
},
|
|
31299
|
+
failure: () => a.failure() ?? b.failure()
|
|
31300
|
+
};
|
|
31301
|
+
}
|
|
31302
|
+
function makeClientTls(io, allocated, vm) {
|
|
31303
|
+
let dir;
|
|
31304
|
+
try {
|
|
31305
|
+
dir = io.makePrivateDir();
|
|
31306
|
+
} catch (err) {
|
|
31307
|
+
throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
|
|
31308
|
+
}
|
|
31309
|
+
allocated.onRollback(() => {
|
|
31310
|
+
try {
|
|
31311
|
+
io.removeTree(dir);
|
|
31312
|
+
} catch (err) {
|
|
31313
|
+
throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
|
|
31314
|
+
}
|
|
31315
|
+
});
|
|
31316
|
+
const certPath = join26(dir, "client.pem");
|
|
31317
|
+
const keyPath = join26(dir, "client-key.pem");
|
|
31318
|
+
const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
|
|
31319
|
+
if (!made.ok) {
|
|
31320
|
+
throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
|
|
31321
|
+
}
|
|
31322
|
+
let metadataValue;
|
|
31323
|
+
try {
|
|
31324
|
+
const pem = io.readTextIfPresent(certPath);
|
|
31325
|
+
if (pem === null)
|
|
31326
|
+
throw new Error(`openssl wrote nothing to ${certPath}`);
|
|
31327
|
+
metadataValue = certMetadataValue(pem);
|
|
31328
|
+
} catch (err) {
|
|
31329
|
+
throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
|
|
31330
|
+
}
|
|
31331
|
+
return { dir, certPath, keyPath, metadataValue };
|
|
31332
|
+
}
|
|
31333
|
+
async function reachConfidentialSpace(a) {
|
|
31334
|
+
const { io, vm } = a;
|
|
31335
|
+
let ports;
|
|
31336
|
+
try {
|
|
31337
|
+
ports = await io.freeLocalPorts(2);
|
|
31338
|
+
} catch (err) {
|
|
31339
|
+
throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
|
|
31340
|
+
}
|
|
31341
|
+
const [attestPort, dockerPort] = ports;
|
|
31342
|
+
if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
|
|
31343
|
+
throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
|
|
31344
|
+
}
|
|
31345
|
+
const forward = (remote, local) => {
|
|
31346
|
+
const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
|
|
31347
|
+
a.allocated.onRollback(() => {
|
|
31348
|
+
try {
|
|
31349
|
+
t.kill("SIGTERM");
|
|
31350
|
+
} catch {
|
|
31351
|
+
}
|
|
31352
|
+
});
|
|
31353
|
+
return t;
|
|
31354
|
+
};
|
|
31355
|
+
const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
|
|
31356
|
+
const deadline = io.now() + CS_READY_BUDGET_MS;
|
|
31357
|
+
let reading = { ok: false, reason: "no request was made" };
|
|
31358
|
+
while (io.now() < deadline) {
|
|
31359
|
+
const dead = tunnel.failure();
|
|
31360
|
+
if (dead !== null) {
|
|
31361
|
+
throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
|
|
31362
|
+
}
|
|
31363
|
+
reading = await io.fetchAttestation({
|
|
31364
|
+
port: attestPort,
|
|
31365
|
+
certPath: a.tls.certPath,
|
|
31366
|
+
keyPath: a.tls.keyPath,
|
|
31367
|
+
timeoutMs: CS_ATTEST_TIMEOUT_MS
|
|
31368
|
+
});
|
|
31369
|
+
if (reading.ok)
|
|
31370
|
+
break;
|
|
31371
|
+
await io.sleep(CS_ATTEST_INTERVAL_MS);
|
|
31372
|
+
}
|
|
31373
|
+
if (!reading.ok) {
|
|
31374
|
+
throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
|
|
31375
|
+
}
|
|
31376
|
+
let spki;
|
|
31377
|
+
let venuePem;
|
|
31378
|
+
try {
|
|
31379
|
+
spki = spkiSha256Hex(reading.peerCertDer);
|
|
31380
|
+
venuePem = new X509Certificate(reading.peerCertDer).toString();
|
|
31381
|
+
} catch (err) {
|
|
31382
|
+
throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
|
|
31383
|
+
}
|
|
31384
|
+
let jwks;
|
|
31385
|
+
try {
|
|
31386
|
+
jwks = await io.fetchConfidentialSpaceJwks();
|
|
31387
|
+
} catch (err) {
|
|
31388
|
+
throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
|
|
31389
|
+
}
|
|
31390
|
+
const verdict = await verifyConfidentialSpaceToken(reading.token, {
|
|
31391
|
+
audience: expectedAudience(a.runId),
|
|
31392
|
+
nonce: a.runId,
|
|
31393
|
+
additionalNonces: [spki],
|
|
31394
|
+
instanceName: vm,
|
|
31395
|
+
allowedZones: [a.zone],
|
|
31396
|
+
projectId: a.project,
|
|
31397
|
+
allowedImageDigests: [a.imageDigest],
|
|
31398
|
+
containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
|
|
31399
|
+
}, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
|
|
31400
|
+
if (!verdict.ok) {
|
|
31401
|
+
throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
|
|
31402
|
+
}
|
|
31403
|
+
const caPath = join26(a.tls.dir, "venue.pem");
|
|
31404
|
+
try {
|
|
31405
|
+
io.writeFile(caPath, venuePem);
|
|
31406
|
+
} catch (err) {
|
|
31407
|
+
throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
|
|
31408
|
+
}
|
|
31409
|
+
const dockerAt = `tcp://localhost:${String(dockerPort)}`;
|
|
31410
|
+
const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
|
|
31411
|
+
const daemon = io.classifyDaemon(docker3);
|
|
31412
|
+
if (!daemon.distinct) {
|
|
31413
|
+
throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
|
|
31414
|
+
}
|
|
31415
|
+
const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
|
|
31416
|
+
timeoutMs: MKDIR_TIMEOUT_MS
|
|
31417
|
+
});
|
|
31418
|
+
if (prepared.error || prepared.status !== 0) {
|
|
31419
|
+
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
|
|
31420
|
+
}
|
|
31421
|
+
const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
|
|
31422
|
+
if (pulled.error || pulled.status !== 0) {
|
|
31423
|
+
throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
|
|
31424
|
+
}
|
|
31425
|
+
return {
|
|
31426
|
+
docker: docker3,
|
|
31427
|
+
tunnel,
|
|
31428
|
+
socketDir: a.tls.dir,
|
|
31429
|
+
socketPath: dockerAt,
|
|
31430
|
+
venueDaemonId: daemon.venueDaemonId,
|
|
31431
|
+
venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
|
|
31432
|
+
transport: dockerStageTransport(docker3, a.env, io)
|
|
31433
|
+
};
|
|
31434
|
+
}
|
|
30783
31435
|
function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
30784
31436
|
const project = opts.project ?? DEFAULT_GCP_PROJECT;
|
|
30785
31437
|
const zone = opts.zone ?? DEFAULT_GCP_ZONE;
|
|
30786
31438
|
const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
|
|
30787
31439
|
const keyFile = resolveServiceAccountKeyFile(opts);
|
|
30788
31440
|
const image = venueImageFromEnv(process.env);
|
|
31441
|
+
const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
|
|
30789
31442
|
const env = venueGcloudEnv(gcloudConfig);
|
|
30790
31443
|
return {
|
|
30791
31444
|
kind: "hosted-pool",
|
|
30792
31445
|
acquire: (runId) => acquireTransactionally(async (allocated) => {
|
|
30793
31446
|
const vm = venueInstanceName(runId);
|
|
30794
31447
|
const stageBase = `/tmp/th-stage-${runId}`;
|
|
30795
|
-
const
|
|
31448
|
+
const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
|
|
30796
31449
|
vmName: vm,
|
|
30797
31450
|
project,
|
|
30798
31451
|
zone,
|
|
30799
31452
|
runId,
|
|
30800
|
-
machineType: opts.machineType ??
|
|
31453
|
+
machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
|
|
31454
|
+
venueImage: "confidential-space",
|
|
31455
|
+
workloadImage: image.workloadImage,
|
|
31456
|
+
clientCert
|
|
31457
|
+
} : {
|
|
31458
|
+
vmName: vm,
|
|
31459
|
+
project,
|
|
31460
|
+
zone,
|
|
31461
|
+
runId,
|
|
31462
|
+
machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
|
|
30801
31463
|
...image
|
|
30802
|
-
}
|
|
31464
|
+
};
|
|
31465
|
+
let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
|
|
30803
31466
|
ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
|
|
31467
|
+
const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
|
|
31468
|
+
if (clientTls !== null)
|
|
31469
|
+
bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
|
|
30804
31470
|
const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
|
|
30805
31471
|
let instanceState = "unknown";
|
|
30806
31472
|
allocated.onRollback(() => {
|
|
@@ -30816,6 +31482,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30816
31482
|
throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
|
|
30817
31483
|
}
|
|
30818
31484
|
instanceState = "exists";
|
|
31485
|
+
if (clientTls !== null) {
|
|
31486
|
+
return finishAcquire(await reachConfidentialSpace({
|
|
31487
|
+
vm,
|
|
31488
|
+
project,
|
|
31489
|
+
zone,
|
|
31490
|
+
runId,
|
|
31491
|
+
stageBase,
|
|
31492
|
+
env,
|
|
31493
|
+
io,
|
|
31494
|
+
allocated,
|
|
31495
|
+
tls: clientTls,
|
|
31496
|
+
imageDigest: clientTls.imageDigest
|
|
31497
|
+
}));
|
|
31498
|
+
}
|
|
30819
31499
|
const deadline = io.now() + SSH_READY_BUDGET_MS;
|
|
30820
31500
|
let last = { retryable: true, reason: "no probe ran" };
|
|
30821
31501
|
let identity = null;
|
|
@@ -30910,43 +31590,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30910
31590
|
if (!prepared.ok) {
|
|
30911
31591
|
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
|
|
30912
31592
|
}
|
|
30913
|
-
|
|
30914
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30915
|
-
const created = docker3.sync(volumeCreateArgv(name, runId), {
|
|
30916
|
-
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
30917
|
-
});
|
|
30918
|
-
if (created.error || created.status !== 0) {
|
|
30919
|
-
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
30920
|
-
}
|
|
30921
|
-
}
|
|
30922
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30923
|
-
const probe = docker3.sync(execProbeArgv(name), {
|
|
30924
|
-
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
30925
|
-
});
|
|
30926
|
-
if (probe.error || probe.status !== 0) {
|
|
30927
|
-
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)}`);
|
|
30928
|
-
}
|
|
30929
|
-
}
|
|
30930
|
-
const after = io.classifyDaemon(docker3);
|
|
30931
|
-
if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
|
|
30932
|
-
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.`);
|
|
30933
|
-
}
|
|
30934
|
-
return makeLease({
|
|
30935
|
-
runId,
|
|
30936
|
-
vm,
|
|
30937
|
-
project,
|
|
30938
|
-
zone,
|
|
30939
|
-
stageBase,
|
|
30940
|
-
stageVolumes,
|
|
31593
|
+
return finishAcquire({
|
|
30941
31594
|
docker: docker3,
|
|
30942
31595
|
tunnel,
|
|
30943
31596
|
socketDir,
|
|
30944
31597
|
socketPath,
|
|
30945
31598
|
venueDaemonId,
|
|
30946
31599
|
venueIdentity,
|
|
30947
|
-
env,
|
|
30948
|
-
io
|
|
31600
|
+
transport: sshStageTransport(vm, project, zone, env, io)
|
|
30949
31601
|
});
|
|
31602
|
+
function finishAcquire(r) {
|
|
31603
|
+
const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
|
|
31604
|
+
const stageVolumes = stageVolumeNames(vm);
|
|
31605
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
31606
|
+
const created = docker4.sync(volumeCreateArgv(name, runId), {
|
|
31607
|
+
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
31608
|
+
});
|
|
31609
|
+
if (created.error || created.status !== 0) {
|
|
31610
|
+
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
31611
|
+
}
|
|
31612
|
+
}
|
|
31613
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
31614
|
+
const probe = docker4.sync(execProbeArgv(name), {
|
|
31615
|
+
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
31616
|
+
});
|
|
31617
|
+
if (probe.error || probe.status !== 0) {
|
|
31618
|
+
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)}`);
|
|
31619
|
+
}
|
|
31620
|
+
}
|
|
31621
|
+
const after = io.classifyDaemon(docker4);
|
|
31622
|
+
if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
|
|
31623
|
+
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.`);
|
|
31624
|
+
}
|
|
31625
|
+
return makeLease({
|
|
31626
|
+
runId,
|
|
31627
|
+
vm,
|
|
31628
|
+
project,
|
|
31629
|
+
zone,
|
|
31630
|
+
stageBase,
|
|
31631
|
+
stageVolumes,
|
|
31632
|
+
docker: docker4,
|
|
31633
|
+
tunnel: r.tunnel,
|
|
31634
|
+
socketDir: r.socketDir,
|
|
31635
|
+
socketPath: socketPath2,
|
|
31636
|
+
venueDaemonId: venueDaemonId2,
|
|
31637
|
+
venueIdentity: r.venueIdentity,
|
|
31638
|
+
transport: r.transport,
|
|
31639
|
+
env,
|
|
31640
|
+
io
|
|
31641
|
+
});
|
|
31642
|
+
}
|
|
30950
31643
|
})
|
|
30951
31644
|
};
|
|
30952
31645
|
}
|
|
@@ -31005,28 +31698,7 @@ function makeLease(p) {
|
|
|
31005
31698
|
return null;
|
|
31006
31699
|
};
|
|
31007
31700
|
let stagedOwner;
|
|
31008
|
-
|
|
31009
|
-
const res = p.io.exec(
|
|
31010
|
-
"gcloud",
|
|
31011
|
-
iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
|
|
31012
|
-
OWNER_PROBE_TIMEOUT_MS,
|
|
31013
|
-
// `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
31014
|
-
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
31015
|
-
// omitted this would ssh under the person's own credential — then report an owner
|
|
31016
|
-
// measured against a session the run does not use.
|
|
31017
|
-
p.env
|
|
31018
|
-
);
|
|
31019
|
-
if (!res.ok) {
|
|
31020
|
-
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.`);
|
|
31021
|
-
}
|
|
31022
|
-
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
31023
|
-
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
31024
|
-
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
31025
|
-
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.`);
|
|
31026
|
-
}
|
|
31027
|
-
return { uid: ids2[0], gid: ids2[1] };
|
|
31028
|
-
};
|
|
31029
|
-
stagedOwner = readStagedOwner();
|
|
31701
|
+
stagedOwner = p.transport.readOwner();
|
|
31030
31702
|
return {
|
|
31031
31703
|
kind: "hosted-pool",
|
|
31032
31704
|
runId: p.runId,
|
|
@@ -31061,7 +31733,7 @@ function makeLease(p) {
|
|
|
31061
31733
|
const dir = venueProxyDir(p.stageBase);
|
|
31062
31734
|
const buffer = stageProxyCode();
|
|
31063
31735
|
try {
|
|
31064
|
-
const res = await p.
|
|
31736
|
+
const res = await p.transport.pushProxy(buffer.dir, dir);
|
|
31065
31737
|
if (!res.ok) {
|
|
31066
31738
|
throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
31067
31739
|
}
|
|
@@ -31071,7 +31743,7 @@ function makeLease(p) {
|
|
|
31071
31743
|
return {
|
|
31072
31744
|
dir,
|
|
31073
31745
|
cleanup: () => {
|
|
31074
|
-
p.
|
|
31746
|
+
p.transport.removeProxy(dir);
|
|
31075
31747
|
}
|
|
31076
31748
|
};
|
|
31077
31749
|
},
|
|
@@ -31079,7 +31751,7 @@ function makeLease(p) {
|
|
|
31079
31751
|
check("staging the tree");
|
|
31080
31752
|
const paths = venueStagePaths(p.stageBase);
|
|
31081
31753
|
const push = async (from, to) => {
|
|
31082
|
-
const res = await p.
|
|
31754
|
+
const res = await p.transport.push(from, to);
|
|
31083
31755
|
if (!res.ok) {
|
|
31084
31756
|
throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
31085
31757
|
}
|
|
@@ -31237,7 +31909,7 @@ function makeLease(p) {
|
|
|
31237
31909
|
}
|
|
31238
31910
|
};
|
|
31239
31911
|
}
|
|
31240
|
-
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;
|
|
31912
|
+
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;
|
|
31241
31913
|
var init_hostedVenue = __esm({
|
|
31242
31914
|
"../../packages/envrun/dist/hostedVenue.js"() {
|
|
31243
31915
|
"use strict";
|
|
@@ -31358,7 +32030,55 @@ var init_hostedVenue = __esm({
|
|
|
31358
32030
|
throw new Error("JWKS fetch returned a body with no keys array");
|
|
31359
32031
|
}
|
|
31360
32032
|
return body;
|
|
31361
|
-
}
|
|
32033
|
+
},
|
|
32034
|
+
fetchConfidentialSpaceJwks: async () => {
|
|
32035
|
+
const getJson = async (url, what) => {
|
|
32036
|
+
const res = await globalThis.fetch(url, {
|
|
32037
|
+
signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
|
|
32038
|
+
});
|
|
32039
|
+
if (!res.ok) {
|
|
32040
|
+
throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
|
|
32041
|
+
}
|
|
32042
|
+
return res.json();
|
|
32043
|
+
};
|
|
32044
|
+
const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
|
|
32045
|
+
const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
|
|
32046
|
+
if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
|
|
32047
|
+
throw new Error("Confidential Space discovery names no https jwks_uri");
|
|
32048
|
+
}
|
|
32049
|
+
const body = await getJson(jwksUri, "key set");
|
|
32050
|
+
if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
|
|
32051
|
+
throw new Error("Confidential Space key set has no keys array");
|
|
32052
|
+
}
|
|
32053
|
+
return body;
|
|
32054
|
+
},
|
|
32055
|
+
freeLocalPorts: async (n) => {
|
|
32056
|
+
const servers = [];
|
|
32057
|
+
try {
|
|
32058
|
+
for (let i = 0; i < n; i += 1) {
|
|
32059
|
+
const server = createServer();
|
|
32060
|
+
servers.push(server);
|
|
32061
|
+
await new Promise((resolve4, reject) => {
|
|
32062
|
+
server.once("error", reject);
|
|
32063
|
+
server.listen(0, "localhost", () => {
|
|
32064
|
+
resolve4();
|
|
32065
|
+
});
|
|
32066
|
+
});
|
|
32067
|
+
}
|
|
32068
|
+
return servers.map((s) => s.address().port);
|
|
32069
|
+
} finally {
|
|
32070
|
+
await Promise.all(servers.map((s) => new Promise((resolve4) => {
|
|
32071
|
+
s.close(() => {
|
|
32072
|
+
resolve4();
|
|
32073
|
+
});
|
|
32074
|
+
})));
|
|
32075
|
+
}
|
|
32076
|
+
},
|
|
32077
|
+
writeFile: (path5, text) => {
|
|
32078
|
+
writeFileSync15(path5, text, { mode: 384, flag: "wx" });
|
|
32079
|
+
},
|
|
32080
|
+
fetchAttestation: (req) => fetchAttestationOverTls(req),
|
|
32081
|
+
dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
|
|
31362
32082
|
};
|
|
31363
32083
|
VENUE_SSH_USER = "th-runner";
|
|
31364
32084
|
GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
|
|
@@ -31379,6 +32099,17 @@ var init_hostedVenue = __esm({
|
|
|
31379
32099
|
SSH_KEY_NOT_READY = /Permission denied \(publickey/;
|
|
31380
32100
|
DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
|
|
31381
32101
|
SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
|
|
32102
|
+
CS_ATTEST_PORT = 8443;
|
|
32103
|
+
CS_DOCKER_PORT = 2376;
|
|
32104
|
+
CS_READY_BUDGET_MS = 48e4;
|
|
32105
|
+
CS_ATTEST_TIMEOUT_MS = 15e3;
|
|
32106
|
+
CS_PULL_TIMEOUT_MS = 6e5;
|
|
32107
|
+
CS_ATTEST_INTERVAL_MS = 5e3;
|
|
32108
|
+
OPENSSL_TIMEOUT_MS = 3e4;
|
|
32109
|
+
CS_RUN_ARGS = ["--oom-score-adj", "1000"];
|
|
32110
|
+
CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
|
|
32111
|
+
CS_STAGE_ROOT = "/tmp";
|
|
32112
|
+
CLIENT_CERT_STAND_IN = "stand-in-certificate";
|
|
31382
32113
|
}
|
|
31383
32114
|
});
|
|
31384
32115
|
|
|
@@ -32278,10 +33009,10 @@ var init_references = __esm({
|
|
|
32278
33009
|
|
|
32279
33010
|
// ../../packages/envspec/dist/repo.js
|
|
32280
33011
|
import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
|
|
32281
|
-
import { join as join27, relative as relative2, sep as
|
|
33012
|
+
import { join as join27, relative as relative2, sep as sep5 } from "path";
|
|
32282
33013
|
function createRepoReader(repoPath) {
|
|
32283
33014
|
const resolveIn = (relativePath) => relativePath === "" ? repoPath : join27(repoPath, relativePath);
|
|
32284
|
-
const toPosix = (absolute) => relative2(repoPath, absolute).split(
|
|
33015
|
+
const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
|
|
32285
33016
|
const readText = (relativePath) => {
|
|
32286
33017
|
try {
|
|
32287
33018
|
return readFileSync14(resolveIn(relativePath), "utf8");
|
|
@@ -35111,7 +35842,7 @@ __export(jpi_claim_exports, {
|
|
|
35111
35842
|
});
|
|
35112
35843
|
import {
|
|
35113
35844
|
readFileSync as readFileSync15,
|
|
35114
|
-
writeFileSync as
|
|
35845
|
+
writeFileSync as writeFileSync16,
|
|
35115
35846
|
mkdirSync as mkdirSync7,
|
|
35116
35847
|
mkdtempSync as mkdtempSync5,
|
|
35117
35848
|
renameSync as renameSync7,
|
|
@@ -35122,7 +35853,7 @@ import {
|
|
|
35122
35853
|
readdirSync as readdirSync3
|
|
35123
35854
|
} from "fs";
|
|
35124
35855
|
import { join as join29, dirname as dirname9, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
|
|
35125
|
-
import { createHash as
|
|
35856
|
+
import { createHash as createHash10 } from "crypto";
|
|
35126
35857
|
import { homedir as homedir15, hostname as osHostname } from "os";
|
|
35127
35858
|
import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
|
|
35128
35859
|
import { promisify as promisify3 } from "util";
|
|
@@ -35140,7 +35871,7 @@ function markClaimNudged(id) {
|
|
|
35140
35871
|
const ids2 = readNudgedClaimIds();
|
|
35141
35872
|
ids2.add(id);
|
|
35142
35873
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
35143
|
-
|
|
35874
|
+
writeFileSync16(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
|
|
35144
35875
|
} catch {
|
|
35145
35876
|
}
|
|
35146
35877
|
}
|
|
@@ -36314,7 +37045,7 @@ async function registerFounderClaim(b) {
|
|
|
36314
37045
|
let clearedLocalCredential = false;
|
|
36315
37046
|
let refusedForPurpose = false;
|
|
36316
37047
|
let preserveBackgroundToken = false;
|
|
36317
|
-
const
|
|
37048
|
+
const refuse4 = (reason) => {
|
|
36318
37049
|
console.error(
|
|
36319
37050
|
`
|
|
36320
37051
|
terminalhire claim: refusing to record \u2014 ${reason}
|
|
@@ -36339,7 +37070,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36339
37070
|
try {
|
|
36340
37071
|
expectLogin = await localLoginForPaidBrowserClaim();
|
|
36341
37072
|
} catch (err) {
|
|
36342
|
-
|
|
37073
|
+
refuse4(
|
|
36343
37074
|
`could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
|
|
36344
37075
|
Run \`terminalhire login\`, then retry this claim. Browser verification was
|
|
36345
37076
|
not opened and no claim was sent.`
|
|
@@ -36355,16 +37086,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36355
37086
|
claimRef: opportunityShortToken(b.bountyId)
|
|
36356
37087
|
});
|
|
36357
37088
|
if (!proofToken) {
|
|
36358
|
-
|
|
37089
|
+
refuse4("could not verify your GitHub identity with terminalhire (see above).");
|
|
36359
37090
|
}
|
|
36360
37091
|
const verifiedLogin = proofTokenLogin(proofToken);
|
|
36361
37092
|
if (!verifiedLogin) {
|
|
36362
|
-
|
|
37093
|
+
refuse4(
|
|
36363
37094
|
"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."
|
|
36364
37095
|
);
|
|
36365
37096
|
}
|
|
36366
37097
|
if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
|
|
36367
|
-
|
|
37098
|
+
refuse4(
|
|
36368
37099
|
`you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
|
|
36369
37100
|
Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
|
|
36370
37101
|
run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
|
|
@@ -36396,7 +37127,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36396
37127
|
try {
|
|
36397
37128
|
res = await sendRegistration(true);
|
|
36398
37129
|
} catch (err) {
|
|
36399
|
-
|
|
37130
|
+
refuse4(
|
|
36400
37131
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36401
37132
|
);
|
|
36402
37133
|
}
|
|
@@ -36447,7 +37178,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36447
37178
|
try {
|
|
36448
37179
|
res = await sendRegistration(true);
|
|
36449
37180
|
} catch (err) {
|
|
36450
|
-
|
|
37181
|
+
refuse4(
|
|
36451
37182
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36452
37183
|
);
|
|
36453
37184
|
}
|
|
@@ -36458,14 +37189,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36458
37189
|
res = await sendRegistration(false);
|
|
36459
37190
|
refusalBody = await readRefusal(res);
|
|
36460
37191
|
} catch (err) {
|
|
36461
|
-
|
|
37192
|
+
refuse4(
|
|
36462
37193
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36463
37194
|
);
|
|
36464
37195
|
}
|
|
36465
37196
|
}
|
|
36466
37197
|
if (!res.ok) {
|
|
36467
37198
|
const detail = refusalBody?.message || refusalBody?.error || "";
|
|
36468
|
-
|
|
37199
|
+
refuse4(
|
|
36469
37200
|
detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
|
|
36470
37201
|
);
|
|
36471
37202
|
}
|
|
@@ -36476,7 +37207,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36476
37207
|
body = null;
|
|
36477
37208
|
}
|
|
36478
37209
|
if (!body || body.ok !== true) {
|
|
36479
|
-
|
|
37210
|
+
refuse4("malformed registration response from the server.");
|
|
36480
37211
|
}
|
|
36481
37212
|
const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
|
|
36482
37213
|
if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
|
|
@@ -37906,7 +38637,7 @@ function writeSliceFiles(destDir, files) {
|
|
|
37906
38637
|
if (typeof f.content === "string") {
|
|
37907
38638
|
const abs = join29(destDir, f.path);
|
|
37908
38639
|
mkdirSync7(dirname9(abs), { recursive: true });
|
|
37909
|
-
|
|
38640
|
+
writeFileSync16(abs, f.content, "utf8");
|
|
37910
38641
|
written.push(f.path);
|
|
37911
38642
|
} else {
|
|
37912
38643
|
unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
|
|
@@ -37955,7 +38686,7 @@ function ensureExcludedPackDir(destDir) {
|
|
|
37955
38686
|
const existing = existsSync13(excludeFile) ? readFileSync15(excludeFile, "utf8") : "";
|
|
37956
38687
|
if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
|
|
37957
38688
|
mkdirSync7(dirname9(excludeFile), { recursive: true });
|
|
37958
|
-
|
|
38689
|
+
writeFileSync16(
|
|
37959
38690
|
excludeFile,
|
|
37960
38691
|
`${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
|
|
37961
38692
|
`,
|
|
@@ -37971,14 +38702,14 @@ function writePackFile(destDir, relPath, content, what) {
|
|
|
37971
38702
|
try {
|
|
37972
38703
|
const abs = join29(destDir, relPath);
|
|
37973
38704
|
mkdirSync7(dirname9(abs), { recursive: true });
|
|
37974
|
-
|
|
38705
|
+
writeFileSync16(abs, content, { encoding: "utf8", flag: "wx" });
|
|
37975
38706
|
} catch (err) {
|
|
37976
38707
|
return { written: false, reason: `the ${what} could not be written (${err.message})` };
|
|
37977
38708
|
}
|
|
37978
38709
|
return { written: true, reason: null, sha256: sha256OfUtf8(content) };
|
|
37979
38710
|
}
|
|
37980
38711
|
function sha256OfUtf8(content) {
|
|
37981
|
-
return
|
|
38712
|
+
return createHash10("sha256").update(content, "utf8").digest("hex");
|
|
37982
38713
|
}
|
|
37983
38714
|
function writeWorkspacePack(destDir, spec, claim, delivery) {
|
|
37984
38715
|
if (delivery !== "full" && delivery !== "sparse") {
|
|
@@ -39335,7 +40066,7 @@ function readClaimPushMarker() {
|
|
|
39335
40066
|
}
|
|
39336
40067
|
function writeClaimPushMarker(marker) {
|
|
39337
40068
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
39338
|
-
|
|
40069
|
+
writeFileSync16(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
39339
40070
|
}
|
|
39340
40071
|
function clearClaimPushMarker() {
|
|
39341
40072
|
try {
|
|
@@ -40487,7 +41218,7 @@ __export(util_exports, {
|
|
|
40487
41218
|
getSizableOrigin: () => getSizableOrigin,
|
|
40488
41219
|
hexToUint8Array: () => hexToUint8Array,
|
|
40489
41220
|
isObject: () => isObject,
|
|
40490
|
-
isPlainObject: () =>
|
|
41221
|
+
isPlainObject: () => isPlainObject2,
|
|
40491
41222
|
issue: () => issue,
|
|
40492
41223
|
joinValues: () => joinValues,
|
|
40493
41224
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
@@ -40649,7 +41380,7 @@ function slugify(input) {
|
|
|
40649
41380
|
function isObject(data) {
|
|
40650
41381
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
40651
41382
|
}
|
|
40652
|
-
function
|
|
41383
|
+
function isPlainObject2(o) {
|
|
40653
41384
|
if (isObject(o) === false)
|
|
40654
41385
|
return false;
|
|
40655
41386
|
const ctor = o.constructor;
|
|
@@ -40666,7 +41397,7 @@ function isPlainObject(o) {
|
|
|
40666
41397
|
return true;
|
|
40667
41398
|
}
|
|
40668
41399
|
function shallowClone(o) {
|
|
40669
|
-
if (
|
|
41400
|
+
if (isPlainObject2(o))
|
|
40670
41401
|
return { ...o };
|
|
40671
41402
|
if (Array.isArray(o))
|
|
40672
41403
|
return [...o];
|
|
@@ -40806,7 +41537,7 @@ function omit(schema, mask) {
|
|
|
40806
41537
|
return clone(schema, def);
|
|
40807
41538
|
}
|
|
40808
41539
|
function extend(schema, shape) {
|
|
40809
|
-
if (!
|
|
41540
|
+
if (!isPlainObject2(shape)) {
|
|
40810
41541
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
40811
41542
|
}
|
|
40812
41543
|
const checks = schema._zod.def.checks;
|
|
@@ -40829,7 +41560,7 @@ function extend(schema, shape) {
|
|
|
40829
41560
|
return clone(schema, def);
|
|
40830
41561
|
}
|
|
40831
41562
|
function safeExtend(schema, shape) {
|
|
40832
|
-
if (!
|
|
41563
|
+
if (!isPlainObject2(shape)) {
|
|
40833
41564
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
40834
41565
|
}
|
|
40835
41566
|
const def = mergeDefs(schema._zod.def, {
|
|
@@ -41990,7 +42721,7 @@ function mergeValues(a, b) {
|
|
|
41990
42721
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
41991
42722
|
return { valid: true, data: a };
|
|
41992
42723
|
}
|
|
41993
|
-
if (
|
|
42724
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
41994
42725
|
const bKeys = Object.keys(b);
|
|
41995
42726
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
41996
42727
|
const newObj = { ...a, ...b };
|
|
@@ -42942,7 +43673,7 @@ var init_schemas = __esm({
|
|
|
42942
43673
|
$ZodType.init(inst, def);
|
|
42943
43674
|
inst._zod.parse = (payload, ctx) => {
|
|
42944
43675
|
const input = payload.value;
|
|
42945
|
-
if (!
|
|
43676
|
+
if (!isPlainObject2(input)) {
|
|
42946
43677
|
payload.issues.push({
|
|
42947
43678
|
expected: "record",
|
|
42948
43679
|
code: "invalid_type",
|
|
@@ -48084,7 +48815,7 @@ var init_zod_json_schema_compat = __esm({
|
|
|
48084
48815
|
});
|
|
48085
48816
|
|
|
48086
48817
|
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
|
|
48087
|
-
function
|
|
48818
|
+
function isPlainObject3(value) {
|
|
48088
48819
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
48089
48820
|
}
|
|
48090
48821
|
function mergeCapabilities(base, additional) {
|
|
@@ -48095,7 +48826,7 @@ function mergeCapabilities(base, additional) {
|
|
|
48095
48826
|
if (addValue === void 0)
|
|
48096
48827
|
continue;
|
|
48097
48828
|
const baseValue = result[k];
|
|
48098
|
-
if (
|
|
48829
|
+
if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
|
|
48099
48830
|
result[k] = { ...baseValue, ...addValue };
|
|
48100
48831
|
} else {
|
|
48101
48832
|
result[k] = addValue;
|
|
@@ -62453,7 +63184,7 @@ __export(config_exports, {
|
|
|
62453
63184
|
readConfig: () => readConfig,
|
|
62454
63185
|
writeConfig: () => writeConfig
|
|
62455
63186
|
});
|
|
62456
|
-
import { readFileSync as readFileSync16, writeFileSync as
|
|
63187
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync17, existsSync as existsSync14 } from "fs";
|
|
62457
63188
|
import { join as join30 } from "path";
|
|
62458
63189
|
import { homedir as homedir16 } from "os";
|
|
62459
63190
|
function readConfig() {
|
|
@@ -62476,7 +63207,7 @@ function writeConfig(config2) {
|
|
|
62476
63207
|
}
|
|
62477
63208
|
delete merged.contributePrompted;
|
|
62478
63209
|
}
|
|
62479
|
-
|
|
63210
|
+
writeFileSync17(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
62480
63211
|
}
|
|
62481
63212
|
function parseNudgeMode(raw) {
|
|
62482
63213
|
if (raw === "session" || raw === "always") return raw;
|