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-claim.js
CHANGED
|
@@ -11137,6 +11137,7 @@ __export(github_auth_exports, {
|
|
|
11137
11137
|
decrypt: () => decrypt,
|
|
11138
11138
|
deleteGitHubToken: () => deleteGitHubToken,
|
|
11139
11139
|
encrypt: () => encrypt,
|
|
11140
|
+
githubTokenDisplayPath: () => githubTokenDisplayPath,
|
|
11140
11141
|
hasGitHubToken: () => hasGitHubToken,
|
|
11141
11142
|
loadKey: () => loadKey,
|
|
11142
11143
|
readGitHubToken: () => readGitHubToken,
|
|
@@ -11147,8 +11148,12 @@ __export(github_auth_exports, {
|
|
|
11147
11148
|
});
|
|
11148
11149
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes5 } from "crypto";
|
|
11149
11150
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync5, rmSync as rmSync3, renameSync as renameSync2 } from "fs";
|
|
11150
|
-
import { join as join8 } from "path";
|
|
11151
|
+
import { join as join8, sep } from "path";
|
|
11151
11152
|
import { homedir as homedir6 } from "os";
|
|
11153
|
+
function githubTokenDisplayPath() {
|
|
11154
|
+
const home = homedir6();
|
|
11155
|
+
return TOKEN_FILE.startsWith(home + sep) ? `~${TOKEN_FILE.slice(home.length)}` : TOKEN_FILE;
|
|
11156
|
+
}
|
|
11152
11157
|
async function loadKey() {
|
|
11153
11158
|
return loadOrCreateSharedKey();
|
|
11154
11159
|
}
|
|
@@ -26551,12 +26556,33 @@ function scrubEndpointEnv(env) {
|
|
|
26551
26556
|
}
|
|
26552
26557
|
return out;
|
|
26553
26558
|
}
|
|
26554
|
-
function
|
|
26559
|
+
function containerFlagsAt(args) {
|
|
26560
|
+
if (args[0] === "run" || args[0] === "create")
|
|
26561
|
+
return 1;
|
|
26562
|
+
if (args[0] === "container" && (args[1] === "run" || args[1] === "create"))
|
|
26563
|
+
return 2;
|
|
26564
|
+
return -1;
|
|
26565
|
+
}
|
|
26566
|
+
function remoteDockerClient(endpoint, opts = {}) {
|
|
26555
26567
|
if (endpoint.trim().length === 0) {
|
|
26556
26568
|
throw new Error("remoteDockerClient: endpoint must be a non-empty docker host");
|
|
26557
26569
|
}
|
|
26558
26570
|
const target = endpoint.trim();
|
|
26559
|
-
const
|
|
26571
|
+
const tlsFlags = [];
|
|
26572
|
+
if (opts.tls) {
|
|
26573
|
+
for (const key of ["caPath", "certPath", "keyPath"]) {
|
|
26574
|
+
if (opts.tls[key].trim().length === 0) {
|
|
26575
|
+
throw new Error(`remoteDockerClient: tls.${key} must name a file`);
|
|
26576
|
+
}
|
|
26577
|
+
}
|
|
26578
|
+
tlsFlags.push("--tlsverify", "--tlscacert", opts.tls.caPath, "--tlscert", opts.tls.certPath, "--tlskey", opts.tls.keyPath);
|
|
26579
|
+
}
|
|
26580
|
+
const runArgs = opts.runArgs ?? [];
|
|
26581
|
+
const argv = (args) => {
|
|
26582
|
+
const at = runArgs.length > 0 ? containerFlagsAt(args) : -1;
|
|
26583
|
+
const sub = at < 0 ? [...args] : [...args.slice(0, at), ...runArgs, ...args.slice(at)];
|
|
26584
|
+
return ["--host", target, ...tlsFlags, ...sub];
|
|
26585
|
+
};
|
|
26560
26586
|
return {
|
|
26561
26587
|
endpoint: target,
|
|
26562
26588
|
// Always empty, and it is a fact about this client rather than about the
|
|
@@ -26573,10 +26599,10 @@ function remoteDockerClient(endpoint) {
|
|
|
26573
26599
|
env: scrubEndpointEnv(process.env)
|
|
26574
26600
|
});
|
|
26575
26601
|
},
|
|
26576
|
-
sync(args,
|
|
26602
|
+
sync(args, opts2) {
|
|
26577
26603
|
return syncResult(spawnSync3(DOCKER_BIN, argv(args), {
|
|
26578
26604
|
encoding: "utf8",
|
|
26579
|
-
timeout:
|
|
26605
|
+
timeout: opts2?.timeoutMs,
|
|
26580
26606
|
env: scrubEndpointEnv(process.env)
|
|
26581
26607
|
}));
|
|
26582
26608
|
},
|
|
@@ -28557,6 +28583,197 @@ var init_gceIdentity = __esm({
|
|
|
28557
28583
|
}
|
|
28558
28584
|
});
|
|
28559
28585
|
|
|
28586
|
+
// ../../packages/attest/dist/confidentialSpace.js
|
|
28587
|
+
import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
|
|
28588
|
+
function refuse2(reason, detail) {
|
|
28589
|
+
return { ok: false, reason, detail };
|
|
28590
|
+
}
|
|
28591
|
+
function isPlainObject(v) {
|
|
28592
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
28593
|
+
}
|
|
28594
|
+
function assertExpectationsComplete2(expectations) {
|
|
28595
|
+
const required = [
|
|
28596
|
+
["audience", expectations.audience],
|
|
28597
|
+
["nonce", expectations.nonce],
|
|
28598
|
+
["instanceName", expectations.instanceName],
|
|
28599
|
+
["projectId", expectations.projectId],
|
|
28600
|
+
...(expectations.additionalNonces ?? []).map((n) => [
|
|
28601
|
+
"additionalNonces[]",
|
|
28602
|
+
n
|
|
28603
|
+
]),
|
|
28604
|
+
...Object.entries(expectations.containerEnv ?? {}).flatMap(([name, value]) => [
|
|
28605
|
+
["containerEnv name", name],
|
|
28606
|
+
[`containerEnv.${name}`, value]
|
|
28607
|
+
])
|
|
28608
|
+
];
|
|
28609
|
+
for (const [name, value] of required) {
|
|
28610
|
+
if (value === "") {
|
|
28611
|
+
throw new TypeError(`verifyConfidentialSpaceToken: expectations.${name} is empty`);
|
|
28612
|
+
}
|
|
28613
|
+
}
|
|
28614
|
+
}
|
|
28615
|
+
function nonceList(v) {
|
|
28616
|
+
if (typeof v === "string")
|
|
28617
|
+
return [v];
|
|
28618
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
28619
|
+
return null;
|
|
28620
|
+
if (!v.every((n) => typeof n === "string"))
|
|
28621
|
+
return null;
|
|
28622
|
+
return v;
|
|
28623
|
+
}
|
|
28624
|
+
async function verifyConfidentialSpaceToken(token, expectations, deps) {
|
|
28625
|
+
assertExpectationsComplete2(expectations);
|
|
28626
|
+
if (!Number.isFinite(deps.now)) {
|
|
28627
|
+
throw new TypeError("verifyConfidentialSpaceToken: deps.now is not a finite number");
|
|
28628
|
+
}
|
|
28629
|
+
if (expectations.allowedImageDigests.length === 0) {
|
|
28630
|
+
return refuse2("image-digest-allowlist-unset", "no workload image digest is allowlisted, so no attestation can be accepted");
|
|
28631
|
+
}
|
|
28632
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
28633
|
+
return refuse2("token-malformed", "longer than any attestation token");
|
|
28634
|
+
}
|
|
28635
|
+
const parts = token.split(".");
|
|
28636
|
+
if (parts.length !== 3) {
|
|
28637
|
+
return refuse2("token-malformed", "not a three-segment compact JWT");
|
|
28638
|
+
}
|
|
28639
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
28640
|
+
const header = decodeJsonSegment(headerB64);
|
|
28641
|
+
if (header === null) {
|
|
28642
|
+
return refuse2("token-malformed", "header segment is not base64url-encoded JSON");
|
|
28643
|
+
}
|
|
28644
|
+
if (header.alg !== "RS256") {
|
|
28645
|
+
return refuse2("algorithm-not-rs256", "token names an unsupported algorithm");
|
|
28646
|
+
}
|
|
28647
|
+
if (typeof header.kid !== "string" || header.kid === "") {
|
|
28648
|
+
return refuse2("unknown-key", "token header names no key id");
|
|
28649
|
+
}
|
|
28650
|
+
const jwks = await deps.fetchJwks();
|
|
28651
|
+
const jwk = jwks.keys.find((k) => k.kid === header.kid && k.kty === "RSA");
|
|
28652
|
+
if (jwk === void 0) {
|
|
28653
|
+
return refuse2("unknown-key", "token key id is not in the published key set");
|
|
28654
|
+
}
|
|
28655
|
+
const signature = strictBase64UrlDecode(signatureB64);
|
|
28656
|
+
if (signature === null) {
|
|
28657
|
+
return refuse2("token-malformed", "signature segment is not base64url");
|
|
28658
|
+
}
|
|
28659
|
+
let signatureValid = false;
|
|
28660
|
+
try {
|
|
28661
|
+
const publicKey = createPublicKey3({ key: jwk, format: "jwk" });
|
|
28662
|
+
signatureValid = cryptoVerify3("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), publicKey, signature);
|
|
28663
|
+
} catch {
|
|
28664
|
+
signatureValid = false;
|
|
28665
|
+
}
|
|
28666
|
+
if (!signatureValid) {
|
|
28667
|
+
return refuse2("signature-invalid", "signature does not verify under the named key");
|
|
28668
|
+
}
|
|
28669
|
+
const payload = decodeJsonSegment(payloadB64);
|
|
28670
|
+
if (payload === null) {
|
|
28671
|
+
return refuse2("token-malformed", "payload segment is not base64url-encoded JSON");
|
|
28672
|
+
}
|
|
28673
|
+
if (payload.iss !== CONFIDENTIAL_SPACE_ISSUER) {
|
|
28674
|
+
return refuse2("wrong-issuer", "issuer is not the Confidential Computing attestation service");
|
|
28675
|
+
}
|
|
28676
|
+
if (payload.aud !== expectations.audience) {
|
|
28677
|
+
return refuse2("wrong-audience", "audience is not the one this run expects");
|
|
28678
|
+
}
|
|
28679
|
+
const nowSeconds = deps.now / 1e3;
|
|
28680
|
+
const exp = payload.exp;
|
|
28681
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= nowSeconds) {
|
|
28682
|
+
return refuse2("expired", "exp is missing, malformed, or in the past");
|
|
28683
|
+
}
|
|
28684
|
+
const iat = payload.iat;
|
|
28685
|
+
if (typeof iat !== "number" || !Number.isFinite(iat) || iat > nowSeconds + IAT_FUTURE_SKEW_MS / 1e3) {
|
|
28686
|
+
return refuse2("iat-in-future", "iat is missing, malformed, or too far in the future");
|
|
28687
|
+
}
|
|
28688
|
+
const nonces = nonceList(payload.eat_nonce);
|
|
28689
|
+
if (nonces === null) {
|
|
28690
|
+
return refuse2("nonce-missing", "eat_nonce is absent or not a string or array of strings");
|
|
28691
|
+
}
|
|
28692
|
+
if (!nonces.includes(expectations.nonce)) {
|
|
28693
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry the nonce this run was dispatched with");
|
|
28694
|
+
}
|
|
28695
|
+
for (const extra of expectations.additionalNonces ?? []) {
|
|
28696
|
+
if (!nonces.includes(extra)) {
|
|
28697
|
+
return refuse2("nonce-mismatch", "eat_nonce does not carry every nonce this caller requires");
|
|
28698
|
+
}
|
|
28699
|
+
}
|
|
28700
|
+
if (payload.swname !== CONFIDENTIAL_SPACE_SWNAME) {
|
|
28701
|
+
return refuse2("swname-not-confidential-space", "the VM is not running a Confidential Space image");
|
|
28702
|
+
}
|
|
28703
|
+
if (payload.dbgstat !== CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION) {
|
|
28704
|
+
return refuse2("debug-enabled", "the image is not a production image with debug disabled");
|
|
28705
|
+
}
|
|
28706
|
+
if (payload.secboot !== true) {
|
|
28707
|
+
return refuse2("secure-boot-off", "Secure Boot is not reported as enabled");
|
|
28708
|
+
}
|
|
28709
|
+
if (typeof payload.hwmodel !== "string" || !payload.hwmodel.startsWith("GCP_")) {
|
|
28710
|
+
return refuse2("hwmodel-not-gcp", "hardware model is not a Google Cloud confidential model");
|
|
28711
|
+
}
|
|
28712
|
+
const hwmodel = payload.hwmodel;
|
|
28713
|
+
const submods = payload.submods;
|
|
28714
|
+
const gce = isPlainObject(submods) ? submods.gce : void 0;
|
|
28715
|
+
if (!isPlainObject(gce)) {
|
|
28716
|
+
return refuse2("submods-gce-missing", "no submods.gce block names the instance");
|
|
28717
|
+
}
|
|
28718
|
+
if (gce.project_id !== expectations.projectId) {
|
|
28719
|
+
return refuse2("project-mismatch", "token names an instance in a different project");
|
|
28720
|
+
}
|
|
28721
|
+
if (gce.instance_name !== expectations.instanceName) {
|
|
28722
|
+
return refuse2("instance-name-mismatch", "token names a different instance than this run booted");
|
|
28723
|
+
}
|
|
28724
|
+
if (typeof gce.instance_id !== "string" || gce.instance_id === "") {
|
|
28725
|
+
return refuse2("instance-id-missing", "instance id is absent or not a string");
|
|
28726
|
+
}
|
|
28727
|
+
const instanceId = gce.instance_id;
|
|
28728
|
+
if (typeof gce.zone !== "string" || !expectations.allowedZones.includes(gce.zone)) {
|
|
28729
|
+
return refuse2("zone-not-allowed", "zone is not one the pool is configured to use");
|
|
28730
|
+
}
|
|
28731
|
+
const zone = gce.zone;
|
|
28732
|
+
const container = isPlainObject(submods) ? submods.container : void 0;
|
|
28733
|
+
if (!isPlainObject(container)) {
|
|
28734
|
+
return refuse2("submods-container-missing", "no submods.container block names the workload");
|
|
28735
|
+
}
|
|
28736
|
+
if (typeof container.image_reference !== "string" || container.image_reference === "") {
|
|
28737
|
+
return refuse2("image-reference-missing", "the workload image reference is absent");
|
|
28738
|
+
}
|
|
28739
|
+
const imageReference = container.image_reference;
|
|
28740
|
+
if (typeof container.image_digest !== "string" || !expectations.allowedImageDigests.includes(container.image_digest)) {
|
|
28741
|
+
return refuse2("image-digest-not-allowed", "the workload image digest is not on the allowlist");
|
|
28742
|
+
}
|
|
28743
|
+
const env = container.env;
|
|
28744
|
+
for (const [name, value] of Object.entries(expectations.containerEnv ?? {})) {
|
|
28745
|
+
if (!isPlainObject(env) || !Object.prototype.hasOwnProperty.call(env, name) || env[name] !== value) {
|
|
28746
|
+
return refuse2("container-env-mismatch", `the signed container environment does not carry the ${name} this caller booted with`);
|
|
28747
|
+
}
|
|
28748
|
+
}
|
|
28749
|
+
return {
|
|
28750
|
+
ok: true,
|
|
28751
|
+
value: {
|
|
28752
|
+
instanceId,
|
|
28753
|
+
instanceName: expectations.instanceName,
|
|
28754
|
+
zone,
|
|
28755
|
+
projectId: expectations.projectId,
|
|
28756
|
+
imageDigest: container.image_digest,
|
|
28757
|
+
imageReference,
|
|
28758
|
+
hwmodel,
|
|
28759
|
+
iat,
|
|
28760
|
+
exp,
|
|
28761
|
+
tokenSha256: createHash7("sha256").update(token, "utf8").digest("hex")
|
|
28762
|
+
}
|
|
28763
|
+
};
|
|
28764
|
+
}
|
|
28765
|
+
var CONFIDENTIAL_SPACE_ISSUER, CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, CONFIDENTIAL_SPACE_SWNAME, CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION;
|
|
28766
|
+
var init_confidentialSpace = __esm({
|
|
28767
|
+
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
28768
|
+
"use strict";
|
|
28769
|
+
init_gceIdentity();
|
|
28770
|
+
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
28771
|
+
CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL = `${CONFIDENTIAL_SPACE_ISSUER}/.well-known/openid-configuration`;
|
|
28772
|
+
CONFIDENTIAL_SPACE_SWNAME = "CONFIDENTIAL_SPACE";
|
|
28773
|
+
CONFIDENTIAL_SPACE_DBGSTAT_PRODUCTION = "disabled-since-boot";
|
|
28774
|
+
}
|
|
28775
|
+
});
|
|
28776
|
+
|
|
28560
28777
|
// ../../packages/attest/dist/venueInstanceName.js
|
|
28561
28778
|
function venueInstanceName(runId) {
|
|
28562
28779
|
return `th-run-${runId}`.replace(/_/g, "-").toLowerCase().slice(0, 62);
|
|
@@ -28591,6 +28808,7 @@ var init_dist2 = __esm({
|
|
|
28591
28808
|
init_attestation();
|
|
28592
28809
|
init_nonce();
|
|
28593
28810
|
init_gceIdentity();
|
|
28811
|
+
init_confidentialSpace();
|
|
28594
28812
|
init_venueInstanceName();
|
|
28595
28813
|
init_verify();
|
|
28596
28814
|
}
|
|
@@ -28841,7 +29059,7 @@ var init_result = __esm({
|
|
|
28841
29059
|
});
|
|
28842
29060
|
|
|
28843
29061
|
// ../../packages/envrun/dist/attestation.js
|
|
28844
|
-
import { createHash as
|
|
29062
|
+
import { createHash as createHash8, randomBytes as randomBytes10 } from "crypto";
|
|
28845
29063
|
function contradicts(outcome, counts, exitCode) {
|
|
28846
29064
|
const budget = OUTCOME_TO_BUDGET[outcome];
|
|
28847
29065
|
if (budget === null)
|
|
@@ -28883,7 +29101,7 @@ function imageDigestOf(ref) {
|
|
|
28883
29101
|
return at === -1 ? null : ref.slice(at + 1);
|
|
28884
29102
|
}
|
|
28885
29103
|
function sha256Hex(data) {
|
|
28886
|
-
return
|
|
29104
|
+
return createHash8("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
|
|
28887
29105
|
}
|
|
28888
29106
|
function toTestRunResult(result, outputSha256) {
|
|
28889
29107
|
return {
|
|
@@ -28895,50 +29113,50 @@ function toTestRunResult(result, outputSha256) {
|
|
|
28895
29113
|
function toAcceptancePredicate(pair, opts = {}) {
|
|
28896
29114
|
const { baseline, patched } = pair;
|
|
28897
29115
|
if (baseline.runId === patched.runId) {
|
|
28898
|
-
return
|
|
29116
|
+
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.`);
|
|
28899
29117
|
}
|
|
28900
29118
|
if (patched.status === "refused") {
|
|
28901
|
-
return
|
|
29119
|
+
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`);
|
|
28902
29120
|
}
|
|
28903
29121
|
if (patched.outcome === null) {
|
|
28904
|
-
return
|
|
29122
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
28905
29123
|
}
|
|
28906
29124
|
if (baseline.status === "refused") {
|
|
28907
|
-
return
|
|
29125
|
+
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`);
|
|
28908
29126
|
}
|
|
28909
29127
|
if (baseline.outcome === null) {
|
|
28910
|
-
return
|
|
29128
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
28911
29129
|
}
|
|
28912
29130
|
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
28913
|
-
return
|
|
29131
|
+
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.`);
|
|
28914
29132
|
}
|
|
28915
29133
|
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
28916
29134
|
if (budget === null) {
|
|
28917
|
-
return
|
|
29135
|
+
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.`);
|
|
28918
29136
|
}
|
|
28919
29137
|
if (patched.testCommand === null) {
|
|
28920
|
-
return
|
|
29138
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
28921
29139
|
}
|
|
28922
29140
|
if (baseline.testCommand !== patched.testCommand) {
|
|
28923
|
-
return
|
|
29141
|
+
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.`);
|
|
28924
29142
|
}
|
|
28925
29143
|
if (baseline.targetSha !== patched.targetSha) {
|
|
28926
|
-
return
|
|
29144
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
28927
29145
|
}
|
|
28928
29146
|
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
28929
|
-
return
|
|
29147
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
28930
29148
|
}
|
|
28931
29149
|
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
28932
|
-
return
|
|
29150
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
28933
29151
|
}
|
|
28934
29152
|
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
28935
|
-
return
|
|
29153
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
28936
29154
|
}
|
|
28937
29155
|
if (baseline.treeDigest === patched.treeDigest) {
|
|
28938
|
-
return
|
|
29156
|
+
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`);
|
|
28939
29157
|
}
|
|
28940
29158
|
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
28941
|
-
return
|
|
29159
|
+
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.`);
|
|
28942
29160
|
}
|
|
28943
29161
|
for (const [side, outcome, counts, exitCode] of [
|
|
28944
29162
|
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
@@ -28946,7 +29164,7 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
28946
29164
|
]) {
|
|
28947
29165
|
const why = contradicts(outcome, counts, exitCode);
|
|
28948
29166
|
if (why !== null) {
|
|
28949
|
-
return
|
|
29167
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
28950
29168
|
}
|
|
28951
29169
|
}
|
|
28952
29170
|
for (const [side, source] of [
|
|
@@ -28955,44 +29173,44 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
28955
29173
|
]) {
|
|
28956
29174
|
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
28957
29175
|
if (why !== null)
|
|
28958
|
-
return
|
|
29176
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
28959
29177
|
}
|
|
28960
29178
|
if (patched.testCommandSource === "developer-declared") {
|
|
28961
|
-
return
|
|
29179
|
+
return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
|
|
28962
29180
|
}
|
|
28963
29181
|
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
28964
|
-
return
|
|
29182
|
+
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.`);
|
|
28965
29183
|
}
|
|
28966
29184
|
if (baseline.containerImage !== patched.containerImage) {
|
|
28967
|
-
return
|
|
29185
|
+
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.`);
|
|
28968
29186
|
}
|
|
28969
29187
|
if (patched.containerImage === null || patched.containerImage === "") {
|
|
28970
|
-
return
|
|
29188
|
+
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");
|
|
28971
29189
|
}
|
|
28972
29190
|
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
28973
|
-
return
|
|
29191
|
+
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.");
|
|
28974
29192
|
}
|
|
28975
29193
|
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
28976
|
-
return
|
|
29194
|
+
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.`);
|
|
28977
29195
|
}
|
|
28978
29196
|
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
28979
|
-
return
|
|
29197
|
+
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.`);
|
|
28980
29198
|
}
|
|
28981
29199
|
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
28982
|
-
return
|
|
29200
|
+
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.");
|
|
28983
29201
|
}
|
|
28984
29202
|
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
28985
|
-
return
|
|
29203
|
+
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.`);
|
|
28986
29204
|
}
|
|
28987
29205
|
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
28988
|
-
return
|
|
29206
|
+
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.`);
|
|
28989
29207
|
}
|
|
28990
29208
|
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
28991
|
-
return
|
|
29209
|
+
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.`);
|
|
28992
29210
|
}
|
|
28993
29211
|
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
28994
29212
|
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
28995
|
-
return
|
|
29213
|
+
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.`);
|
|
28996
29214
|
}
|
|
28997
29215
|
return {
|
|
28998
29216
|
ok: true,
|
|
@@ -29019,7 +29237,7 @@ function signRunStatement(predicate, privateKey, keyid) {
|
|
|
29019
29237
|
const statement = createAcceptanceStatement(predicate);
|
|
29020
29238
|
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
29021
29239
|
}
|
|
29022
|
-
var OUTCOME_TO_BUDGET, BASELINE_IS_A_VERDICT, CONTRADICTS_COUNTS, SOURCE_IS_SIGNABLE, ATTEST_REFUSAL_REASONS,
|
|
29240
|
+
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;
|
|
29023
29241
|
var init_attestation2 = __esm({
|
|
29024
29242
|
"../../packages/envrun/dist/attestation.js"() {
|
|
29025
29243
|
"use strict";
|
|
@@ -29123,7 +29341,7 @@ var init_attestation2 = __esm({
|
|
|
29123
29341
|
"outcome-contradicts-counts",
|
|
29124
29342
|
"test-command-origin-unattested"
|
|
29125
29343
|
];
|
|
29126
|
-
|
|
29344
|
+
refuse3 = (reason, detail) => ({
|
|
29127
29345
|
ok: false,
|
|
29128
29346
|
reason,
|
|
29129
29347
|
detail
|
|
@@ -29506,6 +29724,73 @@ var init_boundary = __esm({
|
|
|
29506
29724
|
});
|
|
29507
29725
|
|
|
29508
29726
|
// ../../packages/envrun/dist/gcpPlacement.js
|
|
29727
|
+
function assertWorkloadImage(fn, value) {
|
|
29728
|
+
if (typeof value !== "string" || !WORKLOAD_IMAGE_REFERENCE.test(value)) {
|
|
29729
|
+
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.`);
|
|
29730
|
+
}
|
|
29731
|
+
}
|
|
29732
|
+
function venueImageFromEnv(env) {
|
|
29733
|
+
const image = env["VENUE_IMAGE"] ?? "";
|
|
29734
|
+
if (image === "" || image === "cos-stable")
|
|
29735
|
+
return { venueImage: "cos-stable" };
|
|
29736
|
+
if (image !== "confidential-space") {
|
|
29737
|
+
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.");
|
|
29738
|
+
}
|
|
29739
|
+
const workloadImage = env["VENUE_WORKLOAD_IMAGE"] ?? "";
|
|
29740
|
+
if (workloadImage === "") {
|
|
29741
|
+
throw new GcpPlacementError("VENUE_IMAGE=confidential-space needs VENUE_WORKLOAD_IMAGE, the image reference the Confidential Space launcher runs. Nothing boots without one.");
|
|
29742
|
+
}
|
|
29743
|
+
assertWorkloadImage("venueImageFromEnv", workloadImage);
|
|
29744
|
+
return { venueImage: "confidential-space", workloadImage };
|
|
29745
|
+
}
|
|
29746
|
+
function bootImageFlags(p) {
|
|
29747
|
+
const image = p.venueImage ?? "cos-stable";
|
|
29748
|
+
if (image === "cos-stable")
|
|
29749
|
+
return ["--image-family=cos-stable", "--image-project=cos-cloud"];
|
|
29750
|
+
if (image !== "confidential-space") {
|
|
29751
|
+
throw new GcpPlacementError("gcpBootArgv: venueImage is not one of cos-stable, confidential-space. An unknown image is refused rather than booted as the default.");
|
|
29752
|
+
}
|
|
29753
|
+
const workloadImage = p.workloadImage;
|
|
29754
|
+
assertWorkloadImage("gcpBootArgv", workloadImage);
|
|
29755
|
+
const clientCert = p.clientCert;
|
|
29756
|
+
if (typeof clientCert !== "string" || !CLIENT_CERT_BASE64URL.test(clientCert)) {
|
|
29757
|
+
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.`);
|
|
29758
|
+
}
|
|
29759
|
+
if (p.runId.length < MIN_NONCE_BYTES) {
|
|
29760
|
+
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.`);
|
|
29761
|
+
}
|
|
29762
|
+
const metadata = [
|
|
29763
|
+
`tee-image-reference=${workloadImage}`,
|
|
29764
|
+
"tee-container-log-redirect=true",
|
|
29765
|
+
'tee-added-capabilities=["CAP_SYS_ADMIN","CAP_NET_ADMIN"]',
|
|
29766
|
+
"tee-cgroup-ns=true",
|
|
29767
|
+
`tee-env-TH_AUDIENCE=${expectedAudience(p.runId)}`,
|
|
29768
|
+
`tee-env-TH_NONCE=${p.runId}`,
|
|
29769
|
+
`tee-env-TH_CLIENT_CERT=${clientCert}`
|
|
29770
|
+
];
|
|
29771
|
+
return [
|
|
29772
|
+
// The production family. The debug family reports `dbgstat: enabled`, which the
|
|
29773
|
+
// intake's verifier refuses, so a debug boot could never produce a run we record.
|
|
29774
|
+
"--image-family=confidential-space",
|
|
29775
|
+
"--image-project=confidential-space-images",
|
|
29776
|
+
"--confidential-compute-type=SEV",
|
|
29777
|
+
// The deploy-workloads page says N2D with SEV may set MIGRATE for live migration,
|
|
29778
|
+
// and every other machine type needs TERMINATE. The spec chose TERMINATE: a
|
|
29779
|
+
// maintenance event ends the VM instead of moving it, which for a VM that lives at
|
|
29780
|
+
// most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
|
|
29781
|
+
// combination is phase 5's measurement.
|
|
29782
|
+
"--maintenance-policy=TERMINATE",
|
|
29783
|
+
`--metadata=^;^${metadata.join(";")}`,
|
|
29784
|
+
// What the standing firewall rule for IAP's range matches, so the tunnels to the
|
|
29785
|
+
// workload's dockerd and attest ports open on these VMs and no others.
|
|
29786
|
+
"--tags=th-cs-venue",
|
|
29787
|
+
// The family's 11 GB default leaves the workload a 7.7 GB root, and dockerd there
|
|
29788
|
+
// runs vfs, which copies every layer and every container's rootfs in full. Pulling
|
|
29789
|
+
// node:20-bookworm left 992 MB, and the next container create failed ENOSPC
|
|
29790
|
+
// (measured, TERM-1094). COS runs overlay2 on its own disk and keeps the default.
|
|
29791
|
+
"--boot-disk-size=50GB"
|
|
29792
|
+
];
|
|
29793
|
+
}
|
|
29509
29794
|
function assertInstanceIdentity(fn, id) {
|
|
29510
29795
|
const fields = [
|
|
29511
29796
|
["vmName", id.vmName, GCE_INSTANCE_NAME],
|
|
@@ -29531,8 +29816,8 @@ function gcpBootArgv(p) {
|
|
|
29531
29816
|
`--project=${p.project}`,
|
|
29532
29817
|
`--zone=${p.zone}`,
|
|
29533
29818
|
`--machine-type=${p.machineType}`,
|
|
29534
|
-
|
|
29535
|
-
|
|
29819
|
+
// What the VM boots, and the one part of this argv the two images do not share.
|
|
29820
|
+
...bootImageFlags(p),
|
|
29536
29821
|
// ── THE OUTER REAP. This, not the `finally` below, is what bounds the bill. ──
|
|
29537
29822
|
//
|
|
29538
29823
|
// An in-process `finally` is a best effort, not a reap. SIGKILL, a crashed
|
|
@@ -29577,7 +29862,13 @@ function gcpBootArgv(p) {
|
|
|
29577
29862
|
// expectation uses (TERM-832): two inline spellings of one value is how
|
|
29578
29863
|
// the boot path and the verifier drift apart silently.
|
|
29579
29864
|
`--service-account=${venueIdentityEmail(p.project)}`,
|
|
29580
|
-
|
|
29865
|
+
// A Confidential Space VM takes `cloud-platform` instead (TERM-1094): the launcher
|
|
29866
|
+
// pulls the workload image and mints the attestation token with this account, and
|
|
29867
|
+
// under `--no-scopes` it can do neither. What bounds a lifted token there is the
|
|
29868
|
+
// account's two grants — `confidentialcomputing.workloadUser` and a reader on the
|
|
29869
|
+
// one venue image repository — plus point 2 above, which is unchanged: the fence
|
|
29870
|
+
// still denies untrusted code the metadata server.
|
|
29871
|
+
...p.venueImage === "confidential-space" ? ["--scopes=cloud-platform"] : ["--no-scopes"],
|
|
29581
29872
|
"--shielded-secure-boot",
|
|
29582
29873
|
"--shielded-vtpm",
|
|
29583
29874
|
"--shielded-integrity-monitoring",
|
|
@@ -29610,7 +29901,7 @@ function gcpDeleteArgv(p) {
|
|
|
29610
29901
|
"--quiet"
|
|
29611
29902
|
];
|
|
29612
29903
|
}
|
|
29613
|
-
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;
|
|
29904
|
+
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;
|
|
29614
29905
|
var init_gcpPlacement = __esm({
|
|
29615
29906
|
"../../packages/envrun/dist/gcpPlacement.js"() {
|
|
29616
29907
|
"use strict";
|
|
@@ -29627,6 +29918,10 @@ var init_gcpPlacement = __esm({
|
|
|
29627
29918
|
GCP_MAX_RUN_DURATION_SECONDS = 3600;
|
|
29628
29919
|
GCP_MANAGED_LABEL_KEY = "th-managed";
|
|
29629
29920
|
GCP_RUN_LABEL_KEY = "th-run";
|
|
29921
|
+
CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
|
|
29922
|
+
WORKLOAD_IMAGE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/;
|
|
29923
|
+
CLIENT_CERT_BASE64URL = /^[A-Za-z0-9_-]{16,8192}$/;
|
|
29924
|
+
MIN_NONCE_BYTES = 10;
|
|
29630
29925
|
GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
|
|
29631
29926
|
GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
|
29632
29927
|
GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -29668,7 +29963,10 @@ var init_emptyGitConfig = __esm({
|
|
|
29668
29963
|
|
|
29669
29964
|
// ../../packages/envrun/dist/hostedVenue.js
|
|
29670
29965
|
import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
|
|
29671
|
-
import {
|
|
29966
|
+
import { createHash as createHash9, X509Certificate } from "crypto";
|
|
29967
|
+
import { chmodSync as chmodSync3, existsSync as existsSync11, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
29968
|
+
import { request as httpsRequest } from "https";
|
|
29969
|
+
import { createServer } from "net";
|
|
29672
29970
|
import { join as join24 } from "path";
|
|
29673
29971
|
import { tmpdir as tmpdir3 } from "os";
|
|
29674
29972
|
function credentialInGitConfig(text) {
|
|
@@ -29870,6 +30168,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
|
|
|
29870
30168
|
});
|
|
29871
30169
|
});
|
|
29872
30170
|
}
|
|
30171
|
+
function socketErrorReason(err) {
|
|
30172
|
+
if (err.message !== "")
|
|
30173
|
+
return err.message;
|
|
30174
|
+
const code = err.code;
|
|
30175
|
+
if (code !== void 0)
|
|
30176
|
+
return code;
|
|
30177
|
+
const inner = err.errors;
|
|
30178
|
+
if (Array.isArray(inner) && inner[0] instanceof Error && inner[0].message !== "") {
|
|
30179
|
+
return inner[0].message;
|
|
30180
|
+
}
|
|
30181
|
+
return err.name;
|
|
30182
|
+
}
|
|
30183
|
+
function fetchAttestationOverTls(req) {
|
|
30184
|
+
return new Promise((resolve4) => {
|
|
30185
|
+
let whole;
|
|
30186
|
+
const settle = (reading) => {
|
|
30187
|
+
if (whole !== void 0)
|
|
30188
|
+
clearTimeout(whole);
|
|
30189
|
+
resolve4(reading);
|
|
30190
|
+
};
|
|
30191
|
+
let cert;
|
|
30192
|
+
let key;
|
|
30193
|
+
try {
|
|
30194
|
+
cert = readFileSync11(req.certPath);
|
|
30195
|
+
key = readFileSync11(req.keyPath);
|
|
30196
|
+
} catch (err) {
|
|
30197
|
+
settle({ ok: false, reason: `the client key could not be read: ${describeErr(err)}` });
|
|
30198
|
+
return;
|
|
30199
|
+
}
|
|
30200
|
+
const call = httpsRequest({
|
|
30201
|
+
host: "localhost",
|
|
30202
|
+
port: req.port,
|
|
30203
|
+
path: "/attest",
|
|
30204
|
+
method: "GET",
|
|
30205
|
+
servername: "localhost",
|
|
30206
|
+
cert,
|
|
30207
|
+
key,
|
|
30208
|
+
rejectUnauthorized: false,
|
|
30209
|
+
agent: false,
|
|
30210
|
+
timeout: req.timeoutMs
|
|
30211
|
+
}, (res) => {
|
|
30212
|
+
const peer = res.socket.getPeerCertificate();
|
|
30213
|
+
let body = "";
|
|
30214
|
+
res.setEncoding("utf8");
|
|
30215
|
+
res.on("data", (chunk) => {
|
|
30216
|
+
body += chunk;
|
|
30217
|
+
if (body.length > MAX_TOKEN_LENGTH) {
|
|
30218
|
+
settle({ ok: false, reason: "the answer is longer than any attestation token" });
|
|
30219
|
+
res.destroy();
|
|
30220
|
+
}
|
|
30221
|
+
});
|
|
30222
|
+
res.on("end", () => {
|
|
30223
|
+
if (res.statusCode !== 200) {
|
|
30224
|
+
settle({
|
|
30225
|
+
ok: false,
|
|
30226
|
+
reason: `the attest endpoint answered HTTP ${String(res.statusCode)}`
|
|
30227
|
+
});
|
|
30228
|
+
return;
|
|
30229
|
+
}
|
|
30230
|
+
const raw = peer.raw;
|
|
30231
|
+
if (raw === void 0 || raw.length === 0) {
|
|
30232
|
+
settle({ ok: false, reason: "the attest endpoint presented no certificate" });
|
|
30233
|
+
return;
|
|
30234
|
+
}
|
|
30235
|
+
settle({ ok: true, token: body.trim(), peerCertDer: new Uint8Array(raw) });
|
|
30236
|
+
});
|
|
30237
|
+
res.on("error", (err) => {
|
|
30238
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
30239
|
+
});
|
|
30240
|
+
});
|
|
30241
|
+
whole = setTimeout(() => {
|
|
30242
|
+
settle({ ok: false, reason: `no complete answer within ${String(req.timeoutMs)}ms` });
|
|
30243
|
+
call.destroy();
|
|
30244
|
+
}, req.timeoutMs);
|
|
30245
|
+
call.on("timeout", () => {
|
|
30246
|
+
call.destroy(new Error(`no answer within ${String(req.timeoutMs)}ms`));
|
|
30247
|
+
});
|
|
30248
|
+
call.on("error", (err) => {
|
|
30249
|
+
settle({ ok: false, reason: socketErrorReason(err) });
|
|
30250
|
+
});
|
|
30251
|
+
call.end();
|
|
30252
|
+
});
|
|
30253
|
+
}
|
|
29873
30254
|
function quoteForRemoteShell(arg) {
|
|
29874
30255
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
29875
30256
|
}
|
|
@@ -30204,25 +30585,370 @@ ${res.stderr}`);
|
|
|
30204
30585
|
function manualDeleteCommand(vm, project, zone) {
|
|
30205
30586
|
return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
|
|
30206
30587
|
}
|
|
30588
|
+
function clientCertArgv(keyPath, certPath) {
|
|
30589
|
+
return [
|
|
30590
|
+
"req",
|
|
30591
|
+
"-x509",
|
|
30592
|
+
"-newkey",
|
|
30593
|
+
"ec",
|
|
30594
|
+
"-pkeyopt",
|
|
30595
|
+
"ec_paramgen_curve:P-256",
|
|
30596
|
+
"-nodes",
|
|
30597
|
+
"-keyout",
|
|
30598
|
+
keyPath,
|
|
30599
|
+
"-out",
|
|
30600
|
+
certPath,
|
|
30601
|
+
"-subj",
|
|
30602
|
+
"/CN=terminalhire-worker",
|
|
30603
|
+
"-days",
|
|
30604
|
+
"1",
|
|
30605
|
+
"-addext",
|
|
30606
|
+
"extendedKeyUsage=clientAuth"
|
|
30607
|
+
];
|
|
30608
|
+
}
|
|
30609
|
+
function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
|
|
30610
|
+
return [
|
|
30611
|
+
"compute",
|
|
30612
|
+
"start-iap-tunnel",
|
|
30613
|
+
vm,
|
|
30614
|
+
String(remotePort),
|
|
30615
|
+
`--local-host-port=localhost:${String(localPort)}`,
|
|
30616
|
+
`--project=${project}`,
|
|
30617
|
+
`--zone=${zone}`,
|
|
30618
|
+
"--iap-tunnel-disable-connection-check"
|
|
30619
|
+
];
|
|
30620
|
+
}
|
|
30621
|
+
function spkiSha256Hex(certDer) {
|
|
30622
|
+
const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
|
|
30623
|
+
return createHash9("sha256").update(spki).digest("hex");
|
|
30624
|
+
}
|
|
30625
|
+
function certMetadataValue(pem) {
|
|
30626
|
+
return new X509Certificate(pem).raw.toString("base64url");
|
|
30627
|
+
}
|
|
30628
|
+
function requireWorkloadDigest(reference) {
|
|
30629
|
+
const named = /@(sha256:[0-9a-f]{64})$/.exec(reference);
|
|
30630
|
+
if (named === null) {
|
|
30631
|
+
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.");
|
|
30632
|
+
}
|
|
30633
|
+
return named[1];
|
|
30634
|
+
}
|
|
30635
|
+
function underStageRoot(path5) {
|
|
30636
|
+
if (!path5.startsWith(`${CS_STAGE_ROOT}/`)) {
|
|
30637
|
+
throw new Error(`${path5} is outside ${CS_STAGE_ROOT}, the only directory the stage helpers mount`);
|
|
30638
|
+
}
|
|
30639
|
+
return path5;
|
|
30640
|
+
}
|
|
30641
|
+
function userFlag(owner) {
|
|
30642
|
+
return `--user=${String(owner.uid)}:${String(owner.gid)}`;
|
|
30643
|
+
}
|
|
30644
|
+
function csStagePrepareArgv(stageBase, owner) {
|
|
30645
|
+
const dirs = Object.values(venueStagePaths(underStageRoot(stageBase))).map(quoteForRemoteShell);
|
|
30646
|
+
return [
|
|
30647
|
+
"run",
|
|
30648
|
+
"--rm",
|
|
30649
|
+
"--network=none",
|
|
30650
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
30651
|
+
"--",
|
|
30652
|
+
STAGE_HELPER_IMAGE,
|
|
30653
|
+
"sh",
|
|
30654
|
+
"-c",
|
|
30655
|
+
`mkdir -p ${dirs.join(" ")} && chown -R ${String(owner.uid)}:${String(owner.gid)} ` + quoteForRemoteShell(stageBase)
|
|
30656
|
+
];
|
|
30657
|
+
}
|
|
30658
|
+
function csUntarArgv(dir, owner) {
|
|
30659
|
+
return [
|
|
30660
|
+
"run",
|
|
30661
|
+
"-i",
|
|
30662
|
+
"--rm",
|
|
30663
|
+
"--network=none",
|
|
30664
|
+
userFlag(owner),
|
|
30665
|
+
`--volume=${underStageRoot(dir)}:/dst:rw`,
|
|
30666
|
+
"--",
|
|
30667
|
+
STAGE_HELPER_IMAGE,
|
|
30668
|
+
"tar",
|
|
30669
|
+
"-xf",
|
|
30670
|
+
"-",
|
|
30671
|
+
"-C",
|
|
30672
|
+
"/dst"
|
|
30673
|
+
];
|
|
30674
|
+
}
|
|
30675
|
+
function csUntarProxyArgv(dir, owner) {
|
|
30676
|
+
const d = quoteForRemoteShell(underStageRoot(dir));
|
|
30677
|
+
return [
|
|
30678
|
+
"run",
|
|
30679
|
+
"-i",
|
|
30680
|
+
"--rm",
|
|
30681
|
+
"--network=none",
|
|
30682
|
+
userFlag(owner),
|
|
30683
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
30684
|
+
"--",
|
|
30685
|
+
STAGE_HELPER_IMAGE,
|
|
30686
|
+
"sh",
|
|
30687
|
+
"-c",
|
|
30688
|
+
`mkdir -p ${d} && tar -C ${d} -xf - && chmod 0644 ${d}/*.js && chmod 0755 ${d}`
|
|
30689
|
+
];
|
|
30690
|
+
}
|
|
30691
|
+
function csRemoveArgv(dir, owner) {
|
|
30692
|
+
return [
|
|
30693
|
+
"run",
|
|
30694
|
+
"--rm",
|
|
30695
|
+
"--network=none",
|
|
30696
|
+
userFlag(owner),
|
|
30697
|
+
`--volume=${CS_STAGE_ROOT}:${CS_STAGE_ROOT}:rw`,
|
|
30698
|
+
"--",
|
|
30699
|
+
STAGE_HELPER_IMAGE,
|
|
30700
|
+
"rm",
|
|
30701
|
+
"-rf",
|
|
30702
|
+
"--",
|
|
30703
|
+
underStageRoot(dir)
|
|
30704
|
+
];
|
|
30705
|
+
}
|
|
30706
|
+
function sshStageTransport(vm, project, zone, env, io) {
|
|
30707
|
+
return {
|
|
30708
|
+
/**
|
|
30709
|
+
* `id -u` / `id -g` on the venue, as the account that untarred the tree.
|
|
30710
|
+
*
|
|
30711
|
+
* That account is the owner BY CONSTRUCTION — the untar runs as the ssh user,
|
|
30712
|
+
* so asking it who it is answers exactly the question the mount needs, without
|
|
30713
|
+
* a chown and without assuming a uid. One ssh round trip, once per run.
|
|
30714
|
+
*
|
|
30715
|
+
* Both ids come from ONE invocation. Two calls could straddle a reconnect and
|
|
30716
|
+
* pair a uid with another session's gid — a third wrong answer rather than a
|
|
30717
|
+
* half-right one, which is the reason `GuestUser` is a pair in the first place.
|
|
30718
|
+
*/
|
|
30719
|
+
readOwner: () => {
|
|
30720
|
+
const res = io.exec(
|
|
30721
|
+
"gcloud",
|
|
30722
|
+
iapSshArgv(vm, project, zone, "id -u && id -g"),
|
|
30723
|
+
OWNER_PROBE_TIMEOUT_MS,
|
|
30724
|
+
// `env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
30725
|
+
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
30726
|
+
// omitted this would ssh under the person's own credential — then report an owner
|
|
30727
|
+
// measured against a session the run does not use.
|
|
30728
|
+
env
|
|
30729
|
+
);
|
|
30730
|
+
if (!res.ok) {
|
|
30731
|
+
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.`);
|
|
30732
|
+
}
|
|
30733
|
+
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
30734
|
+
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
30735
|
+
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
30736
|
+
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.`);
|
|
30737
|
+
}
|
|
30738
|
+
return { uid: ids2[0], gid: ids2[1] };
|
|
30739
|
+
},
|
|
30740
|
+
push: (from, to) => io.pushTree(from, "gcloud", iapUntarArgv(vm, project, zone, to), STAGE_PUSH_TIMEOUT_MS, env),
|
|
30741
|
+
pushProxy: (from, dir) => io.pushTree(from, "gcloud", iapUntarProxyArgv(vm, project, zone, dir), STAGE_PUSH_TIMEOUT_MS, env),
|
|
30742
|
+
removeProxy: (dir) => {
|
|
30743
|
+
io.exec("gcloud", iapSshArgv(vm, project, zone, `rm -rf ${quoteForRemoteShell(dir)}`), PROXY_CLEANUP_TIMEOUT_MS, env);
|
|
30744
|
+
}
|
|
30745
|
+
};
|
|
30746
|
+
}
|
|
30747
|
+
function dockerStageTransport(docker3, env, io) {
|
|
30748
|
+
const pushVia = (from, args) => {
|
|
30749
|
+
const [file, ...rest] = docker3.commandLine(args);
|
|
30750
|
+
if (file === void 0)
|
|
30751
|
+
throw new Error("the docker client returned an empty command line");
|
|
30752
|
+
return io.pushTree(from, file, rest, STAGE_PUSH_TIMEOUT_MS, env);
|
|
30753
|
+
};
|
|
30754
|
+
return {
|
|
30755
|
+
// Fixed rather than probed: the helpers unpack as this account, so it owns the tree.
|
|
30756
|
+
readOwner: () => CS_GUEST_USER,
|
|
30757
|
+
push: (from, to) => pushVia(from, csUntarArgv(to, CS_GUEST_USER)),
|
|
30758
|
+
pushProxy: (from, dir) => pushVia(from, csUntarProxyArgv(dir, CS_GUEST_USER)),
|
|
30759
|
+
removeProxy: (dir) => {
|
|
30760
|
+
docker3.sync(csRemoveArgv(dir, CS_GUEST_USER), { timeoutMs: PROXY_CLEANUP_TIMEOUT_MS });
|
|
30761
|
+
}
|
|
30762
|
+
};
|
|
30763
|
+
}
|
|
30764
|
+
function bothTunnels(a, b) {
|
|
30765
|
+
return {
|
|
30766
|
+
kill: (signal) => {
|
|
30767
|
+
let failed = false;
|
|
30768
|
+
let error;
|
|
30769
|
+
for (const t of [a, b]) {
|
|
30770
|
+
try {
|
|
30771
|
+
t.kill(signal);
|
|
30772
|
+
} catch (err) {
|
|
30773
|
+
if (!failed)
|
|
30774
|
+
error = err;
|
|
30775
|
+
failed = true;
|
|
30776
|
+
}
|
|
30777
|
+
}
|
|
30778
|
+
if (failed)
|
|
30779
|
+
throw error;
|
|
30780
|
+
},
|
|
30781
|
+
failure: () => a.failure() ?? b.failure()
|
|
30782
|
+
};
|
|
30783
|
+
}
|
|
30784
|
+
function makeClientTls(io, allocated, vm) {
|
|
30785
|
+
let dir;
|
|
30786
|
+
try {
|
|
30787
|
+
dir = io.makePrivateDir();
|
|
30788
|
+
} catch (err) {
|
|
30789
|
+
throw new HostedVenueError(`could not create a private directory for ${vm}'s client key: ${describeErr(err)}`, "ours");
|
|
30790
|
+
}
|
|
30791
|
+
allocated.onRollback(() => {
|
|
30792
|
+
try {
|
|
30793
|
+
io.removeTree(dir);
|
|
30794
|
+
} catch (err) {
|
|
30795
|
+
throw new HostedVenueError(`the directory ${dir} holding ${vm}'s client key could not be removed and is left behind: ${describeErr(err)}`);
|
|
30796
|
+
}
|
|
30797
|
+
});
|
|
30798
|
+
const certPath = join24(dir, "client.pem");
|
|
30799
|
+
const keyPath = join24(dir, "client-key.pem");
|
|
30800
|
+
const made = io.exec("openssl", clientCertArgv(keyPath, certPath), OPENSSL_TIMEOUT_MS, {});
|
|
30801
|
+
if (!made.ok) {
|
|
30802
|
+
throw new HostedVenueError(`could not make ${vm}'s client certificate with openssl: ${execDetail(made).slice(0, 300)}`, "ours");
|
|
30803
|
+
}
|
|
30804
|
+
let metadataValue;
|
|
30805
|
+
try {
|
|
30806
|
+
const pem = io.readTextIfPresent(certPath);
|
|
30807
|
+
if (pem === null)
|
|
30808
|
+
throw new Error(`openssl wrote nothing to ${certPath}`);
|
|
30809
|
+
metadataValue = certMetadataValue(pem);
|
|
30810
|
+
} catch (err) {
|
|
30811
|
+
throw new HostedVenueError(`could not read back ${vm}'s client certificate: ${describeErr(err)}`, "ours");
|
|
30812
|
+
}
|
|
30813
|
+
return { dir, certPath, keyPath, metadataValue };
|
|
30814
|
+
}
|
|
30815
|
+
async function reachConfidentialSpace(a) {
|
|
30816
|
+
const { io, vm } = a;
|
|
30817
|
+
let ports;
|
|
30818
|
+
try {
|
|
30819
|
+
ports = await io.freeLocalPorts(2);
|
|
30820
|
+
} catch (err) {
|
|
30821
|
+
throw new HostedVenueError(`could not find two free local ports for ${vm}'s forwards: ${describeErr(err)}`, "ours");
|
|
30822
|
+
}
|
|
30823
|
+
const [attestPort, dockerPort] = ports;
|
|
30824
|
+
if (attestPort === void 0 || dockerPort === void 0 || attestPort === dockerPort) {
|
|
30825
|
+
throw new HostedVenueError(`could not find two distinct free local ports for ${vm}'s forwards`, "ours");
|
|
30826
|
+
}
|
|
30827
|
+
const forward = (remote, local) => {
|
|
30828
|
+
const t = io.spawnTunnel("gcloud", iapPortTunnelArgv(vm, a.project, a.zone, remote, local), a.env);
|
|
30829
|
+
a.allocated.onRollback(() => {
|
|
30830
|
+
try {
|
|
30831
|
+
t.kill("SIGTERM");
|
|
30832
|
+
} catch {
|
|
30833
|
+
}
|
|
30834
|
+
});
|
|
30835
|
+
return t;
|
|
30836
|
+
};
|
|
30837
|
+
const tunnel = bothTunnels(forward(CS_ATTEST_PORT, attestPort), forward(CS_DOCKER_PORT, dockerPort));
|
|
30838
|
+
const deadline = io.now() + CS_READY_BUDGET_MS;
|
|
30839
|
+
let reading = { ok: false, reason: "no request was made" };
|
|
30840
|
+
while (io.now() < deadline) {
|
|
30841
|
+
const dead = tunnel.failure();
|
|
30842
|
+
if (dead !== null) {
|
|
30843
|
+
throw new HostedVenueError(`the IAP port forward to ${vm} could not be kept up \u2014 ${dead}`);
|
|
30844
|
+
}
|
|
30845
|
+
reading = await io.fetchAttestation({
|
|
30846
|
+
port: attestPort,
|
|
30847
|
+
certPath: a.tls.certPath,
|
|
30848
|
+
keyPath: a.tls.keyPath,
|
|
30849
|
+
timeoutMs: CS_ATTEST_TIMEOUT_MS
|
|
30850
|
+
});
|
|
30851
|
+
if (reading.ok)
|
|
30852
|
+
break;
|
|
30853
|
+
await io.sleep(CS_ATTEST_INTERVAL_MS);
|
|
30854
|
+
}
|
|
30855
|
+
if (!reading.ok) {
|
|
30856
|
+
throw new HostedVenueError(`${vm}'s attest endpoint did not answer within ${String(CS_READY_BUDGET_MS / 1e3)}s \u2014 last reason: ${reading.reason}`);
|
|
30857
|
+
}
|
|
30858
|
+
let spki;
|
|
30859
|
+
let venuePem;
|
|
30860
|
+
try {
|
|
30861
|
+
spki = spkiSha256Hex(reading.peerCertDer);
|
|
30862
|
+
venuePem = new X509Certificate(reading.peerCertDer).toString();
|
|
30863
|
+
} catch (err) {
|
|
30864
|
+
throw new HostedVenueError(`${vm}'s attest endpoint presented a certificate that does not parse: ${describeErr(err)}`);
|
|
30865
|
+
}
|
|
30866
|
+
let jwks;
|
|
30867
|
+
try {
|
|
30868
|
+
jwks = await io.fetchConfidentialSpaceJwks();
|
|
30869
|
+
} catch (err) {
|
|
30870
|
+
throw new HostedVenueError(`could not reach the Confidential Space key set to check ${vm}'s attestation \u2014 ` + describeErr(err), "ours", { cause: err });
|
|
30871
|
+
}
|
|
30872
|
+
const verdict = await verifyConfidentialSpaceToken(reading.token, {
|
|
30873
|
+
audience: expectedAudience(a.runId),
|
|
30874
|
+
nonce: a.runId,
|
|
30875
|
+
additionalNonces: [spki],
|
|
30876
|
+
instanceName: vm,
|
|
30877
|
+
allowedZones: [a.zone],
|
|
30878
|
+
projectId: a.project,
|
|
30879
|
+
allowedImageDigests: [a.imageDigest],
|
|
30880
|
+
containerEnv: { TH_CLIENT_CERT: a.tls.metadataValue }
|
|
30881
|
+
}, { fetchJwks: () => Promise.resolve(jwks), now: io.now() });
|
|
30882
|
+
if (!verdict.ok) {
|
|
30883
|
+
throw new HostedVenueError(`${vm} presented an attestation this lease refuses (${verdict.reason}): ${verdict.detail}`);
|
|
30884
|
+
}
|
|
30885
|
+
const caPath = join24(a.tls.dir, "venue.pem");
|
|
30886
|
+
try {
|
|
30887
|
+
io.writeFile(caPath, venuePem);
|
|
30888
|
+
} catch (err) {
|
|
30889
|
+
throw new HostedVenueError(`could not write ${vm}'s attested certificate to ${caPath}: ${describeErr(err)}`, "ours");
|
|
30890
|
+
}
|
|
30891
|
+
const dockerAt = `tcp://localhost:${String(dockerPort)}`;
|
|
30892
|
+
const docker3 = io.dockerOverTls(dockerAt, { caPath, certPath: a.tls.certPath, keyPath: a.tls.keyPath }, CS_RUN_ARGS);
|
|
30893
|
+
const daemon = io.classifyDaemon(docker3);
|
|
30894
|
+
if (!daemon.distinct) {
|
|
30895
|
+
throw new HostedVenueError(`refusing to hand out a hosted lease: ${describeVenueDaemon(daemon)}`);
|
|
30896
|
+
}
|
|
30897
|
+
const prepared = docker3.sync(csStagePrepareArgv(a.stageBase, CS_GUEST_USER), {
|
|
30898
|
+
timeoutMs: MKDIR_TIMEOUT_MS
|
|
30899
|
+
});
|
|
30900
|
+
if (prepared.error || prepared.status !== 0) {
|
|
30901
|
+
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${dockerDetail(prepared).slice(0, 300)}`);
|
|
30902
|
+
}
|
|
30903
|
+
const pulled = docker3.sync(["pull", DEFAULT_CONTAINER_IMAGE], { timeoutMs: CS_PULL_TIMEOUT_MS });
|
|
30904
|
+
if (pulled.error || pulled.status !== 0) {
|
|
30905
|
+
throw new HostedVenueError(`could not pull ${DEFAULT_CONTAINER_IMAGE} on ${vm}: ${dockerDetail(pulled).slice(0, 300)}`);
|
|
30906
|
+
}
|
|
30907
|
+
return {
|
|
30908
|
+
docker: docker3,
|
|
30909
|
+
tunnel,
|
|
30910
|
+
socketDir: a.tls.dir,
|
|
30911
|
+
socketPath: dockerAt,
|
|
30912
|
+
venueDaemonId: daemon.venueDaemonId,
|
|
30913
|
+
venueIdentity: { kind: "confidential-space", token: reading.token, claims: verdict.value },
|
|
30914
|
+
transport: dockerStageTransport(docker3, a.env, io)
|
|
30915
|
+
};
|
|
30916
|
+
}
|
|
30207
30917
|
function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
30208
30918
|
const project = opts.project ?? DEFAULT_GCP_PROJECT;
|
|
30209
30919
|
const zone = opts.zone ?? DEFAULT_GCP_ZONE;
|
|
30210
30920
|
const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
|
|
30211
30921
|
const keyFile = resolveServiceAccountKeyFile(opts);
|
|
30922
|
+
const image = venueImageFromEnv(process.env);
|
|
30923
|
+
const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
|
|
30212
30924
|
const env = venueGcloudEnv(gcloudConfig);
|
|
30213
30925
|
return {
|
|
30214
30926
|
kind: "hosted-pool",
|
|
30215
30927
|
acquire: (runId) => acquireTransactionally(async (allocated) => {
|
|
30216
30928
|
const vm = venueInstanceName(runId);
|
|
30217
30929
|
const stageBase = `/tmp/th-stage-${runId}`;
|
|
30218
|
-
const
|
|
30930
|
+
const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
|
|
30219
30931
|
vmName: vm,
|
|
30220
30932
|
project,
|
|
30221
30933
|
zone,
|
|
30222
30934
|
runId,
|
|
30223
|
-
machineType: opts.machineType ??
|
|
30224
|
-
|
|
30935
|
+
machineType: opts.machineType ?? CONFIDENTIAL_SPACE_MACHINE_TYPE,
|
|
30936
|
+
venueImage: "confidential-space",
|
|
30937
|
+
workloadImage: image.workloadImage,
|
|
30938
|
+
clientCert
|
|
30939
|
+
} : {
|
|
30940
|
+
vmName: vm,
|
|
30941
|
+
project,
|
|
30942
|
+
zone,
|
|
30943
|
+
runId,
|
|
30944
|
+
machineType: opts.machineType ?? DEFAULT_GCP_MACHINE_TYPE,
|
|
30945
|
+
...image
|
|
30946
|
+
};
|
|
30947
|
+
let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
|
|
30225
30948
|
ensureVenueServiceCredentials(gcloudConfig, keyFile, io);
|
|
30949
|
+
const clientTls = csImageDigest === null ? null : { ...makeClientTls(io, allocated, vm), imageDigest: csImageDigest };
|
|
30950
|
+
if (clientTls !== null)
|
|
30951
|
+
bootArgv = gcpBootArgv(bootParams(clientTls.metadataValue));
|
|
30226
30952
|
const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
|
|
30227
30953
|
let instanceState = "unknown";
|
|
30228
30954
|
allocated.onRollback(() => {
|
|
@@ -30238,6 +30964,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30238
30964
|
throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
|
|
30239
30965
|
}
|
|
30240
30966
|
instanceState = "exists";
|
|
30967
|
+
if (clientTls !== null) {
|
|
30968
|
+
return finishAcquire(await reachConfidentialSpace({
|
|
30969
|
+
vm,
|
|
30970
|
+
project,
|
|
30971
|
+
zone,
|
|
30972
|
+
runId,
|
|
30973
|
+
stageBase,
|
|
30974
|
+
env,
|
|
30975
|
+
io,
|
|
30976
|
+
allocated,
|
|
30977
|
+
tls: clientTls,
|
|
30978
|
+
imageDigest: clientTls.imageDigest
|
|
30979
|
+
}));
|
|
30980
|
+
}
|
|
30241
30981
|
const deadline = io.now() + SSH_READY_BUDGET_MS;
|
|
30242
30982
|
let last = { retryable: true, reason: "no probe ran" };
|
|
30243
30983
|
let identity = null;
|
|
@@ -30332,43 +31072,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30332
31072
|
if (!prepared.ok) {
|
|
30333
31073
|
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
|
|
30334
31074
|
}
|
|
30335
|
-
|
|
30336
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30337
|
-
const created = docker3.sync(volumeCreateArgv(name, runId), {
|
|
30338
|
-
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
30339
|
-
});
|
|
30340
|
-
if (created.error || created.status !== 0) {
|
|
30341
|
-
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
30342
|
-
}
|
|
30343
|
-
}
|
|
30344
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30345
|
-
const probe = docker3.sync(execProbeArgv(name), {
|
|
30346
|
-
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
30347
|
-
});
|
|
30348
|
-
if (probe.error || probe.status !== 0) {
|
|
30349
|
-
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)}`);
|
|
30350
|
-
}
|
|
30351
|
-
}
|
|
30352
|
-
const after = io.classifyDaemon(docker3);
|
|
30353
|
-
if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
|
|
30354
|
-
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.`);
|
|
30355
|
-
}
|
|
30356
|
-
return makeLease({
|
|
30357
|
-
runId,
|
|
30358
|
-
vm,
|
|
30359
|
-
project,
|
|
30360
|
-
zone,
|
|
30361
|
-
stageBase,
|
|
30362
|
-
stageVolumes,
|
|
31075
|
+
return finishAcquire({
|
|
30363
31076
|
docker: docker3,
|
|
30364
31077
|
tunnel,
|
|
30365
31078
|
socketDir,
|
|
30366
31079
|
socketPath,
|
|
30367
31080
|
venueDaemonId,
|
|
30368
31081
|
venueIdentity,
|
|
30369
|
-
env,
|
|
30370
|
-
io
|
|
31082
|
+
transport: sshStageTransport(vm, project, zone, env, io)
|
|
30371
31083
|
});
|
|
31084
|
+
function finishAcquire(r) {
|
|
31085
|
+
const { docker: docker4, socketPath: socketPath2, venueDaemonId: venueDaemonId2 } = r;
|
|
31086
|
+
const stageVolumes = stageVolumeNames(vm);
|
|
31087
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
31088
|
+
const created = docker4.sync(volumeCreateArgv(name, runId), {
|
|
31089
|
+
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
31090
|
+
});
|
|
31091
|
+
if (created.error || created.status !== 0) {
|
|
31092
|
+
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
31093
|
+
}
|
|
31094
|
+
}
|
|
31095
|
+
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
31096
|
+
const probe = docker4.sync(execProbeArgv(name), {
|
|
31097
|
+
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
31098
|
+
});
|
|
31099
|
+
if (probe.error || probe.status !== 0) {
|
|
31100
|
+
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)}`);
|
|
31101
|
+
}
|
|
31102
|
+
}
|
|
31103
|
+
const after = io.classifyDaemon(docker4);
|
|
31104
|
+
if (!after.distinct || after.venueDaemonId !== venueDaemonId2) {
|
|
31105
|
+
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.`);
|
|
31106
|
+
}
|
|
31107
|
+
return makeLease({
|
|
31108
|
+
runId,
|
|
31109
|
+
vm,
|
|
31110
|
+
project,
|
|
31111
|
+
zone,
|
|
31112
|
+
stageBase,
|
|
31113
|
+
stageVolumes,
|
|
31114
|
+
docker: docker4,
|
|
31115
|
+
tunnel: r.tunnel,
|
|
31116
|
+
socketDir: r.socketDir,
|
|
31117
|
+
socketPath: socketPath2,
|
|
31118
|
+
venueDaemonId: venueDaemonId2,
|
|
31119
|
+
venueIdentity: r.venueIdentity,
|
|
31120
|
+
transport: r.transport,
|
|
31121
|
+
env,
|
|
31122
|
+
io
|
|
31123
|
+
});
|
|
31124
|
+
}
|
|
30372
31125
|
})
|
|
30373
31126
|
};
|
|
30374
31127
|
}
|
|
@@ -30427,28 +31180,7 @@ function makeLease(p) {
|
|
|
30427
31180
|
return null;
|
|
30428
31181
|
};
|
|
30429
31182
|
let stagedOwner;
|
|
30430
|
-
|
|
30431
|
-
const res = p.io.exec(
|
|
30432
|
-
"gcloud",
|
|
30433
|
-
iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
|
|
30434
|
-
OWNER_PROBE_TIMEOUT_MS,
|
|
30435
|
-
// `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
30436
|
-
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
30437
|
-
// omitted this would ssh under the person's own credential — then report an owner
|
|
30438
|
-
// measured against a session the run does not use.
|
|
30439
|
-
p.env
|
|
30440
|
-
);
|
|
30441
|
-
if (!res.ok) {
|
|
30442
|
-
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.`);
|
|
30443
|
-
}
|
|
30444
|
-
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
30445
|
-
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
30446
|
-
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
30447
|
-
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.`);
|
|
30448
|
-
}
|
|
30449
|
-
return { uid: ids2[0], gid: ids2[1] };
|
|
30450
|
-
};
|
|
30451
|
-
stagedOwner = readStagedOwner();
|
|
31183
|
+
stagedOwner = p.transport.readOwner();
|
|
30452
31184
|
return {
|
|
30453
31185
|
kind: "hosted-pool",
|
|
30454
31186
|
runId: p.runId,
|
|
@@ -30483,7 +31215,7 @@ function makeLease(p) {
|
|
|
30483
31215
|
const dir = venueProxyDir(p.stageBase);
|
|
30484
31216
|
const buffer = stageProxyCode();
|
|
30485
31217
|
try {
|
|
30486
|
-
const res = await p.
|
|
31218
|
+
const res = await p.transport.pushProxy(buffer.dir, dir);
|
|
30487
31219
|
if (!res.ok) {
|
|
30488
31220
|
throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
30489
31221
|
}
|
|
@@ -30493,7 +31225,7 @@ function makeLease(p) {
|
|
|
30493
31225
|
return {
|
|
30494
31226
|
dir,
|
|
30495
31227
|
cleanup: () => {
|
|
30496
|
-
p.
|
|
31228
|
+
p.transport.removeProxy(dir);
|
|
30497
31229
|
}
|
|
30498
31230
|
};
|
|
30499
31231
|
},
|
|
@@ -30501,7 +31233,7 @@ function makeLease(p) {
|
|
|
30501
31233
|
check("staging the tree");
|
|
30502
31234
|
const paths = venueStagePaths(p.stageBase);
|
|
30503
31235
|
const push = async (from, to) => {
|
|
30504
|
-
const res = await p.
|
|
31236
|
+
const res = await p.transport.push(from, to);
|
|
30505
31237
|
if (!res.ok) {
|
|
30506
31238
|
throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
30507
31239
|
}
|
|
@@ -30659,7 +31391,7 @@ function makeLease(p) {
|
|
|
30659
31391
|
}
|
|
30660
31392
|
};
|
|
30661
31393
|
}
|
|
30662
|
-
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;
|
|
31394
|
+
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;
|
|
30663
31395
|
var init_hostedVenue = __esm({
|
|
30664
31396
|
"../../packages/envrun/dist/hostedVenue.js"() {
|
|
30665
31397
|
"use strict";
|
|
@@ -30780,7 +31512,55 @@ var init_hostedVenue = __esm({
|
|
|
30780
31512
|
throw new Error("JWKS fetch returned a body with no keys array");
|
|
30781
31513
|
}
|
|
30782
31514
|
return body;
|
|
30783
|
-
}
|
|
31515
|
+
},
|
|
31516
|
+
fetchConfidentialSpaceJwks: async () => {
|
|
31517
|
+
const getJson = async (url, what) => {
|
|
31518
|
+
const res = await globalThis.fetch(url, {
|
|
31519
|
+
signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS)
|
|
31520
|
+
});
|
|
31521
|
+
if (!res.ok) {
|
|
31522
|
+
throw new Error(`Confidential Space ${what} returned HTTP ${String(res.status)}`);
|
|
31523
|
+
}
|
|
31524
|
+
return res.json();
|
|
31525
|
+
};
|
|
31526
|
+
const discovery = await getJson(CONFIDENTIAL_SPACE_OIDC_DISCOVERY_URL, "discovery");
|
|
31527
|
+
const jwksUri = typeof discovery === "object" && discovery !== null ? discovery.jwks_uri : void 0;
|
|
31528
|
+
if (typeof jwksUri !== "string" || !jwksUri.startsWith("https://")) {
|
|
31529
|
+
throw new Error("Confidential Space discovery names no https jwks_uri");
|
|
31530
|
+
}
|
|
31531
|
+
const body = await getJson(jwksUri, "key set");
|
|
31532
|
+
if (typeof body !== "object" || body === null || !Array.isArray(body.keys)) {
|
|
31533
|
+
throw new Error("Confidential Space key set has no keys array");
|
|
31534
|
+
}
|
|
31535
|
+
return body;
|
|
31536
|
+
},
|
|
31537
|
+
freeLocalPorts: async (n) => {
|
|
31538
|
+
const servers = [];
|
|
31539
|
+
try {
|
|
31540
|
+
for (let i = 0; i < n; i += 1) {
|
|
31541
|
+
const server = createServer();
|
|
31542
|
+
servers.push(server);
|
|
31543
|
+
await new Promise((resolve4, reject) => {
|
|
31544
|
+
server.once("error", reject);
|
|
31545
|
+
server.listen(0, "localhost", () => {
|
|
31546
|
+
resolve4();
|
|
31547
|
+
});
|
|
31548
|
+
});
|
|
31549
|
+
}
|
|
31550
|
+
return servers.map((s) => s.address().port);
|
|
31551
|
+
} finally {
|
|
31552
|
+
await Promise.all(servers.map((s) => new Promise((resolve4) => {
|
|
31553
|
+
s.close(() => {
|
|
31554
|
+
resolve4();
|
|
31555
|
+
});
|
|
31556
|
+
})));
|
|
31557
|
+
}
|
|
31558
|
+
},
|
|
31559
|
+
writeFile: (path5, text) => {
|
|
31560
|
+
writeFileSync13(path5, text, { mode: 384, flag: "wx" });
|
|
31561
|
+
},
|
|
31562
|
+
fetchAttestation: (req) => fetchAttestationOverTls(req),
|
|
31563
|
+
dockerOverTls: (endpoint, tls, runArgs) => remoteDockerClient(endpoint, { tls, runArgs })
|
|
30784
31564
|
};
|
|
30785
31565
|
VENUE_SSH_USER = "th-runner";
|
|
30786
31566
|
GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
|
|
@@ -30801,6 +31581,17 @@ var init_hostedVenue = __esm({
|
|
|
30801
31581
|
SSH_KEY_NOT_READY = /Permission denied \(publickey/;
|
|
30802
31582
|
DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
|
|
30803
31583
|
SSH_NOT_ANSWERING = /Connection refused|Connection reset|Connection closed by|kex_exchange_identification|Operation timed out/;
|
|
31584
|
+
CS_ATTEST_PORT = 8443;
|
|
31585
|
+
CS_DOCKER_PORT = 2376;
|
|
31586
|
+
CS_READY_BUDGET_MS = 48e4;
|
|
31587
|
+
CS_ATTEST_TIMEOUT_MS = 15e3;
|
|
31588
|
+
CS_PULL_TIMEOUT_MS = 6e5;
|
|
31589
|
+
CS_ATTEST_INTERVAL_MS = 5e3;
|
|
31590
|
+
OPENSSL_TIMEOUT_MS = 3e4;
|
|
31591
|
+
CS_RUN_ARGS = ["--oom-score-adj", "1000"];
|
|
31592
|
+
CS_GUEST_USER = { uid: 1e3, gid: 1e3 };
|
|
31593
|
+
CS_STAGE_ROOT = "/tmp";
|
|
31594
|
+
CLIENT_CERT_STAND_IN = "stand-in-certificate";
|
|
30804
31595
|
}
|
|
30805
31596
|
});
|
|
30806
31597
|
|
|
@@ -31700,10 +32491,10 @@ var init_references = __esm({
|
|
|
31700
32491
|
|
|
31701
32492
|
// ../../packages/envspec/dist/repo.js
|
|
31702
32493
|
import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
31703
|
-
import { join as join25, relative as relative2, sep as
|
|
32494
|
+
import { join as join25, relative as relative2, sep as sep5 } from "path";
|
|
31704
32495
|
function createRepoReader(repoPath) {
|
|
31705
32496
|
const resolveIn = (relativePath) => relativePath === "" ? repoPath : join25(repoPath, relativePath);
|
|
31706
|
-
const toPosix = (absolute) => relative2(repoPath, absolute).split(
|
|
32497
|
+
const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
|
|
31707
32498
|
const readText = (relativePath) => {
|
|
31708
32499
|
try {
|
|
31709
32500
|
return readFileSync12(resolveIn(relativePath), "utf8");
|
|
@@ -32346,13 +33137,16 @@ function git(repoDir, args, allowNonZero = false) {
|
|
|
32346
33137
|
}
|
|
32347
33138
|
return res.stdout ?? "";
|
|
32348
33139
|
}
|
|
32349
|
-
function collectWorkingDiff(repoDir) {
|
|
33140
|
+
function collectWorkingDiff(repoDir, opts = {}) {
|
|
32350
33141
|
if (!existsSync12(join26(repoDir, ".git"))) {
|
|
32351
33142
|
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.`);
|
|
32352
33143
|
}
|
|
32353
33144
|
const headSha = git(repoDir, ["rev-parse", "HEAD"]).trim();
|
|
32354
|
-
const
|
|
32355
|
-
const
|
|
33145
|
+
const diffBase = opts.base === void 0 ? headSha : assertDiffBase(repoDir, opts.base);
|
|
33146
|
+
const trackedArgs = opts.base === void 0 ? ["diff", "HEAD"] : ["diff", diffBase];
|
|
33147
|
+
const pathsEnd = opts.base === void 0 ? [] : ["--"];
|
|
33148
|
+
const tracked = git(repoDir, [...trackedArgs, "--binary", ...pathsEnd]);
|
|
33149
|
+
const trackedChanged = git(repoDir, [...trackedArgs, "--name-only", ...pathsEnd]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
32356
33150
|
const untracked = git(repoDir, ["ls-files", "--others", "--exclude-standard"]).split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
32357
33151
|
const parts = [];
|
|
32358
33152
|
if (tracked.trim() !== "")
|
|
@@ -32362,7 +33156,17 @@ function collectWorkingDiff(repoDir) {
|
|
|
32362
33156
|
if (one.trim() !== "")
|
|
32363
33157
|
parts.push(one.replace(/\n*$/, "\n"));
|
|
32364
33158
|
}
|
|
32365
|
-
return { patch: parts.join(""), headSha, trackedChanged, untracked };
|
|
33159
|
+
return { patch: parts.join(""), headSha, diffBase, trackedChanged, untracked };
|
|
33160
|
+
}
|
|
33161
|
+
function assertDiffBase(repoDir, base) {
|
|
33162
|
+
if (!FULL_COMMIT_ID.test(base)) {
|
|
33163
|
+
throw new ThRunError(`the diff base must be a full 40-character commit id, got ${JSON.stringify(base)}.`);
|
|
33164
|
+
}
|
|
33165
|
+
const resolved = git(repoDir, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`], true);
|
|
33166
|
+
if (resolved.trim() !== base) {
|
|
33167
|
+
throw new ThRunError(`the diff base ${base} is not a commit in ${repoDir}.`);
|
|
33168
|
+
}
|
|
33169
|
+
return base;
|
|
32366
33170
|
}
|
|
32367
33171
|
function isLocalPath(p) {
|
|
32368
33172
|
return !UNC_PATH.test(p) && (p.startsWith("/") || WINDOWS_ABSOLUTE.test(p));
|
|
@@ -32934,8 +33738,9 @@ async function runVerification(req, ctx) {
|
|
|
32934
33738
|
let diff = null;
|
|
32935
33739
|
if (source.kind === "working-diff") {
|
|
32936
33740
|
progress("collect", `reading the working diff from ${source.localRepoDir}`);
|
|
32937
|
-
diff = collectWorkingDiff(source.localRepoDir);
|
|
32938
|
-
|
|
33741
|
+
diff = collectWorkingDiff(source.localRepoDir, source.diffBase === void 0 ? {} : { base: source.diffBase });
|
|
33742
|
+
const from = source.diffBase === void 0 ? "" : `, from delivered baseline ${diff.diffBase.slice(0, 12)}`;
|
|
33743
|
+
progress("collect", `${String(diff.trackedChanged.length)} tracked, ${String(diff.untracked.length)} untracked, ${String(diff.patch.length)} bytes${from}`);
|
|
32939
33744
|
} else {
|
|
32940
33745
|
progress("collect", "dispatched run: the tree is the stored commit itself");
|
|
32941
33746
|
}
|
|
@@ -33137,7 +33942,7 @@ async function runVerification(req, ctx) {
|
|
|
33137
33942
|
await releaseWithoutThrowing(lease, progress);
|
|
33138
33943
|
}
|
|
33139
33944
|
}
|
|
33140
|
-
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;
|
|
33945
|
+
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;
|
|
33141
33946
|
var init_thrun = __esm({
|
|
33142
33947
|
"../../packages/envrun/dist/thrun.js"() {
|
|
33143
33948
|
"use strict";
|
|
@@ -33155,6 +33960,7 @@ var init_thrun = __esm({
|
|
|
33155
33960
|
ThRunError = class extends Error {
|
|
33156
33961
|
};
|
|
33157
33962
|
OUTPUT_TAIL_BYTES = 4e3;
|
|
33963
|
+
FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
|
|
33158
33964
|
ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["https", "http", "ssh", "git"]);
|
|
33159
33965
|
SCP_STYLE = /^[A-Za-z0-9._~+-]+@[A-Za-z0-9._-]+:[^:]/;
|
|
33160
33966
|
URL_SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//;
|
|
@@ -34013,6 +34819,7 @@ __export(dist_exports, {
|
|
|
34013
34819
|
ALEMBIC_IMAGE: () => ALEMBIC_IMAGE,
|
|
34014
34820
|
ATTEST_REFUSAL_REASONS: () => ATTEST_REFUSAL_REASONS,
|
|
34015
34821
|
BOOKKEEPING_TABLES: () => BOOKKEEPING_TABLES,
|
|
34822
|
+
CONFIDENTIAL_SPACE_MACHINE_TYPE: () => CONFIDENTIAL_SPACE_MACHINE_TYPE,
|
|
34016
34823
|
CONTAINMENT_UNAVAILABLE_PREFIX: () => CONTAINMENT_UNAVAILABLE_PREFIX,
|
|
34017
34824
|
CloneUnavailableError: () => CloneUnavailableError,
|
|
34018
34825
|
DAEMON_FACTS_FORMAT: () => DAEMON_FACTS_FORMAT,
|
|
@@ -34154,6 +34961,7 @@ __export(dist_exports, {
|
|
|
34154
34961
|
toolingImageFor: () => toolingImageFor,
|
|
34155
34962
|
unquoteDiffPath: () => unquoteDiffPath,
|
|
34156
34963
|
venueGcloudEnv: () => venueGcloudEnv,
|
|
34964
|
+
venueImageFromEnv: () => venueImageFromEnv,
|
|
34157
34965
|
venueStagePaths: () => venueStagePaths,
|
|
34158
34966
|
verifyWorkingDiff: () => verifyWorkingDiff
|
|
34159
34967
|
});
|
|
@@ -34221,7 +35029,7 @@ __export(cache_store_exports, {
|
|
|
34221
35029
|
updateIndexCache: () => updateIndexCache,
|
|
34222
35030
|
writeIndexCache: () => writeIndexCache
|
|
34223
35031
|
});
|
|
34224
|
-
import { readFileSync as readFileSync13, writeFileSync as
|
|
35032
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync14, renameSync as renameSync5 } from "fs";
|
|
34225
35033
|
import { join as join27 } from "path";
|
|
34226
35034
|
import { homedir as homedir13 } from "os";
|
|
34227
35035
|
function readCacheEntry() {
|
|
@@ -34241,7 +35049,7 @@ function updateIndexCache(patch) {
|
|
|
34241
35049
|
ts: Date.now()
|
|
34242
35050
|
};
|
|
34243
35051
|
const tmp = `${INDEX_CACHE_FILE}.${process.pid}.${tmpCounter++}.tmp`;
|
|
34244
|
-
|
|
35052
|
+
writeFileSync14(tmp, JSON.stringify(entry), "utf8");
|
|
34245
35053
|
renameSync5(tmp, INDEX_CACHE_FILE);
|
|
34246
35054
|
return entry;
|
|
34247
35055
|
}
|
|
@@ -34454,7 +35262,7 @@ var init_audit2 = __esm({
|
|
|
34454
35262
|
init_src();
|
|
34455
35263
|
import {
|
|
34456
35264
|
readFileSync as readFileSync14,
|
|
34457
|
-
writeFileSync as
|
|
35265
|
+
writeFileSync as writeFileSync15,
|
|
34458
35266
|
mkdirSync as mkdirSync7,
|
|
34459
35267
|
mkdtempSync as mkdtempSync5,
|
|
34460
35268
|
renameSync as renameSync6,
|
|
@@ -34465,7 +35273,7 @@ import {
|
|
|
34465
35273
|
readdirSync as readdirSync3
|
|
34466
35274
|
} from "fs";
|
|
34467
35275
|
import { join as join28, dirname as dirname8, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
|
|
34468
|
-
import { createHash as
|
|
35276
|
+
import { createHash as createHash10 } from "crypto";
|
|
34469
35277
|
import { homedir as homedir14, hostname as osHostname } from "os";
|
|
34470
35278
|
import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
|
|
34471
35279
|
import { promisify as promisify3 } from "util";
|
|
@@ -34928,7 +35736,7 @@ function markClaimNudged(id) {
|
|
|
34928
35736
|
const ids2 = readNudgedClaimIds();
|
|
34929
35737
|
ids2.add(id);
|
|
34930
35738
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
34931
|
-
|
|
35739
|
+
writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
|
|
34932
35740
|
} catch {
|
|
34933
35741
|
}
|
|
34934
35742
|
}
|
|
@@ -36169,7 +36977,7 @@ async function registerFounderClaim(b) {
|
|
|
36169
36977
|
let clearedLocalCredential = false;
|
|
36170
36978
|
let refusedForPurpose = false;
|
|
36171
36979
|
let preserveBackgroundToken = false;
|
|
36172
|
-
const
|
|
36980
|
+
const refuse4 = (reason) => {
|
|
36173
36981
|
console.error(
|
|
36174
36982
|
`
|
|
36175
36983
|
terminalhire claim: refusing to record \u2014 ${reason}
|
|
@@ -36194,7 +37002,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36194
37002
|
try {
|
|
36195
37003
|
expectLogin = await localLoginForPaidBrowserClaim();
|
|
36196
37004
|
} catch (err) {
|
|
36197
|
-
|
|
37005
|
+
refuse4(
|
|
36198
37006
|
`could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
|
|
36199
37007
|
Run \`terminalhire login\`, then retry this claim. Browser verification was
|
|
36200
37008
|
not opened and no claim was sent.`
|
|
@@ -36210,16 +37018,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36210
37018
|
claimRef: opportunityShortToken(b.bountyId)
|
|
36211
37019
|
});
|
|
36212
37020
|
if (!proofToken) {
|
|
36213
|
-
|
|
37021
|
+
refuse4("could not verify your GitHub identity with terminalhire (see above).");
|
|
36214
37022
|
}
|
|
36215
37023
|
const verifiedLogin = proofTokenLogin(proofToken);
|
|
36216
37024
|
if (!verifiedLogin) {
|
|
36217
|
-
|
|
37025
|
+
refuse4(
|
|
36218
37026
|
"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."
|
|
36219
37027
|
);
|
|
36220
37028
|
}
|
|
36221
37029
|
if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
|
|
36222
|
-
|
|
37030
|
+
refuse4(
|
|
36223
37031
|
`you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
|
|
36224
37032
|
Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
|
|
36225
37033
|
run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
|
|
@@ -36251,7 +37059,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36251
37059
|
try {
|
|
36252
37060
|
res = await sendRegistration(true);
|
|
36253
37061
|
} catch (err) {
|
|
36254
|
-
|
|
37062
|
+
refuse4(
|
|
36255
37063
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36256
37064
|
);
|
|
36257
37065
|
}
|
|
@@ -36302,7 +37110,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36302
37110
|
try {
|
|
36303
37111
|
res = await sendRegistration(true);
|
|
36304
37112
|
} catch (err) {
|
|
36305
|
-
|
|
37113
|
+
refuse4(
|
|
36306
37114
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36307
37115
|
);
|
|
36308
37116
|
}
|
|
@@ -36313,14 +37121,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36313
37121
|
res = await sendRegistration(false);
|
|
36314
37122
|
refusalBody = await readRefusal(res);
|
|
36315
37123
|
} catch (err) {
|
|
36316
|
-
|
|
37124
|
+
refuse4(
|
|
36317
37125
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36318
37126
|
);
|
|
36319
37127
|
}
|
|
36320
37128
|
}
|
|
36321
37129
|
if (!res.ok) {
|
|
36322
37130
|
const detail = refusalBody?.message || refusalBody?.error || "";
|
|
36323
|
-
|
|
37131
|
+
refuse4(
|
|
36324
37132
|
detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
|
|
36325
37133
|
);
|
|
36326
37134
|
}
|
|
@@ -36331,7 +37139,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36331
37139
|
body = null;
|
|
36332
37140
|
}
|
|
36333
37141
|
if (!body || body.ok !== true) {
|
|
36334
|
-
|
|
37142
|
+
refuse4("malformed registration response from the server.");
|
|
36335
37143
|
}
|
|
36336
37144
|
const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
|
|
36337
37145
|
if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
|
|
@@ -37772,7 +38580,7 @@ function writeSliceFiles(destDir, files) {
|
|
|
37772
38580
|
if (typeof f.content === "string") {
|
|
37773
38581
|
const abs = join28(destDir, f.path);
|
|
37774
38582
|
mkdirSync7(dirname8(abs), { recursive: true });
|
|
37775
|
-
|
|
38583
|
+
writeFileSync15(abs, f.content, "utf8");
|
|
37776
38584
|
written.push(f.path);
|
|
37777
38585
|
} else {
|
|
37778
38586
|
unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
|
|
@@ -37826,7 +38634,7 @@ function ensureExcludedPackDir(destDir) {
|
|
|
37826
38634
|
const existing = existsSync13(excludeFile) ? readFileSync14(excludeFile, "utf8") : "";
|
|
37827
38635
|
if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
|
|
37828
38636
|
mkdirSync7(dirname8(excludeFile), { recursive: true });
|
|
37829
|
-
|
|
38637
|
+
writeFileSync15(
|
|
37830
38638
|
excludeFile,
|
|
37831
38639
|
`${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
|
|
37832
38640
|
`,
|
|
@@ -37842,23 +38650,33 @@ function writePackFile(destDir, relPath, content, what) {
|
|
|
37842
38650
|
try {
|
|
37843
38651
|
const abs = join28(destDir, relPath);
|
|
37844
38652
|
mkdirSync7(dirname8(abs), { recursive: true });
|
|
37845
|
-
|
|
38653
|
+
writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
|
|
37846
38654
|
} catch (err) {
|
|
37847
38655
|
return { written: false, reason: `the ${what} could not be written (${err.message})` };
|
|
37848
38656
|
}
|
|
37849
38657
|
return { written: true, reason: null, sha256: sha256OfUtf8(content) };
|
|
37850
38658
|
}
|
|
37851
38659
|
function sha256OfUtf8(content) {
|
|
37852
|
-
return
|
|
38660
|
+
return createHash10("sha256").update(content, "utf8").digest("hex");
|
|
37853
38661
|
}
|
|
37854
|
-
function writeWorkspacePack(destDir, spec, claim) {
|
|
38662
|
+
function writeWorkspacePack(destDir, spec, claim, delivery) {
|
|
38663
|
+
if (delivery !== "full" && delivery !== "sparse") {
|
|
38664
|
+
throw new TypeError(
|
|
38665
|
+
`writeWorkspacePack: delivery must be 'full' or 'sparse', got ${String(delivery)}`
|
|
38666
|
+
);
|
|
38667
|
+
}
|
|
37855
38668
|
const gate = ensureExcludedPackDir(destDir);
|
|
37856
38669
|
if (!gate.ok) {
|
|
37857
38670
|
const refused = { written: false, reason: gate.reason };
|
|
37858
38671
|
return { brief: refused, verify: refused, agents: refused };
|
|
37859
38672
|
}
|
|
37860
38673
|
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");
|
|
37861
|
-
const verify = writePackFile(
|
|
38674
|
+
const verify = writePackFile(
|
|
38675
|
+
destDir,
|
|
38676
|
+
VERIFY_REL_PATH,
|
|
38677
|
+
renderVerifyDoc(claim, delivery),
|
|
38678
|
+
"verify note"
|
|
38679
|
+
);
|
|
37862
38680
|
const agents = writePackFile(
|
|
37863
38681
|
destDir,
|
|
37864
38682
|
AGENTS_REL_PATH,
|
|
@@ -37872,20 +38690,30 @@ function packSafeId(claim) {
|
|
|
37872
38690
|
const id = String(claim?.id ?? "");
|
|
37873
38691
|
return PACK_SAFE_ID.test(id) ? id : "<your claim id \u2014 see: terminalhire claim list>";
|
|
37874
38692
|
}
|
|
37875
|
-
function renderVerifyDoc(claim) {
|
|
38693
|
+
function renderVerifyDoc(claim, delivery) {
|
|
37876
38694
|
const id = packSafeId(claim);
|
|
38695
|
+
const runStep2 = delivery === "full" ? ` ${nextStep("terminalhire run")}
|
|
38696
|
+
|
|
38697
|
+
It tests every change since the delivered baseline, committed or not, which
|
|
38698
|
+
is what submit sends once it is all committed.` : ` ${nextStep("terminalhire run --target <clone URL of the full repository>")}
|
|
38699
|
+
|
|
38700
|
+
It tests every change since the delivered baseline, committed or not, which
|
|
38701
|
+
is what submit sends once it is all committed. This delivery contains only the
|
|
38702
|
+
files you were granted, so the local run needs the whole repository; if you
|
|
38703
|
+
don't have access to it, skip to step 4, because the poster-side run in step 5
|
|
38704
|
+
is the verdict that counts.`;
|
|
37877
38705
|
return `# Verifying claim ${id}
|
|
37878
38706
|
|
|
37879
38707
|
This workspace was delivered by terminalhire for a posting. "Done" is
|
|
37880
38708
|
judged on the diff: the submitted patch is the change from the delivered
|
|
37881
38709
|
baseline (this repo's root commit) to HEAD, so only committed, tracked changes
|
|
37882
|
-
count.
|
|
38710
|
+
count. Submit needs a clean tree; run does not.
|
|
37883
38711
|
|
|
37884
38712
|
1. Read the poster's task first, when there is one: ${BRIEF_REL_PATH}
|
|
37885
38713
|
2. Work on the claim branch this delivery checked out, committing as you go.
|
|
37886
38714
|
3. To verify locally in terminalhire's sandboxed runner, from this directory:
|
|
37887
38715
|
|
|
37888
|
-
|
|
38716
|
+
${runStep2}
|
|
37889
38717
|
|
|
37890
38718
|
4. Submit \u2014 the only step that sends anything off this machine. It prints what it
|
|
37891
38719
|
is about to send first, and asks y/N only at an interactive terminal:
|
|
@@ -38264,11 +39092,13 @@ async function cmdSliceFullTier(claims, id, local, fullTierBody, flags, cloneRep
|
|
|
38264
39092
|
);
|
|
38265
39093
|
process.exit(1);
|
|
38266
39094
|
}
|
|
38267
|
-
const pack = writeWorkspacePack(dest, body.spec, claim);
|
|
39095
|
+
const pack = writeWorkspacePack(dest, body.spec, claim, "full");
|
|
38268
39096
|
const working = claims.updateClaim(claim.id, {
|
|
38269
39097
|
worktreePath: dest,
|
|
38270
39098
|
branch,
|
|
38271
39099
|
state: "working",
|
|
39100
|
+
// `terminalhire run` clones this commit when run from the workspace (TERM-1098).
|
|
39101
|
+
baseSha,
|
|
38272
39102
|
workspacePack: {
|
|
38273
39103
|
brief: pack.brief.written === true,
|
|
38274
39104
|
verify: pack.verify.written === true,
|
|
@@ -38424,7 +39254,7 @@ async function attemptSliceDelivery(id, flags = {}) {
|
|
|
38424
39254
|
let pack;
|
|
38425
39255
|
try {
|
|
38426
39256
|
await sh("git", ["-C", dest, "init"]);
|
|
38427
|
-
pack = writeWorkspacePack(dest, body.spec, claim);
|
|
39257
|
+
pack = writeWorkspacePack(dest, body.spec, claim, "sparse");
|
|
38428
39258
|
await sh("git", ["-C", dest, "add", "-A"]);
|
|
38429
39259
|
await sh("git", [
|
|
38430
39260
|
"-C",
|
|
@@ -38461,6 +39291,11 @@ async function attemptSliceDelivery(id, flags = {}) {
|
|
|
38461
39291
|
worktreePath: dest,
|
|
38462
39292
|
branch,
|
|
38463
39293
|
state: "working",
|
|
39294
|
+
// The commit these files were cut from, so `terminalhire run` can clone it unasked
|
|
39295
|
+
// (TERM-1098). Recorded only when it is a full sha: a slice confirmed before the
|
|
39296
|
+
// server kept one answers null, and a value of any other shape is not a commit we
|
|
39297
|
+
// could clone, so neither is written and `run` asks for `--sha` instead.
|
|
39298
|
+
...typeof body.baseSha === "string" && /^[0-9a-f]{40}$/.test(body.baseSha) ? { baseSha: body.baseSha } : {},
|
|
38464
39299
|
workspacePack: {
|
|
38465
39300
|
brief: pack.brief.written === true,
|
|
38466
39301
|
verify: pack.verify.written === true,
|
|
@@ -39193,7 +40028,7 @@ function readClaimPushMarker() {
|
|
|
39193
40028
|
}
|
|
39194
40029
|
function writeClaimPushMarker(marker) {
|
|
39195
40030
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
39196
|
-
|
|
40031
|
+
writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
39197
40032
|
}
|
|
39198
40033
|
function clearClaimPushMarker() {
|
|
39199
40034
|
try {
|