Appearance
Grant and revoke
A grant is something the user gave and can take back. This page covers what the shell asks, what each answer changes, and the Kernl calls behind the panel.
What the user sees
Before the first run. After kernl.resolve verifies the applet, the launcher compares its requires and optional lists with the baseline and with what this identity already has (kernl.grants.get(identity)). Each domain is a switch: the baseline fixed on, everything else pre-set to what this identity was granted before, so a first run starts with every grantable switch off. That includes your account (identity) when the applet names it: an applet is not told who is signed in until the user turns that switch on, here or later in the grants panel. Required domains are marked as such and a warning names the ones still off. Run applies the switches with allowDomain and revokeDomain, then calls kernl.plan. A required domain left off appears in MountPlan.missingRequired; the applet mounts anyway and sees that domain as unavailable.
During use. kernl asks before signing a destructive change. Whether the applet publishes through relay.publish (signed through ShellAdapter.auth.getSigner(windowId)) or outbox.publish (gated in kernl's outbox service wrapper), the write passes createSigningConsent: for a destructive kind (any replaceable or addressable kind, a deletion or a request to vanish) it looks up the current version, in the event store and then on the outbox and discovery relays for up to 4 seconds, runs describeChange on the unsigned draft, calls host.presentConsent and waits. The host shows the title ("Update your profile", "Change who you follow", "Delete 2 posts" or "Delete 2 items", "Ask services to erase your account", "Change your connections list", "Change the list …", "Replace one of your records (type N)") and one line per change with the value before and after; when the current version could not be loaded, the first line says so. The applet is named by its display name and the short hash of its build. Allow resolves the prompt with true and the event is signed; dismissing it resolves false, the write fails with denied:consent, and nothing is signed or published. Routine writes (posts, reactions and other kinds outside the destructive set) are signed without a prompt, and the shell's own signing is never gated. Nothing is remembered; the next destructive write asks again. kehto's runtime has no destructive-signing consent of its own; this is kernl's.
In the grants panel. Every domain the shell offers, as a switch; a Block applet button (Unblock applet once blocked); and, when the guard has quarantined the window, Release from quarantine. The panel reads kernl.grants.get(identity) and re-renders on kernl.grants.changes$.
What a grant is keyed by
AppletIdentity, { dTag, hash }. A new build has a new hash and no grants; the launcher asks again, showing the new manifest's title, publisher and domains. See Identity and grants.
The calls
kernl.grants is the Grants interface; createGrants implements it over bridge.runtime.aclState with a record per identity at kernl:grants:v1.
ts
import type { AppletIdentity, Kernl } from '@kernl/core';
declare const kernl: Kernl;
declare const identity: AppletIdentity;
kernl.grants.get(identity); // GrantRecord | undefined
kernl.grants.allowDomain(identity, 'relay'); // throws invalid:domain when the shell does not offer it
kernl.grants.revokeDomain(identity, 'relay'); // throws denied:baseline-domain for shell, inc, theme, storage
kernl.grants.block(identity);
kernl.grants.unblock(identity);
kernl.grants.setStateQuota(identity, 4096);
kernl.grants.availableDomains(); // what buildShellCapabilities(adapter) offers
kernl.grants.baselineDomains(); // ['shell', 'inc', 'theme', 'storage']
const subscription = kernl.grants.changes$.subscribe((record) => console.log(record.identity.dTag, record.domains));
subscription.unsubscribe();
for (const handle of kernl.applets()) {
if (handle.summary.identity.hash === identity.hash) handle.close();
}GrantRecord is { identity, domains, capabilities, blocked, stateQuotaBytes }. domains are the ones the user allowed beyond the baseline; capabilities is capabilitiesForDomains([...BASELINE_DOMAINS, ...domains]).
Every change commits the record, which does three things in order: persist it to kernl:grants:v1, write the full capability set into kehto's ACL (each capability in ALL_CAPABILITIES granted or revoked explicitly, then block or unblock, then acl.persist()), and emit the new GrantRecord on changes$. A call that changes nothing (allowing a domain twice, revoking one that was never allowed, allowing a baseline domain) commits nothing and emits nothing.
allowDomainrefuses a domain the shell does not offer withinvalid:domain. A newly allowed domain is in the ACL at once; a running applet was told its environment at mount and is not told again, so the domain appears in itsshell.initthe next time it is opened.revokeDomainrefuses the baseline withdenied:baseline-domain. The ACL loses the domain's capabilities at once, and kehto refuses the applet's next envelope that needs one of them.blockkeeps the record's domains but denies every capability in the ACL and makescreateEnvironmentResolveradvertiseshellalone at the next mount. It does not close an open window, and neither does the panel; the open window keeps running under the ACL's denials until it is closed.unblockrestores the record's capabilities.setStateQuotawrites the quota for thestoragedomain into the ACL and emits a change.ensure(identity), on theKernlGrantstype the shell holds internally, is called bykernl.planso an applet with no record still gets an explicit, baseline-only ACL entry.
Closing and quarantine
Closing a window is handle.close(): bridge.revokeNapplet(windowId, 'host'), which closes the port and drops kehto's sessions for the frame, then the frame is removed and the guard forgets the window. Grants are untouched. When the guard quarantines a window, createKernl's onQuarantine does the same and tells host.onRevoked(windowId, 'quarantine'); the grants panel's control for a quarantined window calls kernl.guard.release(windowId).
Bridges
A resource request from an applet that holds the resource domain is resolved by the bridge registry in the applet's name (resourceInitFor). The registry createKernl builds with createDefaultBridges() has no authorize hook, so the HTTP bridge's SSRF policy is the only check. A host that wants to ask per host or scheme passes its own registry, built with createDefaultBridges({ authorize }), in KernlOptions.bridges.
What is never granted
- A loosened CSP. Not a grant; not possible.
- Access to
window.nostr, raw keys or theEventStore. - A global grant across applets.
- A grant by a developer flag or a URL parameter.
- A domain the shell does not serve.
DOMAIN_CAPABILITIEShas rows forconfig,cvm,upload,intent,fs,llmandcache, butavailableDomains()does not list them andallowDomainrefuses them withinvalid:domain. (keysandmediaare listed, because kehto's runtime serves them natively, though kernl wires nothing behind them.)