@amd-gaia/agent-ui 0.21.2 → 0.23.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/bin/gaia-ui.cjs +1 -1
- package/dist/assets/{browser-B1LxEoD3.js → browser-C-_jUAU-.js} +3 -3
- package/dist/assets/index-C4_mR67p.js +69 -0
- package/dist/assets/index-ChKtY1lF.css +1 -0
- package/dist/index.html +2 -2
- package/main-safety-net.cjs +13 -3
- package/main.cjs +201 -2
- package/package.json +5 -5
- package/preload.cjs +10 -1
- package/services/agent-process-manager.cjs +287 -6
- package/services/agent-seeder.cjs +256 -14
- package/services/auto-updater.cjs +132 -17
- package/services/backend-installer-progress-dialog.cjs +1 -1
- package/services/backend-installer.cjs +151 -69
- package/services/deep-link.cjs +230 -0
- package/services/system-metrics.cjs +154 -0
- package/dist/assets/index-BaBSHxtq.js +0 -72
- package/dist/assets/index-CGT9a3El.css +0 -1
|
@@ -77,25 +77,27 @@ const INSTALL_RETRY_BACKOFF_MS = 3000;
|
|
|
77
77
|
// `curl | sh` path is retained only as an unpackaged-dev fallback so
|
|
78
78
|
// contributors running from source keep working.
|
|
79
79
|
//
|
|
80
|
-
// When bumping uv, update
|
|
80
|
+
// When bumping uv, update:
|
|
81
81
|
// - .github/workflows/build-installers.yml (tarball .tar.gz SHA256 — archive)
|
|
82
|
-
// - BUNDLED_UV_SHA256 below (extracted
|
|
83
|
-
// These are two different digests: the workflow verifies the downloaded
|
|
84
|
-
// archive against upstream's published .sha256, then extracts the `uv` binary
|
|
85
|
-
// which is what `ensureUv()` hashes at runtime.
|
|
82
|
+
// - BUNDLED_UV_SHA256 below (extracted binary SHA256, linux-x64/win-x64 only)
|
|
86
83
|
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
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.
|
|
99
101
|
const BUNDLED_UV_VERSION = "0.5.14";
|
|
100
102
|
const BUNDLED_UV_SHA256 = {
|
|
101
103
|
"linux-x64": "0e05d828b5708e8a927724124db3746396afddad6273c47283d7c562dc795bd6",
|
|
@@ -103,9 +105,12 @@ const BUNDLED_UV_SHA256 = {
|
|
|
103
105
|
// build step. The placeholder MUST be replaced in CI before packaging
|
|
104
106
|
// so runtime verification remains strict.
|
|
105
107
|
"win-x64": "055d55eec85a91cfb5e9c8bc7f6463f9883866796c5bcb205fbcdfed9c088c88",
|
|
106
|
-
// mac-arm64:
|
|
107
|
-
//
|
|
108
|
-
|
|
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().
|
|
109
114
|
};
|
|
110
115
|
|
|
111
116
|
const MANAGED_UV_DIR = path.join(GAIA_HOME, "bin");
|
|
@@ -640,7 +645,8 @@ function _extraCaCertificates() {
|
|
|
640
645
|
try {
|
|
641
646
|
return [fs.readFileSync(file, "utf8")];
|
|
642
647
|
} catch (err) {
|
|
643
|
-
|
|
648
|
+
// err.message already names the offending path — don't re-log the env value.
|
|
649
|
+
log(`Could not read NODE_EXTRA_CA_CERTS: ${err.message}`);
|
|
644
650
|
return [];
|
|
645
651
|
}
|
|
646
652
|
}
|
|
@@ -720,7 +726,10 @@ function _probeViaProxy(target, proxy, ca, finish, setSock) {
|
|
|
720
726
|
try {
|
|
721
727
|
proxyUrl = new URL(proxy);
|
|
722
728
|
} catch {
|
|
723
|
-
|
|
729
|
+
// HTTPS_PROXY/HTTP_PROXY may embed credentials (http://user:pass@host) —
|
|
730
|
+
// strip the userinfo before the value reaches the install log.
|
|
731
|
+
const redacted = proxy.replace(/^(\w+:\/\/)[^@/]*@/, "$1***@");
|
|
732
|
+
finish({ ok: false, kind: "connectivity", message: `${target.href}: invalid proxy URL "${redacted}"` });
|
|
724
733
|
return;
|
|
725
734
|
}
|
|
726
735
|
const headers = {};
|
|
@@ -924,6 +933,27 @@ function sha256File(filePath) {
|
|
|
924
933
|
});
|
|
925
934
|
}
|
|
926
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Verify a macOS binary's code signature is structurally intact via
|
|
938
|
+
* `codesign --verify --strict`. Used in place of a fixed SHA256 pin for
|
|
939
|
+
* mac-arm64 (see the BUNDLED_UV_SHA256 comment) — ad-hoc-signed binaries
|
|
940
|
+
* (identity=-, which is every CI build today) pass this check, but a
|
|
941
|
+
* corrupted, tampered, or actually-unsigned binary fails it. This does NOT
|
|
942
|
+
* depend on the specific bytes any given codesign/Xcode toolchain produces,
|
|
943
|
+
* so it is stable across GitHub-hosted runner image rollovers where a fixed
|
|
944
|
+
* digest pin is not.
|
|
945
|
+
*
|
|
946
|
+
* @param {string} binPath
|
|
947
|
+
* @returns {{ ok: boolean, output: string }}
|
|
948
|
+
*/
|
|
949
|
+
function codesignVerify(binPath) {
|
|
950
|
+
const result = spawnSync("codesign", ["--verify", "--strict", binPath], {
|
|
951
|
+
encoding: "utf8",
|
|
952
|
+
});
|
|
953
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
954
|
+
return { ok: result.status === 0, output };
|
|
955
|
+
}
|
|
956
|
+
|
|
927
957
|
/**
|
|
928
958
|
* Resolve the bundled uv binary path inside the Electron resources dir.
|
|
929
959
|
* Returns null if this isn't an Electron-packaged runtime (no
|
|
@@ -945,15 +975,22 @@ function findBundledUvResource() {
|
|
|
945
975
|
}
|
|
946
976
|
|
|
947
977
|
/**
|
|
948
|
-
* Atomically install the bundled uv into ~/.gaia/bin/uv after verifying
|
|
949
|
-
*
|
|
978
|
+
* Atomically install the bundled uv into ~/.gaia/bin/uv after verifying it.
|
|
979
|
+
* Returns the installed path.
|
|
950
980
|
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
981
|
+
* Verification strategy differs by platform (see the BUNDLED_UV_SHA256
|
|
982
|
+
* comment): mac-arm64 uses `codesign --verify --strict` (darwin only —
|
|
983
|
+
* post-codesign SHA256 is not deterministic across CI runner images);
|
|
984
|
+
* linux-x64 / win-x64 use the SHA256 pin in BUNDLED_UV_SHA256, which IS
|
|
985
|
+
* deterministic for those platforms (no post-build re-signing).
|
|
986
|
+
*
|
|
987
|
+
* Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies, `chmod +x`,
|
|
988
|
+
* then `fs.rename()` (atomic on same filesystem).
|
|
953
989
|
*/
|
|
954
990
|
async function installBundledUv(sourcePath, platformKey) {
|
|
991
|
+
const isDarwin = platformKey === "mac-arm64";
|
|
955
992
|
const expected = BUNDLED_UV_SHA256[platformKey];
|
|
956
|
-
if (!expected || expected.startsWith("<")) {
|
|
993
|
+
if (!isDarwin && (!expected || expected.startsWith("<"))) {
|
|
957
994
|
// Enforce strict verification: builds MUST populate the expected SHA
|
|
958
995
|
// for packaged binaries. Failing fast prevents shipping an unverified
|
|
959
996
|
// uv binary which would be a supply-chain regression.
|
|
@@ -989,18 +1026,39 @@ async function installBundledUv(sourcePath, platformKey) {
|
|
|
989
1026
|
rs.pipe(ws);
|
|
990
1027
|
});
|
|
991
1028
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
`
|
|
999
|
-
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1029
|
+
if (isDarwin) {
|
|
1030
|
+
// chmod BEFORE codesign --verify: codesign needs the execute bit to
|
|
1031
|
+
// resolve the binary's designated requirement on some toolchains.
|
|
1032
|
+
try {
|
|
1033
|
+
fs.chmodSync(tmpPath, 0o700);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
log(`Warning: chmod on tmp uv failed: ${err.message}`);
|
|
1036
|
+
}
|
|
1037
|
+
const { ok, output } = codesignVerify(tmpPath);
|
|
1038
|
+
if (!ok) {
|
|
1039
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
1040
|
+
throw new InstallError(
|
|
1041
|
+
`Bundled uv failed code signature verification (codesign --verify --strict): ${output || "no output"}`,
|
|
1042
|
+
{
|
|
1043
|
+
stage: STAGES.ENSURE_UV,
|
|
1044
|
+
suggestion:
|
|
1045
|
+
"The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
|
|
1046
|
+
}
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
log("Bundled uv passed codesign --verify --strict");
|
|
1050
|
+
} else {
|
|
1051
|
+
let actual;
|
|
1052
|
+
try {
|
|
1053
|
+
actual = await sha256File(tmpPath);
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
1056
|
+
throw new InstallError(
|
|
1057
|
+
`Could not hash copied uv binary: ${err.message}`,
|
|
1058
|
+
{ stage: STAGES.ENSURE_UV }
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1002
1061
|
|
|
1003
|
-
if (expected) {
|
|
1004
1062
|
if (actual !== expected) {
|
|
1005
1063
|
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
1006
1064
|
throw new InstallError(
|
|
@@ -1012,14 +1070,12 @@ async function installBundledUv(sourcePath, platformKey) {
|
|
|
1012
1070
|
}
|
|
1013
1071
|
);
|
|
1014
1072
|
}
|
|
1015
|
-
} else {
|
|
1016
|
-
log("No expected SHA registered for bundled uv; installed binary will not be verified locally.");
|
|
1017
|
-
}
|
|
1018
1073
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1074
|
+
try {
|
|
1075
|
+
if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
log(`Warning: chmod on tmp uv failed: ${err.message}`);
|
|
1078
|
+
}
|
|
1023
1079
|
}
|
|
1024
1080
|
|
|
1025
1081
|
try {
|
|
@@ -1057,9 +1113,11 @@ function addManagedBinToPath() {
|
|
|
1057
1113
|
|
|
1058
1114
|
/**
|
|
1059
1115
|
* Ensure `uv` is available. Preference order (per issue #782 / T3):
|
|
1060
|
-
* 1. Managed copy at ~/.gaia/bin/uv
|
|
1116
|
+
* 1. Managed copy at ~/.gaia/bin/uv already verified (warm-install fast path):
|
|
1117
|
+
* SHA256 pin on linux-x64/win-x64, `codesign --verify --strict` on mac-arm64.
|
|
1061
1118
|
* 2. Bundled binary in process.resourcesPath/vendor/uv/<platform>/uv:
|
|
1062
|
-
* copy atomically to ~/.gaia/bin/uv with
|
|
1119
|
+
* copy atomically to ~/.gaia/bin/uv with the same platform-appropriate
|
|
1120
|
+
* verification.
|
|
1063
1121
|
* 3. DEV-ONLY fallback (app.isPackaged === false OR no resourcesPath):
|
|
1064
1122
|
* the original `curl | sh` from astral.sh. Not a shipped-user path.
|
|
1065
1123
|
* 4. System `uv` on PATH (last resort — unverified version).
|
|
@@ -1071,10 +1129,20 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
1071
1129
|
report(STAGES.ENSURE_UV, 0, "Checking uv (Python package manager)");
|
|
1072
1130
|
|
|
1073
1131
|
const platformKey = bundledUvPlatformKey();
|
|
1132
|
+
const isDarwin = platformKey === "mac-arm64";
|
|
1074
1133
|
const expectedSha = platformKey ? BUNDLED_UV_SHA256[platformKey] : null;
|
|
1075
1134
|
|
|
1076
|
-
// Fast path: warm install already on disk
|
|
1077
|
-
if (
|
|
1135
|
+
// Fast path: warm install already on disk and still passes verification.
|
|
1136
|
+
if (isDarwin && fs.existsSync(MANAGED_UV_BIN)) {
|
|
1137
|
+
const { ok, output } = codesignVerify(MANAGED_UV_BIN);
|
|
1138
|
+
if (ok) {
|
|
1139
|
+
log(`Managed uv at ${MANAGED_UV_BIN} passed codesign --verify --strict — reusing`);
|
|
1140
|
+
addManagedBinToPath();
|
|
1141
|
+
report(STAGES.ENSURE_UV, 100, "uv ready (cached)");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
log(`Managed uv failed codesign verification (${output || "no output"}) — replacing`);
|
|
1145
|
+
} else if (expectedSha && fs.existsSync(MANAGED_UV_BIN)) {
|
|
1078
1146
|
try {
|
|
1079
1147
|
const actual = await sha256File(MANAGED_UV_BIN);
|
|
1080
1148
|
if (actual === expectedSha) {
|
|
@@ -1097,22 +1165,36 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
1097
1165
|
report(STAGES.ENSURE_UV, 30, "Installing bundled uv");
|
|
1098
1166
|
log(`Using bundled uv from ${bundled}`);
|
|
1099
1167
|
|
|
1100
|
-
// Verify the source resource
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
// build-time error
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1168
|
+
// Verify the source resource before copying — catches installer
|
|
1169
|
+
// corruption before we touch the user's home. mac-arm64 uses
|
|
1170
|
+
// codesign --verify --strict (see BUNDLED_UV_SHA256 comment for why a
|
|
1171
|
+
// fixed digest isn't viable there); other platforms use the SHA256 pin,
|
|
1172
|
+
// which CI must have populated — a missing/placeholder value is a
|
|
1173
|
+
// build-time error, rejected at runtime here.
|
|
1174
|
+
if (isDarwin) {
|
|
1175
|
+
const { ok, output } = codesignVerify(bundled);
|
|
1176
|
+
if (!ok) {
|
|
1177
|
+
throw new InstallError(
|
|
1178
|
+
`Bundled uv resource failed code signature verification (codesign --verify --strict): ${output || "no output"}`,
|
|
1179
|
+
{
|
|
1180
|
+
stage: STAGES.ENSURE_UV,
|
|
1181
|
+
suggestion:
|
|
1182
|
+
"The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
|
|
1183
|
+
}
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
} else {
|
|
1187
|
+
const srcHash = await sha256File(bundled);
|
|
1188
|
+
if (srcHash !== expectedSha) {
|
|
1189
|
+
throw new InstallError(
|
|
1190
|
+
`Bundled uv resource SHA256 mismatch (expected ${expectedSha}, got ${srcHash}).`,
|
|
1191
|
+
{
|
|
1192
|
+
stage: STAGES.ENSURE_UV,
|
|
1193
|
+
suggestion:
|
|
1194
|
+
"The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1116
1198
|
}
|
|
1117
1199
|
await installBundledUv(bundled, platformKey);
|
|
1118
1200
|
addManagedBinToPath();
|
|
@@ -1452,11 +1534,11 @@ async function installBackend(opts = {}) {
|
|
|
1452
1534
|
? "GAIA retried with the OS certificate store and it still failed. "
|
|
1453
1535
|
: "") +
|
|
1454
1536
|
"Ask IT to install the proxy's root CA in your system's trusted certificate store, then relaunch GAIA. " +
|
|
1455
|
-
"See https://amd-gaia.ai/quickstart#cli-install";
|
|
1537
|
+
"See https://amd-gaia.ai/docs/quickstart#cli-install";
|
|
1456
1538
|
} else {
|
|
1457
1539
|
suggestion = `Try installing manually:\n uv pip install ${pipPackage} --python ${
|
|
1458
1540
|
IS_WINDOWS ? `${GAIA_VENV_DISPLAY}/Scripts/python.exe` : `${GAIA_VENV_DISPLAY}/bin/python`
|
|
1459
|
-
}\nThen restart GAIA. See https://amd-gaia.ai/quickstart#cli-install`;
|
|
1541
|
+
}\nThen restart GAIA. See https://amd-gaia.ai/docs/quickstart#cli-install`;
|
|
1460
1542
|
}
|
|
1461
1543
|
throw new InstallError(
|
|
1462
1544
|
`Failed to install ${pipPackage} (pip exit ${installResult.code}).`,
|
|
@@ -1473,7 +1555,7 @@ async function installBackend(opts = {}) {
|
|
|
1473
1555
|
`GAIA binary not found at ${GAIA_VENV_DISPLAY} after install.`,
|
|
1474
1556
|
{
|
|
1475
1557
|
stage: STAGES.INSTALL_PACKAGE,
|
|
1476
|
-
suggestion: "The package was installed but the gaia executable is missing. Try reinstalling from https://amd-gaia.ai/quickstart",
|
|
1558
|
+
suggestion: "The package was installed but the gaia executable is missing. Try reinstalling from https://amd-gaia.ai/docs/quickstart",
|
|
1477
1559
|
}
|
|
1478
1560
|
);
|
|
1479
1561
|
}
|
|
@@ -1710,7 +1792,7 @@ async function ensureBackend(opts = {}) {
|
|
|
1710
1792
|
"GAIA backend not found after installation.",
|
|
1711
1793
|
{
|
|
1712
1794
|
stage: STAGES.VERIFY,
|
|
1713
|
-
suggestion: "Check the log file and try reinstalling. See https://amd-gaia.ai/quickstart",
|
|
1795
|
+
suggestion: "Check the log file and try reinstalling. See https://amd-gaia.ai/docs/quickstart",
|
|
1714
1796
|
}
|
|
1715
1797
|
);
|
|
1716
1798
|
setState(STATES.FAILED, {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* deep-link.cjs — parse `gaia://` deep links (issue #1725).
|
|
6
|
+
*
|
|
7
|
+
* The website's "Open in GAIA" button hands the running app a
|
|
8
|
+
* `gaia://hub/install/<id>` URL. This module is the pure, Electron-free
|
|
9
|
+
* parser so it can be unit-tested without a running app; main.cjs owns the
|
|
10
|
+
* OS protocol registration and dispatch to the install runtime.
|
|
11
|
+
*
|
|
12
|
+
* Design: parse errors are THROWN with an actionable message (no silent
|
|
13
|
+
* fallback). main.cjs surfaces the message to the user instead of dropping a
|
|
14
|
+
* malformed link on the floor.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const SCHEME = "gaia:";
|
|
20
|
+
|
|
21
|
+
// Agent ids as published on the hub: start alphanumeric, then letters, digits,
|
|
22
|
+
// dot, dash, underscore. Bounded length keeps a hostile link from ballooning.
|
|
23
|
+
const AGENT_ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `gaia://` deep link into a structured command.
|
|
27
|
+
*
|
|
28
|
+
* Supported form:
|
|
29
|
+
* gaia://hub/install/<id> → { action: "install", agentId }
|
|
30
|
+
*
|
|
31
|
+
* @param {string} rawUrl
|
|
32
|
+
* @returns {{ action: "install", agentId: string }}
|
|
33
|
+
* @throws {Error} with an actionable message on anything malformed/unsupported
|
|
34
|
+
*/
|
|
35
|
+
function parseDeepLink(rawUrl) {
|
|
36
|
+
if (typeof rawUrl !== "string" || !rawUrl.trim()) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Not a GAIA deep link — expected a "gaia://…" URL but got ' +
|
|
39
|
+
(rawUrl === "" ? "an empty string" : JSON.stringify(rawUrl)) +
|
|
40
|
+
"."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const trimmed = rawUrl.trim();
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(trimmed);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`Malformed GAIA deep link "${trimmed}" — not a valid URL.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (parsed.protocol !== SCHEME) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Unsupported URL scheme "${parsed.protocol}" — GAIA deep links must start with "gaia://".`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const host = parsed.hostname;
|
|
59
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
60
|
+
|
|
61
|
+
if (host === "hub" && segments[0] === "install") {
|
|
62
|
+
const agentId = segments[1] ? decodeURIComponent(segments[1]) : "";
|
|
63
|
+
if (!agentId) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`GAIA install link "${trimmed}" is missing an agent id — expected gaia://hub/install/<id>.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!AGENT_ID_RE.test(agentId)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`GAIA install link has an invalid agent id "${agentId}" — ids may contain letters, digits, dot, dash and underscore.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (segments.length > 2) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`GAIA install link "${trimmed}" has unexpected extra path segments — expected gaia://hub/install/<id>.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return { action: "install", agentId };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Unrecognized GAIA deep link "${trimmed}" — only gaia://hub/install/<id> is supported.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find the first `gaia://` argument in an argv array (Windows/Linux hand the
|
|
88
|
+
* deep link to the app as a command-line argument). Returns null if absent.
|
|
89
|
+
*
|
|
90
|
+
* @param {string[]} argv
|
|
91
|
+
* @returns {string | null}
|
|
92
|
+
*/
|
|
93
|
+
function extractDeepLinkFromArgv(argv) {
|
|
94
|
+
if (!Array.isArray(argv)) return null;
|
|
95
|
+
for (const arg of argv) {
|
|
96
|
+
if (typeof arg === "string" && arg.startsWith("gaia://")) return arg;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Act on a parsed deep-link command, with every Electron/runtime effect
|
|
103
|
+
* injected so the security-critical control flow is unit-testable.
|
|
104
|
+
*
|
|
105
|
+
* SECURITY (issue #2196 review): a `gaia://hub/install/<id>` link comes from an
|
|
106
|
+
* untrusted web page, so the install MUST be gated behind an explicit per-agent
|
|
107
|
+
* confirmation — the same trust bar the in-app install enforces (#2201). A bare
|
|
108
|
+
* OS "Open GAIA?" prompt is NOT consent to download and run a specific agent.
|
|
109
|
+
* If `confirm` does not return true, no install is attempted. `confirm` is
|
|
110
|
+
* expected to have shown the agent's real catalog tier and permissions first
|
|
111
|
+
* (see `buildInstallPrompt`), not just its bare id, before resolving true.
|
|
112
|
+
*
|
|
113
|
+
* Once confirmed, the install is sent with the trust override set — the
|
|
114
|
+
* backend's non-verified gate now covers every non-verified agent, not just
|
|
115
|
+
* native ones, and this out-of-band flow already required its own explicit
|
|
116
|
+
* per-agent confirmation regardless of tier.
|
|
117
|
+
*
|
|
118
|
+
* @param {{ action: string, agentId: string }} command
|
|
119
|
+
* @param {{
|
|
120
|
+
* confirm: (command: {action: string, agentId: string}) => Promise<boolean>,
|
|
121
|
+
* installAgent: (agentId: string, opts?: { trustNative?: boolean }) => Promise<any>,
|
|
122
|
+
* focusWindow?: () => void,
|
|
123
|
+
* logger?: { log?: (...a: any[]) => void, error?: (...a: any[]) => void },
|
|
124
|
+
* }} deps
|
|
125
|
+
* @returns {Promise<{ installed: boolean, reason?: string }>}
|
|
126
|
+
*/
|
|
127
|
+
async function dispatchDeepLink(command, deps) {
|
|
128
|
+
const { confirm, installAgent, focusWindow, logger } = deps || {};
|
|
129
|
+
const log = (logger && logger.log) || (() => {});
|
|
130
|
+
|
|
131
|
+
if (!command || command.action !== "install") {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Unsupported deep-link action "${command && command.action}".`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (typeof confirm !== "function" || typeof installAgent !== "function") {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"dispatchDeepLink requires confirm() and installAgent() dependencies"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Surface the app first so the confirmation dialog is clearly GAIA's.
|
|
143
|
+
if (typeof focusWindow === "function") focusWindow();
|
|
144
|
+
|
|
145
|
+
const approved = await confirm(command);
|
|
146
|
+
if (approved !== true) {
|
|
147
|
+
log(`[deep-link] User declined install of "${command.agentId}"`);
|
|
148
|
+
return { installed: false, reason: "declined" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
log(`[deep-link] Confirmed — installing "${command.agentId}"`);
|
|
152
|
+
await installAgent(command.agentId, { trustNative: true });
|
|
153
|
+
return { installed: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Informed trust prompt (security fix: gate ALL non-verified agents) ─────
|
|
157
|
+
//
|
|
158
|
+
// The bare confirm dialog above only ever showed the raw agent id — no tier,
|
|
159
|
+
// no permissions, no AMD-verified indicator. That's per-agent confirmation,
|
|
160
|
+
// but it isn't INFORMED consent. buildInstallPrompt renders the same facts
|
|
161
|
+
// the in-app trust gate (hubLanes.trustGateFor) shows, from a catalog entry,
|
|
162
|
+
// so the caller (main.cjs) can put them in front of the user before they
|
|
163
|
+
// confirm.
|
|
164
|
+
|
|
165
|
+
const TIER_LABEL = {
|
|
166
|
+
verified: "AMD Verified",
|
|
167
|
+
community: "Community",
|
|
168
|
+
experimental: "Experimental",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** Normalize a possibly-missing/unknown tier the same way the in-app gate does. */
|
|
172
|
+
function normalizeTier(entry) {
|
|
173
|
+
const t = entry && entry.security_tier;
|
|
174
|
+
return t === "verified" || t === "community" ? t : "experimental";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the confirmation prompt for a deep-link install from its catalog
|
|
179
|
+
* entry. Pure — no Electron dependency — so the trust posture is
|
|
180
|
+
* unit-testable without a running dialog.
|
|
181
|
+
*
|
|
182
|
+
* @param {{
|
|
183
|
+
* id: string,
|
|
184
|
+
* name?: string,
|
|
185
|
+
* security_tier?: "verified" | "community" | "experimental",
|
|
186
|
+
* permissions?: string[],
|
|
187
|
+
* } | null | undefined} entry — the agent's catalog entry.
|
|
188
|
+
* @returns {{ title: string, message: string, detail: string, requiresTrust: boolean }}
|
|
189
|
+
* @throws {Error} if no entry is given — never build a prompt for an agent
|
|
190
|
+
* that couldn't be looked up; the caller must fail closed instead.
|
|
191
|
+
*/
|
|
192
|
+
function buildInstallPrompt(entry) {
|
|
193
|
+
if (!entry || typeof entry !== "object") {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"buildInstallPrompt requires a catalog entry — refuse rather than prompting blind."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tier = normalizeTier(entry);
|
|
200
|
+
const requiresTrust = tier !== "verified";
|
|
201
|
+
const name = entry.name || entry.id;
|
|
202
|
+
const permissions = Array.isArray(entry.permissions) ? entry.permissions : [];
|
|
203
|
+
|
|
204
|
+
const detail = [
|
|
205
|
+
`Tier: ${TIER_LABEL[tier]}${requiresTrust ? " — NOT AMD-verified" : ""}`,
|
|
206
|
+
permissions.length
|
|
207
|
+
? `Permissions: ${permissions.join(", ")}`
|
|
208
|
+
: "Permissions: none declared",
|
|
209
|
+
requiresTrust
|
|
210
|
+
? "This agent has not been audited by AMD. Only continue if you trust the " +
|
|
211
|
+
"publisher — installing downloads and runs third-party code on your machine."
|
|
212
|
+
: "This agent has been reviewed and verified by AMD.",
|
|
213
|
+
].join("\n\n");
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
title: requiresTrust
|
|
217
|
+
? "Install untrusted agent from the web?"
|
|
218
|
+
: "Install agent from the web?",
|
|
219
|
+
message: `A website asked GAIA to install "${name}" (${entry.id}).`,
|
|
220
|
+
detail,
|
|
221
|
+
requiresTrust,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
parseDeepLink,
|
|
227
|
+
extractDeepLinkFromArgv,
|
|
228
|
+
dispatchDeepLink,
|
|
229
|
+
buildInstallPrompt,
|
|
230
|
+
};
|