The launcher's address field is a plain Row in the Scaffold topBar slot, so
(unlike a Material3 TopAppBar) it didn't apply the status-bar inset and drew
under the status bar. Add statusBarsPadding() to the omnibox row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
The web-app tab and the nsite/napplet tab had different bars, and both used
the same MaterialSymbols.Security "shield" for different things — Tor on the
web tab, sandbox access on the napplet tab — which read as the same icon
meaning two things. (Tor's real mark is the ic_tor onion used elsewhere.)
- Add a shared EmbeddedTabTopBar (sandbox shield · title · reload · pop-out)
used by both tabs, so they're visually identical.
- The shield now consistently means "security & privacy": it opens a sheet.
Tor moves into that sheet (a live toggle on the web-app tab; the napplet
sheet keeps its capability list + network line), so the shield is never
confused with a Tor toggle and there's no standalone Tor icon to mistake
for it.
(The full-screen BrowserHostActivity pop-out still uses its own bar; can
harmonize that next if wanted.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
The full-screen NappletHostActivity already consumes system-bar + cutout
insets at its root so its WebView doesn't pad the page for the status/nav
bars a second time (targetSdk 35+ auto-applies received insets to web
content). The embedded surfaces — the browser tab and the new embedded
nsite/napplet tabs — have no such root: the WebView is the surface view, so
it received and re-applied those insets, leaving an empty band under the
status and navigation bars even though the host already places the surface
in the inset-free content area.
Add WebView.dropSystemBarInsets() (zeroes system-bar + display-cutout
insets, keeps IME for keyboard resize) and apply it in both
NappletBrowserService and NappletHostService when the session WebView is
built.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Replaces the parallel bottomBarItems (List<NavBarItem>) + bottomBarFavoriteIds
with a single ordered List<BottomBarEntry>, so built-in destinations and
favorite apps live in one list and can be pinned and drag-reordered together.
- BottomBarEntry = BuiltIn(NavBarItem) | Favorite(favoriteId). The favorite id
already encodes the route's parameters (the url / addressable coordinate), so
each entry maps deterministically to its Route — BuiltIn via NavBarCatalog,
Favorite via Route.FavoriteWebApp/FavoriteNostrApp. (Storing the raw Route
isn't an option: the sealed Route parent isn't @Serializable, so a List<Route>
can't be persisted without annotating the whole ~100-subtype hierarchy.)
- UiSettings/UiSettingsFlow carry bottomBarItems: List<BottomBarEntry>;
UISharedPreferences serializes it as JSON, with a legacy comma-separated
NavBarItem fallback so existing configs still load.
- BottomBarSettingsScreen now shows one reorderable list mixing built-ins and
favorites; the separate favorites section is gone.
- AppBottomBar renders entries in saved order; warm-keep membership derives
from the list's favorite entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
isMarkdown() treated the second newline of a blank line as a non-space
character, flipping the line-start tracker off. ATX headings, blockquotes,
and list markers that follow the standard blank-line spacing went
undetected, so NIP-23 long-form articles made of prose plus section
headings rendered as raw text. Exclude newline/carriage-return from the
line-start guard so a blank line stays at line start.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements keep-warm (approach A) so a bottom-row embedded tab keeps its
full state across tab swaps, scoped to bottom-row apps per review.
Why a persistent layer: androidx.privacysandbox.ui's SandboxedSdkView
closes its session in onDetachedFromWindow, so a session torn down the
moment a tab leaves composition is unavoidable if the surface lives inside
the per-screen composable. Instead, a single app-shell overlay
(EmbeddedTabLayer) holds every warm session's SandboxedSdkView attached the
whole time — the active one positioned over the current tab's reserved
content area, the rest parked off-screen but alive. No provider changes
needed: the WebView never detaches, so its JS state survives.
- EmbeddedTabHost: process-level holder of warm sessions (keyed by
FavoriteApp.id), the active id, and the active content bounds.
- EmbeddedSurfaceController unifies the browser + napplet controllers so the
layer can attach/park/teardown either; the napplet controller pauses its
applet while parked (onHidden) and on app-background.
- FavoriteWebAppScreen / FavoriteNappletScreen no longer host the surface;
they reserve the content area (reporting window bounds) and drive the warm
controller. The trusted napplet chrome stays in the main process.
Scope (per review): only bottom-bar favorites stay warm — a tab whose app
isn't a bottom-bar favorite is evicted (restarted) when it leaves
(EmbeddedTabHost.retainOnly, driven by the settings list). Genuine memory
pressure (onTrimMemory) drops all warm sessions; mere backgrounding does not.
Needs on-device verification of the surface overlay (alignment, touch
pass-through to the bars, warm re-show) — validated here by build + assemble.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Reworks how favorites get into the bottom bar, per review: instead of a
grid Pin/Unpin that auto-appended to the bar, favorites are now activated
as a dedicated "Favorite apps" section in the bottom-bar settings page —
kept separate from the built-in destinations because favorites are dynamic
data, not the fixed NavBarItem enum.
- UiSettings/UiSettingsFlow/UISharedPreferences gain bottomBarFavoriteIds
(a device-local list of FavoriteApp ids), persisted alongside the
existing bottomBarItems.
- BottomBarSettingsScreen gets a "Favorite apps" section: one toggle per
favorite to activate/deactivate it as a bottom-bar tab.
- AppBottomBar renders the favorite tabs from that settings list instead of
a registry-side pinned set.
- FavoriteAppsRegistry drops the pinned-set machinery; the grid drops its
Pin/Unpin item. The "All apps" grid remains a built-in destination.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Completes the favorites system: a favorited nsite/napplet can now be
pinned to the bottom bar and render as an embedded, swap-in-place tab —
no longer only a full-screen activity launch.
New sandbox surface (:napplet, keyless), mirroring the browser embed:
- NappletHostService hosts the verified-blob WebView (same content server,
shell bridge, and single launch-token broker path as NappletHostActivity)
and ships it as a SandboxedUiAdapter surface, so applet JS still runs only
in the keyless process — never where the keys live.
- NappletHostUiAdapter / NappletEmbedContract are the SurfaceControlViewHost
adapter and the Messenger contract; the create-session bundle reuses
NappletHostContract's EXTRA_* keys, so the embedded and full-screen host
paths launch from identical, main-process-minted parameters.
Main process:
- NappletLauncher.buildLaunchParams extracts the verified param/token minting
so both the activity intent and the embedded session share it.
- EmbeddedNappletController binds the service and attaches the surface
(mirror of EmbeddedBrowserController).
- FavoriteNappletScreen draws the TRUSTED CHROME (sandbox shield, app name,
"what it can access") in the main process around the surface — the sandbox
must never draw chrome the user is meant to trust — plus a pop-out to the
full-screen host. Capability consent still flows through the existing
main-process broker + consent activity, unchanged and host-agnostic.
Security parity with the full-screen host:
- The applet's JS + timers are paused while the app is backgrounded
(lifecycle ON_STOP/ON_START → MSG_PAUSE/MSG_RESUME), so an "allow always"
napplet can't act on the user's behalf when they aren't looking.
- Granted sensitive ops (publish/upload/pay) surface a notice toast.
Both favorite kinds are now pinnable; the bottom bar routes WebUrl →
FavoriteWebApp and NostrApp → FavoriteNostrApp, each embedding in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Builds on the favorites system so a favorite can live in the bottom row
as a real tab instead of only launching a full-screen activity.
- FavoriteAppsRegistry gains a pinned-ids set (persisted device-locally,
alongside the favorites list). Only WebUrl favorites are pinnable today
— they're the ones that embed in-process — so a pinned tab always swaps
in place and never launches an activity from the bottom row. Removing a
favorite unpins it.
- Route.FavoriteWebApp(url) + FavoriteWebAppScreen render the embedded
:napplet browser surface as an in-app tab: the app bottom bar stays, so
switching to/from it is an ordinary tab swap. A pop-out action hands the
same URL to the full-screen BrowserHostActivity for users who want it as
its own window.
- AppBottomBar appends pinned favorites as tabs after the built-in items,
navigating via navBottomBar (marked a tab root, so the bar stays).
- The Favorite Apps grid gains a Pin/Unpin action for WebUrl favorites.
Known follow-ups (intentionally out of this commit): NostrApp favorites
can't embed as tabs yet (they need an embedded nsite/napplet surface in
nappletHost), and embedded tabs rebuild on return rather than staying warm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Introduces a device-local "favorite apps" system that unifies nsites,
napplets, and arbitrary web clients behind one model and three
presentation shells, while removing the editable address bar from any
running app (it now lives only in the browser launcher).
Core spine:
- FavoriteApp (commons): a sealed model with two cases — NostrApp
(nsite/napplet, keyed by addressable coordinate so it survives code
updates) and WebUrl. napplet-vs-nsite is recomputed from the live
event at launch, never stored.
- FavoriteAppsRegistry (amethyst): device-local, DataStore-backed,
StateFlow source of truth; main process only, hydrated at app start.
- FavoriteAppLauncher: dispatches a favorite to its one launch path —
full-screen BrowserHostActivity for a URL, sandboxed NappletLauncher
(re-resolved from LocalCache) for an nsite/napplet.
Presentation:
- BrowserHostActivity: full-screen, single-app host in the main process
that embeds the keyless :napplet browser surface. Its own task/recents
entry (documentLaunchMode=intoExisting), no editable URL — locked to
the app it opened with, keeping one NIP-07 trust context per instance.
- BrowserScreen is now a launcher: an omnibox that opens each URL in its
own host activity, plus the shared favorites grid. The address bar is
gone from the content surface.
- FavoriteAppsScreen + FAVORITE_APPS bottom-bar item: a grid of big
launch buttons, reused inside the browser launcher.
- StaticWebsiteCard gains a header-actions slot; a star toggle on each
nsite/napplet card pins it (strings/store stay in the app layer).
EmbeddedBrowserSurface extracts the chrome-free surface + controller
helper so the tab, the launcher, and the host activity share one piece
of cross-process glue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Replace the self-contained AddressableAuthorRelayLoaderSubAssembler
(which duplicated UserOutboxFinderSubAssembler) with a thin bridge that
injects UserFinderQueryState entries directly into the existing
UserFinderFilterAssembler.
When EventFinderFilterAssembler detects an AddressableNote stub whose
author relay list is unknown, it subscribes that author to userFinder.
UserOutboxFinderSubAssembler already handles the kind-0/10002 fetch and
relay resolution — no logic is duplicated. Subscriptions are cleaned up
when the note loads or the EventFinder key is removed.
The full-screen NappletHostActivity browser-mode and NappletLauncher.launchBrowser
were added alongside the embedded browser but never wired into any UI. The embedded
surface (NappletBrowserService rendered via SurfaceControlViewHost) supersedes them
and draws the address bar in the trusted main process rather than inside the sandbox,
so this removes the weaker, unused surface.
Removed:
- NappletHostActivity browser-mode (setupBrowser, address bar, per-origin bridge) —
reverted the host activity to its pre-browser state.
- NappletLauncher.launchBrowser and the EXTRA_BROWSER_MODE/EXTRA_BROWSER_URL extras.
- The now-unused nappletHost browser strings.
Kept (used by the embedded path): the broker per-origin token mint
(MSG_MINT_BROWSER_TOKEN / MSG_BROWSER_TOKEN / KEY_BROWSER_ORIGIN) in NappletBrokerService
and NappletIpc, so NIP-07 consent stays scoped per visited origin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
Adds a "Browser" navigation destination (drawer + pinnable bottom-nav item,
API 30+) that opens any URL. The page renders in the sandboxed, keyless
`:napplet` process and is streamed into the main activity as a cross-process
surface via androidx.privacysandbox.ui (SurfaceControlViewHost) — only pixels
and input cross the boundary, never the WebView's JS context or the NIP-07
bridge. The trusted address bar is drawn by the main process around the
embedded surface, so the sandbox can never spoof the URL.
NIP-07 `window.nostr` is injected the same way nSite website mode does it, but
scoped per visited origin: each origin gets its own broker-minted launch token
(keyed by the trusted source origin), so a grant to one site never leaks to
another.
- NappletBrowserService (`:napplet`): hosts the live-URL WebView, exposes it as
a SandboxedUiAdapter, and relays the per-origin NIP-07 bridge to the broker.
- NappletBrowserUiAdapter: wraps the WebView session for privacysandbox.ui.
- NappletBrokerService: mints a per-origin synthetic identity so NIP-07 consent
is scoped per host.
- EmbeddedBrowserController + BrowserScreen: bind the service, render the
SandboxedSdkView, and drive the trusted address bar (navigate/reload/back/Tor).
- shim.js: a direct-bridge transport so the injected shim works in a top-level
page that has no trusted shell parent.
- Browser nav item hidden below API 30 (SurfaceControlViewHost requirement).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
The new plain-AGP :nappletHost library only declared debug/release build
types (unlike the KMP :commons/:quartz/:nestsClient libs, which match any
build type). The :amethyst app's `benchmark` build type therefore had no
matching :nappletHost variant, breaking fdroidBenchmark/playBenchmark
resolution and Test/Build CI on main.
Declare a matching `benchmark` build type (initWith release) in :nappletHost
so the app resolves a real variant. Library AARs aren't signed, so no
signing config is needed here. :nappletHost is the only plain-AGP library
:amethyst depends on, so no consumer-side matchingFallbacks is required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tzXcqVUcAPhyDDFak4SQ8
When a kind-16 repost (or any event) has an a-tag pointing to an
addressable event by an unknown author (no NIP-65 relay list loaded),
potentialRelaysToFindAddress() returned an empty set and the event was
never found.
AddressableAuthorRelayLoaderSubAssembler mirrors
UserOutboxFinderSubAssembler but is driven by EventFinderQueryState.
It detects AddressableNote stubs whose author relay list is missing and
emits kind-0/10002 filters to indexer/search relays. On EOSE it calls
invalidateFilters(), causing NoteEventLoaderSubAssembler to re-run with
the now-populated outbox relays and fetch the addressable event.
The new plain-AGP :nappletHost library only declares debug/release build
types (unlike the KMP :commons/:quartz libs, which match any build type).
The :amethyst app's `benchmark` build type had no fallback, so Gradle could
not resolve a matching :nappletHost variant for fdroidBenchmark/playBenchmark,
breaking Test/Build CI on main.
Add `matchingFallbacks += "release"` to the benchmark build type so it falls
back to a library's release variant when no benchmark variant exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tzXcqVUcAPhyDDFak4SQ8
Adds the `inc` pub/sub bus so napplets that declare it boot and can exchange
topic events: `inc.subscribe {topic}` / `inc.unsubscribe {topic}` register
interest, `inc.emit {topic, payload}` fans out an `inc.event {topic, payload,
sender}` to OTHER subscribed napplet sessions (never echoing the sender) — the
kehto runtime's inc contract.
- Router edge ops (gated on the INC declaration alone, like identity.watch —
no per-call consent): SubscribeInc/UnsubscribeInc/EmitInc outcomes.
- Protocol: readTopic/readPayloadRaw + encodeIncEvent.
- NappletIncBus in the broker service routes across the live napplet sessions
(the one service every sandbox binds), keyed by reply Messenger.
- Tests for inc routing + declaration gating; updated capability/router tests
that asserted the old "inc/theme/notify are unknown" behavior.
NOTE: napplets run foreground-only/one-at-a-time, so cross-napplet delivery is
usually a no-op in practice; the bus is correct if sessions ever overlap. It is
app-wide (not author-scoped) — a future refinement could namespace topics by
author. Unblocks feed/profile-viewer/chat/bot. See the plan doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Napplets can now create/list/dismiss user-facing notifications:
`notify.create { title, body }` → `notify.created { id }` (the bespoke
past-tense reply the client listens for, not the generic `.result`),
`notify.list` → `notify.listed { notifications }`, `notify.dismiss` fire-and-forget.
- NotifyCreate/NotifyList/NotifyDismiss requests, NotifyCreated/NotifyListed
responses, NappletNotifyGateway + NappletNotification; broker executes them
(consent-gated, ask-once).
- Protocol encodes the past-tense reply types.
- Android: NappletNotificationStore (per-coordinate, main-process, survives
broker rebuilds — a napplet only ever sees/dismisses its own) + a best-effort
system-tray notification.
- Consent summary + string for notify.
Unblocks the toaster demo. See the plan doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Napplets can now read the host's current theme via `theme.get` →
`theme.get.result { theme: { colors: { background, text, primary } } }`,
mapping it to their CSS variables. This is the universal boot gate for
real-world napplets (e.g. kehto/web's demos all `requires: theme` and abort
if shell.supports('theme') is false).
- NappletCapability gains THEME (+ NOTIFY/INC, wired in following commits);
adds requiresConsent (false for SHELL/THEME — cosmetic/negotiation never prompt).
- ThemeGet request, Theme response, NappletThemeGateway; broker executes it
with no consent prompt.
- Android gateway returns Amethyst's brand purple with a dark/light bg+text pair.
- Capability label/description/icon + strings for theme/notify/inc.
See amethyst/plans/2026-06-23-napplet-nap-theme-notify-inc.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
- StrictMode DiskReadViolation: PrefetchManifestBlobs read context.cacheDir
(ensurePrivateCacheDirExists touches disk) on the composition dispatcher.
Move it into withContext(Dispatchers.IO); the prefetch was already IO-bound.
- Sandbox top-bar network indicator now uses the app's real Tor logo
(ic_tor, copied into :nappletHost since it can't depend on :amethyst) via an
ImageView, instead of the onion/globe emoji. Lit when routing through Tor,
dimmed when the site loads over the open web.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
nSites now load over Tor by default when Tor is active — closing the gap where
only blob fetches were Tor-routed while the site's own web traffic (fetch/img/
script) went out the system network and could leak the user's IP. Users can opt
a specific site out to the open web (e.g. a site that breaks or is slow over
Tor); the choice is remembered per site and makes everything for that site
direct (web + blobs).
- NappletHostActivity sets a process-wide WebView SOCKS proxy override
(socks5://127.0.0.1:<torPort>) for website-mode nSites, or clears it for open
web. Best-effort + on-device-verifiable: SOCKS-over-WebView support varies by
WebView version, so it's isolated to applyWebViewProxy() and never breaks the
site if unsupported.
- Top-bar onion (🧅 Tor / 🌐 open web) shows the routing and toggles it; the
dialog explains the IP-privacy trade. Shown only for nSites when Tor is active.
- NappletNetworkRegistry: main-process per-site preference (coordinate-keyed,
DataStore-backed, Tor-default). The launcher reads it; the broker persists the
sandbox's toggle resolved through the launch token. The key-free sandbox never
touches it.
- Toggling persists via a new MSG_SET_NETWORK_MODE IPC, then relaunches the host
so the proxy + content server rebuild cleanly for the new mode.
- "Open web" also routes blob fetches direct (effective proxy -1); locked
napplets always keep Tor for blobs (no toggle exposed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Two edge-to-edge issues on the napplet/nsite host, both surfacing as a white strip below
the applet on a light page:
- Double bottom inset. The root inset listener turned the system-bar/cutout insets into
padding but returned them un-consumed, so the child WebView (which on targetSdk 35+
auto-applies any insets it receives to its web content) padded the bottom a SECOND time.
Zero those types before they reach the WebView, keeping IME flowing so keyboard resize
still works.
- Over-scroll reveal. Forcing a scroll past the content edge stretched the view and
exposed the shell document's background behind the applet iframe. WebView.overScrollMode
only governs the outer frame, so also inject `overscroll-behavior: none` into the applet
document (alongside the shim) to kill the stretch at the source, theme-independently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nSites now open in "website mode": a normal web app with normal network
access plus a NIP-07 window.nostr provider, so standard Nostr web apps can
"log in with Amethyst" and sign as the active user. Napplets are unchanged
(locked, declared-only sandbox).
- window.nostr (shim.js) installs only when the host sets __nappletNip07
(website mode). getPublicKey/getRelays reuse the existing consent-gated
identity reads; signEvent is a new sign-only op honoring the app-supplied
created_at (no publish — the web app sends to relays itself).
- NappletRequest.SignEvent + nostr.signEvent decode; broker signs as the
user and returns the signed event without publishing. pubkey is still
fixed by the signer, so the app can never sign as another identity.
- Website mode: content server defers off-origin requests to the WebView
and drops the app CSP (normal network); locked napplets keep connect-src
'none' and 404 off-origin.
- Launcher grants IDENTITY + RELAY (consent-gated) for website mode,
independent of the nSite's empty manifest requires.
- Consent dialog shows the kind + content preview for a sign request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
The disclosure used AnimatedVisibility's default transition and the inner servers
Column didn't fill width, so the section faded/expanded while the server list also
grew in horizontally from the left — an inconsistent, weird effect. Make the
transition explicit (fade + expandVertically/shrinkVertically anchored at Top, so
it opens/closes straight down like the card) and fill width on the servers column
so nothing slides in sideways.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
The sandbox host now routes the back gesture into the applet/site's own history
first: an OnBackPressedCallback calls webView.goBack() while there's history to
pop (in-page links, iframe navigations, and history.pushState all count), and
only disables itself — letting system back return to Amethyst — once the WebView
is at its first page. The callback's enabled state tracks canGoBack() via
doUpdateVisitedHistory / onPageFinished. Works with predictive back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Document the move from the opaque `allow-scripts`-only sandbox to a real per-applet
origin (https://<sha256(author:identifier)>.napplet.local, allow-same-origin) under
"Fixed in this pass", plus the residual risks it introduces: the load-bearing invariant
that the applet origin must stay distinct from the shell/bridge origin, persistent client
storage as a new persistence/exfil surface, the origin-id derivation, and service workers
now being reachable-but-unwired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Static-site / SPA nApplets & nSites (Vite/nsyte/CRA/webpack output) rendered as a
blank page, then — once that was fixed — as a fast reload-loop blink. Three layered
causes, each found on-device via logcat:
1. Sub-path serving. The applet loaded under https://napplet.local/app/, but bundlers
emit absolute asset URLs (/assets/app.js, /fonts/…) that resolve against the origin
ROOT, so every script/style/font 404'd. nSites are defined to be hosted at the domain
root; serve there.
2. Opaque-origin storage. The applet ran in an `allow-scripts`-only iframe, so its origin
was opaque ("null"): module scripts + asset fetches were CORS-blocked, and reading
localStorage/IndexedDB/serviceWorker threw SecurityError — which crash-loops every SPA
(gruuv: "cache version 0 < 23 → reset → reload", forever, because IndexedDB never
worked so the version never persisted).
Fix: give each applet its OWN real, persistent, isolated origin — a per-applet subdomain
https://<id>.napplet.local (id = sha256(author:identifier)), framed by the shell with
`allow-scripts allow-same-origin`. A real origin restores localStorage/IndexedDB/SW and
makes the applet's own assets same-origin (no CORS). Isolation is preserved because the
origin is DISTINCT from the shell's: the native bridge stays origin-restricted to the
shell (napplet.local), so the cross-origin applet still can't reach it or read the shell
DOM, and per-applet subdomains keep applets' storage isolated from each other. The shell
HTML's iframe src + CSP frame-src are bound to the specific applet origin at serve time.
Also add an in-memory localStorage/sessionStorage polyfill to the injected shim as
belt-and-suspenders for any context where DOM storage is still unavailable.
By-design sandbox enforcement is unchanged and correctly blocks the rest (external CDN
scripts, direct relay WebSockets via connect-src 'none', external images) — apps must go
through the napplet SDK for those.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three load-time wins on the nApplet/nSite open path:
1. Overlap WebView/Chromium init with the index probe. The host now creates +
warms the WebView (and binds the broker) in onCreate, so its slow first-in-
process init runs concurrently with the IO availability probe instead of
serially after it; the probe just attaches the ready WebView.
2. Prefetch in parallel, index-first. NappletBlobPrefetcher downloads a
manifest's blobs with bounded concurrency (5) and fetches index.html first,
instead of strictly sequentially — faster first paint on cold opens.
3. Serve CAS hits without re-hashing. The content server short-circuits a
content-addressed cache hit (verified on write, addressed by sha256) straight
to the response, skipping the resolver's per-serve sha256 over the whole blob
— the dominant CPU cost for large bundles. Network (cache-miss) path still
fully verifies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Faster opens: while an nApplet/nSite card is on screen, eagerly download +
sha256-verify all of its blobs (Tor-routed) into a shared content-addressed
cache, so tapping Open serves from disk. Prefetch is de-duplicated (in-flight +
on-disk) and cancellation-aware (stops when the card scrolls away).
- New :nappletHost pieces: NappletBlobCache (content-addressed, multi-process-safe
atomic store, replacing the single-process OkHttp DiskLruCache), NappletBlobHttp
(shared Tor OkHttp + size-capped download), NappletBlobPrefetcher.
- NappletContentServer now reads the shared cache first, falling back to a Tor
download that refills it; still re-verifies every blob on serve.
- Cards trigger prefetch via a LaunchedEffect in the StaticWebsite render fns.
Nicer launch: NappletHostActivity probes the index (resolving + caching it)
behind a loading screen (monogram + title + spinner), then shows the app, or a
clear "couldn't load — try again" screen if the publisher's servers are offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
The nApplet (NIP-5D) and nSite (NIP-5A) browse feeds ignored the top-nav
follow-list selection at the relay layer: a single SingleSubEoseManager fired a
hardcoded all-authors REQ against the user's home relays, and the screen merely
re-filtered the dump client-side. Selecting Follows / a People-list never changed
what was fetched, so manifests on an author's own write relays were invisible.
Bring them to Pictures-style parity:
- Replace the SingleSubEoseManager with PerUserAndFollowListEoseManager, watching
defaultX FollowList + the new liveX FollowListsPerRelay outbox flow and rebuilding
the per-relay REQ on change.
- Add makeN{applets,sites}Filter dispatchers + subassemblies (Global, Authors,
Follows, MutedAuthors). These manifests carry no topical tags, so the tag-based
selections (hashtag/geohash/community) correctly fall through to no subscription.
- Add liveNappletsFollowListsPerRelay / liveNsitesFollowListsPerRelay (OutboxLoaderState).
- New authorOnlyRoutes spinner option set: author-based filters only (no geohash,
hashtag, community, relay, interest-set or AroundMe — none can match these events).
- "Mine" is intercepted per-feed (SubAssembler + screen), querying the user's own
pubkey against their outbox relays — the shared TopFilter.Mine flow resolves to
all-follows, so it can't be used directly.
Fix a FATAL duplicate-key crash: these replaceable/addressable kinds were observed
through the versioned note store (observeEvents) and keyed by per-version event id.
Switch to observeNotes, which yields one AddressableNote per address (latest
version), keyed by the stable address (idHex) and auto-updating in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `:napplet` sandbox runtime (NappletHostActivity, NappletContentServer,
NappletIpc, NappletKeyActions) now lives in a new :nappletHost Android library
that depends only on :commons + :quartz — NEVER :amethyst. So the sandbox code
is compile-time incapable of importing Amethyst.instance / LocalCache / Account,
turning the "two-process, no secrets in the sandbox" rule from a convention into
a build-graph guarantee.
- New module + NappletHostContract (Intent-extra keys + broker service FQN), so
the launcher (amethyst) and activity (module) share the launch contract with no
dependency cycle. The activity binds the broker by class name.
- Capability labels for the "what it can access" sheet are resolved by the
launcher (which has app resources) and passed in, so the module needs no
capability string resources. Host-only strings moved into the module.
- amethyst depends on :nappletHost; the broker-side (NappletBrokerService,
gateways, NappletLaunchRegistry) stays in :amethyst. Manifest declares the
activity by FQN (keeps @style/Theme.Amethyst resolvable).
- Docs updated (CLAUDE.md + security plan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Add a heads-up to the Amethyst Application KDoc and .claude/CLAUDE.md that the
app runs in two OS processes (main + :napplet), that Android reuses the single
Application class in both, and that statics/objects (Amethyst.instance,
LocalCache, NappletLaunchRegistry) are per-process — so code must not assume
`instance` exists off the main process or share singletons across the boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Android instantiates the single Amethyst Application in every process, so the
sandbox (`:napplet`) shares it and onCreate early-returns there, leaving the
lateinit `instance` (AppModules) unset. But onTrimMemory — delivered to every
process on real devices — and onTerminate called `instance` unconditionally,
crashing the sandbox with UninitializedPropertyAccessException on a trim.
Gate both with the cached sandbox-process check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
Add a bottom-nav nSites feed for NIP-5A static sites (kinds 15128/35128),
mirroring the nApplets feed: a follow-list FeedFilterSpinner in the top bar
(persisted as defaultNsitesFollowList + liveNsitesFollowLists author-matcher)
and rows rendered through the shared NoteCompose path, so they reuse the author
header, StaticWebsiteCard (with Open), and reaction bar.
New: NsitesScreen, NsitesTopBar, the Nsites discovery datasource trio, Route.Nsites,
NavBarItem.NSITES. Wired into AppNavigation, the relay coordinator, and the
follow-list settings persistence.
Also includes the earlier display rebrand to nApplet/nSite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
User-facing strings only — "Napplet(s)" → "nApplet(s)" and the static-site
label → "nSite". Code identifiers, resource keys, CLI verbs (amy napplet/nsite),
and unrelated profile "Website" labels are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde