- 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 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
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` 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