OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android commits / 2be4992

0.0.9: give the APK a tree, so something can verify it
pgh-runtime does not fetch a file, it installs a TREE: it hashes the entries it is handed and refuses them unless they reproduce the root hash. The worklet bundle has had that since gen-tree-manifest.mjs was written. The 154 MB APK never has — it ships as a sibling artifact with nothing checking it, whoever downloads it and however they got it. This is also the precondition for fetching it over the swarm rather than as one unresumable HTTPS GET: a content-addressed fetcher can only ask for a hash it can verify. The release route ignores Range (measured 2026-08-24, a 0–1000 request returned all 37.8 MB), which the 37.8 MB bundle survives and a 154 MB APK on a handset does not. ⛔ A SEPARATE MANIFEST, NOT AN EXTRA ENTRY IN tree.json. A pgh tree holds many entries so it would encode fine — and it would brick every phone already in the field. The DEPLOYED bundleStore.materialize loops the entries and keeps the LAST file it writes as the bundle to boot, so a two-entry tree stages a 154 MB APK as the worklet bundle. The OTA client on a handset is precisely the thing that cannot be fixed remotely, so its manifest keeps exactly the shape it has always had and the APK is described in a file only new code looks for. gen-tree-manifest.mjs takes --in/--out/--name, defaulting to the bundle so the existing `gen:tree` invocation is unchanged. Emitted in the assemble step, since the APK does not exist until gradle has run — `gen:tree` is in prebuild, three steps earlier. Verified: the generator produces a one-entry manifest whose entries reproduce the root hash, which is the property installTree enforces. Suite green. Nothing consumes apk.tree.json yet. Publishing it first is what lets the fetcher be written without another build round-trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2be499264fc8d6ef3737ef76db408007a66c993f
by plankatron <fleeky@gmail.com> · 2026-08-25T16:26:20.000Z

Changed files (3) +42 -10

.pear-git/ci.yaml +15 -1
……@@ -107,7 +107,17 @@ steps:
107107 # without a 153 MB sideload — pgh-runtime fetches this file and verifies it against tree.json
108108 # (written by `npm run gen:tree` in the prebuild step). Native code still needs a new APK.
109109 cp android/app/src/main/assets/worklet.bundle worklet.bundle
110 ls -l favela-android.apk worklet.bundle tree.json
110 # The APK gets a tree too, and a SEPARATE one. pgh-runtime installs a TREE — it hashes what it is
111 # handed and refuses bytes that do not reproduce the root hash — so until now the 154 MB APK was
112 # the one artifact nothing verified, whoever fetched it. This is the precondition for ever fetching
113 # it over the swarm instead of as a single unresumable HTTPS GET.
114 #
115 # ⛔ NOT merged into tree.json. A pgh tree holds many entries, so it would encode fine and would
116 # brick every phone already in the field: the deployed bundleStore.materialize keeps the LAST file
117 # entry it writes as the bundle to boot, so a two-entry tree stages a 154 MB APK as the worklet
118 # bundle. The OTA client on a handset is exactly what cannot be fixed remotely.
119 npm run gen:apk-tree
120 ls -l favela-android.apk worklet.bundle tree.json apk.tree.json
111121 timeout: 2700
112122
113123artifacts:
……@@ -121,3 +131,7 @@ artifacts:
121131 # existing and nothing scheduling it (hyper-social #47 §2); this makes that trajectory steeper.
122132 - worklet.bundle
123133 - tree.json
134 # The APK's manifest — 289 bytes, and the only thing that makes that 154 MB fetchable-and-verifiable
135 # by anything other than a browser. Nothing consumes it yet; publishing it first is what lets a
136 # swarm-side fetcher be written without another build round-trip.
137 - apk.tree.json
package.json +3 -2
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-android",
3 "version": "0.0.8",
3 "version": "0.0.9",
44 "description": "favela-android — a React Native client over a bare-kit worklet running a FULL favela node (hyper-proto-peer + appview + hyperswarm) on the phone. favela-web is reference for the logic, NOT the rendering: this is a different rendering surface. See spec/architecture.md.",
55 "private": true,
66 "type": "module",
……@@ -11,7 +11,8 @@
1111 "prebuild:android": "npm run pack:worklet && npm run stamp:worklet && npm run gen:tree && npm run stage:addons",
1212 "test": "bare test/adopt.test.mjs && bare test/ipc.test.mjs && bare test/session.test.mjs && bare test/favela-api.test.mjs && bare test/lifecycle.test.mjs && bare test/roster.test.mjs && bare test/describe.test.mjs && bare test/shell.test.mjs && bare test/ota.test.mjs && bare test/ota-e2e.test.mjs",
1313 "stamp:worklet": "node -e \"const fs=require('fs');const p='android/app/src/main/assets/worklet.bundle';fs.writeFileSync(p+'.size',String(fs.statSync(p).size))\"",
14 "gen:tree": "bare scripts/gen-tree-manifest.mjs"
14 "gen:tree": "bare scripts/gen-tree-manifest.mjs",
15 "gen:apk-tree": "bare scripts/gen-tree-manifest.mjs --in favela-android.apk --out apk.tree.json --name favela-android.apk"
1516 },
1617 "dependencies": {
1718 "atproto-identity": "file:../atproto-identity",
scripts/gen-tree-manifest.mjs +24 -7
……@@ -16,21 +16,38 @@
1616//
1717// Run under BARE, not node: hyper-overlay/tree resolves #fs through bare's import map.
1818// bare scripts/gen-tree-manifest.mjs
19// ⛔ ONE FILE PER MANIFEST, AND THE APK GETS ITS OWN. It is tempting to put both artifacts in tree.json
20// — a pgh tree is a set of entries, so it would encode fine. It would also brick every phone already in
21// the field: the deployed bundleStore.materialize loops the entries and keeps the LAST file it writes as
22// the bundle to boot, so a two-entry tree would stage a 154 MB APK as the worklet bundle. The OTA client
23// on a handset is the thing we cannot fix remotely, so its manifest keeps exactly the shape it has
24// always had, and the APK is described in a SEPARATE file that only new code looks for.
25//
26// --in <file> what to hash (default: the worklet bundle)
27// --out <file> where to write (default: tree.json)
28// --name <name> the entry's name (default: treeFor's own, worklet.bundle)
1929import fs from 'bare-fs'
2030import path from 'bare-path'
2131import { treeFor } from '../lib/ota.js'
2232
2333const ROOT = path.join(path.dirname(import.meta.url.replace('file://', '')), '..')
24const BUNDLE = path.join(ROOT, 'android', 'app', 'src', 'main', 'assets', 'worklet.bundle')
25const OUT = path.join(ROOT, 'tree.json')
2634
27if (!fs.existsSync(BUNDLE)) {
28 console.error('no worklet.bundle at ' + BUNDLE + ' — run `npm run pack:worklet` first')
35function arg (flag, fallback) {
36 const i = Bare.argv.indexOf(flag)
37 return i === -1 ? fallback : Bare.argv[i + 1]
38}
39
40const IN = path.resolve(ROOT, arg('--in', path.join(ROOT, 'android', 'app', 'src', 'main', 'assets', 'worklet.bundle')))
41const OUT = path.resolve(ROOT, arg('--out', path.join(ROOT, 'tree.json')))
42const NAME = arg('--name', null)
43
44if (!fs.existsSync(IN)) {
45 console.error('nothing to hash at ' + IN + ' — run `npm run pack:worklet` first (or point --in at a built artifact)')
2946 Bare.exit(1)
3047}
3148
32const bytes = fs.readFileSync(BUNDLE)
33const { entries, treeHash, contentHash, size } = treeFor(bytes)
49const bytes = fs.readFileSync(IN)
50const { entries, treeHash, contentHash, size } = NAME ? treeFor(bytes, NAME) : treeFor(bytes)
3451
3552fs.writeFileSync(OUT, JSON.stringify({ treeHash, size, entries }, null, 2) + '\n')
36console.log(`tree.json — treeHash ${treeHash.slice(0, 16)}… content ${contentHash.slice(0, 16)}… ${size} bytes`)
53console.log(`${path.basename(OUT)} — treeHash ${treeHash.slice(0, 16)}… content ${contentHash.slice(0, 16)}… ${size} bytes`)