terminalhire 0.42.20 → 0.42.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/claim-push-bg.js +1 -1
- package/dist/bin/jpi-chat-read.js +1 -1
- package/dist/bin/jpi-chat.js +1 -1
- package/dist/bin/jpi-claim.js +854 -123
- package/dist/bin/jpi-contribute.js +6 -1
- package/dist/bin/jpi-dispatch.js +891 -160
- package/dist/bin/jpi-hub.js +1 -1
- package/dist/bin/jpi-inbox.js +1 -1
- package/dist/bin/jpi-login.js +10 -5
- package/dist/bin/jpi-mcp-chat.js +1 -1
- package/dist/bin/jpi-mcp.js +863 -132
- package/dist/bin/jpi-refresh.js +1 -1
- package/dist/bin/jpi-run.js +826 -100
- package/dist/src/chat-client.js +1 -1
- package/dist/src/chat-keystore.js +1 -1
- package/dist/src/github-auth.js +6 -1
- package/package.json +1 -1
package/dist/bin/jpi-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
|
},
|
|
@@ -28559,13 +28585,192 @@ var init_gceIdentity = __esm({
|
|
|
28559
28585
|
|
|
28560
28586
|
// ../../packages/attest/dist/confidentialSpace.js
|
|
28561
28587
|
import { createHash as createHash7, createPublicKey as createPublicKey3, verify as cryptoVerify3 } from "crypto";
|
|
28562
|
-
|
|
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;
|
|
28563
28766
|
var init_confidentialSpace = __esm({
|
|
28564
28767
|
"../../packages/attest/dist/confidentialSpace.js"() {
|
|
28565
28768
|
"use strict";
|
|
28566
28769
|
init_gceIdentity();
|
|
28567
28770
|
CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
28568
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";
|
|
28569
28774
|
}
|
|
28570
28775
|
});
|
|
28571
28776
|
|
|
@@ -28908,50 +29113,50 @@ function toTestRunResult(result, outputSha256) {
|
|
|
28908
29113
|
function toAcceptancePredicate(pair, opts = {}) {
|
|
28909
29114
|
const { baseline, patched } = pair;
|
|
28910
29115
|
if (baseline.runId === patched.runId) {
|
|
28911
|
-
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.`);
|
|
28912
29117
|
}
|
|
28913
29118
|
if (patched.status === "refused") {
|
|
28914
|
-
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`);
|
|
28915
29120
|
}
|
|
28916
29121
|
if (patched.outcome === null) {
|
|
28917
|
-
return
|
|
29122
|
+
return refuse3("no-outcome-recorded", "the patched run recorded no outcome");
|
|
28918
29123
|
}
|
|
28919
29124
|
if (baseline.status === "refused") {
|
|
28920
|
-
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`);
|
|
28921
29126
|
}
|
|
28922
29127
|
if (baseline.outcome === null) {
|
|
28923
|
-
return
|
|
29128
|
+
return refuse3("baseline-no-outcome", "the baseline run recorded no outcome");
|
|
28924
29129
|
}
|
|
28925
29130
|
if (!BASELINE_IS_A_VERDICT[baseline.outcome]) {
|
|
28926
|
-
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.`);
|
|
28927
29132
|
}
|
|
28928
29133
|
const budget = OUTCOME_TO_BUDGET[patched.outcome];
|
|
28929
29134
|
if (budget === null) {
|
|
28930
|
-
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.`);
|
|
28931
29136
|
}
|
|
28932
29137
|
if (patched.testCommand === null) {
|
|
28933
|
-
return
|
|
29138
|
+
return refuse3("no-test-command", "no test command was recorded, so none can be attested");
|
|
28934
29139
|
}
|
|
28935
29140
|
if (baseline.testCommand !== patched.testCommand) {
|
|
28936
|
-
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.`);
|
|
28937
29142
|
}
|
|
28938
29143
|
if (baseline.targetSha !== patched.targetSha) {
|
|
28939
|
-
return
|
|
29144
|
+
return refuse3("pair-disagrees-on-base", `baseline is at ${baseline.targetSha} and patched at ${patched.targetSha}`);
|
|
28940
29145
|
}
|
|
28941
29146
|
if (patched.treeDigest === null || patched.treeDigest === "") {
|
|
28942
|
-
return
|
|
29147
|
+
return refuse3("missing-tree-digest", "the patched run recorded no tree digest to bind to");
|
|
28943
29148
|
}
|
|
28944
29149
|
if (patched.patchSha256 === null || patched.patchSha256 === "") {
|
|
28945
|
-
return
|
|
29150
|
+
return refuse3("missing-patch-digest", "the patched run recorded no patch digest");
|
|
28946
29151
|
}
|
|
28947
29152
|
if (baseline.treeDigest === null || baseline.treeDigest === "") {
|
|
28948
|
-
return
|
|
29153
|
+
return refuse3("missing-tree-digest", "the baseline recorded no tree digest, so the two halves cannot be shown to differ");
|
|
28949
29154
|
}
|
|
28950
29155
|
if (baseline.treeDigest === patched.treeDigest) {
|
|
28951
|
-
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`);
|
|
28952
29157
|
}
|
|
28953
29158
|
if (patched.baselinePatchSha256 !== null || baseline.baselinePatchSha256 !== null) {
|
|
28954
|
-
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.`);
|
|
28955
29160
|
}
|
|
28956
29161
|
for (const [side, outcome, counts, exitCode] of [
|
|
28957
29162
|
["patched", patched.outcome, patched.counts, patched.exitCode],
|
|
@@ -28959,7 +29164,7 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
28959
29164
|
]) {
|
|
28960
29165
|
const why = contradicts(outcome, counts, exitCode);
|
|
28961
29166
|
if (why !== null) {
|
|
28962
|
-
return
|
|
29167
|
+
return refuse3("outcome-contradicts-counts", `the ${side} run is ${why}`);
|
|
28963
29168
|
}
|
|
28964
29169
|
}
|
|
28965
29170
|
for (const [side, source] of [
|
|
@@ -28968,44 +29173,44 @@ function toAcceptancePredicate(pair, opts = {}) {
|
|
|
28968
29173
|
]) {
|
|
28969
29174
|
const why = SOURCE_IS_SIGNABLE[source](patched.testCommand);
|
|
28970
29175
|
if (why !== null)
|
|
28971
|
-
return
|
|
29176
|
+
return refuse3("test-command-origin-unattested", `the ${side} run ${why}`);
|
|
28972
29177
|
}
|
|
28973
29178
|
if (patched.testCommandSource === "developer-declared") {
|
|
28974
|
-
return
|
|
29179
|
+
return refuse3("test-command-origin-unattested", "the developer supplied the test command (unreachable \u2014 the source rules above cover it)");
|
|
28975
29180
|
}
|
|
28976
29181
|
if (baseline.testCommandSource !== patched.testCommandSource) {
|
|
28977
|
-
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.`);
|
|
28978
29183
|
}
|
|
28979
29184
|
if (baseline.containerImage !== patched.containerImage) {
|
|
28980
|
-
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.`);
|
|
28981
29186
|
}
|
|
28982
29187
|
if (patched.containerImage === null || patched.containerImage === "") {
|
|
28983
|
-
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");
|
|
28984
29189
|
}
|
|
28985
29190
|
if (patched.containerImageDigest == null || patched.containerImageDigest === "") {
|
|
28986
|
-
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.");
|
|
28987
29192
|
}
|
|
28988
29193
|
if (typeof patched.containerImageDigest !== "string" || !isCanonicalRepoDigest(patched.containerImageDigest)) {
|
|
28989
|
-
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.`);
|
|
28990
29195
|
}
|
|
28991
29196
|
if (imageRepo(patched.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
28992
|
-
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.`);
|
|
28993
29198
|
}
|
|
28994
29199
|
if (baseline.containerImageDigest == null || baseline.containerImageDigest === "") {
|
|
28995
|
-
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.");
|
|
28996
29201
|
}
|
|
28997
29202
|
if (typeof baseline.containerImageDigest !== "string" || !isCanonicalRepoDigest(baseline.containerImageDigest)) {
|
|
28998
|
-
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.`);
|
|
28999
29204
|
}
|
|
29000
29205
|
if (imageRepo(baseline.containerImageDigest) !== imageRepo(patched.containerImage)) {
|
|
29001
|
-
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.`);
|
|
29002
29207
|
}
|
|
29003
29208
|
if (baseline.containerImageDigest !== patched.containerImageDigest) {
|
|
29004
|
-
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.`);
|
|
29005
29210
|
}
|
|
29006
29211
|
const claimedDigest = imageDigestOf(patched.containerImage);
|
|
29007
29212
|
if (claimedDigest !== null && claimedDigest !== imageDigestOf(patched.containerImageDigest)) {
|
|
29008
|
-
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.`);
|
|
29009
29214
|
}
|
|
29010
29215
|
return {
|
|
29011
29216
|
ok: true,
|
|
@@ -29032,7 +29237,7 @@ function signRunStatement(predicate, privateKey, keyid) {
|
|
|
29032
29237
|
const statement = createAcceptanceStatement(predicate);
|
|
29033
29238
|
return { statement, envelope: signStatement(statement, privateKey, keyid) };
|
|
29034
29239
|
}
|
|
29035
|
-
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;
|
|
29036
29241
|
var init_attestation2 = __esm({
|
|
29037
29242
|
"../../packages/envrun/dist/attestation.js"() {
|
|
29038
29243
|
"use strict";
|
|
@@ -29136,7 +29341,7 @@ var init_attestation2 = __esm({
|
|
|
29136
29341
|
"outcome-contradicts-counts",
|
|
29137
29342
|
"test-command-origin-unattested"
|
|
29138
29343
|
];
|
|
29139
|
-
|
|
29344
|
+
refuse3 = (reason, detail) => ({
|
|
29140
29345
|
ok: false,
|
|
29141
29346
|
reason,
|
|
29142
29347
|
detail
|
|
@@ -29547,6 +29752,22 @@ function bootImageFlags(p) {
|
|
|
29547
29752
|
}
|
|
29548
29753
|
const workloadImage = p.workloadImage;
|
|
29549
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
|
+
];
|
|
29550
29771
|
return [
|
|
29551
29772
|
// The production family. The debug family reports `dbgstat: enabled`, which the
|
|
29552
29773
|
// intake's verifier refuses, so a debug boot could never produce a run we record.
|
|
@@ -29559,7 +29780,15 @@ function bootImageFlags(p) {
|
|
|
29559
29780
|
// most GCP_MAX_RUN_DURATION_SECONDS costs one run. Whether GCE boots this exact
|
|
29560
29781
|
// combination is phase 5's measurement.
|
|
29561
29782
|
"--maintenance-policy=TERMINATE",
|
|
29562
|
-
`--metadata
|
|
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"
|
|
29563
29792
|
];
|
|
29564
29793
|
}
|
|
29565
29794
|
function assertInstanceIdentity(fn, id) {
|
|
@@ -29633,7 +29862,13 @@ function gcpBootArgv(p) {
|
|
|
29633
29862
|
// expectation uses (TERM-832): two inline spellings of one value is how
|
|
29634
29863
|
// the boot path and the verifier drift apart silently.
|
|
29635
29864
|
`--service-account=${venueIdentityEmail(p.project)}`,
|
|
29636
|
-
|
|
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"],
|
|
29637
29872
|
"--shielded-secure-boot",
|
|
29638
29873
|
"--shielded-vtpm",
|
|
29639
29874
|
"--shielded-integrity-monitoring",
|
|
@@ -29666,7 +29901,7 @@ function gcpDeleteArgv(p) {
|
|
|
29666
29901
|
"--quiet"
|
|
29667
29902
|
];
|
|
29668
29903
|
}
|
|
29669
|
-
var DEFAULT_GCP_PROJECT, DEFAULT_GCP_ZONE, DEFAULT_GCP_MACHINE_TYPE, GcpPlacementError, GCP_MAX_RUN_DURATION_SECONDS, GCP_MANAGED_LABEL_KEY, GCP_RUN_LABEL_KEY, CONFIDENTIAL_SPACE_MACHINE_TYPE, WORKLOAD_IMAGE_REFERENCE, GCP_LABEL_VALUE, GCE_INSTANCE_NAME, GCP_RESOURCE_ID;
|
|
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;
|
|
29670
29905
|
var init_gcpPlacement = __esm({
|
|
29671
29906
|
"../../packages/envrun/dist/gcpPlacement.js"() {
|
|
29672
29907
|
"use strict";
|
|
@@ -29685,6 +29920,8 @@ var init_gcpPlacement = __esm({
|
|
|
29685
29920
|
GCP_RUN_LABEL_KEY = "th-run";
|
|
29686
29921
|
CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
|
|
29687
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;
|
|
29688
29925
|
GCP_LABEL_VALUE = /^[a-z0-9_-]{1,63}$/;
|
|
29689
29926
|
GCE_INSTANCE_NAME = /^[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
|
29690
29927
|
GCP_RESOURCE_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -29726,7 +29963,10 @@ var init_emptyGitConfig = __esm({
|
|
|
29726
29963
|
|
|
29727
29964
|
// ../../packages/envrun/dist/hostedVenue.js
|
|
29728
29965
|
import { spawn as spawn5, spawnSync as spawnSync5 } from "child_process";
|
|
29729
|
-
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";
|
|
29730
29970
|
import { join as join24 } from "path";
|
|
29731
29971
|
import { tmpdir as tmpdir3 } from "os";
|
|
29732
29972
|
function credentialInGitConfig(text) {
|
|
@@ -29928,6 +30168,89 @@ timed out after ${String(timeoutMs)}ms` : stderr,
|
|
|
29928
30168
|
});
|
|
29929
30169
|
});
|
|
29930
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
|
+
}
|
|
29931
30254
|
function quoteForRemoteShell(arg) {
|
|
29932
30255
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
29933
30256
|
}
|
|
@@ -30262,27 +30585,370 @@ ${res.stderr}`);
|
|
|
30262
30585
|
function manualDeleteCommand(vm, project, zone) {
|
|
30263
30586
|
return `gcloud compute instances delete ${vm} --project=${project} --zone=${zone} --quiet`;
|
|
30264
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
|
+
}
|
|
30265
30917
|
function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
30266
30918
|
const project = opts.project ?? DEFAULT_GCP_PROJECT;
|
|
30267
30919
|
const zone = opts.zone ?? DEFAULT_GCP_ZONE;
|
|
30268
30920
|
const gcloudConfig = opts.gcloudConfig ?? VENUE_GCLOUD_CONFIG;
|
|
30269
30921
|
const keyFile = resolveServiceAccountKeyFile(opts);
|
|
30270
30922
|
const image = venueImageFromEnv(process.env);
|
|
30923
|
+
const csImageDigest = image.venueImage === "confidential-space" ? requireWorkloadDigest(image.workloadImage) : null;
|
|
30271
30924
|
const env = venueGcloudEnv(gcloudConfig);
|
|
30272
30925
|
return {
|
|
30273
30926
|
kind: "hosted-pool",
|
|
30274
30927
|
acquire: (runId) => acquireTransactionally(async (allocated) => {
|
|
30275
30928
|
const vm = venueInstanceName(runId);
|
|
30276
30929
|
const stageBase = `/tmp/th-stage-${runId}`;
|
|
30277
|
-
const
|
|
30930
|
+
const bootParams = (clientCert) => image.venueImage === "confidential-space" ? {
|
|
30278
30931
|
vmName: vm,
|
|
30279
30932
|
project,
|
|
30280
30933
|
zone,
|
|
30281
30934
|
runId,
|
|
30282
|
-
machineType: opts.machineType ??
|
|
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,
|
|
30283
30945
|
...image
|
|
30284
|
-
}
|
|
30946
|
+
};
|
|
30947
|
+
let bootArgv = gcpBootArgv(bootParams(CLIENT_CERT_STAND_IN));
|
|
30285
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));
|
|
30286
30952
|
const boot = io.exec("gcloud", bootArgv, BOOT_TIMEOUT_MS, env);
|
|
30287
30953
|
let instanceState = "unknown";
|
|
30288
30954
|
allocated.onRollback(() => {
|
|
@@ -30298,6 +30964,20 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30298
30964
|
throw new HostedVenueError(`could not boot ${vm}: ${verdict2.reason.slice(0, 400)}`, verdict2.source);
|
|
30299
30965
|
}
|
|
30300
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
|
+
}
|
|
30301
30981
|
const deadline = io.now() + SSH_READY_BUDGET_MS;
|
|
30302
30982
|
let last = { retryable: true, reason: "no probe ran" };
|
|
30303
30983
|
let identity = null;
|
|
@@ -30392,43 +31072,56 @@ function hostedVenue(opts = {}, io = defaultHostedVenueIo) {
|
|
|
30392
31072
|
if (!prepared.ok) {
|
|
30393
31073
|
throw new HostedVenueError(`could not prepare the stage on ${vm}: ${execDetail(prepared).slice(0, 300)}`);
|
|
30394
31074
|
}
|
|
30395
|
-
|
|
30396
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30397
|
-
const created = docker3.sync(volumeCreateArgv(name, runId), {
|
|
30398
|
-
timeoutMs: VOLUME_CREATE_TIMEOUT_MS
|
|
30399
|
-
});
|
|
30400
|
-
if (created.error || created.status !== 0) {
|
|
30401
|
-
throw new HostedVenueError(`could not create the stage volume ${name} on ${vm}: ${dockerDetail(created).slice(0, 300)}`);
|
|
30402
|
-
}
|
|
30403
|
-
}
|
|
30404
|
-
for (const name of [stageVolumes.clone, stageVolumes.jail]) {
|
|
30405
|
-
const probe = docker3.sync(execProbeArgv(name), {
|
|
30406
|
-
timeoutMs: EXEC_PROBE_TIMEOUT_MS
|
|
30407
|
-
});
|
|
30408
|
-
if (probe.error || probe.status !== 0) {
|
|
30409
|
-
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)}`);
|
|
30410
|
-
}
|
|
30411
|
-
}
|
|
30412
|
-
const after = io.classifyDaemon(docker3);
|
|
30413
|
-
if (!after.distinct || after.venueDaemonId !== venueDaemonId) {
|
|
30414
|
-
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.`);
|
|
30415
|
-
}
|
|
30416
|
-
return makeLease({
|
|
30417
|
-
runId,
|
|
30418
|
-
vm,
|
|
30419
|
-
project,
|
|
30420
|
-
zone,
|
|
30421
|
-
stageBase,
|
|
30422
|
-
stageVolumes,
|
|
31075
|
+
return finishAcquire({
|
|
30423
31076
|
docker: docker3,
|
|
30424
31077
|
tunnel,
|
|
30425
31078
|
socketDir,
|
|
30426
31079
|
socketPath,
|
|
30427
31080
|
venueDaemonId,
|
|
30428
31081
|
venueIdentity,
|
|
30429
|
-
env,
|
|
30430
|
-
io
|
|
31082
|
+
transport: sshStageTransport(vm, project, zone, env, io)
|
|
30431
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
|
+
}
|
|
30432
31125
|
})
|
|
30433
31126
|
};
|
|
30434
31127
|
}
|
|
@@ -30487,28 +31180,7 @@ function makeLease(p) {
|
|
|
30487
31180
|
return null;
|
|
30488
31181
|
};
|
|
30489
31182
|
let stagedOwner;
|
|
30490
|
-
|
|
30491
|
-
const res = p.io.exec(
|
|
30492
|
-
"gcloud",
|
|
30493
|
-
iapSshArgv(p.vm, p.project, p.zone, "id -u && id -g"),
|
|
30494
|
-
OWNER_PROBE_TIMEOUT_MS,
|
|
30495
|
-
// `p.env`, like every other gcloud call here. TERM-724 made the venue boot under the
|
|
30496
|
-
// SERVICE configuration rather than whoever is at the terminal, and a probe that
|
|
30497
|
-
// omitted this would ssh under the person's own credential — then report an owner
|
|
30498
|
-
// measured against a session the run does not use.
|
|
30499
|
-
p.env
|
|
30500
|
-
);
|
|
30501
|
-
if (!res.ok) {
|
|
30502
|
-
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.`);
|
|
30503
|
-
}
|
|
30504
|
-
const lines = res.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
30505
|
-
const ids2 = lines.slice(-2).map((l) => Number(l));
|
|
30506
|
-
if (ids2.length !== 2 || !ids2.every((n) => Number.isInteger(n) && n > 0)) {
|
|
30507
|
-
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.`);
|
|
30508
|
-
}
|
|
30509
|
-
return { uid: ids2[0], gid: ids2[1] };
|
|
30510
|
-
};
|
|
30511
|
-
stagedOwner = readStagedOwner();
|
|
31183
|
+
stagedOwner = p.transport.readOwner();
|
|
30512
31184
|
return {
|
|
30513
31185
|
kind: "hosted-pool",
|
|
30514
31186
|
runId: p.runId,
|
|
@@ -30543,7 +31215,7 @@ function makeLease(p) {
|
|
|
30543
31215
|
const dir = venueProxyDir(p.stageBase);
|
|
30544
31216
|
const buffer = stageProxyCode();
|
|
30545
31217
|
try {
|
|
30546
|
-
const res = await p.
|
|
31218
|
+
const res = await p.transport.pushProxy(buffer.dir, dir);
|
|
30547
31219
|
if (!res.ok) {
|
|
30548
31220
|
throw new HostedVenueError(`could not stage the egress proxy onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
30549
31221
|
}
|
|
@@ -30553,7 +31225,7 @@ function makeLease(p) {
|
|
|
30553
31225
|
return {
|
|
30554
31226
|
dir,
|
|
30555
31227
|
cleanup: () => {
|
|
30556
|
-
p.
|
|
31228
|
+
p.transport.removeProxy(dir);
|
|
30557
31229
|
}
|
|
30558
31230
|
};
|
|
30559
31231
|
},
|
|
@@ -30561,7 +31233,7 @@ function makeLease(p) {
|
|
|
30561
31233
|
check("staging the tree");
|
|
30562
31234
|
const paths = venueStagePaths(p.stageBase);
|
|
30563
31235
|
const push = async (from, to) => {
|
|
30564
|
-
const res = await p.
|
|
31236
|
+
const res = await p.transport.push(from, to);
|
|
30565
31237
|
if (!res.ok) {
|
|
30566
31238
|
throw new HostedVenueError(`could not stage ${from} onto ${p.vm}: ${execDetail(res).slice(0, 300)}`);
|
|
30567
31239
|
}
|
|
@@ -30719,7 +31391,7 @@ function makeLease(p) {
|
|
|
30719
31391
|
}
|
|
30720
31392
|
};
|
|
30721
31393
|
}
|
|
30722
|
-
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;
|
|
30723
31395
|
var init_hostedVenue = __esm({
|
|
30724
31396
|
"../../packages/envrun/dist/hostedVenue.js"() {
|
|
30725
31397
|
"use strict";
|
|
@@ -30840,7 +31512,55 @@ var init_hostedVenue = __esm({
|
|
|
30840
31512
|
throw new Error("JWKS fetch returned a body with no keys array");
|
|
30841
31513
|
}
|
|
30842
31514
|
return body;
|
|
30843
|
-
}
|
|
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 })
|
|
30844
31564
|
};
|
|
30845
31565
|
VENUE_SSH_USER = "th-runner";
|
|
30846
31566
|
GCE_METADATA_IDENTITY_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity";
|
|
@@ -30861,6 +31581,17 @@ var init_hostedVenue = __esm({
|
|
|
30861
31581
|
SSH_KEY_NOT_READY = /Permission denied \(publickey/;
|
|
30862
31582
|
DAEMON_NOT_READY = /Cannot connect to the Docker daemon/;
|
|
30863
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";
|
|
30864
31595
|
}
|
|
30865
31596
|
});
|
|
30866
31597
|
|
|
@@ -31760,10 +32491,10 @@ var init_references = __esm({
|
|
|
31760
32491
|
|
|
31761
32492
|
// ../../packages/envspec/dist/repo.js
|
|
31762
32493
|
import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
31763
|
-
import { join as join25, relative as relative2, sep as
|
|
32494
|
+
import { join as join25, relative as relative2, sep as sep5 } from "path";
|
|
31764
32495
|
function createRepoReader(repoPath) {
|
|
31765
32496
|
const resolveIn = (relativePath) => relativePath === "" ? repoPath : join25(repoPath, relativePath);
|
|
31766
|
-
const toPosix = (absolute) => relative2(repoPath, absolute).split(
|
|
32497
|
+
const toPosix = (absolute) => relative2(repoPath, absolute).split(sep5).join("/");
|
|
31767
32498
|
const readText = (relativePath) => {
|
|
31768
32499
|
try {
|
|
31769
32500
|
return readFileSync12(resolveIn(relativePath), "utf8");
|
|
@@ -34298,7 +35029,7 @@ __export(cache_store_exports, {
|
|
|
34298
35029
|
updateIndexCache: () => updateIndexCache,
|
|
34299
35030
|
writeIndexCache: () => writeIndexCache
|
|
34300
35031
|
});
|
|
34301
|
-
import { readFileSync as readFileSync13, writeFileSync as
|
|
35032
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync14, renameSync as renameSync5 } from "fs";
|
|
34302
35033
|
import { join as join27 } from "path";
|
|
34303
35034
|
import { homedir as homedir13 } from "os";
|
|
34304
35035
|
function readCacheEntry() {
|
|
@@ -34318,7 +35049,7 @@ function updateIndexCache(patch) {
|
|
|
34318
35049
|
ts: Date.now()
|
|
34319
35050
|
};
|
|
34320
35051
|
const tmp = `${INDEX_CACHE_FILE}.${process.pid}.${tmpCounter++}.tmp`;
|
|
34321
|
-
|
|
35052
|
+
writeFileSync14(tmp, JSON.stringify(entry), "utf8");
|
|
34322
35053
|
renameSync5(tmp, INDEX_CACHE_FILE);
|
|
34323
35054
|
return entry;
|
|
34324
35055
|
}
|
|
@@ -34531,7 +35262,7 @@ var init_audit2 = __esm({
|
|
|
34531
35262
|
init_src();
|
|
34532
35263
|
import {
|
|
34533
35264
|
readFileSync as readFileSync14,
|
|
34534
|
-
writeFileSync as
|
|
35265
|
+
writeFileSync as writeFileSync15,
|
|
34535
35266
|
mkdirSync as mkdirSync7,
|
|
34536
35267
|
mkdtempSync as mkdtempSync5,
|
|
34537
35268
|
renameSync as renameSync6,
|
|
@@ -34542,7 +35273,7 @@ import {
|
|
|
34542
35273
|
readdirSync as readdirSync3
|
|
34543
35274
|
} from "fs";
|
|
34544
35275
|
import { join as join28, dirname as dirname8, isAbsolute as isAbsolute4, resolve as pathResolve } from "path";
|
|
34545
|
-
import { createHash as
|
|
35276
|
+
import { createHash as createHash10 } from "crypto";
|
|
34546
35277
|
import { homedir as homedir14, hostname as osHostname } from "os";
|
|
34547
35278
|
import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
|
|
34548
35279
|
import { promisify as promisify3 } from "util";
|
|
@@ -35005,7 +35736,7 @@ function markClaimNudged(id) {
|
|
|
35005
35736
|
const ids2 = readNudgedClaimIds();
|
|
35006
35737
|
ids2.add(id);
|
|
35007
35738
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
35008
|
-
|
|
35739
|
+
writeFileSync15(REPO_CONTINUITY_NUDGE_MARKER, JSON.stringify({ claimIds: [...ids2] }), "utf8");
|
|
35009
35740
|
} catch {
|
|
35010
35741
|
}
|
|
35011
35742
|
}
|
|
@@ -36246,7 +36977,7 @@ async function registerFounderClaim(b) {
|
|
|
36246
36977
|
let clearedLocalCredential = false;
|
|
36247
36978
|
let refusedForPurpose = false;
|
|
36248
36979
|
let preserveBackgroundToken = false;
|
|
36249
|
-
const
|
|
36980
|
+
const refuse4 = (reason) => {
|
|
36250
36981
|
console.error(
|
|
36251
36982
|
`
|
|
36252
36983
|
terminalhire claim: refusing to record \u2014 ${reason}
|
|
@@ -36271,7 +37002,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36271
37002
|
try {
|
|
36272
37003
|
expectLogin = await localLoginForPaidBrowserClaim();
|
|
36273
37004
|
} catch (err) {
|
|
36274
|
-
|
|
37005
|
+
refuse4(
|
|
36275
37006
|
`could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
|
|
36276
37007
|
Run \`terminalhire login\`, then retry this claim. Browser verification was
|
|
36277
37008
|
not opened and no claim was sent.`
|
|
@@ -36287,16 +37018,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36287
37018
|
claimRef: opportunityShortToken(b.bountyId)
|
|
36288
37019
|
});
|
|
36289
37020
|
if (!proofToken) {
|
|
36290
|
-
|
|
37021
|
+
refuse4("could not verify your GitHub identity with terminalhire (see above).");
|
|
36291
37022
|
}
|
|
36292
37023
|
const verifiedLogin = proofTokenLogin(proofToken);
|
|
36293
37024
|
if (!verifiedLogin) {
|
|
36294
|
-
|
|
37025
|
+
refuse4(
|
|
36295
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."
|
|
36296
37027
|
);
|
|
36297
37028
|
}
|
|
36298
37029
|
if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
|
|
36299
|
-
|
|
37030
|
+
refuse4(
|
|
36300
37031
|
`you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
|
|
36301
37032
|
Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
|
|
36302
37033
|
run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
|
|
@@ -36328,7 +37059,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36328
37059
|
try {
|
|
36329
37060
|
res = await sendRegistration(true);
|
|
36330
37061
|
} catch (err) {
|
|
36331
|
-
|
|
37062
|
+
refuse4(
|
|
36332
37063
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36333
37064
|
);
|
|
36334
37065
|
}
|
|
@@ -36379,7 +37110,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36379
37110
|
try {
|
|
36380
37111
|
res = await sendRegistration(true);
|
|
36381
37112
|
} catch (err) {
|
|
36382
|
-
|
|
37113
|
+
refuse4(
|
|
36383
37114
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36384
37115
|
);
|
|
36385
37116
|
}
|
|
@@ -36390,14 +37121,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36390
37121
|
res = await sendRegistration(false);
|
|
36391
37122
|
refusalBody = await readRefusal(res);
|
|
36392
37123
|
} catch (err) {
|
|
36393
|
-
|
|
37124
|
+
refuse4(
|
|
36394
37125
|
`terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
|
|
36395
37126
|
);
|
|
36396
37127
|
}
|
|
36397
37128
|
}
|
|
36398
37129
|
if (!res.ok) {
|
|
36399
37130
|
const detail = refusalBody?.message || refusalBody?.error || "";
|
|
36400
|
-
|
|
37131
|
+
refuse4(
|
|
36401
37132
|
detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
|
|
36402
37133
|
);
|
|
36403
37134
|
}
|
|
@@ -36408,7 +37139,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
|
|
|
36408
37139
|
body = null;
|
|
36409
37140
|
}
|
|
36410
37141
|
if (!body || body.ok !== true) {
|
|
36411
|
-
|
|
37142
|
+
refuse4("malformed registration response from the server.");
|
|
36412
37143
|
}
|
|
36413
37144
|
const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
|
|
36414
37145
|
if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
|
|
@@ -37849,7 +38580,7 @@ function writeSliceFiles(destDir, files) {
|
|
|
37849
38580
|
if (typeof f.content === "string") {
|
|
37850
38581
|
const abs = join28(destDir, f.path);
|
|
37851
38582
|
mkdirSync7(dirname8(abs), { recursive: true });
|
|
37852
|
-
|
|
38583
|
+
writeFileSync15(abs, f.content, "utf8");
|
|
37853
38584
|
written.push(f.path);
|
|
37854
38585
|
} else {
|
|
37855
38586
|
unavailable.push({ path: f.path, reason: f.unavailableReason || "(no reason given)" });
|
|
@@ -37903,7 +38634,7 @@ function ensureExcludedPackDir(destDir) {
|
|
|
37903
38634
|
const existing = existsSync13(excludeFile) ? readFileSync14(excludeFile, "utf8") : "";
|
|
37904
38635
|
if (!existing.split("\n").includes(BRIEF_EXCLUDE_LINE)) {
|
|
37905
38636
|
mkdirSync7(dirname8(excludeFile), { recursive: true });
|
|
37906
|
-
|
|
38637
|
+
writeFileSync15(
|
|
37907
38638
|
excludeFile,
|
|
37908
38639
|
`${existing}${existing === "" || existing.endsWith("\n") ? "" : "\n"}${BRIEF_EXCLUDE_LINE}
|
|
37909
38640
|
`,
|
|
@@ -37919,14 +38650,14 @@ function writePackFile(destDir, relPath, content, what) {
|
|
|
37919
38650
|
try {
|
|
37920
38651
|
const abs = join28(destDir, relPath);
|
|
37921
38652
|
mkdirSync7(dirname8(abs), { recursive: true });
|
|
37922
|
-
|
|
38653
|
+
writeFileSync15(abs, content, { encoding: "utf8", flag: "wx" });
|
|
37923
38654
|
} catch (err) {
|
|
37924
38655
|
return { written: false, reason: `the ${what} could not be written (${err.message})` };
|
|
37925
38656
|
}
|
|
37926
38657
|
return { written: true, reason: null, sha256: sha256OfUtf8(content) };
|
|
37927
38658
|
}
|
|
37928
38659
|
function sha256OfUtf8(content) {
|
|
37929
|
-
return
|
|
38660
|
+
return createHash10("sha256").update(content, "utf8").digest("hex");
|
|
37930
38661
|
}
|
|
37931
38662
|
function writeWorkspacePack(destDir, spec, claim, delivery) {
|
|
37932
38663
|
if (delivery !== "full" && delivery !== "sparse") {
|
|
@@ -39297,7 +40028,7 @@ function readClaimPushMarker() {
|
|
|
39297
40028
|
}
|
|
39298
40029
|
function writeClaimPushMarker(marker) {
|
|
39299
40030
|
ensureStateDir(TERMINALHIRE_DIR11);
|
|
39300
|
-
|
|
40031
|
+
writeFileSync15(CLAIM_PUSH_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
39301
40032
|
}
|
|
39302
40033
|
function clearClaimPushMarker() {
|
|
39303
40034
|
try {
|