@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.
@@ -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 BOTH:
80
+ // When bumping uv, update:
81
81
  // - .github/workflows/build-installers.yml (tarball .tar.gz SHA256 — archive)
82
- // - BUNDLED_UV_SHA256 below (extracted ELF binary SHA256)
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
- // Currently pinned: uv v0.5.14 linux-x64, mac-arm64.
88
- // (win-x64 deferred to a follow-up issue — its SHA must ship together with an
89
- // NSIS structural-smoke verifier, not on its own; see the #849 lesson.)
90
- //
91
- // IMPORTANT: per-platform SHA origin differs:
92
- // - linux-x64: raw extracted-from-tarball digest (no post-build modification).
93
- // - mac-arm64: POST-CODESIGN digest. electron-builder code-signs the bundled
94
- // uv during packaging, so this hash matches what ensureUv() sees
95
- // at runtime, NOT the upstream tarball. Bumping this pin means
96
- // running the CI build, then copying the SHA from the
97
- // dmg-structural-smoke failure message — never from `shasum`
98
- // against the freshly downloaded tarball.
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: POST-codesign digest. CI should populate this value when
107
- // packaging the macOS DMG and running the dmg-structural-smoke job.
108
- "mac-arm64": "6099aa8cd701f0c81227ee30c304777ce151e4d47c53a75ce53cd2243448d8c8",
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
- log(`Could not read NODE_EXTRA_CA_CERTS (${file}): ${err.message}`);
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
- finish({ ok: false, kind: "connectivity", message: `${target.href}: invalid proxy URL "${proxy}"` });
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
- * its SHA256 against BUNDLED_UV_SHA256. Returns the installed path.
978
+ * Atomically install the bundled uv into ~/.gaia/bin/uv after verifying it.
979
+ * Returns the installed path.
950
980
  *
951
- * Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies hash,
952
- * `chmod +x`, then `fs.rename()` (atomic on same filesystem).
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
- let actual;
993
- try {
994
- actual = await sha256File(tmpPath);
995
- } catch (err) {
996
- try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
997
- throw new InstallError(
998
- `Could not hash copied uv binary: ${err.message}`,
999
- { stage: STAGES.ENSURE_UV }
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
- try {
1020
- if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
1021
- } catch (err) {
1022
- log(`Warning: chmod on tmp uv failed: ${err.message}`);
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 with matching SHA256 (warm-install fast path).
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 SHA256 verification.
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 with correct hash.
1077
- if (expectedSha && fs.existsSync(MANAGED_UV_BIN)) {
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 matches the manifest before copying —
1101
- // catches AppImage corruption before we touch the user's home.
1102
- // Enforce that the packaged build provides an expected SHA for the
1103
- // bundled resource. CI replaces the placeholder with the extracted
1104
- // binary's SHA during the build; missing/placeholder values are a
1105
- // build-time error and are rejected at runtime here.
1106
- const srcHash = await sha256File(bundled);
1107
- if (srcHash !== expectedSha) {
1108
- throw new InstallError(
1109
- `Bundled uv resource SHA256 mismatch (expected ${expectedSha}, got ${srcHash}).`,
1110
- {
1111
- stage: STAGES.ENSURE_UV,
1112
- suggestion:
1113
- "The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
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
+ };