@amd-gaia/agent-ui 0.21.1 → 0.22.0
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/assets/{browser-DFw6ahKM.js → browser-CmSowSA-.js} +1 -1
- package/dist/assets/index-B31DMoxr.css +1 -0
- package/dist/assets/index-lScW7mUY.js +72 -0
- package/dist/index.html +3 -3
- package/main-safety-net.cjs +13 -3
- package/main.cjs +3 -0
- package/package.json +3 -3
- package/preload.cjs +22 -1
- package/services/agent-seeder.cjs +256 -14
- package/services/auto-updater.cjs +322 -22
- package/services/backend-installer.cjs +436 -118
- package/dist/assets/index-DRnhDSoO.css +0 -1
- package/dist/assets/index-Dj-STgr2.js +0 -72
|
@@ -36,6 +36,8 @@ const fs = require("fs");
|
|
|
36
36
|
const path = require("path");
|
|
37
37
|
const os = require("os");
|
|
38
38
|
const https = require("https");
|
|
39
|
+
const http = require("http");
|
|
40
|
+
const tls = require("tls");
|
|
39
41
|
|
|
40
42
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
41
43
|
|
|
@@ -75,25 +77,27 @@ const INSTALL_RETRY_BACKOFF_MS = 3000;
|
|
|
75
77
|
// `curl | sh` path is retained only as an unpackaged-dev fallback so
|
|
76
78
|
// contributors running from source keep working.
|
|
77
79
|
//
|
|
78
|
-
// When bumping uv, update
|
|
80
|
+
// When bumping uv, update:
|
|
79
81
|
// - .github/workflows/build-installers.yml (tarball .tar.gz SHA256 — archive)
|
|
80
|
-
// - BUNDLED_UV_SHA256 below (extracted
|
|
81
|
-
// These are two different digests: the workflow verifies the downloaded
|
|
82
|
-
// archive against upstream's published .sha256, then extracts the `uv` binary
|
|
83
|
-
// which is what `ensureUv()` hashes at runtime.
|
|
82
|
+
// - BUNDLED_UV_SHA256 below (extracted binary SHA256, linux-x64/win-x64 only)
|
|
84
83
|
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
84
|
+
// IMPORTANT: per-platform verification strategy differs:
|
|
85
|
+
// - linux-x64 / win-x64: BUNDLED_UV_SHA256 pins the raw extracted-binary
|
|
86
|
+
// digest (no post-build modification) — deterministic across CI runs.
|
|
87
|
+
// - mac-arm64: NOT pinned here. electron-builder code-signs the bundled uv
|
|
88
|
+
// during packaging (ad-hoc `identity=-` when no Developer ID cert is
|
|
89
|
+
// configured, which is every CI build today), and ad-hoc codesign output
|
|
90
|
+
// depends on the codesign/Xcode toolchain baked into the GitHub-hosted
|
|
91
|
+
// `macos-latest` runner image — which floats and is NOT reproducible
|
|
92
|
+
// across CI runs (observed: macos-15-arm64 vs macos-26-arm64 images
|
|
93
|
+
// produced two different digests for byte-identical source). A fixed
|
|
94
|
+
// SHA256 pin is therefore not deterministic on macOS and would fail
|
|
95
|
+
// ~half of CI runs and brick first-launch on user machines whenever the
|
|
96
|
+
// runner image rolls. Instead, ensureUv() and the dmg-structural-smoke
|
|
97
|
+
// test both run `codesign --verify --strict` against the bundled binary
|
|
98
|
+
// — this validates the on-disk signature is intact/untampered without
|
|
99
|
+
// depending on the exact signing bytes, and still fails loud on
|
|
100
|
+
// corruption or an actually-invalid signature.
|
|
97
101
|
const BUNDLED_UV_VERSION = "0.5.14";
|
|
98
102
|
const BUNDLED_UV_SHA256 = {
|
|
99
103
|
"linux-x64": "0e05d828b5708e8a927724124db3746396afddad6273c47283d7c562dc795bd6",
|
|
@@ -101,9 +105,12 @@ const BUNDLED_UV_SHA256 = {
|
|
|
101
105
|
// build step. The placeholder MUST be replaced in CI before packaging
|
|
102
106
|
// so runtime verification remains strict.
|
|
103
107
|
"win-x64": "055d55eec85a91cfb5e9c8bc7f6463f9883866796c5bcb205fbcdfed9c088c88",
|
|
104
|
-
// mac-arm64:
|
|
105
|
-
//
|
|
106
|
-
|
|
108
|
+
// mac-arm64: intentionally absent. See the comment block above — the
|
|
109
|
+
// post-codesign digest is not deterministic across CI runner images, so
|
|
110
|
+
// mac-arm64 is verified via codesignVerify() (identity/signature validity)
|
|
111
|
+
// instead of a fixed SHA256 pin. bundledUvPlatformKey() still returns
|
|
112
|
+
// "mac-arm64"; callers must not treat a missing entry here as "unpinned
|
|
113
|
+
// platform" for darwin — see ensureUv()/installBundledUv().
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
const MANAGED_UV_DIR = path.join(GAIA_HOME, "bin");
|
|
@@ -510,6 +517,27 @@ function isTransientNetworkError(output) {
|
|
|
510
517
|
);
|
|
511
518
|
}
|
|
512
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Detect a TLS trust failure in `uv pip install` output. Behind a corporate
|
|
522
|
+
* MITM proxy, PyPI is presented with a certificate signed by a custom root CA
|
|
523
|
+
* that uv's bundled webpki roots don't include, so uv reports
|
|
524
|
+
* "invalid peer certificate: UnknownIssuer" (issue #1693). It surfaces wrapped
|
|
525
|
+
* in "Failed to fetch" / "error sending request", so isTransientNetworkError
|
|
526
|
+
* also matches it — the install loop MUST check this first and retry with
|
|
527
|
+
* --native-tls (the OS trust store, where IT installs the corporate CA) rather
|
|
528
|
+
* than burning the transient retries on the same bundled roots.
|
|
529
|
+
*/
|
|
530
|
+
function isTlsCertError(output) {
|
|
531
|
+
if (!output) return false;
|
|
532
|
+
return (
|
|
533
|
+
/invalid peer certificate/i.test(output) ||
|
|
534
|
+
/unknownissuer/i.test(output) ||
|
|
535
|
+
/certificate verify failed/i.test(output) ||
|
|
536
|
+
/unable to get local issuer certificate/i.test(output) ||
|
|
537
|
+
/self[- ]signed certificate/i.test(output)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
513
541
|
function sleep(ms) {
|
|
514
542
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
515
543
|
}
|
|
@@ -590,69 +618,234 @@ function checkDiskSpace() {
|
|
|
590
618
|
}
|
|
591
619
|
|
|
592
620
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
621
|
+
* Read the OS trust store. Node 22+ exposes `tls.getCACertificates("system")`;
|
|
622
|
+
* older Node lacks it, so this returns `[]` there (callers fall back to
|
|
623
|
+
* NODE_EXTRA_CA_CERTS / the bundled Mozilla set).
|
|
624
|
+
*
|
|
625
|
+
* Why this exists: behind a corporate TLS-inspection proxy, the proxy's root
|
|
626
|
+
* CA is installed in the OS trust store but NOT in Node's bundled Mozilla set,
|
|
627
|
+
* so every HTTPS handshake fails with UNABLE_TO_GET_ISSUER_CERT_LOCALLY and the
|
|
628
|
+
* machine looks "offline" when it is online (issue #1572).
|
|
629
|
+
*/
|
|
630
|
+
function _systemCaCertificates() {
|
|
631
|
+
try {
|
|
632
|
+
if (typeof tls.getCACertificates === "function") {
|
|
633
|
+
return tls.getCACertificates("system") || [];
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
log(`Could not read system CA store: ${err.message}`);
|
|
637
|
+
}
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Read a pinned corporate root CA from NODE_EXTRA_CA_CERTS (any Node version). */
|
|
642
|
+
function _extraCaCertificates() {
|
|
643
|
+
const file = process.env.NODE_EXTRA_CA_CERTS;
|
|
644
|
+
if (!file) return [];
|
|
645
|
+
try {
|
|
646
|
+
return [fs.readFileSync(file, "utf8")];
|
|
647
|
+
} catch (err) {
|
|
648
|
+
log(`Could not read NODE_EXTRA_CA_CERTS (${file}): ${err.message}`);
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Build the CA trust bundle for the network probe. Passing `ca` to
|
|
655
|
+
* `https.request` REPLACES Node's default trust store, so when we add system /
|
|
656
|
+
* extra certs we must re-include the bundled Mozilla set. Returns `undefined`
|
|
657
|
+
* when there is nothing extra to add (probe uses Node's default trust store —
|
|
658
|
+
* behaviour unchanged).
|
|
659
|
+
*/
|
|
660
|
+
function buildCaBundle() {
|
|
661
|
+
const extra = [..._systemCaCertificates(), ..._extraCaCertificates()];
|
|
662
|
+
if (extra.length === 0) return undefined;
|
|
663
|
+
return [...new Set([...tls.rootCertificates, ...extra])];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** First non-empty proxy URL from the standard env vars, or null. */
|
|
667
|
+
function proxyForHttps() {
|
|
668
|
+
return (
|
|
669
|
+
process.env.HTTPS_PROXY ||
|
|
670
|
+
process.env.https_proxy ||
|
|
671
|
+
process.env.HTTP_PROXY ||
|
|
672
|
+
process.env.http_proxy ||
|
|
673
|
+
null
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Classify a network error so the caller can tell a trust-store gap (NOT
|
|
679
|
+
* offline) apart from genuine connectivity loss.
|
|
680
|
+
* - "tls" → certificate/handshake failure (machine is online)
|
|
681
|
+
* - "timeout" → request timed out
|
|
682
|
+
* - "connectivity" → DNS/connect failure (likely offline)
|
|
683
|
+
*/
|
|
684
|
+
function classifyNetworkError(err) {
|
|
685
|
+
const code = (err && err.code) || "";
|
|
686
|
+
const message = (err && err.message) || "";
|
|
687
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO_|_SIGNATURE|ALTNAME/.test(code)) {
|
|
688
|
+
return "tls";
|
|
689
|
+
}
|
|
690
|
+
if (code === "ETIMEDOUT" || /timed out/i.test(message)) {
|
|
691
|
+
return "timeout";
|
|
692
|
+
}
|
|
693
|
+
return "connectivity";
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** HEAD-probe `target` (a URL) directly, trusting `ca`. */
|
|
697
|
+
function _probeDirect(target, ca, finish, setSock) {
|
|
698
|
+
const opts = {
|
|
699
|
+
host: target.hostname,
|
|
700
|
+
port: target.port || 443,
|
|
701
|
+
path: target.pathname || "/",
|
|
702
|
+
method: "HEAD",
|
|
703
|
+
servername: target.hostname,
|
|
704
|
+
headers: { "User-Agent": "gaia-backend-installer/1.0" },
|
|
705
|
+
};
|
|
706
|
+
if (ca) opts.ca = ca;
|
|
707
|
+
try {
|
|
708
|
+
const req = https.request(opts, (res) => {
|
|
709
|
+
res.resume();
|
|
710
|
+
finish({ ok: true, status: res.statusCode });
|
|
711
|
+
});
|
|
712
|
+
setSock(req);
|
|
713
|
+
req.on("error", (err) =>
|
|
714
|
+
finish({ ok: false, kind: classifyNetworkError(err), message: `${target.href}: ${err.message}` })
|
|
715
|
+
);
|
|
716
|
+
req.end();
|
|
717
|
+
} catch (err) {
|
|
718
|
+
finish({ ok: false, kind: classifyNetworkError(err), message: `${target.href}: ${err.message}` });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** HEAD-probe `target` through an HTTP CONNECT proxy, trusting `ca`. */
|
|
723
|
+
function _probeViaProxy(target, proxy, ca, finish, setSock) {
|
|
724
|
+
let proxyUrl;
|
|
725
|
+
try {
|
|
726
|
+
proxyUrl = new URL(proxy);
|
|
727
|
+
} catch {
|
|
728
|
+
finish({ ok: false, kind: "connectivity", message: `${target.href}: invalid proxy URL "${proxy}"` });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const headers = {};
|
|
732
|
+
if (proxyUrl.username) {
|
|
733
|
+
const cred = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
|
|
734
|
+
headers["Proxy-Authorization"] = "Basic " + Buffer.from(cred).toString("base64");
|
|
735
|
+
}
|
|
736
|
+
const connectReq = http.request({
|
|
737
|
+
host: proxyUrl.hostname,
|
|
738
|
+
port: proxyUrl.port || 80,
|
|
739
|
+
method: "CONNECT",
|
|
740
|
+
path: `${target.hostname}:${target.port || 443}`,
|
|
741
|
+
headers,
|
|
742
|
+
});
|
|
743
|
+
setSock(connectReq);
|
|
744
|
+
connectReq.on("connect", (res, socket) => {
|
|
745
|
+
if (res.statusCode !== 200) {
|
|
746
|
+
try { socket.destroy(); } catch { /* ignore */ }
|
|
747
|
+
finish({ ok: false, kind: "connectivity", message: `${target.href}: proxy CONNECT returned ${res.statusCode}` });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
setSock(socket);
|
|
751
|
+
const opts = {
|
|
752
|
+
host: target.hostname,
|
|
753
|
+
port: target.port || 443,
|
|
754
|
+
path: target.pathname || "/",
|
|
755
|
+
method: "HEAD",
|
|
756
|
+
socket,
|
|
757
|
+
agent: false,
|
|
758
|
+
servername: target.hostname,
|
|
759
|
+
headers: { "User-Agent": "gaia-backend-installer/1.0" },
|
|
760
|
+
};
|
|
761
|
+
if (ca) opts.ca = ca;
|
|
762
|
+
try {
|
|
763
|
+
const req = https.request(opts, (r) => {
|
|
764
|
+
r.resume();
|
|
765
|
+
finish({ ok: true, status: r.statusCode });
|
|
766
|
+
});
|
|
767
|
+
req.on("error", (err) =>
|
|
768
|
+
finish({ ok: false, kind: classifyNetworkError(err), message: `${target.href}: ${err.message}` })
|
|
769
|
+
);
|
|
770
|
+
req.end();
|
|
771
|
+
} catch (err) {
|
|
772
|
+
finish({ ok: false, kind: classifyNetworkError(err), message: `${target.href}: ${err.message}` });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
connectReq.on("error", (err) =>
|
|
776
|
+
finish({ ok: false, kind: classifyNetworkError(err), message: `${target.href}: ${err.message}` })
|
|
777
|
+
);
|
|
778
|
+
connectReq.end();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Best-effort reachability probe for a single host. A HEAD that gets any
|
|
783
|
+
* response (even 3xx/4xx) proves connectivity. Trusts the system / pinned CA
|
|
784
|
+
* store (issue #1572) and honors HTTPS_PROXY/HTTP_PROXY. Resolves
|
|
785
|
+
* { ok, status? } or { ok:false, kind, message }.
|
|
595
786
|
*/
|
|
596
787
|
function _checkOneHost(url) {
|
|
597
788
|
return new Promise((resolve) => {
|
|
598
789
|
let settled = false;
|
|
790
|
+
let sock = null;
|
|
791
|
+
let timer = null;
|
|
599
792
|
const finish = (result) => {
|
|
600
793
|
if (settled) return;
|
|
601
794
|
settled = true;
|
|
795
|
+
if (timer) clearTimeout(timer);
|
|
796
|
+
if (sock) {
|
|
797
|
+
try { sock.destroy(); } catch { /* ignore */ }
|
|
798
|
+
}
|
|
602
799
|
resolve(result);
|
|
603
800
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
{
|
|
609
|
-
method: "HEAD",
|
|
610
|
-
timeout: NETWORK_CHECK_TIMEOUT_MS,
|
|
611
|
-
headers: { "User-Agent": "gaia-backend-installer/1.0" },
|
|
612
|
-
},
|
|
613
|
-
(res) => {
|
|
614
|
-
// Any response (even 3xx/4xx) means we have basic connectivity.
|
|
615
|
-
res.resume();
|
|
616
|
-
finish({ ok: true, status: res.statusCode });
|
|
617
|
-
}
|
|
618
|
-
);
|
|
619
|
-
req.on("timeout", () => {
|
|
620
|
-
req.destroy();
|
|
621
|
-
finish({
|
|
622
|
-
ok: false,
|
|
623
|
-
message: `${url}: timed out after ${NETWORK_CHECK_TIMEOUT_MS / 1000}s`,
|
|
624
|
-
});
|
|
625
|
-
});
|
|
626
|
-
req.on("error", (err) => {
|
|
627
|
-
finish({
|
|
628
|
-
ok: false,
|
|
629
|
-
message: `${url}: ${err.message}`,
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
req.end();
|
|
633
|
-
} catch (err) {
|
|
801
|
+
const setSock = (s) => {
|
|
802
|
+
sock = s;
|
|
803
|
+
};
|
|
804
|
+
timer = setTimeout(() => {
|
|
634
805
|
finish({
|
|
635
806
|
ok: false,
|
|
636
|
-
|
|
807
|
+
kind: "timeout",
|
|
808
|
+
message: `${url}: timed out after ${NETWORK_CHECK_TIMEOUT_MS / 1000}s`,
|
|
637
809
|
});
|
|
810
|
+
}, NETWORK_CHECK_TIMEOUT_MS);
|
|
811
|
+
|
|
812
|
+
let target;
|
|
813
|
+
try {
|
|
814
|
+
target = new URL(url);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
finish({ ok: false, kind: "connectivity", message: `${url}: ${err.message}` });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const ca = buildCaBundle();
|
|
820
|
+
const proxy = proxyForHttps();
|
|
821
|
+
if (proxy) {
|
|
822
|
+
_probeViaProxy(target, proxy, ca, finish, setSock);
|
|
823
|
+
} else {
|
|
824
|
+
_probeDirect(target, ca, finish, setSock);
|
|
638
825
|
}
|
|
639
826
|
});
|
|
640
827
|
}
|
|
641
828
|
|
|
642
829
|
/**
|
|
643
|
-
* Probe each host in ``NETWORK_CHECK_HOSTS`` sequentially. Succeed as
|
|
644
|
-
*
|
|
645
|
-
*
|
|
830
|
+
* Probe each host in ``NETWORK_CHECK_HOSTS`` sequentially. Succeed as soon as
|
|
831
|
+
* ANY host responds (even 3xx/4xx counts — it proves connectivity). On total
|
|
832
|
+
* failure, report the dominant failure `kind` so the caller can distinguish a
|
|
833
|
+
* trust-store gap ("tls" — online but Node doesn't trust the proxy CA) from a
|
|
834
|
+
* genuine outage ("connectivity").
|
|
646
835
|
*/
|
|
647
836
|
async function checkNetwork() {
|
|
648
837
|
const errors = [];
|
|
838
|
+
const kinds = new Set();
|
|
649
839
|
for (const url of NETWORK_CHECK_HOSTS) {
|
|
650
840
|
const result = await _checkOneHost(url);
|
|
651
|
-
if (result.ok) return result;
|
|
841
|
+
if (result.ok) return { ok: true, status: result.status };
|
|
652
842
|
errors.push(result.message);
|
|
843
|
+
kinds.add(result.kind || "connectivity");
|
|
653
844
|
}
|
|
845
|
+
const allTls = kinds.size > 0 && [...kinds].every((k) => k === "tls");
|
|
654
846
|
return {
|
|
655
847
|
ok: false,
|
|
848
|
+
kind: allTls ? "tls" : kinds.has("connectivity") ? "connectivity" : [...kinds][0] || "connectivity",
|
|
656
849
|
message: `Network check failed for all hosts: ${errors.join("; ")}`,
|
|
657
850
|
};
|
|
658
851
|
}
|
|
@@ -736,6 +929,27 @@ function sha256File(filePath) {
|
|
|
736
929
|
});
|
|
737
930
|
}
|
|
738
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Verify a macOS binary's code signature is structurally intact via
|
|
934
|
+
* `codesign --verify --strict`. Used in place of a fixed SHA256 pin for
|
|
935
|
+
* mac-arm64 (see the BUNDLED_UV_SHA256 comment) — ad-hoc-signed binaries
|
|
936
|
+
* (identity=-, which is every CI build today) pass this check, but a
|
|
937
|
+
* corrupted, tampered, or actually-unsigned binary fails it. This does NOT
|
|
938
|
+
* depend on the specific bytes any given codesign/Xcode toolchain produces,
|
|
939
|
+
* so it is stable across GitHub-hosted runner image rollovers where a fixed
|
|
940
|
+
* digest pin is not.
|
|
941
|
+
*
|
|
942
|
+
* @param {string} binPath
|
|
943
|
+
* @returns {{ ok: boolean, output: string }}
|
|
944
|
+
*/
|
|
945
|
+
function codesignVerify(binPath) {
|
|
946
|
+
const result = spawnSync("codesign", ["--verify", "--strict", binPath], {
|
|
947
|
+
encoding: "utf8",
|
|
948
|
+
});
|
|
949
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
950
|
+
return { ok: result.status === 0, output };
|
|
951
|
+
}
|
|
952
|
+
|
|
739
953
|
/**
|
|
740
954
|
* Resolve the bundled uv binary path inside the Electron resources dir.
|
|
741
955
|
* Returns null if this isn't an Electron-packaged runtime (no
|
|
@@ -757,15 +971,22 @@ function findBundledUvResource() {
|
|
|
757
971
|
}
|
|
758
972
|
|
|
759
973
|
/**
|
|
760
|
-
* Atomically install the bundled uv into ~/.gaia/bin/uv after verifying
|
|
761
|
-
*
|
|
974
|
+
* Atomically install the bundled uv into ~/.gaia/bin/uv after verifying it.
|
|
975
|
+
* Returns the installed path.
|
|
976
|
+
*
|
|
977
|
+
* Verification strategy differs by platform (see the BUNDLED_UV_SHA256
|
|
978
|
+
* comment): mac-arm64 uses `codesign --verify --strict` (darwin only —
|
|
979
|
+
* post-codesign SHA256 is not deterministic across CI runner images);
|
|
980
|
+
* linux-x64 / win-x64 use the SHA256 pin in BUNDLED_UV_SHA256, which IS
|
|
981
|
+
* deterministic for those platforms (no post-build re-signing).
|
|
762
982
|
*
|
|
763
|
-
* Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies
|
|
764
|
-
*
|
|
983
|
+
* Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies, `chmod +x`,
|
|
984
|
+
* then `fs.rename()` (atomic on same filesystem).
|
|
765
985
|
*/
|
|
766
986
|
async function installBundledUv(sourcePath, platformKey) {
|
|
987
|
+
const isDarwin = platformKey === "mac-arm64";
|
|
767
988
|
const expected = BUNDLED_UV_SHA256[platformKey];
|
|
768
|
-
if (!expected || expected.startsWith("<")) {
|
|
989
|
+
if (!isDarwin && (!expected || expected.startsWith("<"))) {
|
|
769
990
|
// Enforce strict verification: builds MUST populate the expected SHA
|
|
770
991
|
// for packaged binaries. Failing fast prevents shipping an unverified
|
|
771
992
|
// uv binary which would be a supply-chain regression.
|
|
@@ -801,18 +1022,39 @@ async function installBundledUv(sourcePath, platformKey) {
|
|
|
801
1022
|
rs.pipe(ws);
|
|
802
1023
|
});
|
|
803
1024
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
`
|
|
811
|
-
|
|
812
|
-
);
|
|
813
|
-
|
|
1025
|
+
if (isDarwin) {
|
|
1026
|
+
// chmod BEFORE codesign --verify: codesign needs the execute bit to
|
|
1027
|
+
// resolve the binary's designated requirement on some toolchains.
|
|
1028
|
+
try {
|
|
1029
|
+
fs.chmodSync(tmpPath, 0o700);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
log(`Warning: chmod on tmp uv failed: ${err.message}`);
|
|
1032
|
+
}
|
|
1033
|
+
const { ok, output } = codesignVerify(tmpPath);
|
|
1034
|
+
if (!ok) {
|
|
1035
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
1036
|
+
throw new InstallError(
|
|
1037
|
+
`Bundled uv failed code signature verification (codesign --verify --strict): ${output || "no output"}`,
|
|
1038
|
+
{
|
|
1039
|
+
stage: STAGES.ENSURE_UV,
|
|
1040
|
+
suggestion:
|
|
1041
|
+
"The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
log("Bundled uv passed codesign --verify --strict");
|
|
1046
|
+
} else {
|
|
1047
|
+
let actual;
|
|
1048
|
+
try {
|
|
1049
|
+
actual = await sha256File(tmpPath);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
1052
|
+
throw new InstallError(
|
|
1053
|
+
`Could not hash copied uv binary: ${err.message}`,
|
|
1054
|
+
{ stage: STAGES.ENSURE_UV }
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
814
1057
|
|
|
815
|
-
if (expected) {
|
|
816
1058
|
if (actual !== expected) {
|
|
817
1059
|
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
818
1060
|
throw new InstallError(
|
|
@@ -824,14 +1066,12 @@ async function installBundledUv(sourcePath, platformKey) {
|
|
|
824
1066
|
}
|
|
825
1067
|
);
|
|
826
1068
|
}
|
|
827
|
-
} else {
|
|
828
|
-
log("No expected SHA registered for bundled uv; installed binary will not be verified locally.");
|
|
829
|
-
}
|
|
830
1069
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1070
|
+
try {
|
|
1071
|
+
if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
log(`Warning: chmod on tmp uv failed: ${err.message}`);
|
|
1074
|
+
}
|
|
835
1075
|
}
|
|
836
1076
|
|
|
837
1077
|
try {
|
|
@@ -869,9 +1109,11 @@ function addManagedBinToPath() {
|
|
|
869
1109
|
|
|
870
1110
|
/**
|
|
871
1111
|
* Ensure `uv` is available. Preference order (per issue #782 / T3):
|
|
872
|
-
* 1. Managed copy at ~/.gaia/bin/uv
|
|
1112
|
+
* 1. Managed copy at ~/.gaia/bin/uv already verified (warm-install fast path):
|
|
1113
|
+
* SHA256 pin on linux-x64/win-x64, `codesign --verify --strict` on mac-arm64.
|
|
873
1114
|
* 2. Bundled binary in process.resourcesPath/vendor/uv/<platform>/uv:
|
|
874
|
-
* copy atomically to ~/.gaia/bin/uv with
|
|
1115
|
+
* copy atomically to ~/.gaia/bin/uv with the same platform-appropriate
|
|
1116
|
+
* verification.
|
|
875
1117
|
* 3. DEV-ONLY fallback (app.isPackaged === false OR no resourcesPath):
|
|
876
1118
|
* the original `curl | sh` from astral.sh. Not a shipped-user path.
|
|
877
1119
|
* 4. System `uv` on PATH (last resort — unverified version).
|
|
@@ -883,10 +1125,20 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
883
1125
|
report(STAGES.ENSURE_UV, 0, "Checking uv (Python package manager)");
|
|
884
1126
|
|
|
885
1127
|
const platformKey = bundledUvPlatformKey();
|
|
1128
|
+
const isDarwin = platformKey === "mac-arm64";
|
|
886
1129
|
const expectedSha = platformKey ? BUNDLED_UV_SHA256[platformKey] : null;
|
|
887
1130
|
|
|
888
|
-
// Fast path: warm install already on disk
|
|
889
|
-
if (
|
|
1131
|
+
// Fast path: warm install already on disk and still passes verification.
|
|
1132
|
+
if (isDarwin && fs.existsSync(MANAGED_UV_BIN)) {
|
|
1133
|
+
const { ok, output } = codesignVerify(MANAGED_UV_BIN);
|
|
1134
|
+
if (ok) {
|
|
1135
|
+
log(`Managed uv at ${MANAGED_UV_BIN} passed codesign --verify --strict — reusing`);
|
|
1136
|
+
addManagedBinToPath();
|
|
1137
|
+
report(STAGES.ENSURE_UV, 100, "uv ready (cached)");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
log(`Managed uv failed codesign verification (${output || "no output"}) — replacing`);
|
|
1141
|
+
} else if (expectedSha && fs.existsSync(MANAGED_UV_BIN)) {
|
|
890
1142
|
try {
|
|
891
1143
|
const actual = await sha256File(MANAGED_UV_BIN);
|
|
892
1144
|
if (actual === expectedSha) {
|
|
@@ -909,22 +1161,36 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
909
1161
|
report(STAGES.ENSURE_UV, 30, "Installing bundled uv");
|
|
910
1162
|
log(`Using bundled uv from ${bundled}`);
|
|
911
1163
|
|
|
912
|
-
// Verify the source resource
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
// build-time error
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1164
|
+
// Verify the source resource before copying — catches installer
|
|
1165
|
+
// corruption before we touch the user's home. mac-arm64 uses
|
|
1166
|
+
// codesign --verify --strict (see BUNDLED_UV_SHA256 comment for why a
|
|
1167
|
+
// fixed digest isn't viable there); other platforms use the SHA256 pin,
|
|
1168
|
+
// which CI must have populated — a missing/placeholder value is a
|
|
1169
|
+
// build-time error, rejected at runtime here.
|
|
1170
|
+
if (isDarwin) {
|
|
1171
|
+
const { ok, output } = codesignVerify(bundled);
|
|
1172
|
+
if (!ok) {
|
|
1173
|
+
throw new InstallError(
|
|
1174
|
+
`Bundled uv resource failed code signature verification (codesign --verify --strict): ${output || "no output"}`,
|
|
1175
|
+
{
|
|
1176
|
+
stage: STAGES.ENSURE_UV,
|
|
1177
|
+
suggestion:
|
|
1178
|
+
"The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
const srcHash = await sha256File(bundled);
|
|
1184
|
+
if (srcHash !== expectedSha) {
|
|
1185
|
+
throw new InstallError(
|
|
1186
|
+
`Bundled uv resource SHA256 mismatch (expected ${expectedSha}, got ${srcHash}).`,
|
|
1187
|
+
{
|
|
1188
|
+
stage: STAGES.ENSURE_UV,
|
|
1189
|
+
suggestion:
|
|
1190
|
+
"The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
928
1194
|
}
|
|
929
1195
|
await installBundledUv(bundled, platformKey);
|
|
930
1196
|
addManagedBinToPath();
|
|
@@ -1198,10 +1464,38 @@ async function installBackend(opts = {}) {
|
|
|
1198
1464
|
// would otherwise fail the whole bootstrap (and block a release). File-lock
|
|
1199
1465
|
// failures (Windows os-error-32) are NOT retried — they need user action.
|
|
1200
1466
|
let installResult;
|
|
1201
|
-
|
|
1202
|
-
|
|
1467
|
+
let useNativeTls = false;
|
|
1468
|
+
let nativeTlsAttempted = false;
|
|
1469
|
+
// The TLS branch flips useNativeTls and `continue`s, consuming an iteration.
|
|
1470
|
+
// If the signature only surfaces on the final attempt, extend the loop once
|
|
1471
|
+
// so the promised --native-tls retry actually runs (issue #1693 review).
|
|
1472
|
+
for (
|
|
1473
|
+
let attempt = 1;
|
|
1474
|
+
attempt <= INSTALL_MAX_ATTEMPTS || (useNativeTls && !nativeTlsAttempted);
|
|
1475
|
+
attempt++
|
|
1476
|
+
) {
|
|
1477
|
+
const attemptArgs = useNativeTls ? [...pipArgs, "--native-tls"] : pipArgs;
|
|
1478
|
+
if (useNativeTls) nativeTlsAttempted = true;
|
|
1479
|
+
installResult = await runCommand("uv", attemptArgs, { stageLabel: "pip" });
|
|
1203
1480
|
if (installResult.code === 0) break;
|
|
1204
1481
|
const attemptOutput = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
|
|
1482
|
+
// A corporate proxy's custom root CA isn't in uv's bundled webpki roots, so
|
|
1483
|
+
// the fetch fails with UnknownIssuer. This LOOKS transient ("Failed to
|
|
1484
|
+
// fetch") but retrying the bundled roots can't fix it — retry once with
|
|
1485
|
+
// --native-tls so uv trusts the OS certificate store instead (issue #1693).
|
|
1486
|
+
if (!useNativeTls && isTlsCertError(attemptOutput)) {
|
|
1487
|
+
useNativeTls = true;
|
|
1488
|
+
log(
|
|
1489
|
+
"TLS trust failure (corporate proxy CA not in uv's bundled roots) — " +
|
|
1490
|
+
"retrying install with --native-tls to use the OS certificate store"
|
|
1491
|
+
);
|
|
1492
|
+
report(
|
|
1493
|
+
STAGES.INSTALL_PACKAGE,
|
|
1494
|
+
0,
|
|
1495
|
+
"Certificate trust issue — retrying with the system certificate store"
|
|
1496
|
+
);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1205
1499
|
const canRetry =
|
|
1206
1500
|
attempt < INSTALL_MAX_ATTEMPTS &&
|
|
1207
1501
|
isTransientNetworkError(attemptOutput) &&
|
|
@@ -1225,11 +1519,23 @@ async function installBackend(opts = {}) {
|
|
|
1225
1519
|
// main.cjs should have killed it first; if the lock persists, point the
|
|
1226
1520
|
// user at the live process rather than the generic manual-install hint.
|
|
1227
1521
|
const output = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1522
|
+
let suggestion;
|
|
1523
|
+
if (isFileLockedError(output)) {
|
|
1524
|
+
suggestion =
|
|
1525
|
+
"A running GAIA process is locking the install. Close GAIA completely (including any leftover gaia.exe in Task Manager), then relaunch.";
|
|
1526
|
+
} else if (isTlsCertError(output)) {
|
|
1527
|
+
suggestion =
|
|
1528
|
+
"PyPI's certificate isn't trusted on this network — usually a corporate proxy presenting its own root CA. " +
|
|
1529
|
+
(nativeTlsAttempted
|
|
1530
|
+
? "GAIA retried with the OS certificate store and it still failed. "
|
|
1531
|
+
: "") +
|
|
1532
|
+
"Ask IT to install the proxy's root CA in your system's trusted certificate store, then relaunch GAIA. " +
|
|
1533
|
+
"See https://amd-gaia.ai/quickstart#cli-install";
|
|
1534
|
+
} else {
|
|
1535
|
+
suggestion = `Try installing manually:\n uv pip install ${pipPackage} --python ${
|
|
1536
|
+
IS_WINDOWS ? `${GAIA_VENV_DISPLAY}/Scripts/python.exe` : `${GAIA_VENV_DISPLAY}/bin/python`
|
|
1537
|
+
}\nThen restart GAIA. See https://amd-gaia.ai/quickstart#cli-install`;
|
|
1538
|
+
}
|
|
1233
1539
|
throw new InstallError(
|
|
1234
1540
|
`Failed to install ${pipPackage} (pip exit ${installResult.code}).`,
|
|
1235
1541
|
{
|
|
@@ -1426,18 +1732,25 @@ async function ensureBackend(opts = {}) {
|
|
|
1426
1732
|
throw err;
|
|
1427
1733
|
}
|
|
1428
1734
|
|
|
1429
|
-
// Network check
|
|
1735
|
+
// Network check is ADVISORY, never fatal. A failed HEAD probe is most often
|
|
1736
|
+
// a corporate TLS-inspection proxy whose root CA isn't in Node's bundled
|
|
1737
|
+
// trust store (#1572) — the machine is online and `uv`/`pip`, which honor
|
|
1738
|
+
// HTTPS_PROXY and the system trust store, install fine. If the host really
|
|
1739
|
+
// is unreachable, the install step below fails loudly with an accurate
|
|
1740
|
+
// uv/pip error, which is more actionable than a generic "offline".
|
|
1430
1741
|
if (!preChecks.network.ok) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1742
|
+
if (preChecks.network.kind === "tls") {
|
|
1743
|
+
log(
|
|
1744
|
+
`Network pre-check hit a TLS/certificate error (not offline): ${preChecks.network.message}. ` +
|
|
1745
|
+
"Typically a corporate proxy root CA missing from Node's trust store — " +
|
|
1746
|
+
"set NODE_EXTRA_CA_CERTS to it (or relaunch with --use-system-ca) if the install fails. Proceeding."
|
|
1747
|
+
);
|
|
1748
|
+
} else {
|
|
1749
|
+
log(
|
|
1750
|
+
`Network pre-check could not reach any host: ${preChecks.network.message}. ` +
|
|
1751
|
+
"Proceeding anyway; the install step will surface an accurate error if the network is truly unreachable."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1441
1754
|
}
|
|
1442
1755
|
|
|
1443
1756
|
// Fast-path: already installed at the expected version.
|
|
@@ -1521,6 +1834,11 @@ module.exports = {
|
|
|
1521
1834
|
checkNetwork,
|
|
1522
1835
|
isFileLockedError,
|
|
1523
1836
|
isTransientNetworkError,
|
|
1837
|
+
isTlsCertError,
|
|
1838
|
+
buildCaBundle,
|
|
1839
|
+
proxyForHttps,
|
|
1840
|
+
classifyNetworkError,
|
|
1841
|
+
_checkOneHost,
|
|
1524
1842
|
|
|
1525
1843
|
// State machine
|
|
1526
1844
|
getState,
|