@amd-gaia/agent-ui 0.21.2 → 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.
@@ -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");
@@ -924,6 +929,27 @@ function sha256File(filePath) {
924
929
  });
925
930
  }
926
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
+
927
953
  /**
928
954
  * Resolve the bundled uv binary path inside the Electron resources dir.
929
955
  * Returns null if this isn't an Electron-packaged runtime (no
@@ -945,15 +971,22 @@ function findBundledUvResource() {
945
971
  }
946
972
 
947
973
  /**
948
- * Atomically install the bundled uv into ~/.gaia/bin/uv after verifying
949
- * its SHA256 against BUNDLED_UV_SHA256. Returns the installed path.
974
+ * Atomically install the bundled uv into ~/.gaia/bin/uv after verifying it.
975
+ * Returns the installed path.
950
976
  *
951
- * Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies hash,
952
- * `chmod +x`, then `fs.rename()` (atomic on same filesystem).
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).
982
+ *
983
+ * Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies, `chmod +x`,
984
+ * then `fs.rename()` (atomic on same filesystem).
953
985
  */
954
986
  async function installBundledUv(sourcePath, platformKey) {
987
+ const isDarwin = platformKey === "mac-arm64";
955
988
  const expected = BUNDLED_UV_SHA256[platformKey];
956
- if (!expected || expected.startsWith("<")) {
989
+ if (!isDarwin && (!expected || expected.startsWith("<"))) {
957
990
  // Enforce strict verification: builds MUST populate the expected SHA
958
991
  // for packaged binaries. Failing fast prevents shipping an unverified
959
992
  // uv binary which would be a supply-chain regression.
@@ -989,18 +1022,39 @@ async function installBundledUv(sourcePath, platformKey) {
989
1022
  rs.pipe(ws);
990
1023
  });
991
1024
 
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
- }
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
+ }
1002
1057
 
1003
- if (expected) {
1004
1058
  if (actual !== expected) {
1005
1059
  try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
1006
1060
  throw new InstallError(
@@ -1012,14 +1066,12 @@ async function installBundledUv(sourcePath, platformKey) {
1012
1066
  }
1013
1067
  );
1014
1068
  }
1015
- } else {
1016
- log("No expected SHA registered for bundled uv; installed binary will not be verified locally.");
1017
- }
1018
1069
 
1019
- try {
1020
- if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
1021
- } catch (err) {
1022
- log(`Warning: chmod on tmp uv failed: ${err.message}`);
1070
+ try {
1071
+ if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
1072
+ } catch (err) {
1073
+ log(`Warning: chmod on tmp uv failed: ${err.message}`);
1074
+ }
1023
1075
  }
1024
1076
 
1025
1077
  try {
@@ -1057,9 +1109,11 @@ function addManagedBinToPath() {
1057
1109
 
1058
1110
  /**
1059
1111
  * 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).
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.
1061
1114
  * 2. Bundled binary in process.resourcesPath/vendor/uv/<platform>/uv:
1062
- * copy atomically to ~/.gaia/bin/uv with SHA256 verification.
1115
+ * copy atomically to ~/.gaia/bin/uv with the same platform-appropriate
1116
+ * verification.
1063
1117
  * 3. DEV-ONLY fallback (app.isPackaged === false OR no resourcesPath):
1064
1118
  * the original `curl | sh` from astral.sh. Not a shipped-user path.
1065
1119
  * 4. System `uv` on PATH (last resort — unverified version).
@@ -1071,10 +1125,20 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
1071
1125
  report(STAGES.ENSURE_UV, 0, "Checking uv (Python package manager)");
1072
1126
 
1073
1127
  const platformKey = bundledUvPlatformKey();
1128
+ const isDarwin = platformKey === "mac-arm64";
1074
1129
  const expectedSha = platformKey ? BUNDLED_UV_SHA256[platformKey] : null;
1075
1130
 
1076
- // Fast path: warm install already on disk with correct hash.
1077
- if (expectedSha && fs.existsSync(MANAGED_UV_BIN)) {
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)) {
1078
1142
  try {
1079
1143
  const actual = await sha256File(MANAGED_UV_BIN);
1080
1144
  if (actual === expectedSha) {
@@ -1097,22 +1161,36 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
1097
1161
  report(STAGES.ENSURE_UV, 30, "Installing bundled uv");
1098
1162
  log(`Using bundled uv from ${bundled}`);
1099
1163
 
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
- );
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
+ }
1116
1194
  }
1117
1195
  await installBundledUv(bundled, platformKey);
1118
1196
  addManagedBinToPath();