Skip to content

Alpha status ​

kernl is a first draft. This page says what runs, what is a contract without an implementation, and which upstream gaps are open. It is updated in the same pull request as the code it describes.

What works ​

AreaPackageState
Guard@kernlize/guardThree profiles (createGuardConfig, applyNappletRules), envelope classification, ingress flood control with quarantine, visibility gate, focus-aware firewall over @kehto/firewall's evaluate(), the fixed zero-network CSP, an insertion lock on top of kehto's nested-context lock (D-014), eleven mitigation descriptions for the UI. Pure, fully specified in packages/guard/features/.
Messaging@kernlize/messagingMessagingProvider, MessagingBackend, MessagingRegistry with saved-preference restore, the composite backend with provider:innerId namespacing, describeProvider rows, a complete in-memory backend (createMemoryBackend) used by the preview and the tests, and three providers (private, legacy, ratchet) built with defineProvider from backends the shell injects. The network backends for private and legacy are in @kernlize/core (createPrivateMessagesBackend, createLegacyMessagesBackend) and are wired in the live shell (below).
Bridges@kernlize/bridgesLocator parsing for the web schemes, magnet:, nzb:, slsk: and mailto:, the Bridge contract, the registry with its authorize hook and outcome events, a complete HTTP bridge with SSRF policy, the four backend-driven bridges up to their backend interface, createHandoffBackends, which serves all four by opening the locator in an app on the user's device, createWebTorrentBackend, which downloads magnet links inside the shell page over WebRTC, createBridgeResourceResolver for kehto's resource service, and describeBridge rows.
Composition@kernlize/corecreateKernl over one applesauce EventStore, RelayPool and AccountManager, with kehto's real createShellBridge exercised end to end in kernl.feature: an applet is decoded, resolved, planned and mounted, binds its port through the guarded handleWindowMessage, completes shell.ready, is answered by the theme, identity and resource services, and is quarantined and revoked when it floods. Relay tiers, the applesauce adapters, the guard pipeline and runtime firewall rules, grants over bridge.runtime.aclState, consent summaries, the default registries and the layouts each have their own feature file.
Shell UIapps/webLanding site at / and the shell at /app/: sign-in (browser extension, remote signer, read-only, password-protected local account), open by address with the required and optional domains as choices, windows, a grants panel per window, a shell-wide guard panel with the event log, the provider picker, and the offline preview behind ?preview=1 (D-011), hosted at kernl.run/app/?preview=1.

"Works" means the behaviour has a scenario in packages/*/features/, the scenario passes, and the page describing it exists. It does not mean the API is stable. Every package is 0.1.0; on 0.x a breaking change is a minor bump.

Messaging providers in the live shell ​

createKernl registers all three providers and, unless KernlOptions.messagingBackends overrides them, wires two real backends from @kernlize/core: createPrivateMessagesBackend for private (NIP-17 gift-wrapped messages) and createLegacyMessagesBackend for legacy (NIP-04 direct messages). Both sign and encrypt with the active account's signer and nothing else (D-015). Availability follows the active account, and an unavailable provider reports a reason from MESSAGING_UNAVAILABLE_REASONS:

ProviderAvailable whenReason otherwise
privateThe active account's signer exposes nip44 (browser extension, remote signer, password-protected local account)sign-in-required with no active account; signer-cannot-encrypt when the signer has no nip44 (a read-only account)
legacyThe active account's signer exposes nip04sign-in-required with no active account; signer-cannot-encrypt when the signer has no nip04
ratchet (experimental)The active account's signer exposes nip44, which seals the session state kept on this devicesign-in-required with no active account; signer-cannot-encrypt without nip44
group (experimental)The active account's signer exposes nip44, which opens invitations and seals the group state kept on this devicesign-in-required with no active account; signer-cannot-encrypt without nip44

private sends to the recipient's kind 10050 inbox relays, falling back to the sender's outbox and super tiers when the recipient publishes none, and wraps a copy to the sender so the conversation reads the same on every device. legacy is metadata-exposing: sender, recipient and timing are public on the event, and the picker says so. Messages are shown by an applet over the dm domain grant. kernl ships one, pinned by hash and opened without a lookup; it starts with the grants it needs and the user can withdraw each (D-027). private and legacy are switched on for someone who has never chosen, the user orders the providers in Settings, and a reply to someone who last wrote through another provider is asked about first. The preview runs the same applet over the in-memory backend.

Known gaps in messages: the applet cannot say which provider carried a message, so after the question is answered the choice is visible only in Settings; a request another applet hands over to open a conversation opens Messages but not yet that conversation (the applet's toolkit version has no delivery hook); an applet keeps the colours it was opened with when the shell's theme is switched; and the toolkit gives up on a send after thirty seconds, so a reply question left open longer shows the message as still sending until the shell reports it.

What does not work yet ​

  • In-shell transfers for NZB, Soulseek and mail. Magnet links download inside the shell page by default (D-017): a WebTorrent client loaded on the first download reaches browser peers over WebRTC, and the Downloads panel shows progress and lets the user pause, resume, remove and save. It reaches only peers that speak WebRTC, so a link whose swarm has none stays "Looking for peers"; the user can switch to "Open in an app on your device" in Settings. Downloads live in memory and end when the tab closes. NZB, Soulseek and mail still work only by hand-off, after the user confirms leaving kernl, because their protocols need raw TCP sockets a page cannot open. A job result from a hand-off is an id, not a transfer kernl can track, and it is reported even when the user declines to leave. See Bridges.
  • Ratchet sessions, beyond one to one on a shared relay. ratchet works in the live shell (D-016) but is experimental: sessions are one to one, both sides must share an outbox or super relay, the conversation list is in memory and empties on reload (sessions and keys survive, sealed to the account), the first use waits up to the library's 12 s for the account's device list, and an extension or remote signer is asked to decrypt each stored value once per page load and to encrypt each write. The preview reports it unavailable with no-ratchet-runtime. See Messaging.
  • Group sessions, beyond the basics. group works in the live shell (D-018) but is experimental and runs an alpha library: messages are kept in memory, so a reload empties each group's history (membership and secrets survive, sealed to the account); an account is one member of a group, so only one of a person's devices joins it; people can be added only when they have published a key package, which kernl does when the provider is first used; there is no way yet to name a group, add or remove members after it starts, or leave it; and an extension or remote signer is asked to decrypt each sealed store once per page load and to encrypt each write. Relay-based groups (NIP-29) are not offered, because they are not end-to-end encrypted. The preview reports group as needing a sign-in. See Messaging.
  • Web fetch from the browser shell, beyond the simple case. The HTTP bridge runs in the page, so it is subject to CORS, and an opaque redirect it cannot inspect is refused with denied:redirect-unverifiable. Web fetch works from the shell for https origins that permit cross-origin reads and answer without a redirect. Hostnames are checked as literals only; no resolver is wired. See Bridges.
  • Per-host bridge consent. createKernl builds the bridge registry with createDefaultBridges() and no authorize hook, so a resource request from a granted applet is checked by the HTTP bridge's SSRF policy and nothing else. A host that wants a prompt per host or scheme passes its own registry in KernlOptions.bridges.
  • Per-applet guard profiles. GuardFacade.setProfile rebuilds the pipeline for the whole shell and is not persisted. The per-applet, remembered override D-003 describes is not in this draft.
  • Mobile layouts beyond the basics. The layouts are desktop tilings over @kehto/wm (Layouts). At 720 px wide and below the shell switches to a compact layout: every window fills the desktop, and the bar keeps Open and moves the guard, messages, layout, minimised windows, settings and sign-out into a Menu sheet; panels open as full-width sheets. There are no gestures. Every open window is reported to the guard as visible there, although only the front one can be seen: kernl's window manager does not yet treat a window wholly covered by another as hidden (Layouts).
  • Multi-account switching. AccountManager holds several accounts and the sign-in panel lists them, but switching means signing out and in.
  • Grant persistence across devices. Grants, relay tiers and messaging preferences persist in the host's storage only. There is no sync. Every key is listed in Persistence.
  • Published packages. Nothing is released yet, neither @kernlize/* on npm nor @kernl/* on JSR; they are used from a clone of the source repository, which is private during the alpha. See Packages.
  • Source links in the API reference. typedoc.json sets packageOptions.disableSources: true, so the generated reference has no "Defined in" source links. They would point at the source repository, which is private during the alpha.

The Roadmap orders this list.

Upstream gaps ​

kernl composes kehto rather than forking it (D-001). The four seams kernl filed against kehto/web (#397, #398, #399, #400) are closed in @kehto/shell 0.32.0, @kehto/runtime 0.28.0 and @kehto/firewall 0.7.0, and kernl uses them (D-020):

  1. Firewall hooks through ShellAdapter (#397). kernl passes getFocusContext (only focused), onFirewallEvent, which feeds the guard log without the applet's message, and firewallPersistence, which keeps the runtime firewall's configuration under kernl:firewall:v1 in the host storage.
  2. ACL default policy (#398). The adapter sets aclDefaultPolicy: 'restrictive'. KernlGrants.ensure grants the baseline and the user's domains and revokes only what an applet's stored entry holds beyond them.
  3. A hook that can hold or drop port traffic (#399). filterNappletMessage runs kernl's guard pipeline for every port envelope. What the guard will not let through is dropped, with no reply to the applet. A hidden window's requests are held and released with releaseDeferred when it is visible. See Guard.
  4. Unfocused multiplier and burst guard setters (#400). On boot and on every profile change kernl sets the profile's unfocusedMultiplier and burstGuard on the runtime firewall and reinstalls the rates of open windows.

kernl's own layers stay in front of kehto where kehto cannot see what they see: envelope size for downloads and queries, window visibility, a separate deletion budget, and flood quarantine. The focus-aware firewall overlaps the runtime now and stays so that kernl's own decisions remain a complete source of guard events (D-020).

One change upstream is pending. kehto/web#336 makes kehto's firewall a shared approval authority and moves rate limiting into a separate runtime budget. On a host that configures it, the multiplier and burst setters, onFirewallEvent and firewallPersistence are ignored, and getFocusContext is read for focused only. kernl keeps every use of those in one module, packages/core/src/guard/runtime-rules.ts, and will move it to permissions.setRateLimit and onRateLimit when #336 is released. aclDefaultPolicy, filterNappletMessage and releaseDeferred are unchanged by #336.

One gap is new with intents. kehto's ACL (@kehto/acl 0.20.3, and main) maps intent.deliver to no recipient capability, so the runtime's recipient-checked sendToEligibleNapplet refuses every delivery. kernl delivers over the target's bound port with originRegistry.postToNapplet, as kehto's reference host Paja does, after checking the target's live session holds intent:read (D-026). It is to be raised upstream.

A fifth observation, not a gap kernl needs closed: the transport internals createNip5dTransport and createShellReadyState are not exported, so kernl uses createShellBridge unchanged.

One mitigation goes beyond kehto: @kehto/shell 0.30.0 ships experimental.nestedContextLock, which stops an applet creating a child frame whose fresh realm still has WebRTC, and planMount always enables it. kehto's lock guards creation only, and four creation routes get past it (a customized built-in constructed with new, XSLT, and a namespace-prefixed XHTML element brought in with importNode or adoptNode), so @kernlize/guard adds an insertion lock on top until kehto guards insertion. The routes are to be reported to kehto (D-014).

Corrections ​

  • Rate enforcement (corrected 2026-11-03). Earlier drafts of these docs, the README and D-012 said kehto's runtime firewall refused envelopes over the profile's rates. It did not: applyRuntimeFirewallRules also set each applet's runtime policy to allow, and kehto's evaluate() returns pass for an allow policy before it looks at any rate, matcher or burst. No rate was refused before dispatch; only kernl's reports and quarantine applied. The policy call is removed, the rates and kehto's startup burst guard now refuse before dispatch, and D-012 is rewritten to match.
  • Size caps (corrected 2026-11-03). The guard's size matchers were described as able to report matcher:size:<class> in the shell. They cannot fire there: the ingress guard rejects the same envelopes first. Oversized downloads and queries were reported but still handled; they are now refused before dispatch.
  • Focus tightening, bursts and hidden windows (changed 2026-11-04). These were reported to the user but still handled. They were then refused before dispatch the same way size caps were (refuseCurrentDispatch): a request over the smaller unfocused budget, a request in a burst above the per-second rate, and any non-control request from a hidden window got a firewall: error. Control envelopes were never refused. See D-012.
  • Dropped and held, not refused (changed with kehto 0.32.0). kernl now decides each port envelope through kehto's filterNappletMessage. Oversized requests, bursts and requests over the unfocused budget are dropped and get no reply; a hidden window's requests are held and answered once it is visible. The runtime's startup burst is the profile's (8 operations in a window's first second in strict) rather than kehto's default of 20 in 3 s. See D-020.

Versions this draft is built against ​

DependencyRange
@kehto/acl^0.20.3
@kehto/firewall^0.7.0
@kehto/runtime^0.28.0
@kehto/shell^0.32.0
@kehto/services^0.30.1
@kehto/nip^0.8.0
@kehto/wm^0.0.2
applesauce-*^7.0.x
@napplet/core>=0.38.0 <0.39.0 (@kernlize/core); >=0.37.0 <0.39.0 (@kernlize/guard, @kernlize/messaging)
@napplet/nap>=0.37.0 <0.38.0

The protocol itself is a draft. NIP-5D is nostr-protocol/nips PR #2303 and the NAP documents live at napplet/naps. kernl keeps no local copy of either; see Spec conformance.

MIT licensed. Protocol authority: NIP-5D (nostr-protocol/nips PR #2303) and the NAPs (napplet/naps).