Skip to content

Commit 521ecbe

Browse files
author
Shaw
committed
Merge PR #7790: fix(os): harden elizaOS smoke checks and USB installer boundary
2 parents e7816a4 + d6b3b96 commit 521ecbe

10 files changed

Lines changed: 72 additions & 49 deletions

File tree

.github/workflows/elizaos-os-release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ jobs:
4949
node packages/os/android/installer/scripts/validate-release-manifest.mjs \
5050
packages/os/android/installer/manifests/android-release-manifest.example.json
5151
node packages/os/android/installer/scripts/validate-release-manifest.mjs \
52-
packages/os/release/beta-2026-05-16/android-release-manifest.json
52+
packages/os/release/beta-2026-05-16/android-release-manifest.json \
53+
--allow-placeholders
5354
5455
- name: Validate elizaos-setup (formerly aosp-flasher)
5556
run: |

packages/os-usb-installer/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ Windows:
7777

7878
## Electrobun integration notes
7979

80-
This package is structured so Electrobun can host the Vite renderer and bind an
81-
implementation of `UsbInstallerBackend` from the main process. The first real
82-
backend should keep the same interface and replace only the dry-run backend,
83-
with a narrow privileged helper responsible for raw device writes and verify
84-
reads.
80+
This package is structured so Electrobun can host the Vite renderer while the
81+
renderer only talks to a local `UsbInstallerBackend` HTTP/IPC contract. Platform
82+
enumeration and any future raw writes stay in `server.ts` or a signed privileged
83+
helper, never in browser-rendered code. The first real write helper should keep
84+
the same interface and replace only the dry-run execution path, with a narrow
85+
audited helper responsible for raw device writes and verify reads.
Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33
import { HttpUsbInstallerBackend } from "./backend/http-backend";
4-
import type { UsbInstallerBackend } from "./backend/types";
54
import { InstallerApp } from "./components/InstallerApp";
65
import "./styles.css";
76

@@ -13,26 +12,13 @@ if (!root) {
1312

1413
const rootElement: HTMLElement = root;
1514

16-
// In Electrobun (desktop app) the native IPC backend is available.
17-
// In all browser contexts (Vite dev server or static deployment) use the
18-
// HTTP backend which talks to the local Bun server via the /api Vite proxy.
19-
const isElectrobun =
20-
typeof (globalThis as Record<string, unknown>).electrobun !== "undefined";
15+
// Keep raw disk enumeration and write execution out of the renderer bundle.
16+
// The browser/Electrobun view talks to the local backend contract; platform
17+
// helpers stay in server.ts or a future signed privileged helper.
18+
const backend = new HttpUsbInstallerBackend();
2119

22-
async function main() {
23-
let backend: UsbInstallerBackend;
24-
if (isElectrobun) {
25-
const { createPlatformBackend } = await import("./backend/index");
26-
backend = createPlatformBackend();
27-
} else {
28-
backend = new HttpUsbInstallerBackend();
29-
}
30-
31-
createRoot(rootElement).render(
32-
<StrictMode>
33-
<InstallerApp backend={backend} />
34-
</StrictMode>,
35-
);
36-
}
37-
38-
void main();
20+
createRoot(rootElement).render(
21+
<StrictMode>
22+
<InstallerApp backend={backend} />
23+
</StrictMode>,
24+
);

packages/os/RELEASE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Short, mechanical steps for cutting a new OS release. Anything not on this list
99
- `scripts/update-release-manifest.mjs` — sets `sha256`, `sizeBytes`, `downloadUrl`, status on one artifact entry.
1010
- `scripts/validate-release-manifest.mjs` — schema check; pass `--require-publishable-checksums` for strict mode.
1111
- `scripts/generate-release-checksums.mjs` — fills the manifest by hashing local artifact files.
12-
- `android/installer/scripts/validate-release-manifest.mjs` — separate validator for the Android per-partition manifest. Rejects all-zero placeholder hashes.
12+
- `android/installer/scripts/validate-release-manifest.mjs` — separate validator for the Android per-partition manifest. Rejects all-zero placeholder hashes by default; use `--allow-placeholders` only for checked-in pre-release draft manifests.
1313

1414
## Steps
1515

packages/os/android/installer/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ node packages/os/android/installer/scripts/validate-release-manifest.mjs \
156156
packages/os/android/installer/manifests/android-release-manifest.example.json
157157
```
158158

159+
Checked-in pre-release draft manifests may still carry placeholder checksums or
160+
sentinel sizes while artifacts are being produced. That review-only state must
161+
be explicit:
162+
163+
```bash
164+
node packages/os/android/installer/scripts/validate-release-manifest.mjs \
165+
packages/os/release/beta-2026-05-16/android-release-manifest.json \
166+
--allow-placeholders
167+
```
168+
159169
If artifacts are available locally, pass `--artifact-dir` to verify declared
160170
file sizes and SHA-256 values:
161171

packages/os/android/installer/manifests/android-release-manifest.example.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,24 @@
1919
{
2020
"partition": "boot",
2121
"filename": "boot.img",
22-
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
23-
"sizeBytes": 1,
22+
"sha256": "adc7cf0cbb58ab51520a1fa874a80fb0735a4464fd9b1988096761b4c11d8677",
23+
"sizeBytes": 19,
2424
"required": true,
2525
"fastbootMode": "bootloader"
2626
},
2727
{
2828
"partition": "vendor_boot",
2929
"filename": "vendor_boot.img",
30-
"sha256": "1111111111111111111111111111111111111111111111111111111111111111",
31-
"sizeBytes": 1,
30+
"sha256": "dbbb45447bdde07766a48c299ade9573b28bb45c2a81db370ddb3516e95e03ab",
31+
"sizeBytes": 26,
3232
"required": true,
3333
"fastbootMode": "bootloader"
3434
},
3535
{
3636
"partition": "super",
3737
"filename": "super.img",
38-
"sha256": "2222222222222222222222222222222222222222222222222222222222222222",
39-
"sizeBytes": 1,
38+
"sha256": "fb5500dfc6572b380682d824fb84662cfd39c8bb834e5604f3d4772462987628",
39+
"sizeBytes": 20,
4040
"required": true,
4141
"fastbootMode": "fastbootd"
4242
}

packages/os/android/installer/scripts/validate-release-manifest.mjs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const args = process.argv.slice(2);
77

88
function usage() {
99
console.log(`Usage:
10-
validate-release-manifest.mjs MANIFEST.json [--artifact-dir DIR]
10+
validate-release-manifest.mjs MANIFEST.json [--artifact-dir DIR] [--allow-placeholders]
1111
1212
Validates the Android release manifest shape without requiring devices.
13-
When --artifact-dir is provided, artifact sizes and SHA-256 hashes are checked.`);
13+
When --artifact-dir is provided, artifact sizes and SHA-256 hashes are checked.
14+
Use --allow-placeholders only for checked-in pre-release draft manifests.`);
1415
}
1516

1617
function die(message) {
@@ -21,6 +22,7 @@ function die(message) {
2122
function parseArgs(argv) {
2223
let manifestPath = '';
2324
let artifactDir = '';
25+
let allowPlaceholders = false;
2426
for (let index = 0; index < argv.length; index += 1) {
2527
const arg = argv[index];
2628
if (arg === '-h' || arg === '--help') {
@@ -33,12 +35,16 @@ function parseArgs(argv) {
3335
index += 1;
3436
continue;
3537
}
38+
if (arg === '--allow-placeholders') {
39+
allowPlaceholders = true;
40+
continue;
41+
}
3642
if (arg.startsWith('--')) die(`unknown argument: ${arg}`);
3743
if (manifestPath) die(`unexpected extra argument: ${arg}`);
3844
manifestPath = arg;
3945
}
4046
if (!manifestPath) die('provide a manifest path');
41-
return { manifestPath, artifactDir };
47+
return { manifestPath, artifactDir, allowPlaceholders };
4248
}
4349

4450
function readJson(path) {
@@ -57,7 +63,7 @@ function isObject(value) {
5763
return value !== null && typeof value === 'object' && !Array.isArray(value);
5864
}
5965

60-
function validateManifest(manifest) {
66+
function validateManifest(manifest, { allowPlaceholders = false } = {}) {
6167
const errors = [];
6268
expect(isObject(manifest), errors, '$', 'manifest must be an object');
6369
if (!isObject(manifest)) return errors;
@@ -107,9 +113,13 @@ function validateManifest(manifest) {
107113
partitions.add(artifact.partition);
108114
expect(typeof artifact.filename === 'string' && /^[^/\\]+\.img$/.test(artifact.filename), errors, `${path}.filename`, 'must be a local .img filename');
109115
expect(typeof artifact.sha256 === 'string' && /^[a-fA-F0-9]{64}$/.test(artifact.sha256), errors, `${path}.sha256`, 'must be 64 hex characters');
110-
expect(typeof artifact.sha256 !== 'string' || artifact.sha256.toLowerCase() !== '0'.repeat(64), errors, `${path}.sha256`, 'must not be the all-zero placeholder; populate with a real checksum before validating');
116+
if (!allowPlaceholders) {
117+
expect(typeof artifact.sha256 !== 'string' || artifact.sha256.toLowerCase() !== '0'.repeat(64), errors, `${path}.sha256`, 'must not be the all-zero placeholder; populate with a real checksum before validating');
118+
}
111119
expect(Number.isInteger(artifact.sizeBytes) && artifact.sizeBytes > 0, errors, `${path}.sizeBytes`, 'must be a positive integer');
112-
expect(!Number.isInteger(artifact.sizeBytes) || artifact.sizeBytes > 1, errors, `${path}.sizeBytes`, 'must not be the sentinel value 1; populate with the real artifact size');
120+
if (!allowPlaceholders) {
121+
expect(!Number.isInteger(artifact.sizeBytes) || artifact.sizeBytes > 1, errors, `${path}.sizeBytes`, 'must not be the sentinel value 1; populate with the real artifact size');
122+
}
113123
expect(typeof artifact.required === 'boolean', errors, `${path}.required`, 'must be boolean');
114124
expect(fastbootModes.has(artifact.fastbootMode), errors, `${path}.fastbootMode`, 'must be bootloader or fastbootd');
115125
});
@@ -156,9 +166,9 @@ function validateArtifacts(manifest, artifactDir) {
156166
return errors;
157167
}
158168

159-
const { manifestPath, artifactDir } = parseArgs(args);
169+
const { manifestPath, artifactDir, allowPlaceholders } = parseArgs(args);
160170
const manifest = readJson(manifestPath);
161-
const errors = [...validateManifest(manifest), ...validateArtifacts(manifest, artifactDir)];
171+
const errors = [...validateManifest(manifest, { allowPlaceholders }), ...validateArtifacts(manifest, artifactDir)];
162172
if (errors.length > 0) {
163173
errors.forEach((error) => console.error(`error: ${error}`));
164174
process.exit(1);

packages/os/android/installer/tests/run-tests.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ assert_contains() {
3131
BIN_DIR="$TMP_DIR/bin"
3232
ARTIFACT_DIR="$TMP_DIR/artifacts"
3333
mkdir -p "$BIN_DIR" "$ARTIFACT_DIR"
34-
printf '\0' >"$ARTIFACT_DIR/boot.img"
35-
printf '\1' >"$ARTIFACT_DIR/vendor_boot.img"
36-
printf '\2' >"$ARTIFACT_DIR/super.img"
34+
printf 'boot-image-fixture\n' >"$ARTIFACT_DIR/boot.img"
35+
printf 'vendor-boot-image-fixture\n' >"$ARTIFACT_DIR/vendor_boot.img"
36+
printf 'super-image-fixture\n' >"$ARTIFACT_DIR/super.img"
3737

3838
cat >"$BIN_DIR/adb" <<'EOF'
3939
#!/usr/bin/env bash

packages/os/linux/variants/milady-tails/scripts/static-smoke.sh

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,22 @@ SOURCE_ONLY="${ELIZAOS_STATIC_SOURCE_ONLY:-0}"
99
cd "${ROOT}"
1010

1111
stat_mode() {
12-
stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1"
12+
local path="$1"
13+
local index_mode
14+
index_mode="$(
15+
git -C "${ROOT}" ls-files -s -- "${path}" 2>/dev/null | awk 'NR == 1 { print $1 }'
16+
)"
17+
case "${index_mode}" in
18+
100755)
19+
printf '755\n'
20+
return 0
21+
;;
22+
100644)
23+
printf '644\n'
24+
return 0
25+
;;
26+
esac
27+
stat -c %a "${path}" 2>/dev/null || stat -f %Lp "${path}"
1328
}
1429

1530
echo "==> shell syntax"

packages/os/release/beta-2026-05-16/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ All five artifacts in `manifest.json` are in `status: candidate` with `sha256: n
99
This is **not** a broken state. The pipeline gates publication on real values:
1010

1111
- `scripts/validate-release-manifest.mjs --require-publishable-checksums` fails on `null` AND on the all-zero placeholder. It is run by the `populate-and-validate-manifest` job in `.github/workflows/elizaos-os-full-release.yml` after artifacts are downloaded.
12-
- `android/installer/scripts/validate-release-manifest.mjs` rejects the all-zero hash and the `sizeBytes: 1` sentinel — it cannot be tricked into passing the placeholder values that ship in this file.
12+
- `android/installer/scripts/validate-release-manifest.mjs` rejects the all-zero hash and the `sizeBytes: 1` sentinel by default. The pull-request validation workflow uses `--allow-placeholders` for this checked-in draft manifest only, while the publish path must validate real Android artifacts without that flag.
1313
- `release.status` is only promoted to `available` after the strict gate passes.
1414

1515
See `packages/os/RELEASE.md` for the full runbook.

0 commit comments

Comments
 (0)