terminalhire 0.42.19 → 0.42.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/claim-push-bg.js +1 -1
- package/dist/bin/jpi-chat-read.js +1 -1
- package/dist/bin/jpi-chat.js +1 -1
- package/dist/bin/jpi-claim.js +974 -139
- package/dist/bin/jpi-contribute.js +6 -1
- package/dist/bin/jpi-dispatch.js +1144 -186
- package/dist/bin/jpi-hub.js +1 -1
- package/dist/bin/jpi-inbox.js +1 -1
- package/dist/bin/jpi-login.js +10 -5
- package/dist/bin/jpi-mcp-chat.js +1 -1
- package/dist/bin/jpi-mcp.js +983 -148
- package/dist/bin/jpi-refresh.js +1 -1
- package/dist/bin/jpi-run.js +1227 -218
- package/dist/src/chat-client.js +1 -1
- package/dist/src/chat-keystore.js +1 -1
- package/dist/src/github-auth.js +6 -1
- package/package.json +1 -1
package/dist/bin/jpi-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
|
},
|
|
@@ -29075,6 +29101,197 @@ var init_gceIdentity = __esm({
|
|
|
29075
29101
|
}
|
|
29076
29102
|
});
|
|
29077
29103
|
|
|
29104
|
+
// ../../packages/attest/dist/confidentialSpace.js
|
|
29105
|
+
import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
|
|
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;
|
|
29284
|
+
var init_confidentialSpace = __esm({
|
|
29285
|
+
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
29286
|
+
"use strict";
|
|
29287
|
+
init_gceIdentity();
|
|
29288
|
+
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
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";
|
|
29292
|
+
}
|
|
29293
|
+
});
|
|
29294
|
+
|
|
29078
29295
|
// ../../packages/attest/dist/venueInstanceName.js
|
|
29079
29296
|
function venueInstanceName(runId) {
|
|
29080
29297
|
return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
|
|
@@ -29109,6 +29326,7 @@ var init_dist2 = __esm({
|
|
|
29109
29326
|
init_attestation();
|
|
29110
29327
|
init_nonce();
|
|
29111
29328
|
init_gceIdentity();
|
|
29329
|
+
init_confidentialSpace();
|
|
29112
29330
|
init_venueInstanceName();
|
|
29113
29331
|
init_verify();
|
|
29114
29332
|
}
|
|
@@ -29359,7 +29577,7 @@ var init_result = __esm({
|
|
|
29359
29577
|
});
|
|
29360
29578
|
|
|
29361
29579
|
// ../../packages/envrun/dist/attestation.js
|
|
29362
|
-
import { createHash as
|
|
29580
|
+
import { createHash as createHash8, randomBytes as randomBytes10 } from "crypto";
|
|
29363
29581
|
function contradicts(outcome, counts, exitCode) {
|
|
29364
29582
|
const budget = OUTCOME_TO_BUDGET[outcome];
|
|
29365
29583
|
if (budget === null)
|
|
@@ -29401,7 +29619,7 @@ function imageDigestOf(ref) {
|
|
|
29401
29619
|
return at === -1 ? null : ref.slice(at + 1);
|
|
29402
29620
|
}
|
|
29403
29621
|
function sha256Hex(data) {
|
|
29404
|
-
return
|
|
29622
|
+
return createHash8("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
|
|
29405
29623
|
}
|
|
29406
29624
|
function toTestRunResult(result, outputSha256) {
|
|
29407
29625
|
return {
|
|
@@ -29413,50 +29631,50 @@ function toTestRunResult(result, outputSha256) {
|
|
|
29413
29631
|
function toAcceptancePredicate(pair, opts = {}) {
|
|
29414
29632
|
const { baseline, patched } = pair;
|
|
29415
29633
|
if (baseline.runId === patched.runId) {
|
|
29416
|
-
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.`);
|
|
29417
29635
|
}
|
|
29418
29636
|
if (patched.status === "refused") {
|
|
29419
|
-
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`);
|
|
29420
29638
|
}
|
|
29421
29639
|
if (patched.outcome === null) {
|
|
29422
|
-
return
|
|
29640
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
29423
29641
|
}
|
|
29424
29642
|
if (baseline.status === "refused") {
|
|
29425
|
-
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`);
|
|
29426
29644
|
}
|
|
29427
29645
|
if (baseline.outcome === null) {
|
|
29428
|
-
return
|
|
29646
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
29429
29647
|
}
|
|
29430
29648
|
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
29431
|
-
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.`);
|
|
29432
29650
|
}
|
|
29433
29651
|
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
29434
29652
|
if (budget === null) {
|
|
29435
|
-
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.`);
|
|
29436
29654
|
}
|
|
29437
29655
|
if (patched.testCommand === null) {
|
|
29438
|
-
return
|
|
29656
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
29439
29657
|
}
|
|
29440
29658
|
if (baseline.testCommand !== patched.testCommand) {
|
|
29441
|
-
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.`);
|
|
29442
29660
|
}
|
|
29443
29661
|
if (baseline.targetSha !== patched.targetSha) {
|
|
29444
|
-
return
|
|
29662
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
29445
29663
|
}
|
|
29446
29664
|
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
29447
|
-
return
|
|
29665
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
29448
29666
|
}
|
|
29449
29667
|
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
29450
|
-
return
|
|
29668
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
29451
29669
|
}
|
|
29452
29670
|
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
29453
|
-
return
|
|
29671
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
29454
29672
|
}
|
|
29455
29673
|
if (baseline.treeDigest === patched.treeDigest) {
|
|
29456
|
-
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`);
|
|
29457
29675
|
}
|
|
29458
29676
|
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
29459
|
-
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.`);
|
|
29460
29678
|
}
|
|
29461
29679
|
for (const [side, outcome, counts, exitCode] of [
|
|
29462
29680
|
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
@@ -29464,7 +29682,7 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
29464
29682
|
]) {
|
|
29465
29683
|
const why = contradicts(outcome, counts, exitCode);
|
|
29466
29684
|
if (why !== null) {
|
|
29467
|
-
return
|
|
29685
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
29468
29686
|
}
|
|
29469
29687
|
}
|
|
29470
29688
|
for (const [side, source] of [
|
|
@@ -29473,44 +29691,44 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
29473
29691
|
]) {
|
|
29474
29692
|
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
29475
29693
|
if (why !== null)
|
|
29476
|
-
return
|
|
29694
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
29477
29695
|
}
|
|
29478
29696
|
if (patched.testCommandSource === "developer-declared") {
|
|
29479
|
-
return
|
|
29697
|
+
return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
|
|
29480
29698
|
}
|
|
29481
29699
|
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
29482
|
-
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.`);
|
|
29483
29701
|
}
|
|
29484
29702
|
if (baseline.containerImage !== patched.containerImage) {
|
|
29485
|
-
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.`);
|
|
29486
29704
|
}
|
|
29487
29705
|
if (patched.containerImage === null || patched.containerImage === "") {
|
|
29488
|
-
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");
|
|
29489
29707
|
}
|
|
29490
29708
|
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
29491
|
-
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.");
|
|
29492
29710
|
}
|
|
29493
29711
|
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
29494
|
-
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.`);
|
|
29495
29713
|
}
|
|
29496
29714
|
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
29497
|
-
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.`);
|
|
29498
29716
|
}
|
|
29499
29717
|
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
29500
|
-
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.");
|
|
29501
29719
|
}
|
|
29502
29720
|
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
29503
|
-
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.`);
|
|
29504
29722
|
}
|
|
29505
29723
|
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
29506
|
-
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.`);
|
|
29507
29725
|
}
|
|
29508
29726
|
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
29509
|
-
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.`);
|
|
29510
29728
|
}
|
|
29511
29729
|
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
29512
29730
|
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
29513
|
-
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.`);
|
|
29514
29732
|
}
|
|
29515
29733
|
return {
|
|
29516
29734
|
ok: true,
|
|
@@ -29537,7 +29755,7 @@ function signRunStatement(predicate, privateKey, keyid) {
|
|
|
29537
29755
|
const statement = createAcceptanceStatement(predicate);
|
|
29538
29756
|
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
29539
29757
|
}
|
|
29540
|
-
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;
|
|
29541
29759
|
var init_attestation2 = __esm({
|
|
29542
29760
|
"../../packages/envrun/dist/attestation.js"() {
|
|
29543
29761
|
"use strict";
|
|
@@ -29641,7 +29859,7 @@ var init_attestation2 = __esm({
|
|
|
29641
29859
|
"outcome-contradicts-counts",
|
|
29642
29860
|
"test-command-origin-unattested"
|
|
29643
29861
|
];
|
|
29644
|
-
|
|
29862
|
+
refuse3 = (reason, detail) => ({
|
|
29645
29863
|
ok: false,
|
|
29646
29864
|
reason,
|
|
29647
29865
|
detail
|
|
@@ -30024,6 +30242,73 @@ var init_boundary = __esm({
|
|
|
30024
30242
|
});
|
|
30025
30243
|
|
|
30026
30244
|
// ../../packages/envrun/dist/gcpPlacement.js
|
|
30245
|
+
function assertWorkloadImage(fn, value) {
|
|
30246
|
+
if (typeof value !== "string" || !WORKLOAD_IMAGE_REFERENCE.test(value)) {
|
|
30247
|
+
throw new GcpPlacementError(`${fn}: the workload image is missing or is not one plain image reference (${String(WORKLOAD_IMAGE_REFERENCE)}). It lands inside a comma-separated --metadata= list, so anything that could start a second key is refused.`);
|
|
30248
|
+
}
|
|
30249
|
+
}
|
|
30250
|
+
function venueImageFromEnv(env) {
|
|
30251
|
+
const image = env["VENUE_IMAGE"] ?? "";
|
|
30252
|
+
if (image === "" || image === "cos-stable")
|
|
30253
|
+
return { venueImage: "cos-stable" };
|
|
30254
|
+
if (image !== "confidential-space") {
|
|
30255
|
+
throw new GcpPlacementError("VENUE_IMAGE is set to something other than cos-stable or confidential-space. An unknown value is refused rather than booted as the default.");
|
|
30256
|
+
}
|
|
30257
|
+
const workloadImage = env["VENUE_WORKLOAD_IMAGE"] ?? "";
|
|
30258
|
+
if (workloadImage === "") {
|
|
30259
|
+
throw new GcpPlacementError("VENUE_IMAGE=confidential-space needs VENUE_WORKLOAD_IMAGE, the image reference the Confidential Space launcher runs. Nothing boots without one.");
|
|
30260
|
+
}
|
|
30261
|
+
assertWorkloadImage("venueImageFromEnv", workloadImage);
|
|
30262
|
+
return { venueImage: "confidential-space", workloadImage };
|
|
30263
|
+
}
|
|
30264
|
+
function bootImageFlags(p) {
|
|
30265
|
+
const image = p.venueImage ?? "cos-stable";
|
|
30266
|
+
if (image === "cos-stable")
|
|
30267
|
+
return ["--image-family=cos-stable", "--image-project=cos-cloud"];
|
|
30268
|
+
if (image !== "confidential-space") {
|
|
30269
|
+
throw new GcpPlacementError("gcpBootArgv: venueImage is not one of cos-stable, confidential-space. An unknown image is refused rather than booted as the default.");
|
|
30270
|
+
}
|
|
30271
|
+
const workloadImage = p.workloadImage;
|
|
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
|
+
];
|
|
30289
|
+
return [
|
|
30290
|
+
// The production family. The debug family reports `dbgstat: enabled`, which the
|
|
30291
|
+
// intake's verifier refuses, so a debug boot could never produce a run we record.
|
|
30292
|
+
"--image-family=confidential-space",
|
|
30293
|
+
"--image-project=confidential-space-images",
|
|
30294
|
+
"--confidential-compute-type=SEV",
|
|
30295
|
+
// The deploy-workloads page says N2D with SEV may set MIGRATE for live migration,
|
|
30296
|
+
// and every other machine type needs TERMINATE. The spec chose TERMINATE: a
|
|
30297
|
+
// maintenance event ends the VM instead of moving it, which for a VM that lives at
|
|
30298
|
+
// most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
|
|
30299
|
+
// combination is phase 5's measurement.
|
|
30300
|
+
"--maintenance-policy=TERMINATE",
|
|
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"
|
|
30310
|
+
];
|
|
30311
|
+
}
|
|
30027
30312
|
function assertInstanceIdentity(fn, id) {
|
|
30028
30313
|
const fields = [
|
|
30029
30314
|
["vmName", id.vmName, GCE_INSTANCE_NAME],
|
|
@@ -30049,8 +30334,8 @@ function gcpBootArgv(p) {
|
|
|
30049
30334
|
`--project=${p.project}`,
|
|
30050
30335
|
`--zone=${p.zone}`,
|
|
30051
30336
|
`--machine-type=${p.machineType}`,
|
|
30052
|
-
|
|
30053
|
-
|
|
30337
|
+
// What the VM boots, and the one part of this argv the two images do not share.
|
|
30338
|
+
...bootImageFlags(p),
|
|
30054
30339
|
// ── THE OUTER REAP. This, not the `finally` below, is what bounds the bill. ──
|
|
30055
30340
|
//
|
|
30056
30341
|
// An in-process `finally` is a best effort, not a reap. SIGKILL, a crashed
|
|
@@ -30095,7 +30380,13 @@ function gcpBootArgv(p) {
|
|
|
30095
30380
|
// expectation uses (TERM-832): two inline spellings of one value is how
|
|
30096
30381
|
// the boot path and the verifier drift apart silently.
|
|
30097
30382
|
`--service-account=${venueIdentityEmail(p.project)}`,
|
|
30098
|
-
|
|
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"],
|
|
30099
30390
|
"--shielded-secure-boot",
|
|
30100
30391
|
"--shielded-vtpm",
|
|
30101
30392
|
"--shielded-integrity-monitoring",
|
|
@@ -30128,7 +30419,7 @@ function gcpDeleteArgv(p) {
|
|
|
30128
30419
|
"--quiet"
|
|
30129
30420
|
];
|
|
30130
30421
|
}
|
|
30131
|
-
var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
|
|
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;
|
|
30132
30423
|
var init_gcpPlacement = __esm({
|
|
30133
30424
|
"../../packages/envrun/dist/gcpPlacement.js"() {
|
|
30134
30425
|
"use strict";
|
|
@@ -30145,6 +30436,10 @@ var init_gcpPlacement = __esm({
|
|
|
30145
30436
|
GCP_MAX_RUN_DURATION_SECONDS = 3600;
|
|
30146
30437
|
GCP_MANAGED_LABEL_KEY = "th-managed";
|
|
30147
30438
|
GCP_RUN_LABEL_KEY = "th-run";
|
|
30439
|
+
CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
|
|
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;
|
|
30148
30443
|
GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
|
|
30149
30444
|
GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
|
30150
30445
|
GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -30186,7 +30481,10 @@ var init_emptyGitConfig = __esm({
|
|
|
30186
30481
|
|
|
30187
30482
|
// ../../packages/envrun/dist/hostedVenue.js
|
|
30188
30483
|
import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
|
|
30189
|
-
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";
|
|
30190
30488
|
import { join as join26 } from "path";
|
|
30191
30489
|
import { tmpdir as tmpdir3 } from "os";
|
|
30192
30490
|
function credentialInGitConfig(text) {
|
|
@@ -30388,6 +30686,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
|
|
|
30388
30686
|
});
|
|
30389
30687
|
});
|
|
30390
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
|
+
}
|
|
30391
30772
|
function quoteForRemoteShell(arg) {
|
|
30392
30773
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
30393
30774
|
}
|
|
@@ -30722,25 +31103,370 @@ ${res.stderr}`);
|
|
|
30722
31103
|
function manualDeleteCommand(vm, project, zone) {
|
|
30723
31104
|
return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
|
|
30724
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
|
+
}
|
|
30725
31435
|
function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
30726
31436
|
const project = opts.project ?? DEFAULT_GCP_PROJECT;
|
|
30727
31437
|
const zone = opts.zone ?? DEFAULT_GCP_ZONE;
|
|
30728
31438
|
const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
|
|
30729
31439
|
const keyFile = resolveServiceAccountKeyFile(opts);
|
|
31440
|
+
const image = venueImageFromEnv(process.env);
|
|
31441
|
+
const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
|
|
30730
31442
|
const env = venueGcloudEnv(gcloudConfig);
|
|
30731
31443
|
return {
|
|
30732
31444
|
kind: "hosted-pool",
|
|
30733
31445
|
acquire: (runId) => acquireTransactionally(async (allocated) => {
|
|
30734
31446
|
const vm = venueInstanceName(runId);
|
|
30735
31447
|
const stageBase = `/tmp/th-stage-${runId}`;
|
|
30736
|
-
const
|
|
31448
|
+
const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
|
|
30737
31449
|
vmName: vm,
|
|
30738
31450
|
project,
|
|
30739
31451
|
zone,
|
|
30740
31452
|
runId,
|
|
30741
|
-
machineType: opts.machineType ??
|
|
30742
|
-
|
|
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,
|
|
31463
|
+
...image
|
|
31464
|
+
};
|
|
31465
|
+
let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
|
|
30743
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));
|
|
30744
31470
|
const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
|
|
30745
31471
|
let instanceState = "unknown";
|
|
30746
31472
|
allocated.onRollback(() => {
|
|
@@ -30756,6 +31482,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30756
31482
|
throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
|
|
30757
31483
|
}
|
|
30758
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
|
+
}
|
|
30759
31499
|
const deadline = io.now() + SSH_READY_BUDGET_MS;
|
|
30760
31500
|
let last = { retryable: true, reason: "no probe ran" };
|
|
30761
31501
|
let identity = null;
|
|
@@ -30850,43 +31590,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30850
31590
|
if (!prepared.ok) {
|
|
30851
31591
|
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
|
|
30852
31592
|
}
|
|
30853
|
-
|
|
30854
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30855
|
-
const created = docker3.sync(volumeCreateArgv(name, runId), {
|
|
30856
|
-
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
30857
|
-
});
|
|
30858
|
-
if (created.error || created.status !== 0) {
|
|
30859
|
-
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
30860
|
-
}
|
|
30861
|
-
}
|
|
30862
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30863
|
-
const probe = docker3.sync(execProbeArgv(name), {
|
|
30864
|
-
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
30865
|
-
});
|
|
30866
|
-
if (probe.error || probe.status !== 0) {
|
|
30867
|
-
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)}`);
|
|
30868
|
-
}
|
|
30869
|
-
}
|
|
30870
|
-
const after = io.classifyDaemon(docker3);
|
|
30871
|
-
if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
|
|
30872
|
-
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.`);
|
|
30873
|
-
}
|
|
30874
|
-
return makeLease({
|
|
30875
|
-
runId,
|
|
30876
|
-
vm,
|
|
30877
|
-
project,
|
|
30878
|
-
zone,
|
|
30879
|
-
stageBase,
|
|
30880
|
-
stageVolumes,
|
|
31593
|
+
return finishAcquire({
|
|
30881
31594
|
docker: docker3,
|
|
30882
31595
|
tunnel,
|
|
30883
31596
|
socketDir,
|
|
30884
31597
|
socketPath,
|
|
30885
31598
|
venueDaemonId,
|
|
30886
31599
|
venueIdentity,
|
|
30887
|
-
env,
|
|
30888
|
-
io
|
|
31600
|
+
transport: sshStageTransport(vm, project, zone, env, io)
|
|
30889
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
|
+
}
|
|
30890
31643
|
})
|
|
30891
31644
|
};
|
|
30892
31645
|
}
|
|
@@ -30945,28 +31698,7 @@ function makeLease(p) {
|
|
|
30945
31698
|
return null;
|
|
30946
31699
|
};
|
|
30947
31700
|
let stagedOwner;
|
|
30948
|
-
|
|
30949
|
-
const res = p.io.exec(
|
|
30950
|
-
"gcloud",
|
|
30951
|
-
iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
|
|
30952
|
-
OWNER_PROBE_TIMEOUT_MS,
|
|
30953
|
-
// `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
30954
|
-
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
30955
|
-
// omitted this would ssh under the person's own credential — then report an owner
|
|
30956
|
-
// measured against a session the run does not use.
|
|
30957
|
-
p.env
|
|
30958
|
-
);
|
|
30959
|
-
if (!res.ok) {
|
|
30960
|
-
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.`);
|
|
30961
|
-
}
|
|
30962
|
-
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
30963
|
-
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
30964
|
-
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
30965
|
-
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.`);
|
|
30966
|
-
}
|
|
30967
|
-
return { uid: ids2[0], gid: ids2[1] };
|
|
30968
|
-
};
|
|
30969
|
-
stagedOwner = readStagedOwner();
|
|
31701
|
+
stagedOwner = p.transport.readOwner();
|
|
30970
31702
|
return {
|
|
30971
31703
|
kind: "hosted-pool",
|
|
30972
31704
|
runId: p.runId,
|
|
@@ -31001,7 +31733,7 @@ function makeLease(p) {
|
|
|
31001
31733
|
const dir = venueProxyDir(p.stageBase);
|
|
31002
31734
|
const buffer = stageProxyCode();
|
|
31003
31735
|
try {
|
|
31004
|
-
const res = await p.
|
|
31736
|
+
const res = await p.transport.pushProxy(buffer.dir, dir);
|
|
31005
31737
|
if (!res.ok) {
|
|
31006
31738
|
throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
31007
31739
|
}
|
|
@@ -31011,7 +31743,7 @@ function makeLease(p) {
|
|
|
31011
31743
|
return {
|
|
31012
31744
|
dir,
|
|
31013
31745
|
cleanup: () => {
|
|
31014
|
-
p.
|
|
31746
|
+
p.transport.removeProxy(dir);
|
|
31015
31747
|
}
|
|
31016
31748
|
};
|
|
31017
31749
|
},
|
|
@@ -31019,7 +31751,7 @@ function makeLease(p) {
|
|
|
31019
31751
|
check("staging the tree");
|
|
31020
31752
|
const paths = venueStagePaths(p.stageBase);
|
|
31021
31753
|
const push = async (from, to) => {
|
|
31022
|
-
const res = await p.
|
|
31754
|
+
const res = await p.transport.push(from, to);
|
|
31023
31755
|
if (!res.ok) {
|
|
31024
31756
|
throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
31025
31757
|
}
|
|
@@ -31177,7 +31909,7 @@ function makeLease(p) {
|
|
|
31177
31909
|
}
|
|
31178
31910
|
};
|
|
31179
31911
|
}
|
|
31180
|
-
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;
|
|
31181
31913
|
var init_hostedVenue = __esm({
|
|
31182
31914
|
"../../packages/envrun/dist/hostedVenue.js"() {
|
|
31183
31915
|
"use strict";
|
|
@@ -31298,7 +32030,55 @@ var init_hostedVenue = __esm({
|
|
|
31298
32030
|
throw new Error("JWKS fetch returned a body with no keys array");
|
|
31299
32031
|
}
|
|
31300
32032
|
return body;
|
|
31301
|
-
}
|
|
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 })
|
|
31302
32082
|
};
|
|
31303
32083
|
VENUE_SSH_USER = "th-runner";
|
|
31304
32084
|
GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
|
|
@@ -31319,6 +32099,17 @@ var init_hostedVenue = __esm({
|
|
|
31319
32099
|
SSH_KEY_NOT_READY = /Permission denied \(publickey/;
|
|
31320
32100
|
DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
|
|
31321
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";
|
|
31322
32113
|
}
|
|
31323
32114
|
});
|
|
31324
32115
|
|
|
@@ -32218,10 +33009,10 @@ var init_references = __esm({
|
|
|
32218
33009
|
|
|
32219
33010
|
// ../../packages/envspec/dist/repo.js
|
|
32220
33011
|
import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
|
|
32221
|
-
import { join as join27, relative as relative2, sep as
|
|
33012
|
+
import { join as join27, relative as relative2, sep as sep5 } from "path";
|
|
32222
33013
|
function createRepoReader(repoPath) {
|
|
32223
33014
|
const resolveIn = (relativePath) => relativePath === "" ? repoPath : join27(repoPath, relativePath);
|
|
32224
|
-
const toPosix = (absolute) => relative2(repoPath, absolute).split(
|
|
33015
|
+
const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
|
|
32225
33016
|
const readText = (relativePath) => {
|
|
32226
33017
|
try {
|
|
32227
33018
|
return readFileSync14(resolveIn(relativePath), "utf8");
|
|
@@ -32864,13 +33655,16 @@ function git(repoDir, args, allowNonZero = false) {
|
|
|
32864
33655
|
}
|
|
32865
33656
|
return res.stdout ?? "";
|
|
32866
33657
|
}
|
|
32867
|
-
function collectWorkingDiff(repoDir) {
|
|
33658
|
+
function collectWorkingDiff(repoDir, opts = {}) {
|
|
32868
33659
|
if (!existsSync12(join28(repoDir, ".git"))) {
|
|
32869
33660
|
throw new ThRunError(`${repoDir} is not a git checkout (no .git). \`th run\` ships the working diff, so it needs a repository to read one from.`);
|
|
32870
33661
|
}
|
|
32871
33662
|
const headSha = git(repoDir, ["rev-parse", "HEAD"]).trim();
|
|
32872
|
-
const
|
|
32873
|
-
const
|
|
33663
|
+
const diffBase = opts.base === void 0 ? headSha : assertDiffBase(repoDir, opts.base);
|
|
33664
|
+
const trackedArgs = opts.base === void 0 ? ["diff", "HEAD"] : ["diff", diffBase];
|
|
33665
|
+
const pathsEnd = opts.base === void 0 ? [] : ["--"];
|
|
33666
|
+
const tracked = git(repoDir, [...trackedArgs, "--binary", ...pathsEnd]);
|
|
33667
|
+
const trackedChanged = git(repoDir, [...trackedArgs, "--name-only", ...pathsEnd]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
32874
33668
|
const untracked = git(repoDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
32875
33669
|
const parts = [];
|
|
32876
33670
|
if (tracked.trim() !== "")
|
|
@@ -32880,7 +33674,17 @@ function collectWorkingDiff(repoDir) {
|
|
|
32880
33674
|
if (one.trim() !== "")
|
|
32881
33675
|
parts.push(one.replace(/\n*$/, "\n"));
|
|
32882
33676
|
}
|
|
32883
|
-
return { patch: parts.join(""), headSha, trackedChanged, untracked };
|
|
33677
|
+
return { patch: parts.join(""), headSha, diffBase, trackedChanged, untracked };
|
|
33678
|
+
}
|
|
33679
|
+
function assertDiffBase(repoDir, base) {
|
|
33680
|
+
if (!FULL_COMMIT_ID.test(base)) {
|
|
33681
|
+
throw new ThRunError(`the diff base must be a full 40-character commit id, got ${JSON.stringify(base)}.`);
|
|
33682
|
+
}
|
|
33683
|
+
const resolved = git(repoDir, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`], true);
|
|
33684
|
+
if (resolved.trim() !== base) {
|
|
33685
|
+
throw new ThRunError(`the diff base ${base} is not a commit in ${repoDir}.`);
|
|
33686
|
+
}
|
|
33687
|
+
return base;
|
|
32884
33688
|
}
|
|
32885
33689
|
function isLocalPath(p) {
|
|
32886
33690
|
return !UNC_PATH.test(p) && (p.startsWith("/") || WINDOWS_ABSOLUTE.test(p));
|
|
@@ -33452,8 +34256,9 @@ async function runVerification(req, ctx) {
|
|
|
33452
34256
|
let diff = null;
|
|
33453
34257
|
if (source.kind === "working-diff") {
|
|
33454
34258
|
progress("collect", `reading the working diff from ${source.localRepoDir}`);
|
|
33455
|
-
diff = collectWorkingDiff(source.localRepoDir);
|
|
33456
|
-
|
|
34259
|
+
diff = collectWorkingDiff(source.localRepoDir, source.diffBase === void 0 ? {} : { base: source.diffBase });
|
|
34260
|
+
const from = source.diffBase === void 0 ? "" : `, from delivered baseline ${diff.diffBase.slice(0, 12)}`;
|
|
34261
|
+
progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes${from}`);
|
|
33457
34262
|
} else {
|
|
33458
34263
|
progress("collect", "dispatched run: the tree is the stored commit itself");
|
|
33459
34264
|
}
|
|
@@ -33655,7 +34460,7 @@ async function runVerification(req, ctx) {
|
|
|
33655
34460
|
await releaseWithoutThrowing(lease, progress);
|
|
33656
34461
|
}
|
|
33657
34462
|
}
|
|
33658
|
-
var ThRunError, OUTPUT_TAIL_BYTES, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
|
|
34463
|
+
var ThRunError, OUTPUT_TAIL_BYTES, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
|
|
33659
34464
|
var init_thrun = __esm({
|
|
33660
34465
|
"../../packages/envrun/dist/thrun.js"() {
|
|
33661
34466
|
"use strict";
|
|
@@ -33673,6 +34478,7 @@ var init_thrun = __esm({
|
|
|
33673
34478
|
ThRunError = class extends Error {
|
|
33674
34479
|
};
|
|
33675
34480
|
OUTPUT_TAIL_BYTES = 4e3;
|
|
34481
|
+
FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
|
|
33676
34482
|
ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["https", "http", "ssh", "git"]);
|
|
33677
34483
|
SCP_STYLE = /^[A-Za-z0-9._~+-]+@[A-Za-z0-9._-]+:[^:]/;
|
|
33678
34484
|
URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
|
|
@@ -34531,6 +35337,7 @@ __export(dist_exports, {
|
|
|
34531
35337
|
ALEMBIC_IMAGE: () => ALEMBIC_IMAGE,
|
|
34532
35338
|
ATTEST_REFUSAL_REASONS: () => ATTEST_REFUSAL_REASONS,
|
|
34533
35339
|
BOOKKEEPING_TABLES: () => BOOKKEEPING_TABLES,
|
|
35340
|
+
CONFIDENTIAL_SPACE_MACHINE_TYPE: () => CONFIDENTIAL_SPACE_MACHINE_TYPE,
|
|
34534
35341
|
CONTAINMENT_UNAVAILABLE_PREFIX: () => CONTAINMENT_UNAVAILABLE_PREFIX,
|
|
34535
35342
|
CloneUnavailableError: () => CloneUnavailableError,
|
|
34536
35343
|
DAEMON_FACTS_FORMAT: () => DAEMON_FACTS_FORMAT,
|
|
@@ -34672,6 +35479,7 @@ __export(dist_exports, {
|
|
|
34672
35479
|
toolingImageFor: () => toolingImageFor,
|
|
34673
35480
|
unquoteDiffPath: () => unquoteDiffPath,
|
|
34674
35481
|
venueGcloudEnv: () => venueGcloudEnv,
|
|
35482
|
+
venueImageFromEnv: () => venueImageFromEnv,
|
|
34675
35483
|
venueStagePaths: () => venueStagePaths,
|
|
34676
35484
|
verifyWorkingDiff: () => verifyWorkingDiff
|
|
34677
35485
|
});
|
|
@@ -35034,7 +35842,7 @@ __export(jpi_claim_exports, {
|
|
|
35034
35842
|
});
|
|
35035
35843
|
import {
|
|
35036
35844
|
readFileSync as readFileSync15,
|
|
35037
|
-
writeFileSync as
|
|
35845
|
+
writeFileSync as writeFileSync16,
|
|
35038
35846
|
mkdirSync as mkdirSync7,
|
|
35039
35847
|
mkdtempSync as mkdtempSync5,
|
|
35040
35848
|
renameSync as renameSync7,
|
|
@@ -35045,7 +35853,7 @@ import {
|
|
|
35045
35853
|
readdirSync as readdirSync3
|
|
35046
35854
|
} from "fs";
|
|
35047
35855
|
import { join as join29, dirname as dirname9, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
|
|
35048
|
-
import { createHash as
|
|
35856
|
+
import { createHash as createHash10 } from "crypto";
|
|
35049
35857
|
import { homedir as homedir15, hostname as osHostname } from "os";
|
|
35050
35858
|
import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
|
|
35051
35859
|
import { promisify as promisify3 } from "util";
|
|
@@ -35063,7 +35871,7 @@ function markClaimNudged(id) {
|
|
|
35063
35871
|
const ids2 = readNudgedClaimIds();
|
|
35064
35872
|
ids2.add(id);
|
|
35065
35873
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
35066
|
-
|
|
35874
|
+
writeFileSync16(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
|
|
35067
35875
|
} catch {
|
|
35068
35876
|
}
|
|
35069
35877
|
}
|
|
@@ -36237,7 +37045,7 @@ async function registerFounderClaim(b) {
|
|
|
36237
37045
|
let clearedLocalCredential = false;
|
|
36238
37046
|
let refusedForPurpose = false;
|
|
36239
37047
|
let preserveBackgroundToken = false;
|
|
36240
|
-
const
|
|
37048
|
+
const refuse4 = (reason) => {
|
|
36241
37049
|
console.error(
|
|
36242
37050
|
`
|
|
36243
37051
|
terminalhire claim: refusing to record \u2014 ${reason}
|
|
@@ -36262,7 +37070,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36262
37070
|
try {
|
|
36263
37071
|
expectLogin = await localLoginForPaidBrowserClaim();
|
|
36264
37072
|
} catch (err) {
|
|
36265
|
-
|
|
37073
|
+
refuse4(
|
|
36266
37074
|
`could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
|
|
36267
37075
|
Run \`terminalhire login\`, then retry this claim. Browser verification was
|
|
36268
37076
|
not opened and no claim was sent.`
|
|
@@ -36278,16 +37086,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36278
37086
|
claimRef: opportunityShortToken(b.bountyId)
|
|
36279
37087
|
});
|
|
36280
37088
|
if (!proofToken) {
|
|
36281
|
-
|
|
37089
|
+
refuse4("could not verify your GitHub identity with terminalhire (see above).");
|
|
36282
37090
|
}
|
|
36283
37091
|
const verifiedLogin = proofTokenLogin(proofToken);
|
|
36284
37092
|
if (!verifiedLogin) {
|
|
36285
|
-
|
|
37093
|
+
refuse4(
|
|
36286
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."
|
|
36287
37095
|
);
|
|
36288
37096
|
}
|
|
36289
37097
|
if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
|
|
36290
|
-
|
|
37098
|
+
refuse4(
|
|
36291
37099
|
`you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
|
|
36292
37100
|
Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
|
|
36293
37101
|
run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
|
|
@@ -36319,7 +37127,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36319
37127
|
try {
|
|
36320
37128
|
res = await sendRegistration(true);
|
|
36321
37129
|
} catch (err) {
|
|
36322
|
-
|
|
37130
|
+
refuse4(
|
|
36323
37131
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36324
37132
|
);
|
|
36325
37133
|
}
|
|
@@ -36370,7 +37178,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36370
37178
|
try {
|
|
36371
37179
|
res = await sendRegistration(true);
|
|
36372
37180
|
} catch (err) {
|
|
36373
|
-
|
|
37181
|
+
refuse4(
|
|
36374
37182
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36375
37183
|
);
|
|
36376
37184
|
}
|
|
@@ -36381,14 +37189,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36381
37189
|
res = await sendRegistration(false);
|
|
36382
37190
|
refusalBody = await readRefusal(res);
|
|
36383
37191
|
} catch (err) {
|
|
36384
|
-
|
|
37192
|
+
refuse4(
|
|
36385
37193
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36386
37194
|
);
|
|
36387
37195
|
}
|
|
36388
37196
|
}
|
|
36389
37197
|
if (!res.ok) {
|
|
36390
37198
|
const detail = refusalBody?.message || refusalBody?.error || "";
|
|
36391
|
-
|
|
37199
|
+
refuse4(
|
|
36392
37200
|
detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
|
|
36393
37201
|
);
|
|
36394
37202
|
}
|
|
@@ -36399,7 +37207,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36399
37207
|
body = null;
|
|
36400
37208
|
}
|
|
36401
37209
|
if (!body || body.ok !== true) {
|
|
36402
|
-
|
|
37210
|
+
refuse4("malformed registration response from the server.");
|
|
36403
37211
|
}
|
|
36404
37212
|
const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
|
|
36405
37213
|
if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
|
|
@@ -37829,7 +38637,7 @@ function writeSliceFiles(destDir, files) {
|
|
|
37829
38637
|
if (typeof f.content === "string") {
|
|
37830
38638
|
const abs = join29(destDir, f.path);
|
|
37831
38639
|
mkdirSync7(dirname9(abs), { recursive: true });
|
|
37832
|
-
|
|
38640
|
+
writeFileSync16(abs, f.content, "utf8");
|
|
37833
38641
|
written.push(f.path);
|
|
37834
38642
|
} else {
|
|
37835
38643
|
unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
|
|
@@ -37878,7 +38686,7 @@ function ensureExcludedPackDir(destDir) {
|
|
|
37878
38686
|
const existing = existsSync13(excludeFile) ? readFileSync15(excludeFile, "utf8") : "";
|
|
37879
38687
|
if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
|
|
37880
38688
|
mkdirSync7(dirname9(excludeFile), { recursive: true });
|
|
37881
|
-
|
|
38689
|
+
writeFileSync16(
|
|
37882
38690
|
excludeFile,
|
|
37883
38691
|
`${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
|
|
37884
38692
|
`,
|
|
@@ -37894,23 +38702,33 @@ function writePackFile(destDir, relPath, content, what) {
|
|
|
37894
38702
|
try {
|
|
37895
38703
|
const abs = join29(destDir, relPath);
|
|
37896
38704
|
mkdirSync7(dirname9(abs), { recursive: true });
|
|
37897
|
-
|
|
38705
|
+
writeFileSync16(abs, content, { encoding: "utf8", flag: "wx" });
|
|
37898
38706
|
} catch (err) {
|
|
37899
38707
|
return { written: false, reason: `the ${what} could not be written (${err.message})` };
|
|
37900
38708
|
}
|
|
37901
38709
|
return { written: true, reason: null, sha256: sha256OfUtf8(content) };
|
|
37902
38710
|
}
|
|
37903
38711
|
function sha256OfUtf8(content) {
|
|
37904
|
-
return
|
|
38712
|
+
return createHash10("sha256").update(content, "utf8").digest("hex");
|
|
37905
38713
|
}
|
|
37906
|
-
function writeWorkspacePack(destDir, spec, claim) {
|
|
38714
|
+
function writeWorkspacePack(destDir, spec, claim, delivery) {
|
|
38715
|
+
if (delivery !== "full" && delivery !== "sparse") {
|
|
38716
|
+
throw new TypeError(
|
|
38717
|
+
`writeWorkspacePack: delivery must be 'full' or 'sparse', got ${String(delivery)}`
|
|
38718
|
+
);
|
|
38719
|
+
}
|
|
37907
38720
|
const gate = ensureExcludedPackDir(destDir);
|
|
37908
38721
|
if (!gate.ok) {
|
|
37909
38722
|
const refused = { written: false, reason: gate.reason };
|
|
37910
38723
|
return { brief: refused, verify: refused, agents: refused };
|
|
37911
38724
|
}
|
|
37912
38725
|
const brief = typeof spec !== "string" || spec.trim() === "" ? { written: false, reason: "the server sent no task for this posting" } : writePackFile(destDir, BRIEF_REL_PATH, spec, "task");
|
|
37913
|
-
const verify = writePackFile(
|
|
38726
|
+
const verify = writePackFile(
|
|
38727
|
+
destDir,
|
|
38728
|
+
VERIFY_REL_PATH,
|
|
38729
|
+
renderVerifyDoc(claim, delivery),
|
|
38730
|
+
"verify note"
|
|
38731
|
+
);
|
|
37914
38732
|
const agents = writePackFile(
|
|
37915
38733
|
destDir,
|
|
37916
38734
|
AGENTS_REL_PATH,
|
|
@@ -37923,20 +38741,30 @@ function packSafeId(claim) {
|
|
|
37923
38741
|
const id = String(claim?.id ?? "");
|
|
37924
38742
|
return PACK_SAFE_ID.test(id) ? id : "<your claim id \u2014 see: terminalhire claim list>";
|
|
37925
38743
|
}
|
|
37926
|
-
function renderVerifyDoc(claim) {
|
|
38744
|
+
function renderVerifyDoc(claim, delivery) {
|
|
37927
38745
|
const id = packSafeId(claim);
|
|
38746
|
+
const runStep2 = delivery === "full" ? ` ${nextStep("terminalhire run")}
|
|
38747
|
+
|
|
38748
|
+
It tests every change since the delivered baseline, committed or not, which
|
|
38749
|
+
is what submit sends once it is all committed.` : ` ${nextStep("terminalhire run --target <clone URL of the full repository>")}
|
|
38750
|
+
|
|
38751
|
+
It tests every change since the delivered baseline, committed or not, which
|
|
38752
|
+
is what submit sends once it is all committed. This delivery contains only the
|
|
38753
|
+
files you were granted, so the local run needs the whole repository; if you
|
|
38754
|
+
don't have access to it, skip to step 4, because the poster-side run in step 5
|
|
38755
|
+
is the verdict that counts.`;
|
|
37928
38756
|
return `# Verifying claim ${id}
|
|
37929
38757
|
|
|
37930
38758
|
This workspace was delivered by terminalhire for a posting. "Done" is
|
|
37931
38759
|
judged on the diff: the submitted patch is the change from the delivered
|
|
37932
38760
|
baseline (this repo's root commit) to HEAD, so only committed, tracked changes
|
|
37933
|
-
count.
|
|
38761
|
+
count. Submit needs a clean tree; run does not.
|
|
37934
38762
|
|
|
37935
38763
|
1. Read the poster's task first, when there is one: ${BRIEF_REL_PATH}
|
|
37936
38764
|
2. Work on the claim branch this delivery checked out, committing as you go.
|
|
37937
38765
|
3. To verify locally in terminalhire's sandboxed runner, from this directory:
|
|
37938
38766
|
|
|
37939
|
-
|
|
38767
|
+
${runStep2}
|
|
37940
38768
|
|
|
37941
38769
|
4. Submit \u2014 the only step that sends anything off this machine. It prints what it
|
|
37942
38770
|
is about to send first, and asks y/N only at an interactive terminal:
|
|
@@ -38302,11 +39130,13 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
|
|
|
38302
39130
|
);
|
|
38303
39131
|
process.exit(1);
|
|
38304
39132
|
}
|
|
38305
|
-
const pack = writeWorkspacePack(dest, body.spec, claim);
|
|
39133
|
+
const pack = writeWorkspacePack(dest, body.spec, claim, "full");
|
|
38306
39134
|
const working = claims.updateClaim(claim.id, {
|
|
38307
39135
|
worktreePath: dest,
|
|
38308
39136
|
branch,
|
|
38309
39137
|
state: "working",
|
|
39138
|
+
// `terminalhire run` clones this commit when run from the workspace (TERM-1098).
|
|
39139
|
+
baseSha,
|
|
38310
39140
|
workspacePack: {
|
|
38311
39141
|
brief: pack.brief.written === true,
|
|
38312
39142
|
verify: pack.verify.written === true,
|
|
@@ -38462,7 +39292,7 @@ async function attemptSliceDelivery(id, flags = {}) {
|
|
|
38462
39292
|
let pack;
|
|
38463
39293
|
try {
|
|
38464
39294
|
await sh("git", ["-C", dest, "init"]);
|
|
38465
|
-
pack = writeWorkspacePack(dest, body.spec, claim);
|
|
39295
|
+
pack = writeWorkspacePack(dest, body.spec, claim, "sparse");
|
|
38466
39296
|
await sh("git", ["-C", dest, "add", "-A"]);
|
|
38467
39297
|
await sh("git", [
|
|
38468
39298
|
"-C",
|
|
@@ -38499,6 +39329,11 @@ async function attemptSliceDelivery(id, flags = {}) {
|
|
|
38499
39329
|
worktreePath: dest,
|
|
38500
39330
|
branch,
|
|
38501
39331
|
state: "working",
|
|
39332
|
+
// The commit these files were cut from, so `terminalhire run` can clone it unasked
|
|
39333
|
+
// (TERM-1098). Recorded only when it is a full sha: a slice confirmed before the
|
|
39334
|
+
// server kept one answers null, and a value of any other shape is not a commit we
|
|
39335
|
+
// could clone, so neither is written and `run` asks for `--sha` instead.
|
|
39336
|
+
...typeof body.baseSha === "string" && /^[0-9a-f]{40}$/.test(body.baseSha) ? { baseSha: body.baseSha } : {},
|
|
38502
39337
|
workspacePack: {
|
|
38503
39338
|
brief: pack.brief.written === true,
|
|
38504
39339
|
verify: pack.verify.written === true,
|
|
@@ -39231,7 +40066,7 @@ function readClaimPushMarker() {
|
|
|
39231
40066
|
}
|
|
39232
40067
|
function writeClaimPushMarker(marker) {
|
|
39233
40068
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
39234
|
-
|
|
40069
|
+
writeFileSync16(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
39235
40070
|
}
|
|
39236
40071
|
function clearClaimPushMarker() {
|
|
39237
40072
|
try {
|
|
@@ -40383,7 +41218,7 @@ __export(util_exports, {
|
|
|
40383
41218
|
getSizableOrigin: () => getSizableOrigin,
|
|
40384
41219
|
hexToUint8Array: () => hexToUint8Array,
|
|
40385
41220
|
isObject: () => isObject,
|
|
40386
|
-
isPlainObject: () =>
|
|
41221
|
+
isPlainObject: () => isPlainObject2,
|
|
40387
41222
|
issue: () => issue,
|
|
40388
41223
|
joinValues: () => joinValues,
|
|
40389
41224
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
@@ -40545,7 +41380,7 @@ function slugify(input) {
|
|
|
40545
41380
|
function isObject(data) {
|
|
40546
41381
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
40547
41382
|
}
|
|
40548
|
-
function
|
|
41383
|
+
function isPlainObject2(o) {
|
|
40549
41384
|
if (isObject(o) === false)
|
|
40550
41385
|
return false;
|
|
40551
41386
|
const ctor = o.constructor;
|
|
@@ -40562,7 +41397,7 @@ function isPlainObject(o) {
|
|
|
40562
41397
|
return true;
|
|
40563
41398
|
}
|
|
40564
41399
|
function shallowClone(o) {
|
|
40565
|
-
if (
|
|
41400
|
+
if (isPlainObject2(o))
|
|
40566
41401
|
return { ...o };
|
|
40567
41402
|
if (Array.isArray(o))
|
|
40568
41403
|
return [...o];
|
|
@@ -40702,7 +41537,7 @@ function omit(schema, mask) {
|
|
|
40702
41537
|
return clone(schema, def);
|
|
40703
41538
|
}
|
|
40704
41539
|
function extend(schema, shape) {
|
|
40705
|
-
if (!
|
|
41540
|
+
if (!isPlainObject2(shape)) {
|
|
40706
41541
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
40707
41542
|
}
|
|
40708
41543
|
const checks = schema._zod.def.checks;
|
|
@@ -40725,7 +41560,7 @@ function extend(schema, shape) {
|
|
|
40725
41560
|
return clone(schema, def);
|
|
40726
41561
|
}
|
|
40727
41562
|
function safeExtend(schema, shape) {
|
|
40728
|
-
if (!
|
|
41563
|
+
if (!isPlainObject2(shape)) {
|
|
40729
41564
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
40730
41565
|
}
|
|
40731
41566
|
const def = mergeDefs(schema._zod.def, {
|
|
@@ -41886,7 +42721,7 @@ function mergeValues(a, b) {
|
|
|
41886
42721
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
41887
42722
|
return { valid: true, data: a };
|
|
41888
42723
|
}
|
|
41889
|
-
if (
|
|
42724
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
41890
42725
|
const bKeys = Object.keys(b);
|
|
41891
42726
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
41892
42727
|
const newObj = { ...a, ...b };
|
|
@@ -42838,7 +43673,7 @@ var init_schemas = __esm({
|
|
|
42838
43673
|
$ZodType.init(inst, def);
|
|
42839
43674
|
inst._zod.parse = (payload, ctx) => {
|
|
42840
43675
|
const input = payload.value;
|
|
42841
|
-
if (!
|
|
43676
|
+
if (!isPlainObject2(input)) {
|
|
42842
43677
|
payload.issues.push({
|
|
42843
43678
|
expected: "record",
|
|
42844
43679
|
code: "invalid_type",
|
|
@@ -47980,7 +48815,7 @@ var init_zod_json_schema_compat = __esm({
|
|
|
47980
48815
|
});
|
|
47981
48816
|
|
|
47982
48817
|
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
|
|
47983
|
-
function
|
|
48818
|
+
function isPlainObject3(value) {
|
|
47984
48819
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
47985
48820
|
}
|
|
47986
48821
|
function mergeCapabilities(base, additional) {
|
|
@@ -47991,7 +48826,7 @@ function mergeCapabilities(base, additional) {
|
|
|
47991
48826
|
if (addValue === void 0)
|
|
47992
48827
|
continue;
|
|
47993
48828
|
const baseValue = result[k];
|
|
47994
|
-
if (
|
|
48829
|
+
if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
|
|
47995
48830
|
result[k] = { ...baseValue, ...addValue };
|
|
47996
48831
|
} else {
|
|
47997
48832
|
result[k] = addValue;
|
|
@@ -62349,7 +63184,7 @@ __export(config_exports, {
|
|
|
62349
63184
|
readConfig: () => readConfig,
|
|
62350
63185
|
writeConfig: () => writeConfig
|
|
62351
63186
|
});
|
|
62352
|
-
import { readFileSync as readFileSync16, writeFileSync as
|
|
63187
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync17, existsSync as existsSync14 } from "fs";
|
|
62353
63188
|
import { join as join30 } from "path";
|
|
62354
63189
|
import { homedir as homedir16 } from "os";
|
|
62355
63190
|
function readConfig() {
|
|
@@ -62372,7 +63207,7 @@ function writeConfig(config2) {
|
|
|
62372
63207
|
}
|
|
62373
63208
|
delete merged.contributePrompted;
|
|
62374
63209
|
}
|
|
62375
|
-
|
|
63210
|
+
writeFileSync17(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
62376
63211
|
}
|
|
62377
63212
|
function parseNudgeMode(raw) {
|
|
62378
63213
|
if (raw === "session" || raw === "always") return raw;
|