Refactor Immortal provisioning and unify program model - #6
Merged
Conversation
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).
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
Refactor/catalog at root
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.
Owner
Author
|
close #3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.