Skip to content

AppForge-1 — install an app from a local folder or .brainstorm file (sideload path + packaged-path fix) - #364

Merged
th3-br41n merged 1 commit into
mainfrom
feat/appforge-1-sideload-install
Jul 29, 2026
Merged

AppForge-1 — install an app from a local folder or .brainstorm file (sideload path + packaged-path fix)#364
th3-br41n merged 1 commit into
mainfrom
feat/appforge-1-sideload-install

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

AppForge-1 — install an app from a local folder or .brainstorm file

First real producer of the already-schema'd sideload path (InstallOrigin.LocalFile): a user can now install an app bundle they have on disk, straight from the Marketplace. This is the install half of the "app built inside Brainstorm becomes a real app" loop.

What's new

  • apps:install-from-folder / apps:install-from-file IPC (main/ipc/sideload-handlers.ts): main-side OS dialog (openDirectory, and openFile filtered to *.brainstorm per the dialog-filter rule) → manifest pre-check → AppInstaller.install with InstallOrigin.LocalFile provenance → dashboard icon pinned (mirrors the catalog install path). Results are always a typed SideloadInstallResult (installed with app summary + granted caps | cancelled | failed with a SideloadFailureCode) — the handler never throws raw errors at the renderer.
  • Marketplace UI: an "Install from…" toolbar button opening a shared fancy-menus picker (folder / .brainstorm), success/failure toasts (typed-code → message map), and honest unsigned surfacing — a "Local install" source label, an Unsigned advisory chip + Signature spec row on the listing detail (signatureStatus now flows registry row → MarketplaceService → listing wire type).
  • Packaged-path fix: registerMarketplaceHandlers resolved first-party bundles via a __dirname relative walk that lands inside the asar in packaged builds — now resolveFirstPartyAppsDir picks process.resourcesPath/apps when app.isPackaged (same tree bootstrapApps uses). reinstallFirstPartyApp gains prebuiltOnly (passed as app.isPackaged): installs the prebuilt extraResources bundle with bootstrap-cache provenance and never spawns vite build in a packaged shell.

Security posture (⚠️ flagging for security review — new IPC + install surface)

This is hostile-input handling; the following is the intended posture, please poke at it:

  • Privileged surface: the two channels are exposed only on the dashboard preload, and additionally sender-checked in main against the dashboard WebContents — any other sender gets a typed not-allowed failure (fail-closed). App renderers cannot reach the installer.
  • User gesture: the bundle path only ever comes from an OS dialog the user drove; the renderer cannot pass a path.
  • Untrusted manifest: size-bounded read (512 KiB) before parse; the installer's fail-closed validator is authoritative (the handler pre-runs it only to classify typed failures). Tested from this call site: traversal in entry, malformed id, non-JSON, oversized.
  • Untrusted archive: file size-bounded before read (64 MiB default); decompression expansion-bounded via new unpackBundle({ maxOutputBytes }) (gzip-bomb defense, 256 MiB default); unpack goes to a fresh mkdtemp staging dir removed in finally; the tar codec's absolute/.. rejection + unpackBrainstormBundleToDir's per-entry containment re-check are both exercised from this call site (zip-slip test).
  • Signature policy unchanged: unsigned installs record AppSignatureStatus.Unsigned (advisory in v1, shouldBlockInstall remains the single enforcement chokepoint); trust quarantine (sideloaded action tier) applies as before. The unsigned state is surfaced to the user rather than hidden.

Tests

  • main/ipc/sideload-handlers.test.ts (new, 13 cases): happy folder install (registry row provenance + icon pin + unsigned surfaced), cancel, hostile manifests (traversal entry / bad id / bad JSON / oversized), id collision, non-dashboard sender, no-session, .brainstorm round-trip via packBrainstormBundle, garbage bytes, zip-slip entry, over-limit file, decompression bomb.
  • main/dev/seed-demo-apps.test.ts: prebuiltOnly installs with bootstrap-cache provenance and a spawn spy proves vite build is never spawned (including the missing-bundle failure path).
  • main/apps/first-party.test.ts: resolveFirstPartyAppsDir dev + packaged branches.
  • main/catalog/brainstorm-package.test.ts: maxOutputBytes bomb bound (tiny archive, big expansion → throws; within bound → unpacks).
  • bun run verify, bun run lint, bun run typecheck green.

🤖 Generated with Claude Code

…torm file

- apps:install-from-folder / apps:install-from-file IPC: dashboard-sender-
  gated (fail-closed), typed SideloadInstallResult, InstallOrigin.LocalFile
  provenance, dashboard icon pinned like catalog installs
- Marketplace "Install from…" toolbar menu (shared fancy-menus picker) +
  unsigned-advisory surfaced honestly (Local install source label, Unsigned
  chip + Signature spec row; signatureStatus flows registry → listing wire)
- Hostile-input bounds: manifest size cap + fail-closed validator, archive
  size cap before read, decompression-expansion cap via new
  unpackBundle({maxOutputBytes}) (gzip-bomb defense), zip-slip guard
  exercised from the install call site, mkdtemp staging removed in finally
- Packaged-path fix: marketplace first-party resolution via
  process.resourcesPath/apps when app.isPackaged (was an asar-relative walk
  that doesn't exist); reinstallFirstPartyApp prebuiltOnly never spawns
  vite build in a packaged shell (installs the extraResources bundle with
  bootstrap-cache provenance)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@th3-br41n
th3-br41n merged commit e09362b into main Jul 29, 2026
3 checks passed
@th3-br41n
th3-br41n deleted the feat/appforge-1-sideload-install branch July 29, 2026 14:28
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