# NAP-INC — 2026-11-01

Status: implemented; checked in real Chromium on desktop, not yet on an Android device.

## Why

Most napplets published by napplet.run (Relay Browser, Blobbi, Emoji Pack Creator, the SeedHammer
CAD viewer, …) require `inc`. Amethyst listed `inc` as unsupported, so `isLaunchable()` refused
every one of them before it loaded.

Spec: [`napplet/naps` `naps/NAP-INC.md`](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/naps/NAP-INC.md)
(changelog up to `24ab958`, endpoint identifiers instead of `d` tags).

## What

| Piece | Where |
| --- | --- |
| `inc.*` request model | `commons/…/napplet/protocol/NappletIncRequest.kt`, codec in `NappletProtocolJson` (`decodeIncRequest`, `encodeInc*`) |
| Routing | `NappletRequestRouter.routeInc`: non-suspending, so hosts call it on the thread a message arrives on and a napplet's messages reach the bus in order. Authorized on the `inc` declaration alone. |
| Bus | `commons/src/jvmAndroid/…/napplet/NappletIncBus.kt`, generic over the host's surface type, partitioned by the launch account (a napplet never reaches, or learns of, one running as another account). Topics (exact match, no echo to the sender) and channels (validated once on open; the target's `inc.channel.opened` is queued before the opener's result; `inc.channel.closed` to both ends; `peer destroyed` when a surface goes). Endpoint identifiers are 128 random bits per live surface, never the `d` tag. At most 256 topics and 64 channels per endpoint. |
| Android | `NappletBrokerService`: one bus; an endpoint is a tab's reply `Messenger` plus its launch token, dropped on `MSG_RELEASE_CLIENT`, token release, registry eviction and service destroy. |
| Desktop | `DesktopNappletRuntime.incBus`; each `CefNappletSession` leaves it on `close()`. |
| Web binding | `shim.js` `window.napplet.inc`: `emit` (convention-URI query → text payload; a fragment, a repeated name, bad percent-encoding, or a query plus a payload throw), `on` (handlers share one shell subscription per topic; `subscription.ready`), `channel.open/onOpened/list/broadcast`, handles with `emit/on/onClosed/close`. Unclaimed inbound handles and messages sent before a handler are kept (up to 256, then the channel closes); a closed handle keeps its terminal record for a late `onClosed`. |
| Not held while unattended | `inc.emit` left `NappletActingRequests`: holding some `inc.*` messages and not others reordered them (a held emit landed after the channel close sent behind it), and a napplet in the background could never answer a peer. INC signs nothing and leaves the app nowhere; what a receiving napplet does with a message is held on its own surface. |
| Malformed requests | A known `inc.*` request that fails to decode and has an id gets `<type>.result` with `error`; an unknown `inc.*` type stays silent. A subscribe without an id still subscribes. |

## Verification

- `NappletIncBusTest` (11), `NappletRequestRouterTest`, `NappletCapabilityTest`, `NappletActingRequestsTest`.
- `CefNappletSessionTest.twoNappletsTalkOverInc` in real Chromium (`AMETHYST_CEF_TEST=1 xvfb-run -a ./gradlew :desktopApp:test --tests '*twoNappletsTalkOverInc'`): a convention-URI emit, the payload, the runtime sender id, a channel opened back to the sender, messages both ways, and the close reaching the other end.

## Not done

- `intent` (Feed Manager requires it), `common` and `upload` (Emoji Pack Creator) stay unsupported, so
  those napplets are still refused.
- No user-facing ACL on channels: any live napplet of the same account that declared `inc` may open
  one to another (NAP-INC leaves channel ACL to the shell). Only a peer's endpoint identifier, learned from an
  event it sent, lets a napplet target it.
