Skip to content

Refactor Immortal provisioning and unify program model - #6

Merged
andronedev merged 21 commits into
mainfrom
develop
Jul 2, 2026
Merged

Refactor Immortal provisioning and unify program model#6
andronedev merged 21 commits into
mainfrom
develop

Conversation

@andronedev

Copy link
Copy Markdown
Owner

No description provided.

andronedev and others added 21 commits June 20, 2026 22:35
Add `setLauncher` toggle to let users keep Meta's launcher after
provisioning. Add `resetLauncher()` to revert to stock launcher
post-install. Update provisioning docs and i18n (en/fr).
Upstream provision.sh moved on main (db693f2 -> b27cb0e, a
copyright-header change only, no procedural change). Refresh the
vendored snapshot baseline so the scheduled drift check passes again,
and pin the offline-fallback latestReleaseTag to the current release
v1.52.
Replace the hand-ported provision.ts with a versioned host/program
contract: the launcher author's provisioning procedure runs in a
sandboxed Web Worker (no DOM, no credential store, no live Adb) and
drives the device only through a constrained `portal` capability API
that RPCs to a main-thread broker. The broker holds the Adb handle,
validates every call (https-only, path/flag/package allowlists), and
exposes an audit log plus kill switch. The panel form is driven by a
manifest the program declares, and the built-in default.program.js is
a 1:1 offline fallback of upstream provision.sh. SDK for program
authors lives in sdk/.

Re-vendor the upstream snapshot against starbrightlab/immortal@main to
clear drift (provision.sh copyright-header change only, no procedural
change); pin latestReleaseTag offline fallback to v1.52.
Local drag-&-drop, catalog download, and the provisioning worker each
had their own install code (sync-push vs installFromUrl vs -r -d, three
progress idioms, two Success/Failure checks). Collapse them into a single
installApp(adb, source, opts) in src/lib/adb/install.ts where source is a
local File or a URL list (+ optional sha256), sharing one pm install and
temp-file cleanup. online-install.ts keeps only the device-fetch
primitives (deviceFetchText/detectDownloader); the provisioning broker
routes its installFromUrl RPC through the unified installer. InstallStage
now has one definition, imported by the runtime types.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
The catalog had three unrelated ways to describe post-install work
(setup.kind:"commands", custom React panels, and the Immortal
provisioning runtime hidden behind a hardcoded panel id), which read as
three separate concepts. Collapse them into one `program` field with
three kinds that are all understood as the same "configure" phase:

  - commands   declarative shell (was setup.kind:"commands")
  - panel      bespoke React UI, by id (was setup.kind:"custom")
  - sandboxed  a partner's program fetched + run in the worker (NEW,
               first-class; was implied by the immortal-provision panel)

De-immortalize the sandboxed runtime: the loader no longer hardcodes
Immortal's repo/path. `repo`, `programPath`, and a `trust` tier come from
the catalog entry, so any verified partner can ship a sandboxed program
as data. loadProgram enforces trust (only verified/first-party fetch
live) and only offers the vendored offline snapshot for the repo we ship
one for. loadProvisionConfig is likewise parameterized by repo.

Panel selection is now by kind, not a per-app registry id: panel
programs resolve their React panel from SETUP_PANELS; sandboxed programs
all share one generic runner, ImmortalProvisioning.tsx renamed to the
data-driven SandboxedProgramPanel. The uninstall lifecycle derives its
revert (the program's restore) from the catalog entry instead of a
hardcoded immortal-provision key + com.immortal.launcher package.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Rewrite the setup/provisioning docs for the unified `program` model:
CONTRIBUTING.md now describes the three `program` kinds (commands /
panel / sandboxed) and the trust rule; docs/provisioning.md is retitled
"Sandboxed provisioning programs", frames Immortal as the reference
consumer of a general runtime, adds a Trust section, and points at
SandboxedProgramPanel + the spec-driven loader; CLAUDE.md gains an
"install and program" section; the SDK README explains that a program
runs only when its catalog entry is verified/first-party.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Merge the lone src/lib/utils.ts into the utils/ folder (cn.ts + an index
barrel so @/lib/utils keeps resolving), and move the Portal model metadata
to src/lib/device/models.ts. No behavior change.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Regroup the old src/lib/portal grab-bag into clear concerns:

- src/lib/catalog/  : catalog data + types (split into types.ts), standard
  source resolution (sources.ts), morphe verification primitive (morphe.ts,
  was src/lib/security/*), README fetch.
- src/lib/programs/ : the sandboxed program runtime (was portal/provision/),
  with config.ts (was provision-config.ts). Rename the Immortal-flavored
  Provision* types to Program* (ProgramConfig, ProgramManifest, ProgramRun,
  loadProgramConfig, ...); the program entry verbs provision()/restore()/
  status() and the host<->program contract are unchanged.

Kill custom sources: immortal-launcher already installs through its sandboxed
program (handlesInstall), so drop source:"custom", the customSource field, and
src/lib/portal/custom-sources/. canAutoInstall now also covers handlesInstall
programs, and the passive update-check keys off a new hasResolvableSource so
program-driven apps (immortal) cleanly drop out of it.

Data still lives in a single catalog.json here; it moves to a root catalog/
in the next commit. Green on pnpm lint + pnpm build.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
The catalog is the heart of the project, so surface it at the repo root as
data anyone browsing the repo sees first, instead of burying it in src/lib.

- catalog/apps/<id>/app.json : one folder per app (was a single catalog.json
  array). An app is added by a PR that drops in one folder, no code change.
- catalog/index.json         : curation only, order (display order) + featured
  (the "Made for Portal" pins, moved out of the per-app files).
- catalog/_template/, README : the strict template + how-to for contributors.
- Immortal's bundled program.js and its vendored upstream/ snapshot now live in
  its own app folder (catalog/apps/immortal-launcher/).

src/lib/catalog/index.ts loads the folders with import.meta.glob at build time
and applies the index order/featured. Biome now ignores the whole catalog/
folder; the drift/vendor scripts point at the new snapshot path. Green on
pnpm lint + pnpm build.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Move the morphe (modded app) install logic out of the native library and into
a first-party program, so "anything non-standard is a program" holds for morphe
too. Only the security-critical piece stays native.

- portal.verifyMorpheManifest(text): new host capability (broker + worker +
  contract) that verifies the Ed25519 envelope against the pinned key and
  returns the parsed manifest. The key and verification never leave the host.
- catalog/programs/morphe/program.js: first-party program that fetches the
  manifest URLs, verifies via the capability, picks the arch-matched build, and
  installs it. A 1:1 port of the old resolveMorpheApk + pickArch.
- src/lib/programs/morphe-runner.ts runs it headlessly for a target package;
  use-app-actions routes source:"morphe" installs through it.
- Drop the native resolveMorpheApk/fetchMorpheManifest/pickArch from
  catalog/sources.ts. morphe leaves hasResolvableSource (so it drops out of the
  passive update-check, like immortal) but canAutoInstall still covers it.

Behavioral change: morphe apps install through the program (step events instead
of a download-% bar) and no longer show a passive "update available" badge.
NEEDS ON-DEVICE VALIDATION - demo mode cannot exercise a real manifest fetch +
verify + install. Green on pnpm lint + pnpm build.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Point every reference at the new tree and align the vocabulary with the
"programs" rename:

- Rename docs/provisioning.md -> docs/programs.md and sdk/provision-sdk.d.ts ->
  sdk/program-sdk.d.ts; retitle, fix the path table, and refresh the SDK header.
- Add the verifyMorpheManifest capability + MorpheManifest types to the SDK
  contract; rename the SDK's Provision* types to Program*.
- Rewrite the catalog sections of README, CONTRIBUTING, and CLAUDE.md for the
  root catalog/ (per-app folders, index.json, no custom sources); the field
  reference now lives in catalog/README.md.
- Fix the drift/vendor scripts' vendored-note and the provision-drift workflow
  comment to the catalog snapshot path.

No behavior change. Green on pnpm lint + pnpm build.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Morphe is first-party shared infrastructure, not a contributor-submitted
app, so its program does not belong beside the per-app catalog folders.
Relocate catalog/programs/morphe/program.js to src/lib/programs/morphe/,
removing the catalog/programs/ directory entirely. The catalog now holds
only apps/ plus its index/template/readme.

- Fix the program's editor reference path for the new depth.
- Point morphe-runner at the new bundled path.
- Biome-ignore the moved program.js (an SDK-authored asset, like the
  catalog programs).

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Adds the declarative primitives a setup program needs to render its own
configuration UI through the one generic panel, with no bespoke React:

- a `file` manifest field (with `accept`), plus `portal.pushUploadedFile`
  to place the picked file on the device (bytes stay main-thread, never
  enter the worker, mirroring pushUserPhotos)
- `manifest.presentation` (intro, steps, external link) for static guidance
- a `view` a program returns from provision() (links, text, a download),
  sanitized by the broker before render via a new http(s)-only
  `safeExternalUrl` and `normalizeResult`
- `portal.app` ({ packageName, name }) so a program knows its target app
  without abusing the launcher-shaped `cfg`

All additive: PORTAL_API_VERSION stays 1 and immortal's frozen live
contract (fleet, status, config.env) is untouched. Updates the SDK d.ts,
README, template (also fixing stale Provision* JSDoc names), and docs.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Generalizes the loader so a sandboxed program can be a first-party module
bundled in the app's own folder, not only a partner repo fetched live:

- discover catalog/apps/*/program.js via a build-time glob keyed by app id
  (drops the hardcoded Immortal import; loadVendoredProgram reads Immortal
  from the same glob)
- loadProgram(adb, spec, appId): a repo-less spec loads its bundled program
  directly; a spec with a repo keeps Immortal's live-fetch + vendored
  fallback path unchanged
- AppProgram sandboxed `repo` is now optional
- loadProgramConfig(adb, repo?) returns an empty config with no network when
  there is no repo, so a config-only program never fetches Immortal's
  config.env by default

Callers pass app.id. Immortal's behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Generalizes SandboxedProgramPanel so a config-only program drives its whole
UI declaratively through it:

- render manifest.presentation (intro, numbered steps, external link) with
  app-id-scoped i18n fallback keys
- render a `file` manifest field (picked file collected into a files map and
  passed to the run, never into the worker)
- render a returned result view (links with a copy button, text, a JSON
  download)
- pass portal.app (packageName, name) into every run

Immortal-specific chrome is now gated on capability instead of assumed: the
source badge/link only shows when the program has a repo, and the Restore
button only when the program declares revertOnUninstall. The primary button
label follows handlesInstall/isInstalled. The status grid and reset-launcher
were already null-gated, so a program without status() shows neither.
Immortal's panel is unchanged. Adds apply/copy/chooseFile strings (en, fr).

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
… kind

The two apps that needed bespoke React setup panels are now first-party
sandboxed programs bundled in their own catalog folders, so their whole
config UI is declarative catalog data:

- google-photos-screensaver/program.js: a file field for client_secret.json,
  pushed via pushUploadedFile, then grants + screensaver via portal, then
  launch. Presentation carries the intro/steps/guide link.
- portal-calendar/program.js: launches the app, reads the device IP, and
  returns it as a result-view link with a copy button.

Both app.json blocks flip to { kind: "sandboxed", trust: "first-party",
handlesInstall: false } and keep source: "github" (install still via GitHub;
the program only configures).

Deletes GPhotosSetup, PortalCalendarSetup, the setup registry, the panel
branch in AppSetupPanel, and the `panel` member of AppProgram; isPanelProgram
collapses to the sandboxed check (all 5 call sites keep working). French
copy is re-homed under app-id-scoped presentation keys; English comes from
the program manifests.

Behavioral change: these two configure through the sandboxed worker now.
Needs an on-device test (Google Photos credential push + screensaver, Portal
Calendar address link).

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
…r repo

Immortal's partner repo does not publish openportal.program.js; the program
(our 1:1 port of provision.sh) lives in our catalog. So mark it trust:
first-party and load it from the bundle, like Google Photos and Portal
Calendar, instead of attempting a live fetch that only ever falls back.

loadProgram now keys on trust: first-party programs are always bundled (the
`repo`, when present, is used only for config.env and the source link);
verified programs are still fetched live from the partner release with the
vendored snapshot as fallback. Immortal keeps its repo, so config.env still
live-syncs and the source link still resolves.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
Standardize the sandboxed setup program filename to a branded, userscript-style
openportal.js: one per app folder (catalog/apps/<id>/openportal.js) and the
partner remote default path provisioning/openportal.js. Renames the four bundled
programs (immortal, google-photos, portal-calendar, morphe) and updates the
loader glob, morphe runner import, biome ignore, drift/vendor scripts, catalog
types, panel source link, and all docs/SDK references.

Claude-Session: https://claude.ai/code/session_01JA4pXVAABHDqs4gLjz7zK7
The portal API had grown a long tail of thin wrappers that a single
portal.shell(...) reproduces, plus surface left over from specializing it
for the Immortal launcher. Cut it to a general core (API v2):

- Keep only what a raw shell can't do cleanly: shell, installFromUrl
  (staged + sha256 + progress), resolveGithubLatest/resolveFdroidLatest
  (host-side source resolvers), pushText/pushUserPhotos/pushUploadedFile
  (bytes never enter the worker), step, and the sdk/cfg/app props.
- Drop the device-utility wrappers (getprop, getIpAddress, deviceFetchText,
  makeDirectory, removePath, get/putSetting, dump/clearLogcat, launchApp,
  log, sleep) from the Portal interface, worker stubs, and broker dispatch.
- Move Morphe fully host-side: morphe-runner.ts now fetches, verifies
  (Ed25519, pinned key already on the host), picks the arch, and installs
  directly with the adb wrappers. The sandboxed morphe program is deleted
  and verifyMorpheManifest leaves the portal surface. Morphe is first-party
  and the sandbox added nothing.
- Migrate our first-party programs (Immortal vendored, Google Photos,
  Portal Calendar) to portal.shell for the removed wrappers.
- De-Immortalize the public SDK: cfg is now a generic Record<string,string>;
  ProgramStatus/resetLauncher and the Morphe types leave the public d.ts
  (the runtime keeps them for the launcher). Template and README rewritten
  on the reduced surface.
- Bump PORTAL_API_VERSION 1 -> 2 and update docs/programs.md.
Key the model table on the actual ro.product.device strings reported over
ADB (aloha, ohana, omni, atlas, cipher, ripley, terry) instead of the
invented codenames porto/sansa/pltv, which no Portal firmware reports.

Fixes 1st Gen detection: aloha was mapped to Portal (2nd Gen)/generation 2,
but firmware (Android 9) confirms it is the 1st Gen 10" Portal. ohana is
added as Portal+ (1st Gen). Generation stays marketing/topology metadata;
live Android/apiLevel is read separately in getDeviceInfo.

The USB filter is left unchanged: every generation enumerates under Meta VID
0x2ec6 in ADB mode, so the existing filter already covers Gen 1 aloha.
@andronedev

Copy link
Copy Markdown
Owner Author

close #3

@andronedev
andronedev merged commit da38b4b into main Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant