# Run the shell

This tutorial takes you from nothing to an applet running in a window, signed in with your own account, and shows the `Kernl` calls behind each step. The hosted preview takes a minute and needs no install; running the shell from source takes about ten minutes and needs access to the source repository.

> **Alpha**
>
> The shell runs, but it is a first draft. Expect rough edges in the UI and read [Alpha status](https://kernlize.github.io/docs/alpha-status.md) for what is not there yet.

## 0. Try it without installing anything

Open the hosted preview:

```
https://kernlize.github.io/app/?preview=1
```

The preview runs the same shell UI over an offline stand-in for the `Kernl` contract, with two sample applets and no network ([D-011](https://kernlize.github.io/docs/decisions.md#d-011)). It behaves like the live shell where it stands in for it: it asks before a write only for the kinds `@kernl/core` rates destructive, using the core's own `isDestructiveKind` and `describeChange`; its guard observes and stops an applet only by quarantine; it starts in `strict` on every load; and its sample applets remove WebRTC the way kehto's prelude does. Messaging here goes nowhere; in the live shell private and legacy messages are sent for real once you sign in with an account that can encrypt ([Alpha status](https://kernlize.github.io/docs/alpha-status.md#messaging-providers-in-the-live-shell)).

**You should now see** a card headed "Who is at the keyboard?" with a **Try the preview** button. Click it, then **Open an applet**; under "Sample applets" the launcher lists *Notes* and *Pulse*, each with an **Open** button. Open *Notes*, turn on the *network* switch (it starts off, like every grant on a first run), and click **Run**. It appears in a window with buttons to try:

- **Publish a note** goes out without a prompt: a note is routine.
- **Update your profile** and **Delete a note** ask first: the consent dialog shows what changes, and nothing happens until you answer.
- **Flood the shell** sends 40 network reads at once; the guard panel (*Guard · strict · N* in the bar) records the burst.

Without `?preview=1`, `https://kernlize.github.io/app/` is the live shell: everything below from step 3 on works there too, against real relays.

## Prerequisites for running from source

- Access to the kernl source repository. It is private during the alpha; the packages are not on npm either ([Packages](https://kernlize.github.io/docs/packages/index.md#install)). Without access, use the hosted shell above.
- Node 22 or newer and pnpm 10. The repository pins `pnpm@10.24.0` through `packageManager`; `corepack enable` picks it up.
- A way to sign: a browser extension that speaks NIP-07, a remote signer that speaks NIP-46, or nothing, in which case the shell creates a local account protected by a password. kernl never asks for a private key directly and never stores one in plaintext.
- The address of an applet to open, which you get from the applet's publisher (an `naddr1…` or `nevent1…` string, or a coordinate). The [napplet.run app store](https://napplet.run/apps/) lists published applets; step 4 gives one you can try.

## 1. Clone, install and build

```bash
git clone https://github.com/kernlize/web.git kernl
cd kernl
pnpm install
pnpm build
```

`pnpm install` links the four workspace packages, the docs and the app. `pnpm build` compiles the packages into their `dist/` folders, which is what the app imports; skip it and the dev server cannot resolve `@kernl/…` ([Troubleshooting](https://kernlize.github.io/docs/guides/troubleshooting.md#pnpm-site-dev-before-pnpm-build)). Nothing is downloaded at runtime later; the shell's CSP would not allow it.

**You should now see** `pnpm build` finish with a `dist/` folder in each of `packages/guard`, `packages/messaging`, `packages/bridges` and `packages/core`.

## 2. Start the site

```bash
pnpm site:dev
```

This runs the Astro dev server for `apps/web`. The landing site is at the root and the shell at `/app/`:

```
http://localhost:4321/app/
```

The first load shows the shell with no account. `boot.ts` has called `createKernl({ host })`, where `host` is the page's `KernlHost`: `localStorage`, the consent dialog, the notices, link confirmation and window visibility. The guard is already running in its `strict` profile and the relay tiers are `DEFAULT_RELAY_TIERS`. Add `?preview=1` to the URL to run the offline preview instead.

**You should now see** the sign-in card, "Who is at the keyboard?", with a list headed "Sign in another way". If instead a notice says "Running the preview" with "The live shell could not start (…)", the core failed to load; see [Troubleshooting](https://kernlize.github.io/docs/guides/troubleshooting.md).

## 3. Sign in

Under "Sign in another way", pick one:

- **Use a browser extension** (button **Use**, shown only when an extension is present). The shell wraps the extension in an applesauce `ExtensionAccount`; your key stays in the extension.
- **Connect a remote signer** (button **Connect**). Either paste the code your signer app shows (a `bunker://` URI) into "Pairing code from your signer app" and click **Connect**, or click **Or pair from the signer**: the shell shows a QR code to scan with your signer app and a **Copy pairing code** button for pasting it there instead (a `nostrconnect://` URI), then waits with "Waiting for the signer to accept…". Either way the shell holds a `NostrConnectAccount` over an applesauce `NostrConnectSigner` that talks through the shell's own `RelayPool`.
- **Read-only** (button **Choose**). Paste a public key into "Account id" and click **Continue**. The session is `readOnly`: applets you grant *your account* see that public key, and every write is refused because `createAuthHooks` returns no signer.
- **Create a local account** (button **Create**). Choose a password, repeat it, and click **Create and sign in**. The shell generates a key inside an applesauce `PasswordSigner` and persists it as a `PasswordAccount`, whose serialised form is NIP-49 encrypted. A plaintext-key account is refused by `KernlAccountManager` with `denied:plaintext-key-persistence`.

Each path ends in `kernl.login(account)`, which adds the account to the `AccountManager` if it is new, makes it active and emits a `Session` on `kernl.session$`. Accounts and the active choice persist at `kernl:accounts:v1` and `kernl:active-account:v1`, so the next load lists the account under "Remembered accounts" with a **Sign in** button (a password account asks to be unlocked). Nothing is fetched at sign-in: profile, follows and relay list are loaded from the `discovery` tier the first time an applet or the shell asks for them.

```ts
import { createKernl, type KernlHost } from '@kernl/core';
import type { IAccount } from 'applesauce-accounts';

declare const host: KernlHost;
declare const account: IAccount;

const kernl = createKernl({ host, relays: { apps: ['wss://relay.napplet.soy'] } });

const session = await kernl.login(account);
session.readOnly; // true for a ReadonlyAccount; writes are then refused
```

**You should now see** the desktop: an **Open an applet** button, "Workspace 1 · 0 windows" with a button naming the current layout (`floating` or `tiled`; click it to switch), "No applets open. Press Alt + / or use the button above.", and on the right *Guard · strict · 0*, **Messages** and your account name, whose menu holds **Settings** and **Sign out**. At 720 px wide or less the bar shows **Open** and a **Menu** button instead; the menu sheet holds the guard, messages, layout, minimised windows, **Settings** and **Sign out**, and each window fills the screen.

**Settings** lists the four server tiers (Discovery, Applet catalogue, Your connections, General) by display name, so `wss://relay.damus.io` reads `damus.io`; add one by typing it into "Server address" (a bare hostname gets `wss://`). Under "Bridges" it names each bridge by what it does, *Downloads (web)*, *Downloads (from peers)*, *Downloads (archives)*, *Sharing* and *Mail*, with an "Availability" of "Ready" or "Needs a backend" and what the other side learns. "About" shows the shell's version, `kernl 0.1.0`, and whether it is live or the offline preview.

## 4. Open an applet by address

Click **Open an applet** (or press Alt+/), paste the address the applet's publisher gave you into "Applet address", and click **Look up**.

**An applet you can try.** *Freeze Frame Response*, a physics puzzle, is published by a third party in the [napplet.run](https://napplet.run/apps/) catalogue, not by kernl. It needs no grants:

```
naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqythwumn8ghj7un9d3shjtnwv9c8qmr9wsh8xmmeqqxkuttzvy6nyefnxpjr2e3jqg5uk8
```
 `kernl.decodePointer` accepts an `naddr1…`, an `nevent1…` or `note1…`, a bare 64-hex event id, any of those behind `nostr:`, and a coordinate in either `<pubkey>:<kind>:<identifier>` or `<kind>:<pubkey>:<identifier>` order. Anything else is `invalid:pointer`, shown as "That does not look like an applet address."

`kernl.resolve(pointer)` fetches the manifest from the pointer's relay hints plus the `apps` tier, verifies its signature, fetches the artifact bytes by hash and verifies them. Nothing runs before verification passes; a tampered artifact is `invalid:blob-hash-mismatch`, a missing one `unavailable:blob`, a manifest nobody serves `unavailable:manifest`. When it passes, the launcher shows the `AppletSummary`: title, publisher, description, and under "What it may use" the domains it `requires` and lists as `optional`, each as a switch.

Baseline domains (`shell`, `inc`, `theme`, `storage`) are marked "always on" and cannot be switched off. Every other switch starts where this identity's grants already are, which on a first run is off. That includes *your account* (`identity`): until you turn it on, the applet is not told who is signed in; a required domain is marked "the applet says it needs this", and while one is off the launcher warns "Without …, this applet may not work. You can still run it." Click **Run**. The launcher calls `kernl.grants.allowDomain` for each switch you turned on and `kernl.grants.revokeDomain` for each you turned off, then `kernl.plan(summary)`, and opens a window for the `MountPlan`. If `plan.missingRequired` is not empty, the applet sees those domains as unavailable.

<!-- sample:continues -->
```ts
declare const address: string; // from the applet's publisher

const summary = await kernl.resolve(kernl.decodePointer(address));
summary.requires; // advisory, never a grant
const plan = await kernl.plan(summary);
plan.missingRequired; // required domains the user has not allowed
```

`kernl.plan` also refuses an applet whose verified markup contains a frame element of its own, with `denied:nested-frame`, and puts kernl's frame lock in the `srcdoc` right after kehto's prelude ([D-014](https://kernlize.github.io/docs/decisions.md#d-014)). The window's iframe is mounted with `kernl.mount(plan, iframe)`: the frame gets `sandbox="allow-scripts"` and the controller-denying `allow` policy, is registered with its verified identity, has its environment frozen, is watched for a second load, and only then receives `srcdoc`. The applet sends its `hello`, which passes through the guard pipeline on its way to kehto's bridge, and `shell.init` tells it the domains it has.

**You should now see** a window titled with the applet's name, with **Grants**, **Minimise** and **Close** in its title bar. It shows "Starting …" briefly, then the applet.

## 5. Watch the guard

The desktop bar shows *Guard · strict · N*, where N counts guard events this session. Click it to see the profile, the eleven protections with the limit each one applies in the current profile (`kernl.guard.mitigations()`), and "Recent events" (`kernl.guard.events$`). Each event is a sentence naming the applet, what happened and why, for example "Applet Notes went over a limit: too many publishes", which is the guard's `rate:relay:publish`. A quarantined applet has its binding revoked and its frame removed; the window shows "This applet was stopped." and a notice says so.

Over-budget requests are refused before they are handled, by kehto's runtime firewall with the rates kernl installs at mount, and so are oversized downloads and queries; the guard panel shows kernl's count of the same budgets. Changing the profile here calls `kernl.guard.setProfile(profile)`. kernl's own counting switches at once; windows already open keep the refusal rates they were mounted with until you open them again, and the choice is not remembered across reloads. [Change the guard profile](https://kernlize.github.io/docs/how-tos/change-guard-profile.md) has the details and the numbers.

**You should now see** the guard panel with `strict (default)` selected and, for a quiet applet, no events.

## 6. Grant, revoke, block

Each window's **Grants** button opens its grants panel. It lists every domain the shell offers (`kernl.grants.availableDomains()`) as a switch, with the baseline fixed on. Turning a switch on is `kernl.grants.allowDomain(identity, domain)`; off is `kernl.grants.revokeDomain(identity, domain)`. Both write kehto's ACL at once, so the applet's next call on a revoked domain is refused; the domains it was told about at mount do not change until it is opened again. Under "Standing", **Block applet** is `kernl.grants.block(identity)`: the ACL denies everything and the next mount advertises `shell` alone (**Unblock applet** reverses it). Closing the window is `handle.close()`, which revokes the binding and removes the frame; the grants stay, keyed by identity, for the next time you open the same build.

[Grant and revoke](https://kernlize.github.io/docs/how-tos/grant-and-revoke.md) covers each call and what it changes.

**You should now see** the grants panel's "Standing" rows: the storage quota, "Blocked: no" and the number of guard events this window has had.

## Where next

- [Grant and revoke](https://kernlize.github.io/docs/how-tos/grant-and-revoke.md) for what each prompt means and what it changes.
- [Applets under kernl](https://kernlize.github.io/docs/concepts/applets-under-kernl.md) if you came here to test your own applet.
- [Embed kernl core](https://kernlize.github.io/docs/how-tos/embed-kernl-core.md) to run the core in your own page instead of `apps/web`.
- [Deploy](https://kernlize.github.io/docs/how-tos/deploy.md) to publish your own instance of the shell.
