Introduction
Naming: The project was renamed from Layer36 to Krate (by Krate Labs) in July 2026. Code, commands, and
krate:*API namespaces keep the legacy name until the scheduled code-level rename lands. Docs may use both names during the transition.
Krate is an attempt to make apps portable in the way files are portable.
The goal is simple to say and hard to build:
Write one app. Run it on Windows, Linux, macOS, Android, iOS, ChromeOS, and the web through the same Krate runtime model.
Today every platform has its own SDK, app format, permission model, UI rules, and hardware APIs. That is why the same product often becomes six different codebases. Krate puts one common layer in the middle.
The Core Idea
An app compiles to WebAssembly. The app does not call Windows, Android, or iOS directly. It calls Krate APIs. Each host then translates those calls into the native platform underneath.
flowchart LR
A["App source"] --> B["WASM component"]
B --> C["Krate runtime"]
C --> D["Krate APIs"]
D --> E["Host adapter"]
E --> F["Native OS and hardware"]
classDef done fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
classDef current fill:#fff3bf,stroke:#b7791f,color:#2d2100,stroke-width:2px;
classDef pending fill:#eeeeee,stroke:#999999,color:#777777,stroke-width:1px;
class A,B,C,D current;
class E current;
class F pending;
What exists today is a pre-alpha runtime that already delivers the founding
claim on desktop. Krate runs WebAssembly components through the CLI on Linux,
macOS, and Windows from byte-identical artifacts, routes app calls through
UAPI modules, and enforces manifest-declared capabilities before any host
access. The first GUI component opens a real native window on macOS (native
button and text field, human-verified click round trip) and real winit
windows on Linux and Windows — proven in CI — with the first drawn-widget
pass painting the UI in those windows. AI agents can execute components in
the sandbox through an embedding API, krate run --json, and an MCP
server, receiving permission decisions as data. Mobile hosts, bundles, and
distribution are still later work.
What We Have Built So Far
- A Rust workspace and public GitHub project.
- A
krateCLI withrun,version,doctor, and manifest commands. - A Wasmtime based runtime that loads WebAssembly components.
- Phase 2 UAPI slices for
io,fs,net,time, andlocale. - UCap manifests, launch grants, policy checks, and grant logs.
- Sample apps for
krate-clock,krate-cat, andkrate-curl. - CI and evidence scripts for samples, UCap enforcement, language variants, adapter boundaries, benchmarks, and exit readiness.
- The Phase 3 GUI path:
ui/gfx/audioWIT drafts, a widget tree with Taffy layout, a UCap-gated UI dispatcher, native AppKit windows and widgets on macOS, winit windows on Linux and Windows, the first drawn widget pass, and thekrate-hello-guisample (import-pure, runs on all three OSes,sh scripts/demo-hello-gui.sh). - The agent surface:
krate_runtime::embed,krate run --json(schemakrate.run.v1), andkrate-mcp-server. - A prerelease,
v0.1.0-rc1, with platform archives and checksums. - Docs, threat models, benchmarks, architecture records, and Phase 2 exit evidence pages.
What We Have Not Built Yet
- The full GUI surface: the vello renderer, richer widgets, text input and
IME, accessibility trees, dialogs, menus, and the
krate-notesflagship (windows and first widgets exist; the rest of the surface does not yet). - Graphics (
gfx) and audio beyond honestunsupportedstubs; sensors, identity, and production app lifecycle APIs. - A
.kratebundle format. - Mobile hosts.
- Security strong enough for untrusted third party apps.
- A finished developer SDK or app store.
- A formally frozen Phase 2 UAPI.
- External developer validation.
So the honest status is: the runtime proof is real on all three desktop OSes — windows included — but the platform is not done.
Why WebAssembly?
WebAssembly gives Krate a portable, compact, sandboxed program format. The Component Model gives it typed interfaces between app code and host code. WIT lets us describe those interfaces in a language neutral way.
Krate is the missing product layer around those pieces: APIs, permissions, host adapters, tools, packaging, and distribution.
Vision
Krate exists because app portability is still broken.
A text file can move from one device to another. A photo can move. A web page can move. A serious native app usually cannot. It has to be rebuilt for each platform, then tested, packaged, signed, distributed, and maintained again.
Krate is a plan to put one runtime layer between apps and operating systems.
The 6 x 6 Problem
The dream is a full matrix:
| App origin | Runs on |
|---|---|
| Windows app | Windows, Linux, ChromeOS, Android, macOS, iOS |
| Linux app | Windows, Linux, ChromeOS, Android, macOS, iOS |
| Web app | Windows, Linux, ChromeOS, Android, macOS, iOS |
| Android app | Windows, Linux, ChromeOS, Android, macOS, iOS |
| macOS app | Windows, Linux, ChromeOS, Android, macOS, iOS |
| iOS app | Windows, Linux, ChromeOS, Android, macOS, iOS |
Krate does not magically run every existing native app today. The path is more practical: define a new portable app target that can become good enough for new apps, then build bridges and tooling over time.
The Bet
The same pattern has worked before:
| Old problem | Middle layer | What changed |
|---|---|---|
| Many CPUs | LLVM IR | One compiler front end can reach many chips. |
| Many servers | JVM and .NET bytecode | One backend app can run on many OSes. |
| Many browsers | HTML, CSS, and JS | One site can reach almost every device. |
Krate tries to bring that pattern to native apps:
- WebAssembly is the portable program format.
- UAPI is the standard app API.
- UCap is the permission model.
- Host adapters translate Krate calls into native OS calls.
- A bundle format and marketplace make apps installable.
Why This Might Work Now
- WebAssembly is stable and widely understood.
- The Component Model makes host APIs cleaner than raw WASM imports.
- Rust, Go, TypeScript, C, and other languages can target WASM.
- Desktop and mobile hardware are closer than they used to be.
- Developers are tired of maintaining the same product in many stacks.
What Success Looks Like
At v1.0, a developer should be able to build one Krate app and ship it to the main desktop and mobile platforms with platform specific adapters doing the native work.
The hard requirements are:
| Area | Target |
|---|---|
| Hosts | Windows, macOS, Linux, iOS, Android, and web where possible |
| App format | .krate bundle |
| Runtime | Fast cold start and predictable memory use |
| APIs | Files, network, time, locale, UI, graphics, sensors, identity |
| Permissions | Clear grants instead of silent host access |
| Developer flow | New project to running app in about a minute |
| Real proof | ParkSure or another real product running on Krate |
Roadmap
This roadmap is an estimate, not a promise. Early phases may move faster because the project is still small. Later phases depend on hardware access, app store rules, security review, and real users.
The current state is:
- Phase 0 is mostly done. The repo, docs, CI, issues, labels, and release setup exist. Community and public launch items remain.
- Phase 1 engineering is done. One shared
.wasmcomponent proved the base runtime path. - Phase 2 is active and close in engineering terms. Krate now has UAPI slices for CLI-style apps, UCap manifests and launch grants, sample apps, and repeatable evidence scripts. Formal Phase 2 exit still needs final cross-host evidence, UAPI freeze review, and an outside developer walkthrough.
- Phase 3 is about half done. One portable component opens real windows
on all three desktop OSes (native AppKit widgets on macOS; winit windows
with the first drawn-widget pass on Linux and Windows), and the agent
surface (embedding API,
--jsonreports, MCP server) is complete. The original contract-layer note follows: the firstguiworld and theui,gfx, andaudioWIT drafts now parse and have a CI checker. This does not freeze the API and does not replace Phase 2's remaining outside review.
System Timeline
Green means built or proven. Yellow means built enough for the current proof. Gray means planned.
flowchart LR
P0["0. Foundation<br/>repo, docs, CI"]
P1["1. Runtime proof<br/>same WASM on 3 desktops"]
P2["2. UAPI v0.1<br/>files, network, time, locale"]
P3["3. Desktop UI<br/>native windowed app"]
P4["4. Mobile hosts<br/>iOS and Android"]
P5["5. SDK<br/>templates, debug, hot reload"]
P6["6. Distribution<br/>bundles, signing, identity"]
P7["7. v1.0<br/>real product migration"]
P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
classDef done fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
classDef current fill:#fff3bf,stroke:#b7791f,color:#2d2100,stroke-width:2px;
classDef pending fill:#eeeeee,stroke:#999999,color:#777777,stroke-width:1px;
class P0 done;
class P1 done;
class P2 current;
class P3 current;
class P4,P5,P6,P7 pending;
Phase Table
| # | Phase | Goal | Estimate | Status |
|---|---|---|---|---|
| 0 | Foundation | Make the project real enough to work in public. | Done enough for development; external items pending | Mostly done |
| 1 | Runtime proof | Run one WASM component on Linux, macOS, and Windows. | Done | Engineering done |
| 2 | UAPI v0.1 | Build useful CLI APIs and sample apps. | in progress | Active; exit evidence in progress |
| 3 | Desktop UI | Run one GUI app on Windows, macOS, and Linux. | est. 6 to 10 weeks | About half done: real windows on all three OSes, native macOS widgets, first drawn pass, agent surface complete |
| 4 | Mobile hosts | Run the same app on iOS and Android. | est. 8 to 12 weeks | Planned |
| 5 | Developer SDK | Make project creation, debug, and packaging smooth. | est. 6 to 10 weeks | Planned |
| 6 | Distribution | Add bundles, signing, updates, and identity. | est. 8 to 12 weeks | Planned |
| 7 | v1.0 hardening | Migrate a real app and clean up for public launch. | est. after Phase 6 | Planned |
What Must Happen Before Phase 2 Exits
The code has moved well past Phase 1. Phase 2 should not be called closed until the evidence is crisp:
- Freeze UAPI v0.1 after final review.
- Collect clean Linux, macOS, and Windows evidence for the sample apps and UCap denial paths.
- Keep benchmark, dependency, and fuzz evidence current for the final commit.
- Complete one timed outside developer walkthrough.
- Finalize the Phase 2 retrospective.
- Keep Phase 3 implementation limited to draft contracts and prototypes until the Phase 2 outside review is complete.
Phase 2 In One Sentence
Phase 2 makes Krate useful: a WebAssembly app can call Krate for files, network, time, locale, and terminal I/O, and the runtime can enforce explicit capability grants before host access.
Full planning details live in the Plan/ directory.
Build Log
Short public notes on what shipped, written as it happens. One entry per
milestone — the long-form detail always lives in STATUS.md and the phase
pages.
2026-07-20 — The windows learn to scroll
Scroll containers landed, and the portable app never lifts a finger: a component declares a Scroll node with children, and everything else is the host's job — clipping to the container, tracking the offset, catching the mouse wheel, clamping at the ends. That's how native platforms treat scrolling, so that's how Krate treats it, and the contract needed zero changes. The demo app now carries an eight-line list in a 120-pixel window, and the CI robot wheels it to the middle before taking its daily photograph.
2026-07-06 — Widgets get memory: checkboxes, switches, sliders
The portable contract learned state. A widget node can now carry checked on/off and a normalized value, so checkboxes, radio buttons, switches, sliders, and progress bars render as real controls in the drawn windows, not just kinds in an enum. hello-gui shows it off: a checkbox you can toggle by clicking (the full pointer round trip through the sandbox) and a progress bar that fills live as you type. The demo app still imports only krate:* interfaces, and the CI click coordinates didn't move.
2026-07-05 — The windows learn to listen to the keyboard
Keyboard input flows end to end on the host side: real key presses in the Linux and Windows windows become portable key and text events attached to the focused widget, and clicking a text field now moves focus there. The contract needed zero changes — key, text-input, and focus-changed events were designed into the WIT months ago and were waiting for a backend to feed them. Raw keys travel the same loop-proof drain channel the pointer uses. And the component that visibly types landed the same evening: hello-gui renders every keystroke into its text field live and echoes the final text on exit, so CI now runs the complete loop on Linux — click the field, type "hi krate" with a synthetic keyboard, photograph the window with the words in it, click the button, and check the app repeated the text back. A keyboard round trip through nine layers, machine-verified on every full run.
2026-07-05 — The drawn widgets learn manners
Styling pass: buttons and fields now have rounded corners, and buttons respond to the pointer — a lighter fill on hover, a deeper one while pressed — repainted the moment the state changes. The interaction state is a tiny value both painters accept, the cursor hit-testing is one shared, unit-tested helper, and the pixel tests assert the corner rounding and the state colors directly. Small slice, but it is the difference between "a drawing of a button" and "a button."
2026-07-05 — The drawn windows get real typography
The renderer slice's first pass: widget frames on Linux and Windows now render as full vector scenes — antialiased labels laid out by parley from the host's real fonts, rasterized by vello_cpu on the CPU, so CI needs no GPU and the pipeline stays byte-inspectable. The two platform painters collapsed into one shared implementation first, so this swap (and the GPU vello swap later) touches exactly one module. The 5x7 bitmap font stays as the zero-dependency fallback for hosts with no usable fonts. A five-minute compile spike against the released crates de-risked the dependency before a single line entered the tree.
2026-07-04 — The Linux button gets clicked by a robot, then learns to speak
Two proofs in one day. First, input routing: the full CI matrix now runs a
synthetic click — xdotool moves the pointer to the drawn button inside a
real winit window under Xvfb and presses it, and the portable component
observes the press and exits clean. The same click round trip a human hand
proved on macOS is now machine-proved on Linux, on every full CI run.
Second, drawn text: a small 5x7 bitmap font (a deliberate placeholder until
the vello renderer brings real typography) now paints actual labels into
the drawn windows on Linux and Windows — the button says "Click me",
fields show their text, and Text widgets are words instead of gray
blocks. The click proof also captures a screenshot of the drawn window and
publishes it as a CI artifact: visual evidence of the Linux UI produced
entirely by hosted CI.
2026-07-04 — The rename lands: the system is Krate everywhere
Phase B of the rename executed: 272 files of content, 25 renamed paths
with history preserved, regenerated bindings and contract-lock hashes
under the krate:* namespace, all four sample components rebuilt
import-pure, the CLI reborn as krate, the JSON schema as
krate.run.v1, the repository moved to incyashraj/krate (old links
redirect), and the self-hosted runner re-badged krate-local. Verified
before pushing: full workspace tests, lints, and a live
krate run --json answering with its new name. The future bundle
format becomes .krate — the brand you can attach to an email.
2026-07-04 — The Linux window shows its first pixels
The drawn-widget pass landed the same day: the Krate window on Linux now paints its UI — background, a filled button block, a bordered text field — from the same lowered placements macOS turns into native controls. It is deliberately humble rendering (solid rectangles through a CPU framebuffer, every system library loaded at runtime) because the pipeline was the point; the vello GPU renderer replaces the painter behind the same contract. All three OS lanes green with it aboard.
2026-07-04 — Layer36 becomes Krate
The project has a new name: Krate, by Krate Labs. A crate ships
goods anywhere unchanged; a Rust crate ships code. Both are exactly what
this runtime does to applications. The code, commands, and krate:* API
namespaces keep the legacy name for a short transition — the code-level
rename is scheduled to land before the UAPI freeze, so early adopters never
face a breaking rename after stability is promised.
2026-07-04 — All three desktops, one file, three real windows
Hours after Linux, Windows followed — and the winit backend cloned from the CI-proven Linux implementation compiled and worked on its first attempt. The full test matrix is green with both proofs inside it: the portable hello-gui component, the same bytes that opened the clicked native macOS window, opened a real winit window on a Linux host and a real winit window on a Windows host, exiting cleanly on both. The platform's founding claim — write once, run on every desktop, natively — is now a machine-checked fact on every full CI run. Next: drawing widgets inside those windows.
2026-07-04 — The same file opens a real window on Linux
Eight CI iterations after the slice began — driven entirely from a Mac that can't run the code — the Linux winit backend went green: a thread-locally owned, non-blockingly pumped X11 event loop feeding the same shared event stream the macOS window uses. The proof is in the CI log itself: the portable hello-gui component, byte-identical to the one that opened the clicked AppKit window, opened a real winit window on a Linux host under Xvfb and exited cleanly, with the adapter's window round-trip smoke passing beside it. Two of three desktops now open real windows from one file; Windows is next, and the component still will not change.
2026-07-03 — Krate speaks MCP
The agent-embedding track is complete. krate-mcp-server is a small
binary any MCP-capable agent framework can attach to: one run_component
tool, executing portable components inside the capability sandbox and
returning the full machine-readable report. In the first end-to-end run an
agent asked to read a file without permission and was told exactly which
capability was missing; asked again with grants, it got the file. The
permission wall is now something agents can see and reason about, not just
hit.
2026-07-03 — Agents can now call Krate
The embedding surface landed the same day as the native window. Any program
— including an AI-agent framework — can now execute a component inside
Krate's capability sandbox in under 30 lines of Rust: grants supplied as
data, no prompts, stdout captured, exit classified. And krate run --json
turns every run into one machine-readable object: which app, which
capabilities were granted (with exact boundaries), what was denied, how it
exited, how long it took, what it printed. The wedge — safe execution of
generated software — now has a socket for the machines that need it.
2026-07-03 — One portable file opens a native window
The vertical slice is complete. krate run --native-window hello-gui.wasm
now takes a single portable WebAssembly component — the same bytes on any
OS — and opens a real native macOS window containing a real native button
and text field, laid out by our engine, permission-checked by our capability
layer. Click the native button and the component receives a portable event
and updates the native text. Headless, the same file runs everywhere in CI.
The component imports only krate:* interfaces — no WASI, no host
specifics — which required teaching the events contract to deliver one event
at a time and the guest to allocate strings the way generated bindings do.
This is the platform's core promise, demonstrated end to end for the first
time.
2026-07-02 — A real native button, driven by a Krate widget tree
The first native widget lowering landed, hours after the amendments that
ordered it. A Krate widget tree now becomes a real AppKit NSButton and
NSTextField, positioned by our layout engine inside the prototype window. A
native click travels AppKit's own target-action path into Krate's shared
event stream as a routed event carrying the widget id — the same shape drawn
widgets use. The smoke run proves the loop end to end without a human: lower
the widgets, click the real button programmatically, observe the routed
event, update the native text field. This is the core Phase 3 bet (native
lowering, ADR-0013) working for the first time. Also today: the self-hosted
fuzz runner is back online as a proper service, with a green verification
run.
2026-07-02 — Direction check: plan amendments adopted
Nine weeks in, we stopped and audited the whole project against its own plans
before starting the next big slice. The result is a formal change order
(Plan/Plan-Amendments-2026-07.md). The three changes that matter:
- Linux widgets go drawn-first (ADR-0015). The original plan paired winit windows with native GTK4 widgets — but GTK4 removed foreign-window embedding, so those two choices cannot compose. Caught on paper before any code was written against it. Linux v0.1 now draws every widget (vello) inside winit windows; macOS keeps native AppKit lowering and Windows keeps native Win32 controls.
- The next milestone is a vertical slice, not more scaffolding. P3-VS-01:
one WebAssembly component opens a real macOS window containing a real
native
NSButtonandNSTextField, and receives the click back — end-to-end through the permission layer and the runtime dispatcher. It proves the riskiest architectural bet first. - An agent-embedding track. After the slice: a runtime embedding API,
krate run --json, and an MCP server wrapper, so AI-agent frameworks can execute generated components inside Krate's capability sandbox.
Also: Phase 2 closeout is timeboxed (the engineering has been done for a while; what remains is evidence paperwork), and the self-hosted fuzz nightly is paused while its runner is offline.
2026-06-23 — AppKit prototype complete through the event loop
The opt-in macOS native path now covers the full prototype chain: an owned
NSWindow bound to a Krate window id, a real retained NSWindowDelegate
recording close/resize/focus/scale callbacks, an attached NSView draw
surface with a visible clear color, a non-blocking event-loop step driver,
and a local smoke command that creates, shows, pumps, inspects, and closes
the window through the runtime dispatcher. Linux and Windows gained Winit
session scaffolding and a callback collector bridge, ready for real windows.
2026-06 — Phase 3 contract layer landed
WIT drafts for ui, gfx, and audio; a host-neutral widget tree with
stable IDs; a Taffy-backed layout crate with prepared-tree reuse, hit
testing, and 1k/10k-node benchmarks; and a UCap-gated runtime dispatcher
with pointer, key, text, host-window, theme, and scale event routes. All of
it headless and tested before any native backend depends on it.
2026-05 — Phase 2: real CLI apps with real permissions
The runtime runs krate-clock, krate-cat, and krate-curl from a
single .wasm per app on Linux, macOS, and Windows. Apps declare
capabilities in a manifest; the runtime refuses undeclared or ungranted
access before any host call happens. Cross-host CI records evidence for
samples, permission enforcement, adapters, and benchmarks on every change.
2026-05-03 — One binary, three operating systems
krate run hello.wasm produced identical output on Linux, macOS, and
Windows in hosted CI from one shared artifact. The portability bet works.
Krate for Everyone
Updated on July 3, 2026.
This page explains the project in plain language. It is written for people who are not deep in systems programming.
The Problem We Are Solving
Most software teams build the same product many times.
- One code path for Windows
- One for macOS
- One for Linux
- One for Android
- One for iOS
- One for web
That costs time, money, and focus.
Krate is trying to reduce that duplication. The long term goal is one app model that can run across many hosts.
The Basic Idea
The app runs as a WebAssembly component. WebAssembly is a portable binary format.
The app does not call host APIs directly. It calls Krate APIs.
The host adapter translates those calls to the real operating system.
flowchart LR
A["App source code"] --> B["WebAssembly component"]
B --> C["Krate runtime"]
C --> D["Krate API calls"]
D --> E["Host adapter"]
E --> F["Native OS and hardware"]
What Is Working Right Now
- The runtime can execute one component on Linux, macOS, and Windows.
- The Phase 2 API surface is active for CLI style apps:
- file access
- network requests
- time and locale
- standard input and output
- Sample apps are implemented:
krate-clockkrate-catkrate-curl
- Capability checks are in place, so apps only get access they request and are granted.
- The first windowed app works: one portable file opens a real native window with a real button and text field on macOS (a human click travels into the app and back out to the native control), and the exact same file runs without a window on Linux and Windows — the automated test matrix proves the identical bytes execute on all three systems.
- AI agents can run apps safely: a library API, a
--jsonrun report, and an MCP server let any agent framework execute an app inside the sandbox and see exactly what was allowed and what was denied. - Language fixture automation is active:
- TypeScript fixtures are built automatically in CI
- Go fixture promotion is now attempted automatically when TinyGo tools are available
- strict Go modes fail clearly if Go fixtures are missing or not import-pure
- The Rust sample apps now have a repeatable evidence recorder, so each host can produce the same kind of proof file for clock, cat, and curl.
- Phase 2 now has a simple readiness command that reads the exit ledger and shows what is done, what has proof in progress, and what is still blocked. The full mode lists every open proof item and next step for handoff.
- Hosted CI and Pages stability can now be recorded as a plain evidence file. The strict exit bundle fails if either hosted workflow does not show a completed green run in the selected review window.
- Hosted full CI now has its own evidence recorder, so we can tell the difference between normal fast CI and the heavier Linux, macOS, Windows proof run. The latest full run passed the three host lanes and the evidence compare jobs for language variants, UCap, adapters, and samples.
- Self-hosted full-gate history can now be recorded the same way. The strict exit bundle fails if that history does not show a completed green run, and the report can be narrowed to the final review date window.
- The exit bundle now has a final review mode, so the fuller Phase 2 packet can be collected with one command when the final candidate is ready.
- Fuzz runs now have a markdown evidence recorder too, so short smoke runs and longer self-hosted soak runs can be reviewed in the same format.
- The outside developer walkthrough now has a checker, so a filled timing report must include the basics before we count it as Phase 2 evidence. A local rehearsal script now checks that the Rust walkthrough path works before we hand it to a reviewer.
- The Phase 2 retrospective and Phase 3 kickoff issue now exist as drafts, and CI checks that they stay in draft form until exit evidence is ready.
- The UAPI freeze decision now has its own packet and checker, so we cannot accidentally call the API frozen before the final evidence is reviewed.
- Phase 3 has started at the contract layer. The first desktop
guiworld and the firstui,gfx, andaudioAPI drafts now parse and have a checker. This is not a finished GUI yet. It is the first map for the work. - Phase 3 now has the first permission names for desktop UI, graphics, and audio. It also has a small in-memory window model that lets us test window IDs, sizes, titles, and events before we connect real native windows.
- The runtime now has a first UI dispatcher scaffold. In simple terms, a future window request now has a checked path inside the runtime before any native window code is called.
- The runtime now talks to a shared UI adapter trait instead of direct draft storage. In simple terms, the plug point for real macOS, Windows, and Linux window adapters is now in place.
- The macOS, Linux, and Windows adapter crates now each expose that UI plug point. Today it is still headless, but each host crate can run the same blank-window smoke path.
- The runtime can now select the current host UI adapter. In simple terms, the runtime can ask macOS, Linux, or Windows for the UI adapter path instead of only using a test object.
- Phase 3 now has a written widget rule. In simple terms, use native controls when the host has a real match, and use drawn fallback surfaces when it does not.
- The shared adapter code now has the first widget tree model. In simple terms, Krate can represent stable widget IDs, widget types, labels, roles, and parent links before it connects them to real OS controls.
- The runtime can now move that draft widget tree through the UI adapter boundary. In simple terms, Krate can set a root widget, add or update child widgets, remove widgets, and track focus before real native controls exist.
- The first layout layer now exists. In simple terms, Krate can take the draft widget tree and calculate rectangles for each widget. Nothing is drawn yet, but native controls, drawn fallback, hit testing, and accessibility can now share the same position and size answer.
- Layout now has deeper proof. In simple terms, it is tested across 100 generated screen shapes, has a large-tree benchmark target, and can answer "which widget is under this point?" before real mouse or touch events are connected.
- Layout now has a faster repeated-frame path. In simple terms, Krate can prepare the layout tree once and reuse it when only the window size changes. The local prepared 10,000-widget path is under the Phase 3 budget now, but we still need formal Linux, macOS, and Windows evidence before calling that exit item done.
- Pointer routing has started. In simple terms, Krate can now take a pointer position, ask layout which widget is under it, and queue an event with that widget ID. Real native mouse and touch events are still pending, but the runtime route they will use now exists.
- Keyboard and text routing have started too. In simple terms, Krate can now send a key press or committed typed text to the focused widget. Full native keyboard handling and IME composition are still pending, but the runtime route is in place.
- Event polling has started. In simple terms, the runtime can now hand one UI
event at a time to the future app-facing
events.poll()path. Real native event loops still need to feed this queue. - Basic host window events have routes now. In simple terms, a future native window can tell Krate that the user tried to close it, resized it, or moved focus, and the app can decide what to do next.
- Theme and display scale events have routes now. In simple terms, a future native window can tell Krate that dark mode changed or that the window moved to a screen with a different scale. This is needed for correct DPI behavior before we draw real pixels.
- The window layer has a clearer name now. In simple terms,
WindowAdapteris the lower layer for creating windows and reading host window events.UiAdaptersits above it for widgets, input, and clipboard. The host adapters also say which real backend they are aiming at next: AppKit on macOS, winit on Linux and Windows. - The native window handoff has started. In simple terms, Krate has its own stable window id, and the operating system has its own real window object. We now have a checked place to connect those two. macOS has the first AppKit handoff method. It still does not show a real window yet.
- macOS now has the first real native window prototype. In simple terms,
Krate can create an AppKit
NSWindow, keep it alive, attach it to the Krate window id, and show it. This path is opt-in for now. The normal adapter still stays headless until native events and drawing are ready. - The macOS window prototype now has event bridge points. In simple terms, the real AppKit window has a checked place to report close requests, resize, focus changes, and display scale changes back into the Krate event queue. The next step is connecting AppKit delegate callbacks to those bridge points.
- The macOS window prototype now has session state. In simple terms, one object owns the AppKit window, remembers the last native state, and only reports changes. That is the shape a real event loop needs before it starts receiving AppKit callbacks.
- The macOS adapter now has a callback-shaped native event path. In simple terms, a future AppKit delegate can report "the window resized", "focus changed", "scale changed", or "close was requested" through one tested Rust object instead of scattering that logic across the adapter.
- The macOS redraw path has started. In simple terms, when the future native AppKit view needs to paint again, it can ask Krate for a redraw through the same event queue as resize, focus, scale, and close events.
- The macOS delegate bridge has started. In simple terms, the future AppKit delegate can use normal AppKit callback names, then hand them to tested Rust code that queues the right Krate event.
- The macOS drawing surface preparation has started. In simple terms, Krate can now remember the size, scale, clear color, redraw count, and frame number for an AppKit-backed surface. It still does not paint pixels, but the next native AppKit view has a small state object ready to use.
- The macOS prototype can now attach a first draw view. In simple terms, the
opt-in AppKit path can put an
NSViewinside the real window, set a visible clear color, mark the view as needing display, and record the first frame. This is still a prototype path, not the normal runtime path. - The macOS prototype now has a real window delegate object. In simple terms, macOS can call a small Krate-owned object when the native window resizes, gains focus, loses focus, changes display scale, or asks to close. Krate records those callbacks in order and drains them through the same tested event bridge.
- The macOS prototype now has a first event-loop tick. In simple terms, Krate can take one small step through the native AppKit path: read the latest window state, drain queued native callbacks, and ask for a redraw if needed. This is still opt-in, but it is the shape the runtime needs next.
- The runtime can now ask for that macOS prototype path by name. In simple terms, the default path is still the safe headless one, but there is now a clear switch for code that wants the AppKit prototype.
- The native event-loop step now has a shared runtime shape. In simple terms, the runtime can ask any UI adapter for one small non-blocking event-loop step. Headless adapters say "nothing native happened"; the AppKit prototype returns a small report with callback, snapshot, and redraw information.
- The selectable macOS prototype path now has a local smoke command. In simple terms, a developer can run one script that asks the runtime for the AppKit path, opens the native window, pumps one event-loop step, checks the result, and closes the window.
- Linux and Windows now have named Winit prototype boundaries. In simple terms, the code now has the right entry points and handle handoff checks for those hosts, but it still does not open real Linux or Windows windows yet.
- Linux and Windows now have a first Winit session owner scaffold. In simple terms, Krate can track a future native window session and route prepared resize, focus, scale, redraw, and close events through the same UI queue. The missing piece is connecting this to real Winit OS windows.
- Linux and Windows now have a Winit callback collector bridge. In simple terms, future native Winit event handlers have a small inbox where they can place resize, focus, scale, redraw, and close callbacks. The normal Krate event-loop pump can drain that inbox in order.
Current Build Timeline
Green is complete. Yellow is active. Gray is planned.
flowchart LR
P0["Phase 0 Foundation"] --> P1["Phase 1 Runtime proof"] --> P2["Phase 2 UAPI v0.1"] --> P3["Phase 3 Desktop UI"] --> P4["Phase 4 Mobile hosts"] --> P5["Phase 5 Developer SDK"] --> P6["Phase 6 Distribution"] --> P7["Phase 7 v1 hardening"]
classDef done fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
classDef current fill:#fff3bf,stroke:#b7791f,color:#2d2100,stroke-width:2px;
classDef pending fill:#eeeeee,stroke:#999999,color:#777777,stroke-width:1px;
class P0,P1 done;
class P2 current;
class P3 current;
class P4,P5,P6,P7 pending;
How Close We Are to the Big Goal
The vision is a full 6 by 6 host matrix.
- We have strong desktop runtime proof.
- We have early useful API coverage for command line apps.
- We do not have full GUI, mobile, or store distribution yet.
So we are beyond concept stage, but not near final product stage yet.
Progress Snapshot
This is a simple status view for non technical readers.
| Area | Status today |
|---|---|
| Runtime base | Working |
| Security model (capability checks) | Working in current Phase 2 scope |
| CLI sample apps | Working |
| Phase 2 proof tracking | Working, with a readiness command and evidence pages |
| CI and docs stability proof | Working, with a GitHub run-history recorder |
| Hosted full cross-host proof | Latest full Linux, macOS, Windows run is green |
| Self-hosted full-gate proof | Ready to record through GitHub run history |
| Fuzz proof | Ready to record as smoke or longer soak evidence |
| UAPI freeze decision path | Working, with a draft packet and CI checker |
| Outside walkthrough proof | Ready to collect, with a timing packet, checker, and local rehearsal |
| Phase 3 handoff | Started at contract level, still waiting on Phase 2 outside review for formal phase close |
| Desktop GUI path | WIT draft, GUI manifest recognition, first capability names, draft window model, explicit WindowAdapter, native window handle handoff, shared widget tree model, draft widget-tree dispatch, first Taffy-backed layout wrapper, 100 generated layout-shape tests, 1k/10k layout benchmark target, prepared repeated-layout path, first layout hit-test helper, draft window, pointer, key, text, FIFO polling, host window, theme, and scale event routes, shared UI adapter trait, runtime UI dispatcher, host adapter entry points, runtime host adapter discovery, planned native backend reporting, the widget lowering rule, an opt-in macOS AppKit window prototype, AppKit event bridge targets, AppKit window session state, AppKit native event state, AppKit redraw bridge, AppKit delegate callback bridge, AppKit draw-surface state, AppKit draw view surface, AppKit native window delegate, AppKit event-loop step driver, selectable AppKit runtime mode, shared runtime event-loop pump, a local runtime smoke command for the AppKit path, guarded Linux/Windows Winit prototype boundaries, a shared Winit session owner scaffold, and a Winit callback collector bridge are in place. Real Winit OS window creation is still pending |
| Mobile host path | Not started in implementation |
| Packaging and app store style distribution | Not started in implementation |
Phase 2 In Simple Terms
Phase 2 is close in engineering terms. The app can call Krate for files, network, time, locale, and terminal input or output. Those calls go through permission checks before native host code runs.
The remaining Phase 2 work is mostly proof, not a rewrite:
- freeze the API contract after review
- fill the UAPI freeze decision packet
- include the green hosted full CI run in the final evidence bundle
- decide whether Go is promoted now or marked experimental
- run longer fuzz and benchmark checks
- have one outside developer follow the tutorial and pass the filled-packet check
To check the current gate state from the repo:
scripts/phase2-exit-readiness.sh
Key Terms in Simple Words
- WebAssembly (WASM): A portable binary format that can run on different systems through a runtime.
- Runtime: The engine that loads and executes the app component.
- UAPI: The app facing API that Krate exposes. This is how apps ask for files, network, time, and more.
- Host adapter: The translation layer from Krate calls to native operating system calls.
- Capability: A specific permission, such as reading files from one path or connecting to one network endpoint.
What Happens Next
The main Phase 2 work still open is:
- Final UAPI freeze review.
- Cross host evidence for the sample apps and adapters.
- Keep Go experimental for runtime parity until its compiled components stop importing host APIs directly.
- Longer fuzz, benchmark, and dependency signoff runs.
- One timed outside developer walkthrough using the packet checker.
- Finalize the retrospective and external Phase 2 review packet.
Phase 3 has started carefully with contracts, a shared draft window model, a runtime dispatcher path, and the native handle handoff needed by real OS windows. The first opt-in AppKit window prototype now exists on macOS, with bridge points, session state, and a tested Rust callback path for the main window events. Redraw requests and AppKit-style delegate callbacks now use that path too. The real AppKit window delegate object now records native callbacks for the Rust session to drain. A small AppKit event-loop driver can now process one native tick without blocking. The runtime can now select that AppKit prototype path explicitly while keeping the default path headless. That native tick now has a common runtime report, so Linux and Windows can plug into the same pump method later. The selectable AppKit runtime path now also has a local smoke command that proves create, show, pump, inspect, and close through the runtime dispatcher. Linux and Windows now have guarded Winit prototype boundaries, a shared Winit session owner scaffold, and a callback collector bridge too. The next useful step is creating a real Winit window and feeding actual Winit callbacks into that collector.
Architecture
Krate has one job: keep app code above the platform line.
An app should not need to know whether it is running on Linux, Windows, macOS, Android, or iOS for common work. It should call Krate. The host adapter should do the platform work.
Full Shape Of The System
flowchart LR
SRC["App source<br/>Rust, Go, TS, C, etc."]
WASM["WASM component<br/>portable app code"]
MAN["Manifest<br/>name, version, permissions"]
BUNDLE[".krate bundle<br/>component, assets, signature"]
RT["Krate runtime"]
UAPI["UAPI<br/>io, files, net, time, locale"]
UCAP["UCap<br/>permission grants"]
ADAPT["Host adapter"]
OS["Native OS"]
HW["Hardware"]
SRC --> WASM
WASM --> BUNDLE
MAN --> BUNDLE
BUNDLE --> RT
RT --> UAPI
RT --> UCAP
UAPI --> ADAPT
UCAP --> ADAPT
ADAPT --> OS
OS --> HW
classDef done fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
classDef current fill:#fff3bf,stroke:#b7791f,color:#2d2100,stroke-width:2px;
classDef pending fill:#eeeeee,stroke:#999999,color:#777777,stroke-width:1px;
class SRC,WASM,MAN,RT,UAPI,UCAP,ADAPT current;
class BUNDLE,OS,HW pending;
Phase 2 has the yellow part: WebAssembly components, manifests, the runtime,
UAPI slices, UCap checks, and early host adapter paths. The .krate bundle,
GUI APIs, mobile hosts, signing, and distribution are later phases.
Runtime Flow Today
sequenceDiagram
participant User
participant CLI as krate CLI
participant Policy as UCap policy
participant Runtime
participant Wasmtime
participant Adapter as Host adapter
participant App as WASM component
User->>CLI: krate run --manifest app.toml --auto-grant app.wasm
CLI->>Policy: resolve manifest and launch grants
CLI->>Runtime: run_file(path, config, policy)
Runtime->>Wasmtime: load component and link Phase 2 UAPI
Wasmtime->>App: call run()
App->>Wasmtime: fs.read or net.connect through UAPI
Wasmtime->>Runtime: generated UAPI host call
Runtime->>Policy: check required capability
Policy-->>Runtime: grant or deny
Runtime->>Adapter: granted host operation
Adapter-->>Runtime: normalized result
Runtime-->>CLI: stdout/stderr and exit code
What The Current Proof Shows
Phase 1 proved that the loader works: one hello-world component can run through the Krate runtime. Phase 2 builds on that with useful app calls. The current proof shows:
krate-clockcan use time, locale, timezone, and stdout.krate-catcan read granted files and deny missing or out-of-scope file grants.krate-curlcan fetch a granted local HTTP endpoint and deny missing network grants.- sample and UCap evidence scripts can record repeatable reports for exit review.
That matters because the promise is not "three hosts can build similar source." The promise is "one app artifact can run under the same runtime model on different hosts."
Crates Today
flowchart TD
CLI["crates/cli"]
RT["crates/runtime"]
POL["crates/policy"]
MAN["crates/manifest"]
ADAPT["crates/adapter-*"]
SDK["crates/bindings-rust"]
APPS["apps/krate-*"]
WIT["wit/krate"]
TEST["test/integration"]
CLI --> RT
CLI --> POL
CLI --> MAN
RT --> POL
RT --> ADAPT
RT --> WIT
SDK --> WIT
APPS --> SDK
TEST --> WIT
crates/runtime owns loading, Wasmtime setup, generated UAPI host bindings,
fuel, memory limits, resource tables, adapter calls, and runtime errors.
crates/cli owns arguments, manifests, launch grants, output, exit codes, and
developer diagnostics.
crates/policy owns UCap session policy and capability matching.
crates/adapter-* owns OS-specific host behavior behind the shared runtime
boundary.
Trust Boundary
The WebAssembly component is untrusted. The runtime, policy, and host adapters are trusted project code. The operating system is outside the project boundary.
Phase 2 has real capability checks for the current UAPI slice, but it is not a production sandbox. Do not run untrusted third-party components yet. The current goal is to prove that host access can be declared, granted, denied, and recorded before Krate moves into GUI and distribution work.
Later Phases
Phase 3 adds desktop UI and graphics. Phase 4 adds mobile hosts. Later phases add SDK polish, app bundles, signing, identity, updates, marketplace behavior, and release hardening.
Core Concepts
These words show up everywhere in Krate. This page keeps them plain.
| Concept | Meaning |
|---|---|
| WASM component | The portable program file that Krate runs. |
| Runtime | The krate engine installed on a host machine. |
| UAPI | The common app API. Apps call this instead of calling each OS directly. |
| UCap | The permission system. It decides what an app is allowed to use. |
| Host adapter | The code that turns a Krate API call into a native OS call. |
.krate bundle | The future app package containing code, assets, manifest, and signature. |
| Manifest | The file that describes an app and the permissions it asks for. |
| Marketplace | The future distribution, update, and identity layer. |
How They Fit
flowchart LR
APP["WASM component"] --> UAPI["UAPI call"]
UAPI --> UCAP["UCap check"]
UCAP --> ADAPTER["Host adapter"]
ADAPTER --> OS["Native OS"]
Example: a Krate app wants to read a file.
- The app calls the Krate file API.
- UCap checks whether that app has a grant for the file path.
- The host adapter calls the native file API for the current OS.
- The app gets a result that looks the same on every host.
Phase 1 proved the base runtime path. Phase 2 is the current working slice: real UAPI calls for CLI-style apps, real capability checks, sample apps, and evidence tracking. Later phases extend that model to GUI, mobile hosts, bundles, signing, and distribution.
UAPI Overview
UAPI means Universal API.
It is the standard app API every Krate app will call. Instead of calling Windows files, macOS files, Linux files, Android files, or iOS files directly, an app calls the Krate file API. The host adapter then does the native work.
Phase 2 starts this layer.
Planned Modules
krate:
io/ stdio, pipes, stdout, stderr
fs/ files, paths, metadata
net/ HTTP first, more network APIs later
time/ clocks and timers
locale/ language, region, formatting
ui/ windows, widgets, layout, input
gfx/ 2D drawing and GPU work
audio/ playback and capture
sensors/ motion, location, camera, mic
storage/ key-value, SQL, object storage
crypto/ hashes, signing, encryption, random
identity/ user identity and signing
notify/ system notifications
accessibility/ screen readers and reduced-motion settings
platform/ device info and host capabilities
Phase 2 Scope
Phase 2 only covers:
iofsnettimelocale
That is enough to build the first useful CLI apps without pretending the whole platform is ready.
The first Phase 2 draft is checked into wit/krate/phase2. It is a review
draft, not a frozen compatibility promise yet.
The generated reference page is built from those WIT files: UAPI Reference.
The contract checker validates the current package shape:
sh scripts/check-uapi.sh
It parses the WIT package, checks the cli world imports and run export,
checks package versions and kebab-case names, and verifies protected filesystem
and network errors include permission-denied.
App Manifest
Phase 2 apps can also carry a sidecar manifest.toml.
The manifest says:
- what the app is called
- which
.wasmfile is the entry point - which UAPI world it targets
- which capabilities it wants
Example:
[app]
id = "com.example.hello"
name = "Hello"
version = "1.0.0"
entry = "hello.wasm"
world = "krate:app/cli@0.1.0"
[[capabilities]]
cap = "fs.read:~/Documents/notes/**"
rationale = "Read saved notes"
required = true
[[capabilities]]
cap = "net.connect:api.example.com:443"
rationale = "Sync to cloud"
required = false
You can validate the file today:
cargo run -p krate-cli -- manifest check manifest.toml
To read it in human form:
cargo run -p krate-cli -- manifest explain manifest.toml
This prints app identity, every requested capability, whether the capability is default-granted, and whether a launch grant is needed.
You can also create a starter manifest from the CLI:
cargo run -p krate-cli -- manifest init \
--id com.example.notes \
--name Notes \
--entry notes.wasm \
--cap io.stdout \
--cap 'fs.read:./notes/**' \
--output manifest.toml
By default, manifest init prints TOML to stdout. Use --output to write a
file, and --force if you really want to replace an existing file.
You can also print the capability strings this runtime understands:
cargo run -p krate-cli -- manifest capabilities
For scripts and editor tools, the manifest inspection commands can print JSON too:
cargo run -p krate-cli -- manifest check --format json manifest.toml
cargo run -p krate-cli -- manifest explain --format json manifest.toml
cargo run -p krate-cli -- manifest capabilities --format json
That JSON includes the app identity, capability counts, each requested
capability, whether it is a default grant, and whether a launch grant is needed.
The capability table now includes the first Phase 3 draft names too, such as
ui.window:create, ui.dialog:*, gfx.gpu:basic, and audio.capture.
The generated Phase 2 UAPI reference stays Phase 2 only, so the CLI reference
does not pretend GUI calls are implemented before they are.
Phase 3 GUI manifests are also recognized as a draft target:
[app]
id = "com.example.notes"
name = "Notes"
version = "0.1.0"
entry = "notes.wasm"
world = "krate:app/gui@0.2.0"
Manifest tools can check and explain this world today. krate run still exits
early for it because the window runtime has not been implemented yet.
Internally, Phase 3 now has a small runtime UI dispatcher scaffold that checks
window and clipboard permissions before calling a shared UI adapter trait. The
current macOS, Linux, and Windows adapter entry points still use a headless
draft backend, not a real native window backend yet. The runtime can select
that current host adapter and report whether the backend is still headless or
native.
krate run also reads manifest.toml when it sits next to the .wasm file:
cargo run -p krate-cli -- run app.wasm --grant 'fs.read:~/Documents/notes/**'
cargo run -p krate-cli -- run app.wasm --auto-grant
cargo run -p krate-cli -- run --prompt app.wasm
cargo run -p krate-cli -- run --dump-caps app.wasm
cargo run -p krate-cli -- run --dump-caps --dump-caps-format json app.wasm
For now, this starts as a launch-time session check. If a required capability is
missing and no prompt is available, Krate exits before the component starts.
When --prompt is passed, or when the command is running in a real terminal,
Krate can ask for the missing manifest capabilities and add them to the
current run session.
The manifest entry is checked too. If manifest.toml says entry = "app.wasm"
but you run a different file, Krate stops before grant resolution. That keeps
a manifest from accidentally applying to the wrong component.
--dump-caps is for debugging. It resolves the same session policy as a real
run, prints the effective capabilities, and exits before the component starts.
That makes it easier to understand why a UAPI call is allowed or denied. Use
--dump-caps-format json when a script needs the resolved grants, app identity,
and component path.
For a local audit trail, pass --log-grants:
cargo run -p krate-cli -- run \
--manifest manifest.toml \
--auto-grant \
--log-grants krate-grants.log \
app.wasm
This appends the app identity and effective session grants to a text log. Full
signed audit records are later work; this is the Phase 2 developer-facing proof.
Use --log-grants-format jsonl when a script needs one structured audit record
per line:
cargo run -p krate-cli -- run \
--manifest manifest.toml \
--auto-grant \
--log-grants krate-grants.jsonl \
--log-grants-format jsonl \
app.wasm
The runtime now also has the next piece: a UAPI guard. It is small, but it is the path every future adapter should use before it touches the host OS.
Simple version:
- App calls a UAPI function.
- Runtime turns that call into a capability string.
- The session policy checks whether that capability was granted.
- Only then does the host adapter read the file, write the file, or connect to the network.
flowchart LR
APP["WASM app"] --> CALL["UAPI call"]
CALL --> MAP["Map call to capability"]
MAP --> CHECK{"Granted?"}
CHECK -- yes --> ADAPT["Host adapter"]
ADAPT --> OS["Host OS"]
CHECK -- no --> DENY["Permission denied"]
Today this guard is in the real Phase 2 path for the imports we have wired so far. That is why the sample apps can prove both outcomes: granted calls reach a host adapter, denied calls return a UAPI error first.
Dispatcher Scaffold
The runtime now has the first dispatcher layer too:
WIT import -> UapiDispatcher -> UapiGuard -> HostAdapter trait -> native OS
For the Phase 3 UI draft, the shape is:
GUI app world -> Phase3UiDispatcher -> UapiGuard -> UiAdapter trait -> native OS
The value of this step is that the boundary is testable:
- a denied
fs.opendoes not call the file adapter - a denied
net.fetchdoes not call the network adapter - a granted call reaches the adapter
- file and network permission failures are mapped to module-level errors
- file handles carry opened path and mode, so later file reads, writes, seeks, and stats re-check the right grant
- stdio stream handles remember whether they are stdin, stdout, or stderr, so later stream reads, writes, and flushes re-check the right grant too
- policy coverage tests check that every supported capability name has a UAPI call mapping and that the current dispatcher adapter surface is reached through the policy gate
- Phase 3 window calls now reach a shared
UiAdaptertrait after UCap checks, while draft clipboard calls still return unsupported after permission checks - macOS, Linux, and Windows adapter crates expose headless UI adapter entry points, each with a blank-window smoke test
Phase3UiRuntime::with_host_adapterselects the current host UI adapter and exposes capability info for that backendPhase3UiDispatcher::pump_event_loop_oncegives native adapters one shared non-blocking event-loop pump. Headless adapters return no native tick, and the AppKit prototype maps its native step into the common report.- a local runtime smoke command now proves the selectable AppKit prototype can create, show, pump, inspect, and close through that dispatcher path
- Linux and Windows now expose guarded Winit prototype boundaries with tested native-handle handoff helpers. They do not open native windows yet.
- Linux and Windows now have that first shared session owner scaffold too. It
can track a Winit session and route prepared native events through the shared
UI queue. Real
winitwindow creation is still pending. - Linux and Windows now have a Winit callback collector bridge as well. Future real Winit handlers can record callbacks into that collector, and the shared event-loop pump can drain them in order.
- draft widget-tree calls now pass through the same dispatcher and adapter boundary for set root, upsert node, remove node, and focus node
- draft layout calls can now turn the stored widget tree into a
LayoutSnapshot, with one logical rectangle per stable widget ID - draft layout can also be prepared for repeated passes, which avoids rebuilding the layout engine tree for every frame when the widget tree has not changed
- the layout crate can also convert those local rectangles into root-window coordinates and hit-test a point against them, ready for future input routing
The bridge between generated WIT types and dispatcher types now exists too.
It converts things like open-mode, HTTP requests, file stats, locale IDs, and
WIT module errors into the runtime's internal structs and enums. That keeps the
future import code simple: receive a WIT value, convert it, call the dispatcher,
convert the result back.
The first generated host implementation now exists as well. It wires Wasmtime's generated Phase 2 traits to the dispatcher for:
- HTTP fetch
- path-level filesystem calls such as
stat,list,mkdir, andrename - time and sleep
- locale info and formatting
- logging
- stdio
That host implementation also has the first resource table. When an app opens a file or asks for stdio, the runtime gives it a resource ID owned by the host. Later reads, writes, seeks, stats, and flushes use that ID to find the real host handle and call the adapter. This keeps handles inside the runtime instead of letting guest code pass around raw host IDs.
This host is now installed into the real krate run path. The runtime still
tries the Phase 1 world first for the original proof app. If that world does
not match, it tries the Phase 2 cli world and installs the generated UAPI
imports.
The local adapter is still small on purpose. It can handle stdio, basic files,
time, locale, and plain HTTP request framing. Relative filesystem paths now go
through the runtime sandbox root instead of the process working directory by
accident. The default root is ., and krate run --sandbox-root <dir> lets a
run point app-relative paths at a specific directory. Path cleanup and
filesystem grant matching share the same rules, so simple separator differences
do not change permission behavior, .. traversal is rejected, and colon-based
prefix forms are denied before host I/O. Reserved Windows device-style names
such as con and nul are also denied at this shared path layer to avoid
cross-host device-name edge behavior. Relative sandbox paths also get a first symlink escape check: existing
targets must resolve inside the sandbox root, and new files must have a real
parent inside the sandbox root. That keeps a simple fixtures/file.txt style
path from quietly following a symlink to another part of the host. On Unix and
Windows hosts, file open now also refuses a final symlink during the actual
open call. That shrinks the race between checking a path and opening it.
Remove and rename calls now have a shared operation-intent check as well, so a
component cannot use . or / as a destructive target before native filesystem
I/O begins.
The HTTP path is only a first useful slice: good enough for localhost and fixed
test servers, not yet a full web client. get(url) remains the simple body-only
path. fetch(req) can send the selected method, app headers, and a buffered
body, while the host controls transport headers such as Host, Connection,
Content-Length, and Transfer-Encoding. Responses above 1 MiB are rejected by default so local
tests do not accidentally depend on unbounded host reads. Use
--max-http-response-bytes to lower or raise that limit for a run. When the
response is too large, the app receives net-error.body-too-large, not a vague
network failure. Timeouts and malformed responses also cross the WIT boundary as
net-error.timeout and net-error.protocol. The shared parser now rejects
whitespace, control characters, empty ports, and port 0 before the host builds
the request line. It also rejects unsupported authority forms in this early
slice. App-provided header values now reject control characters, and
Transfer-Encoding is now host-controlled with Host, Connection, and
Content-Length. Policy-side endpoint extraction now also uses a shared parser
for http:// and https://, so capability checks and adapter-side URL parsing
stay aligned. The plain http:// request parser now reuses that same authority
path, so host/port validation logic is no longer duplicated across net layers.
Helper get(url) calls now use a runtime-configured default timeout:
krate run --http-timeout-millis (default 5000, set 0 to disable).
HTTPS, redirects, streaming, and deeper protocol work are
still open.
The time adapter now uses shared host-clock code for the first clock slice: fixed test time, wall-clock milliseconds since Unix epoch, monotonic elapsed nanoseconds, and blocking sleep. The API remains small, but the behavior is no longer duplicated in the runtime alone. The shared helper now also protects time edge cases by saturating monotonic nanoseconds instead of wrapping and rejecting out-of-range Unix-millisecond values.
The locale adapter also uses shared host-locale code for its first slice:
environment locale detection, timezone fallback, simple BCP 47-style cleanup,
and deterministic baseline formatting for date and number styles. This is not
the final ICU4X-quality behavior, but it gives stable cross-host outputs while
the deeper locale work lands in one shared adapter module. Timezone
normalization now also accepts UTC offset forms (UTC+05:30, GMT-02:00,
+0530, -07) and stores them in canonical UTC±HH:MM form. Date formatting
now applies those normalized offsets to the rendered timestamp. Locale fallback
now also considers LC_TIME between LANG and LC_MESSAGES.
The first proof component lives at test/integration/phase2-smoke. It reads a
file, checks time and locale, and writes output through the Phase 2 imports.
This is the first end-to-end proof that the UAPI path is more than generated
types. The matching denial test runs the same component without fs.read; the
host returns permission denied before native file access happens.
The first named sample app is apps/krate-clock. It uses the same Phase 2
world but focuses on time, locale, and stdout. A hidden --test-time runner
flag lets the test suite freeze wall-clock time. Hidden --test-locale and
--test-timezone flags can pin locale and timezone too, so the clock fixture
can assert one exact snapshot across machines.
The next sample is apps/krate-cat. It forced one important addition:
Krate-native app arguments. You pass app arguments after --:
krate run --grant fs.read:fixtures/** krate_cat.wasm -- fixtures/a.txt fixtures/b.txt
Inside the component, those arguments come from krate:io/args.raw. The first
draft returns a newline-separated string. That is intentionally simple while the
CLI UAPI is still taking shape.
If a requested file is missing from the session grants, the sample prints a
short permission-denied message and exits with code 5. The same happens when
the app has a grant for one path glob but asks for a different file outside
that glob.
The third sample is apps/krate-curl. It uses those same app arguments for a
URL, then calls krate:net/http-client.get:
krate run --grant net.connect:127.0.0.1:8080 krate_curl.wasm -- http://127.0.0.1:8080/file.txt
The important part is the grant. Krate checks net.connect:HOST:PORT before
the adapter opens a socket. If the grant is missing, the app gets permission
denied, exits with code 5, and the host network is never touched.
If the response is too large, times out, or cannot be parsed as HTTP, the sample
prints a specific error instead of a generic fetch failure.
Rust Binding Checkpoint
The runtime has a feature named phase2-bindings that asks Wasmtime to generate
Rust host bindings from the Phase 2 WIT:
cargo test -p krate-runtime --features phase2-bindings
This is not the public SDK yet. It is a safety check for us while the WIT is still moving. It tells us whether the current WIT names turn into usable Rust names before we build adapter code on top of them.
The first guest-side Rust SDK crate has started now too:
#![allow(unused)] fn main() { use krate::{ io::{stdio, streams::OutputStreamExt}, locale::{self, DateStyle}, time, Guest, }; }
It lives at crates/bindings-rust and builds as package krate. For now it
is a thin facade over generated WIT imports, plus a few helpers for app
arguments, text output, file reads and writes, HTTP GET, time, and locale.
The Rust sample apps now use that facade instead of raw generated module paths. That gives us a real app-facing surface to improve.
Read the short guide here: Rust SDK.
UAPI Reference
Generated from
wit/krate/phase2. Do not edit this page by hand.
Krate Phase 2 exposes the cli world from krate:app@0.1.0.
The current world imports these interfaces:
krate:io/types@0.1.0krate:io/streams@0.1.0krate:io/stdio@0.1.0krate:io/args@0.1.0krate:io/log@0.1.0krate:fs/types@0.1.0krate:fs/files@0.1.0krate:net/types@0.1.0krate:net/http-client@0.1.0krate:time/clock@0.1.0krate:time/sleep@0.1.0krate:locale/types@0.1.0krate:locale/info@0.1.0krate:locale/format@0.1.0
The app exports:
run() -> s32
krate:fs/files@0.1.0
Filesystem entry points. All host file access should pass through these functions and resource methods.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
fs.read:<path-glob>- manifest or session grant -
fs.write:<path-glob>- manifest or session grant -
fs.list:<path-glob>- manifest or session grant -
fs.remove:<path-glob>- manifest or session grant -
fs.mkdir:<path-glob>- manifest or session grant -
open,stat, andlistrequire a matchingfs.read:PATHgrant for read-style access. -
Write, mkdir, remove, and rename operations are part of the Phase 2 shape, but the first runtime slice focuses on read grants.
Rust SDK Example
#![allow(unused)] fn main() { let text = krate::fs::read_to_string("notes.txt")?; krate::io::stdio::println(&text)?; }
Functions
Open a path and return a file resource.
open(path: string, mode: open-mode) -> result<own<file>, fs-error>- Opens a host file through Krate and returns a
filehandle. readneedsfs.read:PATH;write,append, andread-writealso need the matching write grant.
- Opens a host file through Krate and returns a
Read metadata for a path without opening it as a file resource.
stat(path: string) -> result<file-stat, fs-error>- Reads file metadata without opening the file body.
- Requires
fs.read:PATHfor the path being inspected.
List directory entry names for a path.
list(path: string) -> result<list<string>, fs-error>- Returns directory entry names for a granted directory.
- Requires
fs.list:PATHbefore the adapter reads the directory.
Remove one file.
remove-file(path: string) -> result<_, fs-error>- Deletes one file.
- Requires
fs.remove:PATH; missing grants fail before host deletion is attempted.
Remove one directory.
remove-dir(path: string) -> result<_, fs-error>- Deletes one directory.
- Requires
fs.remove:PATH; hosts can still reject non-empty directories.
Create one directory.
mkdir(path: string) -> result<_, fs-error>- Creates one directory.
- Requires
fs.mkdir:PATHfor the directory being created.
Rename or move a path.
rename(from: string, to: string) -> result<_, fs-error>- Moves or renames a file or directory.
- Requires grants for both sides: remove/write style access to the source and write style access to the destination.
Types
file resource
Open file resource.
file methods
Read up to
nbytes from the current file cursor.
read(n: u32) -> result<list<u8>, fs-error>- Reads up to
nbytes from an opened file handle. - The runtime rechecks the handle path before each adapter read.
- Reads up to
Write bytes at the current file cursor.
write(bytes: list<u8>) -> result<u32, fs-error>- Writes bytes to an opened file handle and returns the number written.
- The runtime rechecks write permission before each adapter write.
Seek to an absolute byte position.
seek-set(pos: u64) -> result<u64, fs-error>- Moves the file cursor to an absolute byte position.
- The handle must still be valid and backed by a granted file.
Seek to the end of the file.
seek-end() -> result<u64, fs-error>- Moves the file cursor to the end and returns the new position.
- Useful before append-style writes or size checks.
Read metadata for this open file handle.
stat() -> result<file-stat, fs-error>- Reads metadata for the opened file handle.
- The runtime rechecks the handle path before the adapter stat call.
krate:fs/types@0.1.0
Shared filesystem records, modes, and error shapes.
Types
file-stat record
Metadata returned for files and directories.
Size in bytes for files. Directory size is host-defined.
size:u64
Last modified time in Unix epoch milliseconds.
modified-millis:u64
True when the path is a directory.
is-dir:bool
open-mode variant
How a file should be opened.
Open for reads.
read
Open for writes, creating or truncating according to host policy.
write
Open for both reads and writes.
read-write
Open for appending writes.
append
fs-error variant
Filesystem error shape used by path and file-handle calls.
Path does not exist.
not-found
Capability policy or sandbox rules denied the operation.
permission-denied
The target already exists.
already-exists
Path text is not accepted by the Phase 2 path rules.
invalid-path
Operation needed a directory but found something else.
not-a-directory
Operation needed a file but found a directory.
is-a-directory
Host-specific filesystem error text.
io:string
krate:io/args@0.1.0
Raw Krate app arguments. These are the arguments passed after -- in krate run.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
io.stdin- default grant -
io.stdout- default grant -
io.stderr- default grant -
io.args- default grant -
io.log- default grant -
io.argsis granted by default for CLI apps. -
The current draft encodes args as newline-separated text.
Rust SDK Example
#![allow(unused)] fn main() { let raw = krate::io::args::raw(); let first = krate::io::args::first_raw(&raw); }
Functions
Raw argument payload for the current CLI slice.
The Phase 2 host encodes arguments as newline-separated text. SDKs should expose friendlier argument helpers over this raw transport.
raw() -> string- Returns the app arguments passed after
--inkrate run. - Current encoding is newline-separated text, so SDK helpers should parse it for app code.
- Returns the app arguments passed after
krate:io/log@0.1.0
Structured app logs. Hosts can route these to native logs, developer consoles, or test captures.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
io.stdin- default grant -
io.stdout- default grant -
io.stderr- default grant -
io.args- default grant -
io.log- default grant -
io.logis a low-risk default grant.
Functions
Emit one structured log event to the host.
emit(level: log-level, message: string, fields: list<field>)- Sends one structured log event to the host.
- Fields are plain key/value strings so native hosts can map them to their own log systems.
Types
field record
One key/value pair attached to a log event.
Field name.
key:string
Field value rendered as text.
value:string
krate:io/stdio@0.1.0
Standard input, output, and error streams for CLI-style apps.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
io.stdin- default grant -
io.stdout- default grant -
io.stderr- default grant -
io.args- default grant -
io.log- default grant -
io.stdin,io.stdout, andio.stderrare low-risk default grants for CLI apps.
Rust SDK Example
#![allow(unused)] fn main() { krate::io::stdio::println("Hello from Krate")?; krate::io::stdio::eprintln("debug line")?; }
Functions
Host standard input.
stdin() -> own<input-stream>- Returns an input stream connected to the host standard input.
- Granted by default for CLI apps.
Host standard output for normal app output.
stdout() -> own<output-stream>- Returns an output stream connected to host standard output.
- Use this for normal command output that other tools may read.
Host standard error for diagnostics.
stderr() -> own<output-stream>- Returns an output stream connected to host standard error.
- Use this for diagnostics and permission errors.
krate:io/streams@0.1.0
Byte streams used by stdio and other UAPI modules.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
io.stdin- default grant -
io.stdout- default grant -
io.stderr- default grant -
io.args- default grant -
io.log- default grant -
io.stdin,io.stdout, andio.stderrare low-risk default grants for CLI apps.
Rust SDK Example
#![allow(unused)] fn main() { use krate::io::streams::OutputStreamExt; let out = krate::io::stdio::stdout(); out.write_line("ok")?; out.flush()?; }
Types
input-stream resource
Readable byte stream owned by the runtime.
output-stream resource
Writable byte stream owned by the runtime.
input-stream methods
Read up to
nbytes from the stream.
read(n: u32) -> result<list<u8>, io-error>- Reads up to
nbytes from an input stream. - A short read is valid; an empty read means the stream has no more bytes right now or is closed.
- Reads up to
Read the stream as UTF-8 text.
read-to-string() -> result<string, io-error>- Reads the stream as UTF-8 text.
- Invalid UTF-8 returns
io-error.invalid-utf8instead of lossy text.
output-stream methods
Write some bytes and return the number accepted by the host.
write(bytes: list<u8>) -> result<u32, io-error>- Writes bytes to an output stream and returns the number accepted.
- Apps that need all bytes written should use
write-allor an SDK helper.
Write the whole byte buffer or return an error.
write-all(bytes: list<u8>) -> result<_, io-error>- Writes the full byte buffer or returns an IO error.
- This is the right primitive for line-oriented CLI output.
Flush host-side output buffers.
flush() -> result<_, io-error>- Asks the host to push buffered output through.
- Use it before exiting after important diagnostics or prompts.
krate:io/types@0.1.0
Shared IO log and error types.
Types
log-level enum
Severity level for app log events.
Very detailed diagnostic data.
trace
Developer-focused diagnostic data.
debug
Normal informational event.
info
Something unexpected happened, but the app can continue.
warn
The app hit an error condition.
error
io-error variant
Error shape for byte streams and text stream helpers.
The stream was already closed.
closed
The host interrupted the operation.
interrupted
The stream ended before enough bytes were read.
unexpected-eof
Bytes could not be decoded as UTF-8 text.
invalid-utf8
Host-specific IO error text.
other:string
krate:locale/format@0.1.0
Host-backed date and number formatting.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
locale.info- default grant -
locale.format- default grant -
Locale reads and formatting are default grants for CLI apps.
Rust SDK Example
#![allow(unused)] fn main() { let locale = krate::locale::current(); let text = krate::locale::format_number(42.0, krate::locale::NumberStyle::Decimal, &locale); }
Functions
Format Unix epoch milliseconds using a timezone, style, and locale.
format-date(millis: u64, tz: string, style: date-style, loc: locale-id) -> string- Formats a timestamp using a requested timezone, date style, and locale.
- The host owns the native formatting behavior so output can match the platform.
Format a number using a style and locale.
format-number(value: f64, style: number-style, loc: locale-id) -> string- Formats a number using a requested style and locale.
- Currency style is present in the shape, but richer currency-code handling remains future work.
krate:locale/info@0.1.0
The host user's current locale and timezone.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
locale.info- default grant -
locale.format- default grant -
Locale reads and formatting are default grants for CLI apps.
Rust SDK Example
#![allow(unused)] fn main() { let locale = krate::locale::current(); let timezone = krate::locale::timezone(); }
Functions
The user's preferred locale as reported by the host.
current() -> locale-id- Returns the host user's preferred locale as a BCP 47 string.
- Good for display choices, not for security decisions.
IANA timezone name, for example "Asia/Singapore".
timezone() -> string- Returns the host timezone name.
- Expected form is an IANA name such as
Asia/Singaporewhen the host can provide one.
krate:locale/types@0.1.0
Locale and formatting type definitions.
Types
locale-id record
Host locale identifier using a BCP 47 language tag.
Canonicalized BCP 47 locale tag, for example
en-US.
bcp47:string
date-style enum
Date rendering style requested from the host.
Compact numeric date form.
short
Medium-length date form.
medium
Long date form.
long
Full date form.
full
number-style enum
Number rendering style requested from the host.
Decimal number formatting.
decimal
Percent formatting.
percent
Currency formatting. Currency code selection remains future work.
currency
krate:net/http-client@0.1.0
HTTP client calls. Phase 2 starts with simple request and response bodies.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
net.connect:<host>:<port>- manifest or session grant -
getandfetchrequire a matchingnet.connect:HOST:PORTgrant before the adapter opens a socket. -
The current host adapter supports plain HTTP request framing first, with a 1 MiB full-response cap; HTTPS, redirects, streaming, and richer network behavior are still Phase 2 work.
Rust SDK Example
#![allow(unused)] fn main() { let body = krate::net::get_text("http://127.0.0.1:8080/data.txt")?; krate::io::stdio::println(&body)?; }
Functions
Perform a simple GET request and return only the response body.
get(url: string) -> result<list<u8>, net-error>- Performs a simple HTTP GET and returns only the response body.
- Requires
net.connect:HOST:PORT; Phase 2 currently supports the plain HTTP adapter path.
Perform a buffered HTTP request and return status, headers, and body.
fetch(req: request) -> result<response, net-error>- Performs a lower-level HTTP request and returns status, headers, and body.
- The plain HTTP adapter now forwards the method, app headers, and buffered body while keeping
Host,Connection, andContent-Lengthunder host control. - Timeouts, oversized bodies, malformed responses, and missing grants are typed as
net-errorcases.
krate:net/types@0.1.0
Shared network request, response, and error types.
Types
http-method enum
HTTP method for Phase 2 client requests.
HTTP GET.
get
HTTP POST.
post
HTTP PUT.
put
HTTP DELETE.
delete
HTTP PATCH.
patch
HTTP HEAD.
head
HTTP OPTIONS.
options
header record
One HTTP header field.
Header name.
name:string
Header value.
value:string
request record
Buffered HTTP request shape.
Request method.
method:http-method
Absolute request URL.
url:string
App-provided headers. Host-controlled transport headers are rejected.
headers:list<header>
Buffered request body.
body:list<u8>
Optional timeout in milliseconds for this request.
timeout-millis:option<u32>
response record
Buffered HTTP response shape.
Numeric HTTP status code.
status:u16
Response headers accepted by the host adapter.
headers:list<header>
Buffered response body.
body:list<u8>
net-error variant
Network error shape returned by HTTP client calls.
URL syntax or unsupported Phase 2 URL shape.
invalid-url
Hostname resolution failed.
dns-failure:string
Socket connection failed.
connect-failure:string
TLS setup failed. HTTPS is not yet implemented in the first Phase 2 adapter slice.
tls-failure:string
Request timed out.
timeout
Response exceeded the configured body-size limit.
body-too-large
Capability policy denied the request before socket access.
permission-denied
Response framing or protocol parsing failed.
protocol:string
Host-specific network error text.
other:string
krate:time/clock@0.1.0
Wall-clock and monotonic clock reads.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
time.clock- default grant -
time.monotonic- default grant -
time.sleep- default grant -
time.clockandtime.monotonicare default grants.
Rust SDK Example
#![allow(unused)] fn main() { let now = krate::time::now_millis(); let tick = krate::time::monotonic_nanos(); }
Functions
Milliseconds since Unix epoch. Wall-clock; can jump.
now-millis() -> u64- Reads host wall-clock time in milliseconds since Unix epoch.
- This value can move backward or forward if the host clock changes.
Monotonic nanoseconds since an arbitrary origin. Guaranteed non-decreasing; suitable for measuring intervals.
monotonic-nanos() -> u64- Reads a non-decreasing timer in nanoseconds.
- Use this for durations instead of wall-clock time.
krate:time/sleep@0.1.0
Blocking sleep for CLI-style components.
Capability Notes
Accepted capability strings for this module, generated from the runtime manifest table:
-
time.clock- default grant -
time.monotonic- default grant -
time.sleep- default grant -
sleep-millisrequirestime.sleep.
Rust SDK Example
#![allow(unused)] fn main() { krate::time::sleep_millis(100); }
Functions
Block the calling task for at least
millismilliseconds.
sleep-millis(millis: u32)- Blocks the calling component task for at least the requested milliseconds.
- Requires
time.sleep; hosts may wake slightly later than requested.
Rust SDK
The Rust SDK lives in crates/bindings-rust and builds as the package
krate.
It is still small. That is intentional. Phase 2 is where the UAPI shape is settling, so the SDK gives Rust apps a clean front door without hiding the actual platform contract.
The crate now has publish-facing metadata and a crate README. We still do not
publish it to crates.io while UAPI v0.1 is moving, but cargo package -p krate is part of CI so packaging problems show up early. CI also runs
scripts/smoke-rust-sdk.sh, which creates a temporary app outside the
workspace and checks that the packaged SDK can compile a tiny Krate component.
The smoke path now has a matching Phase 2 evidence recorder:
Rust SDK Evidence. Normal hosted CI uploads
that report as the rust-sdk-evidence artifact.
What It Gives You
Instead of importing generated WIT paths directly, app code can use short Krate modules:
#![allow(unused)] fn main() { use krate::{ fs::{self, OpenMode}, io::{args, stdio, streams::OutputStreamExt}, net, time, Guest, }; }
The current modules are:
| Module | Use it for |
|---|---|
krate::io | app arguments, stdin, stdout, stderr, logging |
krate::fs | opening, reading, writing, and checking files |
krate::net | HTTP client calls |
krate::time | wall clock, monotonic clock, sleep |
krate::locale | locale, timezone, date and number formatting |
A Minimal App
Every Phase 2 CLI app exports a run function through the Guest trait:
#![allow(unused)] fn main() { use krate::{io::stdio, io::streams::OutputStreamExt, Guest}; struct Component; impl Guest for Component { fn run() -> i32 { let out = stdio::stdout(); if out.write_line("Hello from Krate").is_err() { return 20; } 0 } } krate::export!(Component); }
That krate::export! line is the bridge between normal Rust code and the
WASM component export that the runtime calls.
Reading Arguments
Krate app arguments are passed after --:
krate run app.wasm -- notes.txt
Inside the component:
#![allow(unused)] fn main() { let raw = krate::io::args::raw(); let first = krate::io::args::first_raw(&raw); }
For normal apps, use the owned helpers:
#![allow(unused)] fn main() { let args = krate::io::args::all(); let first = krate::io::args::first(); }
The current draft stores arguments as newline separated text under the hood.
Use raw when you need exact control, split_raw when you already have the raw
string, and all or first for app code.
The small argument helpers are marked inline on purpose. That keeps guest components from pulling in WASI environment imports. Krate apps should depend on Krate UAPI, not host WASI argument APIs.
Writing Output
The generated stream type already has write_all. The SDK adds text helpers:
#![allow(unused)] fn main() { use krate::io::{stdio, streams::OutputStreamExt}; let out = stdio::stdout(); out.write_text("status: ")?; out.write_line("ok")?; out.flush()?; }
The sample apps use this pattern for stdout and stderr.
Files
For low level control:
#![allow(unused)] fn main() { use krate::fs::{self, OpenMode}; let file = fs::open("input.txt", OpenMode::Read)?; let bytes = file.read(8192)?; }
For common cases:
#![allow(unused)] fn main() { let text = krate::fs::read_to_string("input.txt")?; krate::fs::write("out.txt", text.as_bytes())?; }
The runtime still checks capabilities before file access. The SDK does not skip UCap.
Network
The first network helper is deliberately narrow:
#![allow(unused)] fn main() { let body = krate::net::get("http://127.0.0.1:8080/data.txt")?; }
The runtime checks a net.connect:HOST:PORT grant before opening the socket.
For lower-level work, krate::net::fetch(req) sends the request method, app
headers, and a buffered body, then returns status, headers, and body. The host
still owns transport headers such as Host, Connection, and
Content-Length. The current plain HTTP adapter caps the full response at 1
MiB by default, with krate run --max-http-response-bytes available for test
runs that need a different limit. Helper krate::net::get calls use a default
timeout of 5000 ms, configurable with krate run --http-timeout-millis (0
disables the default timeout). HTTPS, redirects, and streaming bodies are still
Phase 2 work.
Current Limits
This is not a finished SDK yet.
- It is package-checked, but not published to crates.io.
- A fresh outside-workspace smoke app now checks the packaged crate.
- Its public helper layer now has rustdoc comments and a local doc build check.
- It wraps the generated Phase 2 guest bindings, which are still draft.
- It has enough helpers for the Rust samples, not a full developer experience.
- Go and TypeScript SDK work is still pending.
That is fine for now. The important part is that app code now points at
krate::fs, krate::net, and friends. That is the layer we can keep
improving without making app authors learn the generated binding layout.
For the end-to-end app flow, read Your First UAPI App In Rust.
Your First UAPI App In Rust
This walkthrough is the shortest current path from "I know Rust" to "I ran a
Krate Phase 2 app." It uses the real Rust SDK, the real manifest commands, and
the current krate run path.
Phase 2 is still pre-alpha, so this guide uses the repo workspace directly. The
future version will start with cargo add krate.
What You Build
A tiny CLI app that:
- reads one file path from app arguments
- reads that file through Krate
fs - writes the text through Krate
stdout - declares its permissions in
manifest.toml
That is enough to touch the core Phase 2 loop:
Rust app -> Krate SDK -> UAPI import -> UCap check -> host adapter
1. Check Your Tools
From the repo root:
cargo run -p krate-cli -- doctor
For this walkthrough, you need:
- Rust installed
wasm32-wasip1installedcargo-componentinstalled
If cargo-component is missing:
cargo install cargo-component --locked --version 0.21.1
2. Start From The Cat Sample
The current Rust SDK is local, so the easiest first app is the existing sample:
apps/krate-cat/
The core code is intentionally plain:
#![allow(unused)] fn main() { use krate::{ fs, io::{args, stdio, streams::OutputStreamExt}, Guest, }; struct Component; impl Guest for Component { fn run() -> i32 { let Some(path) = args::first() else { let _ = stdio::stderr().write_line("usage: krate-cat <file>"); return 64; }; match fs::read_to_string(&path) { Ok(text) => { let _ = stdio::stdout().write_text(&text); 0 } Err(err) => { let _ = stdio::stderr().write_line(&format!("{err:?}")); 5 } } } } krate::export!(Component); }
The important part is what it does not do. It does not call std::fs, std::env,
or direct WASI imports. It asks Krate for arguments, file access, stdout, and
stderr.
3. Build The Component
From the repo root:
cargo build -p krate-cli
scripts/build-krate-cat-component.sh
The script prints a component path like:
apps/krate-cat/target/wasm32-wasip1/release/krate_cat.wasm
4. Generate A Manifest
Use the CLI to create a starter manifest:
cargo run -p krate-cli -- manifest init \
--id dev.krate.cat \
--name krate-cat \
--entry target/wasm32-wasip1/release/krate_cat.wasm \
--cap io.args \
--cap io.stdout \
--cap io.stderr \
--cap 'fs.read:./fixtures/**' \
--output apps/krate-cat/manifest.toml \
--force
This writes valid TOML and checks every capability string before it writes.
5. Explain The Manifest
Before running the app, inspect what it asks for:
cargo run -p krate-cli -- manifest explain apps/krate-cat/manifest.toml
You should see that io.args, io.stdout, and io.stderr are default grants.
You should also see that fs.read:./fixtures/** needs a launch grant.
If you want the same explanation as structured data, use JSON:
cargo run -p krate-cli -- manifest explain \
--format json \
apps/krate-cat/manifest.toml
The check and capability table commands can print JSON too, which is useful when you start wiring your own CI scripts.
That is the Phase 2 permission model in simple form:
low-risk app plumbing -> default grant
host file/network access -> explicit launch grant
If you want a local record of the grants used for a run, add:
--log-grants krate-grants.log
For scripts, use JSON Lines. Krate writes one audit record per line:
--log-grants krate-grants.jsonl --log-grants-format jsonl
If you want a script-readable preview before the component starts, use:
--dump-caps --dump-caps-format json
6. Run It
Create a test file:
mkdir -p apps/krate-cat/fixtures
printf 'hello from Krate\n' > apps/krate-cat/fixtures/hello.txt
Run with the sample manifest:
cd apps/krate-cat
../../target/debug/krate run \
--manifest manifest.toml \
--auto-grant \
target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
cd ../..
Expected output:
hello from Krate
7. See The Denial Path
Run without granting the file capability:
cd apps/krate-cat
printf '' | ../../target/debug/krate run \
--manifest manifest.toml \
target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
cd ../..
In a non-interactive shell, Krate exits before starting the component and prints the missing required capability.
That is the behavior we want. The runtime should refuse the app before native file access happens.
8. What To Change For Your Own App
Start by changing:
apps/krate-cat/src/lib.rsapps/krate-cat/Cargo.tomlapps/krate-cat/manifest.toml
Then rebuild and rerun. As the SDK gets published, this flow will move from a repo-local sample to a fresh project outside the Krate tree.
Current Limits
- The SDK is local to the repo and still draft.
- The manifest is unsigned.
- Grants are session-only.
- Cross-host sample proof is still running through CI, not through a released installer.
Even with those limits, this is now the core shape of a Krate CLI app.
Go SDK
The Go SDK is now started at packages/sdk-go. This is the first Go/TinyGo
shape for Phase 2, not the final component proof.
Current Status
What exists now:
- A Go module at
packages/sdk-go. - Public packages for
io,fs,net,time, andlocale. - Example source files for Go clock, cat, and curl-style CLI apps.
- A dependency-free shape check that guards the package layout.
What still needs proof:
- Install Go and TinyGo.
- Generate or wire WIT bindings behind the public helpers.
- Build a real Go component.
- Run that component through
krate run. - Add fixture-backed tests like the Rust samples.
Example
package main
import (
l36io "github.com/incyashraj/krate/packages/sdk-go/krate/io"
l36net "github.com/incyashraj/krate/packages/sdk-go/krate/net"
)
func main() {
args := l36io.Args()
if len(args) == 0 {
_ = l36io.Eprintln("usage: krate-go-curl <url>")
return
}
body, err := l36net.GetText(args[0])
if err != nil {
_ = l36io.Eprintln(err.Error())
return
}
_ = l36io.Print(body)
}
The longer examples live here:
packages/sdk-go/examples/krate-clock/main.gopackages/sdk-go/examples/krate-cat/main.gopackages/sdk-go/examples/krate-curl/main.go
Tooling
Run:
krate doctor
For this track, these lines should be present:
tinygo ...
go ...
If either is missing, the Go runtime proof is blocked. The rest of Krate can continue without them.
Current Check
The normal CI path runs a small package shape check:
node packages/sdk-go/scripts/check-shape.mjs
This does not compile a component. It catches simple mistakes such as missing
helper packages, wrong module path, accidental wasi:* imports, or missing
public helper names.
Your First UAPI App In Go
This is the current shortest path to start building Krate apps in Go. Today, the Go track is still in scaffold mode. So this walkthrough focuses on the parts that are already real and testable right now.
What You Build Today
- a small Go app shape that uses Krate SDK imports
- a checked package layout
- a clear view of what is already wired and what is still pending
1. Check Tooling
From repo root:
cargo run -p krate-cli -- doctor
Look for:
gotinygo
If either is missing, you can still follow the SDK structure work, but runtime component proof will stay blocked.
2. Start From The Go Cat Example
Use the sample at:
packages/sdk-go/examples/krate-cat/main.go
That sample already shows the right direction:
- read app args through Krate SDK
- read files through Krate SDK
- print through Krate SDK
This keeps host access behind UAPI and capability checks.
3. Run The Shape Check
From repo root:
node packages/sdk-go/scripts/check-shape.mjs
This check is cheap and fast. It confirms that Go SDK package structure and public helper names still match our current Phase 2 contract.
4. Fixture Build Step (Current State)
From repo root:
scripts/build-phase2-language-variant-fixtures.sh
Today this script can auto-build the TypeScript fixture trio when jco is
available and reports Go status clearly. Go fixtures still need either manual
provisioning for runtime tests.
There is now also a Go TinyGo build-smoke lane:
scripts/build-phase2-go-variant-smoke.sh
scripts/record-phase2-go-readiness-evidence.sh
The first command builds Go clock/cat/curl WASI Preview 2 artifacts and checks
their component export shape (wasi:cli/run). The second command writes a small
evidence report with tool versions, artifact hashes, and the import-purity log.
The current TinyGo artifacts do build, but they are not Krate-import pure yet. The promotion step checks all three artifacts together and reports every non-Krate import it sees. Current blockers are:
- clock and cat still import WASI stdout, IO stream/error, random, and the TinyGo run import helper
- curl imports a wider WASI surface because it currently pulls in environment, exit, stdio, clocks, filesystem, IO, random, and the TinyGo run import helper
That is useful progress: the build shape works, and the remaining Go task is to
replace those WASI host calls with real krate:* imports before runtime
fixtures can be promoted.
5. Optional Runtime Variant Test Hook
If you already have compiled Go WASM fixtures at:
test/integration/language-variants/
run:
scripts/test-phase2-language-variants.sh
If those fixtures are not present, the script exits cleanly and explains that
tests were skipped. If you provide Go fixture env vars, provide all three
(clock, cat, and curl) so the runtime lane runs as one complete set.
When all three are present, the script also runs the component import-purity
check before runtime assertions.
The Go curl runtime assertions now include missing-grant and unresolved-host
paths that do not require localhost fixture sockets.
You can force stricter CI behavior with KRATE_LANGUAGE_VARIANTS_MODE.
Useful values are optional (default), go, ts, any, and both.
6. Where This Fits In Phase 2
Go is now at "SDK and test harness ready" stage.
Still pending:
- Krate-import pure runtime fixture proof for Go variants
- always-on runtime fixture gate for Go variants
That means we are not blocked. We can keep improving UAPI and policy hardening while finishing the Go build lane in parallel.
TypeScript SDK
The TypeScript SDK is now started at packages/sdk-ts. It is not the final
binding proof yet. Think of it as the first clean shape for TypeScript app code:
stable import names, clear types, and small helpers over the Phase 2 UAPI.
Current Status
What exists now:
@krate/sdkpackage metadata.- Type declarations for the Krate WIT import modules.
- Helpers for arguments, stdout, stderr, file reads and writes, HTTP GET, time, and locale calls.
- Example source files for TypeScript clock, cat, and curl-style CLI apps.
- A dependency-free shape check that guards the package layout and import names.
- Runtime fixture auto-build support through
scripts/build-phase2-language-variant-fixtures.sh. - Local runtime fixture proof for TypeScript clock and cat.
What still needs proof:
- Keep full runtime fixture proof stable in hosted CI.
- Keep advancing the Go TinyGo lane from build-smoke to Krate-runtime fixture proof so language-variant checks can move from optional to strict by default.
- Keep curl fixture evidence stable on restricted runners where local socket bind policy may differ.
Example
import { io, net } from "@krate/sdk";
const url = io.args()[0];
if (!url) {
io.eprintln("usage: krate-ts-curl <url>");
throw new Error("missing url");
}
io.print(net.getText(url));
The longer examples live here:
packages/sdk-ts/examples/krate-clock.tspackages/sdk-ts/examples/krate-cat.tspackages/sdk-ts/examples/krate-curl.ts
This code is meant to compile into a WebAssembly component with jco, then run
inside Krate. It should not call Node filesystem or network APIs directly.
All real access must go through Krate UAPI imports so the manifest and UCap
checks stay in charge.
Tooling
Run:
krate doctor
For this track, these lines should be present:
node v...
npm ...
jco ... (or "... (via npx)")
If jco is missing, install it as a local project dependency:
npm install -D @bytecodealliance/jco typescript
The CLI doctor command now reports jco from either the direct binary path or
npx --no-install jco, so local Node-based installs are visible immediately.
Binding Shape Note
WIT variant values are represented as tagged objects in this binding path.
For example, filesystem open mode is passed as { tag: "read" } instead of the
plain string "read". The SDK helper exports the correct values so app code
can stay simple.
Current Check
The normal CI path runs a small package shape check:
npm --prefix packages/sdk-ts run check:shape
This does not compile a component. It catches simple mistakes such as missing
helper files, wrong package metadata, accidental wasi:* imports, or missing
Krate import declarations.
For runtime fixture builds, use:
scripts/build-phase2-language-variant-fixtures.sh
When jco is available, that script now builds the TypeScript fixture trio in
test/integration/language-variants/ and lets the existing runtime-variant
tests run without manually setting fixture env vars.
Your First UAPI App In TypeScript
This walkthrough gets you started with the TypeScript Krate SDK in the current Phase 2 state.
Right now, TypeScript is in scaffold mode: SDK shape is live, and hosted full CI now runs the TypeScript runtime fixture lane by default.
What You Build Today
- a small TypeScript app shape using
@krate/sdk - a validated package structure
- a practical map of what works now vs what is still pending
1. Check Tooling
From repo root:
cargo run -p krate-cli -- doctor
Look for:
nodenpmjco
If jco is missing, SDK shape work can continue, but component build proof is
not ready on that machine yet.
2. Start From The TypeScript Cat Example
Use:
packages/sdk-ts/examples/krate-cat.ts
The sample path is intentionally simple:
- read args through Krate SDK
- read files through Krate SDK
- print through Krate SDK
No direct Node filesystem or socket APIs are used in the app path.
3. Run The Shape Check
From repo root:
npm --prefix packages/sdk-ts run check:shape
This confirms package metadata, helper exports, and import declarations still match the current UAPI-facing SDK contract.
4. Build Runtime Variant Fixtures
From repo root:
scripts/build-phase2-language-variant-fixtures.sh
This script now tries to build the TypeScript variant fixtures automatically
from test/integration/language-variants-src/ when jco is available.
Outputs go to:
test/integration/language-variants/
If jco is missing, it exits cleanly in default mode and tells you what is
missing. For hosted full CI, Krate now allows npx-driven jco installation
for this step (with a pinned jco package version), so the TypeScript lane can
stay active by default.
5. Optional Runtime Variant Test Hook
If TypeScript variant WASM fixtures exist under:
test/integration/language-variants/
run:
scripts/test-phase2-language-variants.sh
If fixtures are not present, the script exits with a skip message and no error.
If you provide TypeScript fixture env vars, provide all three (clock, cat,
and curl) so the runtime lane runs as one complete set.
When all three are present, the script also runs the component import-purity
check before runtime assertions.
You can force stricter CI behavior with KRATE_LANGUAGE_VARIANTS_MODE.
Useful values are optional (default), go, ts, any, and both.
6. Where This Fits In Phase 2
TypeScript is now at "SDK, harness, and first fixture-build path ready" stage.
Still pending:
- cross-host stability evidence for restricted-runner curl fixture behavior
So this tutorial is intentionally honest: strong SDK structure today, full build lane in progress, and no blocker for continuing core runtime work.
Quickstart: Run A Phase 2 Krate Component
This walkthrough builds the Krate CLI, builds the current Phase 2 sample components, and runs a file-reading WebAssembly component through the Krate UAPI and capability path.
At the end your terminal should print:
hello from Krate
Krate is still pre-alpha. This quickstart is for local developer proof, not for running untrusted third-party components.
Prerequisites
Install:
- Git
- Rust via
rustup cargo-component
Krate pins its Rust toolchain in rust-toolchain.toml, so entering the repo
lets rustup install the right compiler and WASM targets.
Install the component tooling:
cargo install cargo-component --locked --version 0.21.1
Get The Source
git clone https://github.com/incyashraj/krate.git
cd layer6x6
Build Krate
cargo build -p krate-cli
Check the local environment:
target/debug/krate doctor
doctor reports core Rust tools first, then Phase 2 language tools such as
wasm-tools, tinygo, go, node, npm, and jco when they are available.
Build The Phase 2 Samples
scripts/build-krate-clock-component.sh
scripts/build-krate-cat-component.sh
scripts/build-krate-curl-component.sh
The scripts print component paths like:
apps/krate-cat/target/wasm32-wasip1/release/krate_cat.wasm
Inspect A Manifest
Phase 2 apps carry a manifest.toml for app identity and capability requests.
Before running the file sample, inspect what it asks for:
target/debug/krate manifest explain apps/krate-cat/manifest.toml
You should see:
io.args,io.stdout, andio.stderras default-granted app plumbingfs.read:fixtures/**as a non-default launch grant
That is the current permission model in simple form:
low-risk app plumbing -> default grant
host file/network access -> explicit launch grant
Run The Clock Sample
Run a deterministic clock sample through the Phase 2 UAPI path:
target/debug/krate run \
--auto-grant \
--manifest apps/krate-clock/manifest.toml \
--test-time 1234567890 \
--test-locale en-US \
--test-timezone UTC \
apps/krate-clock/target/wasm32-wasip1/release/krate_clock.wasm
This exercises time, locale, timezone, and stdout. The test flags make output stable enough for evidence runs.
Run The File Sample
Create a test file:
mkdir -p apps/krate-cat/fixtures
printf 'hello from Krate\n' > apps/krate-cat/fixtures/hello.txt
Run with the sample manifest and grant approval:
cd apps/krate-cat
../../target/debug/krate run \
--manifest manifest.toml \
--auto-grant \
target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
cd ../..
Expected output:
hello from Krate
See The Denial Path
Run the same app without granting the file capability:
cd apps/krate-cat
printf '' | ../../target/debug/krate run \
--manifest manifest.toml \
target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
cd ../..
In a non-interactive shell, Krate exits before starting the component and prints the missing required capability. That is intentional: host file access should be explicit.
Optional Network Sample
Start a local HTTP server in one terminal:
mkdir -p /tmp/krate-demo-http
printf 'portable runtime response\n' > /tmp/krate-demo-http/demo.txt
cd /tmp/krate-demo-http
python3 -m http.server 8765
In another terminal, run the curl sample with an explicit network grant:
cd /path/to/layer6x6
target/debug/krate run \
--grant net.connect:127.0.0.1:8765 \
apps/krate-curl/target/wasm32-wasip1/release/krate_curl.wasm \
-- http://127.0.0.1:8765/demo.txt
If localhost sockets are restricted in your environment, skip this sample and use the file and clock samples first.
Check Phase 2 Readiness
scripts/phase2-exit-readiness.sh
This command reads the Phase 2 exit ledger and prints how many gates are done, partial, pending, or blocked. It does not declare Phase 2 complete. It gives a repeatable status snapshot.
Run Evidence Helpers
For a local evidence packet:
scripts/record-phase2-exit-bundle.sh --strict
scripts/check-phase2-exit-evidence.sh
scripts/phase2-exit-readiness.sh
For sample output evidence:
scripts/record-phase2-sample-evidence.sh
For permission enforcement evidence:
scripts/record-phase2-ucap-evidence.sh --strict
Historical Phase 1 Proof
The original hello-world proof still exists and is useful for understanding the runtime base:
scripts/build-hello-component.sh
target/debug/krate run test/integration/hello-world/target/wasm32-wasip1/release/hello_world.wasm
Expected output:
Hello, Krate!
For new app work, use the Phase 2 UAPI path instead:
See It With a Window
The GUI vertical slice has a one-command demo. On macOS it opens a real native window (click the button within 30 seconds and watch the text field change); on Linux and Windows the same portable file runs headless — and the full CI matrix proves the identical bytes open real windows there too.
sh scripts/demo-hello-gui.sh
Exit codes are the assertions: 0 = native click observed, 1 = clean
bounded run without a click, 2 = window closed early. The full test
manual, including manual commands and troubleshooting, is on the
Hello GUI Demo & Testing page.
Machine-Readable Runs
Add --json to any run to get one krate.run.v1 object describing it —
app identity, granted capabilities with boundaries, denials, exit class,
duration, and captured output. This is the same report AI agents receive
through krate-mcp-server; see
Embedding & JSON Runs.
Contributing to Krate
Thanks for your interest. Krate is young and every contribution compounds.
Where to start
- Read the Code of Conduct. We enforce it.
- Skim the vision and roadmap.
- Find a
good first issue: https://github.com/incyashraj/krate/labels/good%20first%20issue - Read your first PR for a complete walkthrough.
Guides
- Your first PR: from zero to merged
- ADR process: when and how to write an Architecture Decision Record
- Setting up your environment
- WIT style guide: how we write WIT interfaces
Quick links
The Discord #help channel will be linked here once the community server is
ready for public use.
Your first PR
This guide walks from a fresh clone to a small merged pull request.
1. Pick a small issue
Start with the good first issue label:
https://github.com/incyashraj/krate/labels/good%20first%20issue
Good Phase 0 starter tasks are documentation fixes, broken-link fixes, CI polish, and small scaffolding improvements. If an issue looks bigger than two hours, leave a comment and ask for help trimming the scope.
2. Fork and clone
Use GitHub's Fork button, then clone your fork:
git clone https://github.com/<your-handle>/krate.git
cd krate
git remote add upstream https://github.com/incyashraj/krate.git
3. Create a branch
Branch names follow the task phase:
git checkout -b p0-docs-fix-first-pr-guide
Use p0- for Phase 0 work, p1- for Phase 1 work, and so on.
4. Run the baseline checks
Before changing anything, confirm the repo works on your machine:
cargo build --workspace
cargo test --workspace
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
For documentation-only work, also build the book:
mdbook build docs/book
5. Make the change
Keep the pull request focused on one idea. If you discover a second problem, open a separate issue or mention it in the PR notes instead of expanding the diff.
6. Commit
Use a Conventional Commit subject:
git add .
git commit -m "docs(p0): fix first-pr guide"
7. Open the PR
Push your branch and open a pull request against main:
git push origin p0-docs-fix-first-pr-guide
Fill out the PR template, link the issue or task ID, and include the checks you ran. If the PR changes visible documentation, add a screenshot or a short note describing the rendered page.
What happens next
A maintainer will review the PR, ask questions if needed, and merge once CI is green and the scope is clear. Review is a conversation, not an exam.
Screenshot checklist
When GitHub Pages is live, the guide should include screenshots of:
- The
good first issuelabel page. - The GitHub fork button.
- The pull request form with the template filled in.
Until then, the commands above are the source of truth for the local workflow.
ADR process
Architecture Decision Records (ADRs) capture the significant technical decisions made in Krate: what was decided, why, and what the consequences are.
When to write an ADR
Write an ADR when you make a decision that:
- Affects multiple crates, phases, or contributors
- Is difficult or costly to reverse
- Would otherwise be invisible in code
Examples that need an ADR:
- Choosing a new dependency for the runtime
- Changing the WIT versioning strategy
- Adding a new UAPI module
- Changing the bundle format
Examples that do not need an ADR:
- Bug fixes
- Test additions
- Documentation updates
- Refactors that don't change observable behavior
Process
- Copy
docs/adr/template.mdtodocs/adr/NNNN-short-title.md(next sequential number). - Fill out every section. Be honest about alternatives rejected.
- Open a PR titled
ADR: <title>. - Minimum 2 maintainers approve, or 1 approve + 7 days open.
- Merged ADRs are immutable. To supersede an ADR, write a new one that
references the old one with
Supersedes: ADR-NNNN.
Index
| ADR | Title | Status |
|---|---|---|
| ADR-0001 | Rust for the Krate runtime | Accepted |
| ADR-0002 | Wasmtime as runtime engine | Accepted |
| ADR-0003 | Component Model from day one | Accepted |
| ADR-0006 | WIT versioning strategy | Accepted |
| ADR-0007 | UCap v0.1 soft enforcement | Accepted |
| ADR-0008 | Host async runtime | Accepted |
| ADR-0009 | Sandbox link-semantics guardrails | Accepted |
| ADR-0010 | Locale and timezone discovery fallbacks | Accepted |
| ADR-0011 | Phase 2 benchmark regression policy | Accepted |
| ADR-0012 | Adapter crate split per host OS | Accepted |
| ADR-0013 | Widget lowering strategy | Proposed |
| ADR-0014 | Layout engine uses Taffy | Proposed |
WIT Style Guide
Krate UAPI is written in WIT because WIT is the contract between an app and every host we support. Once a module is frozen, app authors build against it and host adapters must keep honoring it. This guide keeps that contract boring, small, and portable.
Phase 2 uses this guide for io, fs, net, time, and locale.
The Rule
Every WIT change should pass this sentence:
A Rust, Go, or TypeScript app can call this API, and Linux, macOS, and Windows can implement it without guessing what the words mean.
If a type only makes sense on one host, it is not ready for UAPI. Put it behind an adapter detail, defer it, or write an ADR.
Package Names
Use one package per UAPI module:
package krate:fs@0.1.0;
package krate:net@0.1.0;
Rules:
- Use
krate:<module>@<semver>. - Use short nouns for modules:
fs,net,time,locale. - Do not put host names in package names.
- Do not use marketing names in WIT. WIT is an ABI, not a product page.
The app world imports versioned module interfaces:
package krate:app@0.1.0;
world cli {
import krate:io/stdio@0.1.0;
import krate:fs/files@0.1.0;
export run: func() -> s32;
}
Interface Names
Use interface names for one clear job:
interface files
interface http-client
interface clock
interface format
Rules:
- Use kebab-case.
- Use nouns for groups of stateful operations:
files,streams. - Use a noun phrase for a service:
http-client. - Avoid buckets like
utils,common,helpers, ormisc.
If an interface needs three unrelated paragraphs to explain it, split it.
Function Names
Use small verbs or verb phrases:
open: func(path: string, mode: open-mode) -> result<file, fs-error>;
format-date: func(millis: u64, timezone: string, style: date-style, locale: locale-id) -> string;
Rules:
- Use kebab-case.
- Prefer verbs:
open,read,write,flush,rename. - Include the object only when it removes ambiguity:
remove-file,remove-dir,format-date. - Do not expose host syscall names.
- Do not encode permission checks in names. The capability system handles that.
Bad:
win32-create-file-w: func(path: string) -> result<u32, fs-error>;
Good:
open: func(path: string, mode: open-mode) -> result<file, fs-error>;
Records, Enums, And Variants
Use records for data with named fields:
record file-stat {
size: u64,
modified-millis: u64,
is-dir: bool,
}
Use enums for closed sets with no attached data:
enum http-method {
get,
post,
put,
}
Use variants when at least one case needs data:
variant net-error {
invalid-url,
dns-failure(string),
permission-denied,
other(string),
}
Rules:
- Use kebab-case for type names, fields, and cases.
- Prefer explicit units in field names:
modified-millis,timeout-millis. - Keep records flat unless nesting gives a real domain boundary.
- Do not add an enum case unless every host can produce or consume it.
- Use
other(string)only as a temporary escape hatch. If it becomes common, promote the case to a real variant.
Resources Vs Functions
Use a resource when the host owns state across calls:
resource file {
read: func(n: u32) -> result<list<u8>, fs-error>;
write: func(bytes: list<u8>) -> result<u32, fs-error>;
stat: func() -> result<file-stat, fs-error>;
}
Use a plain function when the call has no lasting host-owned handle:
stat: func(path: string) -> result<file-stat, fs-error>;
get: func(url: string) -> result<list<u8>, net-error>;
Choose a resource when:
- The app needs repeated operations on the same host object.
- The host must control lifetime.
- The value cannot be copied safely into app memory.
- Future calls need a cursor, buffer, stream, socket, window, or device session.
Choose a function when:
- The call is request-response.
- All data can be copied in and out.
- There is no cleanup beyond returning the result.
Error Style
Most UAPI calls should return result<T, module-error>.
read: func(n: u32) -> result<list<u8>, io-error>;
open: func(path: string, mode: open-mode) -> result<file, fs-error>;
Use typed errors so apps can handle normal failures:
variant fs-error {
not-found,
permission-denied,
invalid-path,
io(string),
}
Rules:
- Every capability-protected module must have
permission-denied. - Use module-specific errors:
fs-error,net-error,io-error. - Use variants for expected failures.
- Trap only for runtime bugs, invalid ABI state, or exhausted runtime resources.
- Do not return raw OS error numbers. Translate them at the adapter boundary.
For example, a missing file is not a trap. It is fs-error.not-found.
Capability Names
Capabilities are not WIT syntax, but every protected WIT call needs a matching capability shape.
Current Phase 2 examples:
| WIT call | Capability |
|---|---|
fs.files.open(path, read) | fs.read:<path-or-glob> |
fs.files.open(path, write) | fs.write:<path-or-glob> |
net.http-client.get(url) | net.connect:<host>:<port> |
time.clock.now-millis() | time.now |
io.stdio.stdout() | io.stdout |
Rules:
- The capability name should describe the permission, not the function name.
- Resource scopes should be human-readable: paths, host:port pairs, device IDs.
- Default grants must stay low risk: stdout, stderr, args, locale, and clock are acceptable in Phase 2; filesystem and network are not.
- If a WIT call reaches the host OS, decide its capability before merging it.
Comments
Use WIT comments for anything that will help an app author:
/// Return the current wall-clock time in milliseconds since Unix epoch.
now-millis: func() -> u64;
Rules:
- Write comments for public interfaces, records, resources, and non-obvious functions.
- Say what the API means, not how the current Rust adapter happens to work.
- Mention units, ordering, encoding, and limits.
- Keep comments stable. The generated UAPI reference publishes them.
Versioning
Phase 2 is still draft, but the versioning rule is already strict:
- Additive changes can stay in the same minor draft while the module is not frozen.
- Removing a function, changing a field type, or renaming a case is breaking.
- After v0.1.0 is frozen, breaking changes need a new module version.
- A frozen app world should import exact module versions.
Before freezing a module, run through every sample app and ask whether the names would still make sense in Phase 3 GUI, Phase 4 mobile, and Phase 6 packaged apps. If not, fix the name before the freeze.
The Native Three Test
Before adding a UAPI type, write down how it maps to Linux, macOS, and Windows.
For Phase 4 mobile-facing types, use the native three of five test: the type must map naturally to at least three of Windows, Linux, macOS, Android, and iOS. If it only maps cleanly to one platform, it is probably an adapter detail or a higher-level convenience API.
This test is not about lowest common denominator design. It is about avoiding fake portability.
Review Checklist
Before a WIT PR merges:
- Package and interface names are kebab-case and versioned.
- Every function has a clear host behavior.
- Every protected call has a capability shape.
- Errors are typed and include
permission-deniedwhere needed. - Resources are used only for host-owned state.
- Units are named in fields and docs.
- The generated UAPI reference is updated.
- The relevant sample app still reads naturally through the Rust SDK.
- Any hard-to-reverse choice has an ADR.
Small WIT is good WIT. Add the narrow thing we can support everywhere, then grow it after real apps prove the missing shape.
CI and runners
Krate uses two CI paths.
The normal CI workflow runs on GitHub-hosted runners. Because the repository
is public, this is the best default path for daily work. Pushes to main and
pull requests run the cheap checks:
- Rust formatting
- Clippy
- Linux workspace tests
- mdBook docs build
The expensive checks stay opt-in. Run the CI workflow manually with
full = true, or include [full-ci] in a push commit message, when you want:
- shared component fixture builds
- Linux, macOS, and Windows runtime checks
- Phase 2 host-binding checkpoint
- benchmarks
cargo-deny
Hosted workflows use Node 24-ready core actions (actions/checkout@v5 and
actions/setup-node@v5). Cache and artifact steps use the Node 24 action
families too: actions/cache@v5, actions/upload-artifact@v7, and
actions/download-artifact@v8. Pages uses actions/configure-pages@v5 and
actions/upload-pages-artifact@v4.
All workflows also set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true, so any action
that has not moved its major tag yet still runs under the Node 24 action runtime.
This keeps CI evidence cleaner and avoids waiting for the June 2026 runtime
switch to surprise us.
In hosted full CI, the Phase 2 TypeScript language-variant lane now runs in
ts mode by default. The fixture build step can install jco through npx
when needed, and the full-test matrix pins Node 22 for this lane, so TypeScript
runtime fixtures stay active without manual runner tool preinstall.
Self-hosted CI now uses the same pinned npx fallback for jco in its fixture
build step.
Dependency audit now runs through scripts/check-dependencies.sh. This keeps
licenses, bans, and sources as hard failures, while known advisory-db
parser or lock-path failures in the current cargo-deny path are downgraded to
warnings until upstream compatibility catches up.
There is also a Self-hosted CI workflow. It is manual-only and targets a
runner labeled krate-local. Use it when you want GitHub to run the full
local gate on your own machine instead of a hosted runner.
Self-hosted workflows currently keep actions/checkout@v4 to preserve
compatibility with older local runner installs.
They still opt JavaScript actions into Node 24, so a local runner must be new
enough to support that runtime. For the newer cache and artifact action
families, use runner 2.327.1 or newer.
The scheduled Self-hosted Fuzz Nightly workflow now keeps older queued runs
when the runner is offline. Automatic schedule runs do not cancel each other
anymore, while manual dispatch still cancels in-progress runs for the same
branch.
That local gate now also runs a short Phase 2 fuzz smoke over the first fuzz
targets.
The benchmark regression step is warning-only by default in this manual
workflow, and you can switch it to strict fail mode with the
benchmark_regression_mode input when you want to enforce performance gating.
It now runs a TinyGo WASI Preview 2 build-smoke lane for Go clock/cat/curl
samples and then tries to promote those artifacts into Krate runtime fixtures
through:
scripts/promote-phase2-go-runtime-fixtures.sh
The promotion script still builds with TinyGo and checks wasi:cli/run export
shape. It then runs import-purity checks. Only import-pure outputs are copied to
test/integration/language-variants/krate_go_*.wasm and picked up by runtime
variant tests. When imports are still host wasi:*, the script keeps the Go
runtime fixtures disabled and prints a clear skip message.
The TypeScript curl variant now has extra runtime assertions that do not depend on localhost fixture sockets. Missing-grant and unresolved-host paths are checked directly, so restricted runners still provide useful curl evidence. The Go curl variant now has matching non-localhost denial and unresolved-host assertions too, so we keep useful failure-path evidence across both language lanes even when localhost fixture sockets are blocked.
The runtime tests now include an optional Phase 2 language-variant slice for Go and TypeScript sample components. It runs through:
scripts/test-phase2-language-variants.sh
When both Go and TypeScript fixture sets are present, this script now also runs
cross-language parity checks for Rust, Go, and TypeScript clock, cat, and
curl samples.
By default it skips unless any of these env vars are set to built component paths:
KRATE_GO_CLOCK_WASMKRATE_GO_CAT_WASMKRATE_GO_CURL_WASMKRATE_TS_CLOCK_WASMKRATE_TS_CAT_WASMKRATE_TS_CURL_WASM
It can also auto-discover built variant fixtures at these paths when the env vars are not set:
test/integration/language-variants/krate_go_clock.wasmtest/integration/language-variants/krate_go_cat.wasmtest/integration/language-variants/krate_go_curl.wasmtest/integration/language-variants/krate_ts_clock.wasmtest/integration/language-variants/krate_ts_cat.wasmtest/integration/language-variants/krate_ts_curl.wasm
Setting up a local runner
In GitHub, open:
Settings -> Actions -> Runners -> New self-hosted runner
Choose macOS if you are using your Mac. GitHub will show the exact commands to download and configure the runner. When it asks for labels, add:
krate-local
Start the runner with the command GitHub shows, usually:
./run.sh
After that, open Actions -> Self-hosted CI -> Run workflow.
The runner uses your machine, so close it when you do not want jobs to start. For a personal project machine, the safest habit is to run it only while you are actively working.
Fuzzing
Krate now has a first Phase 2 fuzz harness set in:
fuzz/
Current targets:
manifest_parseforManifest::parselogical_path_parsefor shared path normalization and operation intent checkspolicy_matchfor capability parse and session-policy matching behavior
Install
cargo install cargo-fuzz --locked
rustup toolchain install nightly --profile minimal
Run a short smoke
From repo root:
scripts/run-phase2-fuzz-smoke.sh
This runs each target for a short window to catch immediate crashes.
Run longer sessions
cargo fuzz run manifest_parse -- -max_total_time=300
cargo fuzz run logical_path_parse -- -max_total_time=300
cargo fuzz run policy_match -- -max_total_time=300
For full Phase 2 exit work, we still need nightly multi-hour runs and trend tracking. This page covers the first harness setup only.
Setting up your environment
Prerequisites
- Rust (stable, via rustup)
- Git
Optional (needed for specific phases):
cargo-component: for building WASM components (Phase 1+)wasm-tools: for WASM inspection and optimization (Phase 1+)mdbook: for building the docs site (cargo install mdbook)tinygoandgo: for the Phase 2 Go binding pathnode,npm, andjco: for the Phase 2 TypeScript binding path
If you want GitHub to run jobs on your own machine, see CI and runners.
Run this after setup:
krate doctor
It reports core Rust tools first, then Phase 2 language tools, including
wasm-tools, TinyGo, Go, Node, npm, and jco.
Missing Go or TypeScript tools are fine until you work on those binding tracks.
The first Go and TypeScript SDK scaffolds live in packages/sdk-go and
packages/sdk-ts. They do not require TinyGo or jco for normal Rust runtime
work, but the Go and TypeScript sample builds will need those tools.
Clone and build
git clone https://github.com/incyashraj/krate.git
cd krate
cargo build --workspace
scripts/test-phase1.sh
This should work in under 10 minutes on a fresh machine. If it doesn't, open an issue: that's a bug.
Run the docs site locally
cargo install mdbook # one-time
mdbook serve docs/book
# then open http://localhost:3000
Useful commands
cargo fmt --all -- --check # check formatting
cargo clippy --all-targets --all-features -- -D warnings # lint
scripts/test-phase1.sh # build fixture + run all tests
cargo build --workspace # build everything
Phase 0: Foundation
Status: Mostly done Estimate: mostly complete; external launch items remain Goal: Make the project real enough that work can happen in public.
Done
- Dual license files are in place.
- The Rust workspace builds.
- CI runs format, lint, tests, docs, and dependency checks.
- GitHub Pages publishes this book.
- Issues, labels, templates, contributor docs, and security docs exist.
- ADR-0001 records the Rust runtime decision.
- The project name moved to Krate after the first naming screen.
- The repo has been pushed to
incyashraj/krate.
Still Important
- Keep branch protection strict on
main. - Confirm public repo visibility, social preview, and repo metadata.
- Have one outside reader review the README.
- Create Discord or another community home before a public announcement.
- Secure
krate.devor another chosen domain. - Finish official naming and trademark checks.
- Get one external contributor PR opened and merged.
These are mostly public launch tasks. They should not stop Phase 2 design work, but they should be finished before we claim Phase 0 is fully closed.
See the detailed checklist in Phase 0 Status.
Phase 0 Status
This page tracks only the important Phase 0 exit work. The detailed checklist
lives in Plan/Phase-0-Plan.md.
Done
| Area | Status |
|---|---|
| Licenses | MIT and Apache-2.0 files are present. |
| Rust workspace | Cargo build and test pass. |
| Toolchain | Rust 1.91.1 is pinned. |
| Dependency policy | cargo-deny passes. |
| CI | Format, lint, tests, docs, and dependency audit run on GitHub Actions. |
| Docs site | mdBook is live at https://incyashraj.github.io/krate/. |
| Project docs | README, CONTRIBUTING, SECURITY, code of conduct, ADR template, and first-PR guide exist. |
| Issues and labels | First five good-first issues were created and labeled. |
| Phase 1 kickoff | GitHub issue #6 exists. |
Still Important
| Area | Next action |
|---|---|
| Public repo settings | Confirm public visibility, social preview, description, homepage, and topics. |
| Branch rules | Keep main protected by green CI and pull request review. Owner bypass should stay temporary. |
| CI history | Keep main green for five consecutive days. First green runs started on 2026-05-03. |
| README review | Ask one outside reader to follow the README and mark what confused them. |
| Discord or community home | Create channels, rules, and an invite link before public launch. |
| Announcement | Publish only after docs, repo settings, and community home are ready. |
| Domain | Secure krate.dev or a chosen equivalent. |
| Naming | Finish official trademark and registry checks. |
| External PR | Get one outside contributor PR opened and merged. |
| Retrospective | Finish the Phase 0 retro after the external work is done. |
Bottom Line
Phase 0 is ready enough for engineering work to continue. It is not 100% closed as a public launch phase because the outside-world checks are still open.
Phase 0 Retrospective
Status: Draft; fill after external Phase 0 work completes.
What Shipped
- Repository scaffold.
- Cargo workspace sentinel.
- CI and docs workflows.
- mdBook documentation.
- ADR-0001.
- Contributor, security, conduct, and first-PR docs.
- Legal/naming search record.
What Took Longer Than Expected
- The original OneOS naming search uncovered material conflicts, so the project moved to Krate before public announcement.
What Changed In The Plan
- Krate is the active project name, pending formal trademark clearance.
Open Risks
- Final product name is not cleared.
- External GitHub settings are not yet confirmed.
- Discord and public launch are blocked on naming/docs readiness.
Phase 1 Readiness
Fill this section when every Phase 0 exit criterion is complete or explicitly waived.
Phase 1: Runtime Proof
Status: Engineering done; historical proof path retained
Estimate: done
Goal: Prove one .wasm file runs through Krate on Linux, macOS, and
Windows.
Done
crates/runtimeembeds Wasmtime.crates/clibuilds thekratecommand.krate run,krate version, andkrate doctorwork.- A small hello-world WASM component prints
Hello, Krate!. - Runtime fuel and memory limits fail with clear errors.
- CI builds one hello
.wasmartifact and runs the same bytes on Linux, macOS, and Windows. v0.1.0-rc1is published with five archives andSHA256SUMS.- Quickstart, architecture notes, benchmarks, threat model, and retrospective are published.
- ADR-0002 and ADR-0003 are merged.
What This Means
We have proven the base runtime path:
flowchart LR
W["One WASM file"] --> R["Krate runtime"]
R --> L["Linux"]
R --> M["macOS"]
R --> X["Windows"]
classDef done fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
class W,R,L,M,X done;
That is a real milestone. It does not mean Krate can run full apps yet. It means the engine can load a portable component and execute it consistently on three desktop hosts.
Current Role
Phase 1 is no longer the active development path. It remains in the repository
as the original proof that one .wasm artifact can run through the Krate
runtime. The active app model is Phase 2.
For new app work, use the Phase 2 UAPI path. See Migrating From Phase 1 To Phase 2.
Phase 1 Benchmarks
These numbers are the first local baseline for the proof-of-concept runtime. They are not product performance promises; they give us a repeatable way to see whether future runtime changes move Krate in the right direction.
Reference Machine
| Field | Value |
|---|---|
| Date | 2026-05-02 |
| CPU | Apple M4 |
| Cores | 10 physical / 10 logical |
| Memory | 16 GB |
| OS | macOS 26.3.1 (25D2128) |
| Architecture | arm64 |
| Rust | rustc 1.91.1 |
| Wasmtime | 43.0.2 |
Microbenchmarks
Run with:
scripts/build-phase1-components.sh
cargo bench -p krate-runtime --bench startup
scripts/check-benchmark-regression.sh
| Metric | Baseline | Phase 1 target | Notes |
|---|---|---|---|
| Wasmtime engine construction | 864 ns | < 100 ms | Measures Runtime::new. |
Component::from_binary for hello-world | 2.366 ms | < 20 ms | Measures component compilation from bytes. |
Cold runtime run to run() completion | 2.451 ms | < 200 ms | Includes runtime creation, compile, instantiate, and one host print to a sink. |
First host print on a loaded component | 10.067 us | Track | Measures instantiate plus one host call with output suppressed. |
1,000 host print calls on a loaded component | 88.189 us | < 1 us/call | Criterion throughput: about 88 ns per call including loop overhead. |
| RSS after hello-world exits | 14.9 MiB | < 40 MiB | Measured with /usr/bin/time -l on macOS. |
The committed JSON baseline lives at
docs/book/src/phase1/benchmark-baseline.json. CI runs the same benchmark suite
and emits a warning if a metric is more than 10% slower than the recorded
baseline. It does not fail the build during Phase 1 because GitHub-hosted
runners are noisy.
Binary Size
| Artifact | Size |
|---|---|
target/release/krate | 11 MB |
dist/krate-0.1.0-dev-aarch64-apple-darwin.tar.gz | 4.4 MB |
Notes
- The host output is routed to an in-process sink for microbenchmarks so terminal I/O does not dominate host-call dispatch measurements.
- There is no AOT cache yet. Phase 2 can use this baseline to prove cache wins.
- The benchmark suite is small enough to run in CI, but the published baseline should be refreshed only from the documented reference machine.
Phase 1 Threat Model v0.1
Krate Phase 1 is a proof-of-concept runtime. It can load a WebAssembly
component, register a temporary krate:phase1/host interface, call the
component's exported run function, and route print/exit back to the CLI.
This threat model is narrow on purpose. It records what Phase 1 defends today, what it only partially defends, and what must wait for later phases.
Scope
In scope:
krate run <component.wasm>- The
krate-runtimeWasmtime embedding - The temporary
krate:phase1/hostWIT imports:print(msg: string)exit(code: s32)
- Runtime fuel and memory limits
- CLI error classification for invalid components, traps, and limit failures
Out of scope:
- Real UAPI modules such as filesystem, network, UI, sensors, and identity
.kratebundles, manifests, signing, and marketplace distribution- Long-running app lifecycle, windows, background services, and updates
- Running adversarial WebAssembly as a hardened security boundary
Assets
| Asset | Why It Matters |
|---|---|
| Host process memory | The runtime must not let a component corrupt host memory. |
| Host filesystem and network | Phase 1 should not expose either to components. |
| Terminal output | Components can write through print; users should know that output is untrusted. |
| Runtime availability | Components should not trivially hang or exhaust the host. |
| Build and dependency integrity | Wasmtime and cargo dependencies are part of the trusted base. |
Trust Boundaries
flowchart LR
subgraph Untrusted["Untrusted"]
WASM["WASM component"]
end
subgraph Trusted["Trusted Krate Process"]
RT["krate-runtime"]
HOST["Phase 1 host imports"]
CLI["krate CLI"]
end
subgraph Platform["Host Platform"]
OS["Host OS"]
TERM["Terminal"]
end
WASM -- "component imports" --> RT
RT --> HOST
HOST -- "stdout" --> TERM
CLI --> RT
RT --> OS
Phase 1 has one primary trust boundary: the WebAssembly component is untrusted; the Krate runtime, CLI, Wasmtime engine, and host OS are trusted.
STRIDE Analysis
| Category | Threat | Current Mitigation | Residual Risk |
|---|---|---|---|
| Spoofing | A component pretends to be a trusted Krate app. | Phase 1 has no app identity, no install flow, and no marketplace. Components are run directly by path. | Users may infer trust from filenames or local paths. Deferred to Phase 6 signing and identity. |
| Tampering | A component attempts to corrupt runtime memory or modify host state. | WebAssembly linear memory is sandboxed by Wasmtime. Phase 1 exposes no filesystem, network, environment, or process-spawning host imports. | Wasmtime bugs or unsafe host code could still be exploitable. Dependency advisories are tracked with cargo-deny. |
| Repudiation | A component denies having printed output or exited with a code. | CLI stdout/stderr and process exit code are observable by the caller. | No durable audit log exists. Deferred to Phase 2 logging UAPI and later policy/audit work. |
| Information Disclosure | A component reads host files, env vars, memory, network data, or secrets. | Phase 1 registers only print and exit; no WASI filesystem, network, env, or clock capabilities are linked. | Side channels, engine vulnerabilities, and terminal escape output are not fully mitigated. |
| Denial of Service | A component loops forever or grows memory until the process/host is unhealthy. | --fuel enables Wasmtime fuel metering; --mem-limit uses a Wasmtime resource limiter; limit failures return exit code 4. | Fuel is opt-in for now. CPU and wall-clock timeouts are not enforced by default. |
| Elevation of Privilege | A component escapes the WASM sandbox and executes host code. | Krate relies on Wasmtime's sandbox and keeps the host import surface tiny. | A Wasmtime or codegen vulnerability could break this assumption. Security response depends on upstream patches. |
Current Controls
- No filesystem host imports.
- No network host imports.
- No environment variable host imports.
- No app-to-app IPC.
- No native plugin loading.
- No bundle install/update path.
- Wasmtime Component Model validation rejects invalid components.
cargo-denychecks advisories, licenses, bans, and sources.krate run --fuel Ncan bound instruction execution.krate run --mem-limit MBbounds each linear memory.
Required User Warning
Phase 1 users must treat krate run foo.wasm like running a local developer
tool, not like installing a sandboxed app from a store.
Krate is pre-alpha. Do not run untrusted WASM through
kratein Phase 1. Treatkrate run foo.wasmexactly as you would treat running a local executable from a developer checkout. The sandbox is real, but the platform is not adversarially hardened yet. Real capability boundaries arrive in Phase 2.
Deferred Items
| Deferred Item | Target Phase | Notes |
|---|---|---|
| Capability declarations and grants | Phase 2 | UCap begins when real UAPI modules exist. |
| Filesystem/network permission enforcement | Phase 2 | First useful CLI apps will need explicit capabilities. |
| Structured runtime logging and audit records | Phase 2 | Needed for debugging and repudiation controls. |
| GUI/window security boundaries | Phase 3 | UI introduces input, focus, clipboard, accessibility, and rendering risk. |
| Mobile platform sandbox integration | Phase 4 | iOS/Android require adapter-specific review. |
| Bundle manifests and deterministic package verification | Phase 5 | Developer SDK starts producing app bundles. |
| Code signing, release trust, and marketplace moderation | Phase 6 | Required before user-facing distribution. |
| External security audit | Phase 7 | Must happen before v1.0. |
Review Triggers
Update this threat model when any of these happen:
- A new host import is added.
- A WASI interface is linked into the runtime.
- The runtime gains filesystem, network, clock, IPC, UI, or device access.
krate runstarts reading manifests or bundle metadata.- Wasmtime is upgraded across a major version.
- A security advisory affects Wasmtime, WIT tooling, or Krate runtime code.
Phase 1 Retrospective
Status: Engineering closeout complete; external validation still pending.
Phase 1 proved the smallest useful Krate loop: build one WASM component once, run the exact same bytes through the Krate runtime on Linux, macOS, and Windows, and package release artifacts that a real user can download and verify.
What Shipped
crates/runtimeembeds Wasmtime with Component Model support.crates/cliships the firstkratebinary withrun,version, anddoctorcommands.- A temporary
krate:phase1/hostWIT interface supportsprintandexitfor the hello-world proof. scripts/test-phase1.shvalidates runtime behavior locally and in CI.- CI builds one shared hello
.wasmfixture, records its SHA-256 hash, and runs that same artifact on Linux, macOS, and Windows. v0.1.0-rc1published five platform archives plusSHA256SUMS.- Quickstart, architecture notes, benchmark baseline, Threat Model v0.1, ADR-0002, and ADR-0003 are published.
What Worked
The shared-fixture CI design was the right proof. Earlier runs compared
host-built .wasm hashes and failed for a reason that was true but not useful:
different hosts can produce different component bytes. The real Krate promise
is that one app artifact runs everywhere, so CI now tests that directly.
Keeping Phase 1 narrow also helped. The runtime only has the temporary host interface it needs, and UAPI design is left for Phase 2 where it can be reviewed as a platform contract instead of a quick demo surface.
What Took Longer Than Expected
The GitHub Actions path uncovered real launch details early: Pages needed manual
configuration, the dependency audit action lagged behind CVSS 4.0 advisories,
and artifact upload paths had to be made explicit for hidden .wasm files.
Release packaging also forced the project to prove naming, archive layout, and checksum publishing sooner than planned. That was useful pressure; it made the runtime feel less like a local experiment and more like a product foundation.
What Changed In The Plan
Krate remains the product name, while layer6x6 remains the development repo
name for now. The 6x6 framing is still the strategic map; Krate is the name of
the platform that grows out of solving that matrix.
The Phase 1 release gate now explicitly accepts RC tags such as v0.1.0-rc1.
That keeps early platform proof honest without pretending the API is stable.
Open Risks
- One external user still needs to complete the quickstart in 10 minutes or less.
- Phase 0 external gates remain open: Discord, public announcement, domain, and external contributor PR.
- GitHub branch protection exists as a ruleset, but direct owner bypass should be treated as temporary while the project is founder-only.
- GitHub Actions is warning that several actions still run on Node.js 20; this is not failing today, but it should be cleaned up before the forced Node.js 24 transition.
Phase 2 Readiness
The engineering foundation is ready for Phase 2 design work. The next durable
choices are WIT versioning, UCap enforcement shape, host adapter boundaries, and
the first real UAPI modules: io, fs, net, time, and locale.
Do not freeze Phase 2 WIT quickly. The first app examples should pressure-test the interfaces before the project treats them as stable v0.1 contracts.
Phase 2: UAPI v0.1
Status: Active; exit evidence in progress Estimate: est. 4 to 8 weeks Goal: Make Krate useful for small command line apps.
Security baseline for this phase is now published: Phase 2 Threat Model v0.2.
Phase 2 replaces the temporary Phase 1 host interface with real APIs:
iofsnettimelocale
The first draft of those WIT contracts now lives at wit/krate/phase2.
It is not frozen yet, but it is real source code and CI parses it so syntax
mistakes are caught early.
Phase 2 also has a quick readiness command now:
scripts/phase2-exit-readiness.sh. It reads the exit ledger and prints the
current count of done, partial, pending, and blocked gates, so progress checks
do not depend on memory.
Hosted CI stability can now be recorded too:
scripts/record-phase2-ci-stability-evidence.sh writes recent CI and Pages run
history into one markdown report for exit review.
The same report can be included in the exit bundle with
scripts/record-phase2-exit-bundle.sh --strict --include-ci-stability when we
prepare a final review packet.
Self-hosted full-gate history has its own recorder too:
scripts/record-phase2-self-hosted-evidence.sh. This keeps the local runner
proof separate from hosted CI while still letting the exit bundle include it
when needed.
The UAPI freeze decision has its own packet now as well:
scripts/check-phase2-freeze-decision.sh checks that the packet names the
scope, required evidence, no-go conditions, and pending reviewer signoff before
we call the contract frozen.
The capability layer has also started. Krate can parse a sidecar
manifest.toml, check launch-time grants, and carry the session policy into the
runtime. The newest piece is a runtime UAPI guard: it translates calls like
fs.read ./data/file.txt or net.connect api.example.com:443 into the exact
capability check that must pass before an adapter talks to the host OS.
Manifest capability parsing now also validates resource shapes earlier: unsafe
filesystem resource patterns and malformed net.connect endpoint forms fail
during manifest parsing instead of reaching runtime policy resolution.
Manifest net.connect host parsing is stricter now too: invalid dot placement,
invalid leading or trailing - in host labels, and malformed numeric IPv4
host forms are rejected at parse time.
Session policy matching now also normalizes net.connect host casing and
numeric ports before wildcard matching, so grant checks stay stable when users
type host names with mixed case or port values like 0443.
Manifest capability parsing now stores that same canonical net.connect
resource shape too, so duplicate-capability checks and capability displays stay
deterministic across host case and numeric port formatting differences.
Manifest capability parsing now applies the same deterministic storage rule to
fs.* resources through shared logical-path normalization, so formatting
variants like ./notes/** and notes\\** are stored as one canonical
notes/** shape for duplicate checks and capability output.
Interactive grant prompts now keep rationale text aligned with those canonical
capability shapes too, so prompts still show the right human reason lines even
when the manifest used non-canonical resource formatting.
There is also a first dispatcher scaffold now. In simple terms: we have the place where generated UAPI calls will enter the runtime, get checked by policy, and then move to the host adapter. The current tests prove denied file and network calls stop before any adapter code runs.
The Phase 2 WIT also has a Rust host-binding checkpoint now. That means CI asks
Wasmtime to generate bindings for the new cli world and checks a few important
names. So far the shape is usable: run returns an i32, OpenMode::Read
exists, and HttpMethod::Get exists.
There is also a first Rust guest SDK crate now. It lives at
crates/bindings-rust, builds as package krate, and gives app code simple
module names such as krate::io, krate::time, and krate::locale.
The Rust sample apps now use that SDK facade, so normal app code talks to
krate::fs::open, krate::net::get, and krate::time::now_millis
instead of deep generated binding paths.
The SDK now has its first small helper layer too: argument helpers, stdout and stderr text helpers, common file helpers, HTTP body helpers, and top-level time and locale shortcuts. The important rule is still the same: guest apps should import Krate UAPI, not host WASI APIs. The current sample components are checked for that. The Rust SDK also has a packaged-crate smoke now: CI creates a temporary app outside the workspace and checks that it can compile a tiny Krate component against the packaged SDK.
Go now has a clear Phase 2 decision too. The Go examples and TinyGo smoke build
stay in scope, but Go runtime parity is experimental until the compiled
components import only krate:* UAPI packages. We are not promoting Go
fixtures that still import wasi:* directly, because that would weaken the
runtime boundary Phase 2 is meant to prove.
The latest runtime piece is the generated type bridge. It maps WIT records and errors into the dispatcher's Rust types, then back again. Put simply: the runtime now understands the words that generated WIT code will use when it asks for files, network, time, locale, and logs.
The generated host traits are also wired for the first useful slice. HTTP,
path-level filesystem operations, time, locale, logs, and stdio now call the
dispatcher, which means UCap sits in front of those calls. A small resource
table now owns opened file and stdio handles, so reads, writes, seeks, stats,
and flushes can route through the adapter without exposing raw host IDs. The
local runtime now also caps that open-handle table, so Phase 2 components
cannot grow stream/file handles without bound. Generated resource drop
callbacks now close underlying local adapter handles too, so released
resources return slots back to the same local runtime session. The generated
host-side resource table now has its own active-resource cap too, so both host
and adapter layers have bounded handle growth in this phase. Both tables now
also reuse released resource IDs before allocating fresh IDs, so long-running
sessions keep resource identity stable and avoid unbounded ID growth.
The runtime also has an initial Phase 2 execution path now. krate run keeps
supporting the Phase 1 proof world, then falls back to the Phase 2 cli world
and installs the generated UAPI imports. The local adapter currently covers
stdio, basic filesystem calls, time, locale, and a first plain HTTP request
path. Relative filesystem paths now resolve through an explicit runtime
sandbox root. The default is ., and --sandbox-root <dir> lets a run point
app-relative file access at a specific directory. Shared path cleanup also keeps
the filesystem adapter and UCap grant matcher using the same rules. It now also
rejects colon-based prefix forms up front, so Windows drive-style and alternate
data stream style paths do not cross host-specific parsing rules. It also
rejects reserved Windows device-style names such as con, nul, com1, and
lpt1 before host I/O, and now rejects path segments ending in . or a
trailing space to avoid Windows filename normalization edge behavior. It now also
rejects oversized path segments and oversized normalized logical paths before
host I/O so cross-host path behavior remains predictable in this phase. Read
and list bounds are now explicit too: one file read call is capped at 8 MiB,
and one directory list call is capped at 4096 entries in this early adapter
slice. Write bounds now match that direction: one stream or file write call is
capped at 8 MiB in the same early adapter path. Absolute
logical paths are now sandbox-rooted too, so /notes/file.txt resolves under
the configured sandbox root instead of host root. For relative
paths, the local adapter now checks canonical existing targets, or the canonical
parent for new files, before host I/O. If a symlink would take the path outside
the sandbox root, the adapter denies the call. On Unix and Windows hosts, file
open also uses a no-follow final-symlink flag, so the final filename cannot be
a symlink at open time.
That no-follow open behavior now routes through the per-OS adapter crate path
instead of runtime-local cfg blocks.
Runtime path resolution now also denies symlinked path segments inside the
sandbox traversal path itself, for both existing-target and create-path
operations, so this phase has less directory traversal race exposure.
For Windows parity, the same traversal guard now treats reparse-point segments
as blocked link semantics too, so junction-style hops are denied in the same
sandbox traversal path checks.
This behavior is now captured as an architectural decision in
docs/adr/0009-sandbox-link-semantics.md.
Destructive filesystem operations now go through a shared operation-intent check
too. That means remove and rename cannot target root-like paths such as . or
/ before the adapter reaches native host I/O.
The HTTP path is still small on purpose: it is for localhost and
test-server proofs while HTTPS, redirects, streaming, and production hardening
stay open. It now forwards lower-level fetch methods, app headers, and
buffered bodies, while keeping host-controlled transport headers owned by the
adapter. It also has a response-size guard and typed errors for oversized
responses, timeouts, and malformed HTTP responses, so apps can react to the real
problem instead of receiving one generic network failure. The shared URL parser
now also rejects whitespace, control characters, empty ports, and port 0
before anything reaches the request line or socket layer. It also rejects
unsupported authority forms in this early plain-HTTP slice and rejects control
characters in app-provided header values. Transfer-Encoding is now treated as
host-controlled with Host, Connection, and Content-Length.
Response parsing now also goes through shared adapter-common code, with strict
validation for HTTP version, status range, malformed header lines, header count
limits, and unsafe header values before data reaches runtime-facing response
types.
That shared response parser now also enforces a strict header-block size cap
(16 KiB), so oversized header sections fail early in a deterministic way.
Request framing now also enforces a strict total request-frame size cap, so
oversized combinations of valid headers and body fail before socket writes.
The response read loop is shared too, so timeout mapping and full-response size
limits use one helper across adapters.
Response integrity checks now reject unsupported response Transfer-Encoding
and conflicting or mismatched Content-Length shapes in this early plain-HTTP
slice.
Request framing now also enforces a shared buffered body size limit so this
early plain-HTTP path cannot consume unbounded request payloads.
Shared host parsing now also rejects invalid domain-label forms and invalid
numeric IPv4 literals so URL and capability-path validation stay aligned.
Numeric IPv4 textual parsing is now strict dotted-decimal as well, so ambiguous
leading-zero forms like 001.2.3.4 are rejected in both URL endpoint parsing
and manifest net.connect validation. Runtime dispatcher tests now also prove
these URLs fail as invalid before adapter network calls.
Host length is now bounded in the same way for both runtime URL endpoint
parsing and manifest net.connect parsing (maximum 253-byte host text), so
oversized hostnames fail early with consistent behavior.
It now also rejects wildcard or non-unicast numeric IPv4 endpoint forms
(0.0.0.0, 255.255.255.255, and multicast ranges) during endpoint parsing,
so runtime URL checks and manifest net.connect validation stay aligned on
connectable target shapes. Dispatcher tests now also prove these values are
rejected before any adapter network call runs.
For manifest host patterns, wildcard is now constrained to explicit shapes only:
a full left-most * label (* or *.example.com). Partial-label wildcard
forms (for example exa*mple.com) and multiple wildcard labels are rejected.
Policy matching now also treats *.example.com as a single-label wildcard
scope only (for example api.example.com, but not deep.api.example.com),
while * host grants remain the broad opt-in form. Runtime dispatcher tests
now cover this grant behavior directly.
Host names are now normalized to lowercase in shared URL parsing, so capability
checks stay stable across input case differences like EXAMPLE.com and
example.com. URL scheme checks are now case-insensitive as well, so
HTTP:// and HTTPS:// forms follow the same grant matching path.
Resolved socket-address lists now also go through shared normalization:
duplicates are removed, IPv4 addresses are preferred before IPv6 while
first-seen order is still preserved inside each family, unspecified wildcard
targets (0.0.0.0 and ::) are dropped, unusable targets (port=0 and
scope-less IPv6 link-local addresses) are filtered, non-unicast targets
(IPv4 broadcast, IPv4 multicast, and IPv6 multicast) are filtered, and
connect-attempt lists are capped to a fixed maximum so DNS result variance
cannot create unbounded retry loops.
When all resolved targets are filtered out by these guards, runtime resolution
now maps that case to a deterministic not-found path instead of leaving
host-specific behavior to later connect loops.
In this early plain-HTTP slice, URL parsing is also ASCII-only. Non-ASCII URLs
are rejected early so request framing and capability endpoint checks stay
deterministic until broader URL handling lands in a later hardening pass.
Request targets now also have a shared size limit before request framing so
this early plain-HTTP path rejects oversized path/query payloads up front.
The runtime's network capability gate now also uses a shared endpoint parser for
http:// and https:// URLs, so policy checks and adapter-side URL validation
no longer drift as separate parsers evolve. The plain http:// URL parser now
reuses that same authority parsing path to keep host/port validation in one
place.
For helper-style net.http-client.get calls, the default request timeout is now
runtime-configurable through krate run --http-timeout-millis. The default is
5000 milliseconds, and --http-timeout-millis 0 disables that default timeout
for the helper get path. For multi-address connect attempts, timed requests
now spend one shared connect-time budget across all resolved addresses instead
of applying the full timeout per address.
Time is also starting to move into shared adapter code. The local runtime now uses a common host clock helper for fixed test time, Unix-epoch milliseconds, monotonic elapsed time, and sleep. That keeps future desktop adapters from each making slightly different clock choices. It now also guards edge cases: monotonic nanoseconds saturate instead of wrapping, and out-of-range Unix-millisecond values fail with a clear conversion error.
Locale has the same first shared path now. The runtime uses common helper code
for LC_ALL/LANG locale detection, TZ fallback, basic locale normalization,
deterministic baseline date/number formatting, and locale-tag canonicalization
to stable language/script/region casing with a safe fallback for malformed
values, including stricter primary locale-subtag and bounded-subtag checks so
invalid locale-tag shapes fall back to en-US. Timezone normalization is now
conservative too, accepting only simple timezone-name shapes for this phase and
falling back to UTC on invalid input. It now also accepts normalized UTC
offset forms such as UTC+05:30, GMT-02:00, +0530, and -07, then stores
them as canonical UTC±HH:MM. Locale discovery now also has practical
fallbacks for LC_TIME, LC_NUMERIC, LC_MONETARY, LC_CTYPE,
LC_COLLATE, LC_MESSAGES, LANGUAGE (first preferred token), and
AppleLocale when LC_ALL/LANG are absent. Timezone discovery now has a
Unix fallback too: when TZ is not set and /etc/localtime is a zoneinfo
symlink, Krate derives a normalized timezone from that link target. It now
also falls back to /etc/timezone parsing if the localtime symlink path is not
usable, with strict parsing rules (ignore comments/blank lines, accept first
valid candidate, reject malformed timezone shapes). Real ICU4X formatting and
broader host-native per-OS locale/timezone discovery are still open, but the
early behavior now has one home instead of being copied in the runtime. This
fallback order is captured in
docs/adr/0010-locale-timezone-discovery-fallbacks.md.
Date formatting now also applies normalized UTC offsets to the rendered time
value, so UTC+05:30 and UTC-01:00 shift timestamps instead of only changing
the timezone label text.
As part of the per-OS adapter split, runtime locale and clock construction now
route through dedicated Linux, macOS, and Windows adapter crates, with fallback
logic kept for unsupported hosts.
The same is now true for time.sleep-millis, and locale read/format paths
(current, timezone, format-date, format-number), which now route
through the per-OS adapter path instead of runtime-local direct calls.
Filesystem no-follow open-flag setup and plain HTTP TCP connect calls now also
route through the same per-OS adapter crate path.
Socket-address resolution for plain HTTP fetch now follows that same per-OS
adapter route too.
Socket timeout setup for that fetch path now also routes through per-OS
adapters.
Sandbox link-metadata checks now also route through per-OS adapters, so both
symlink/reparse-point detection and network host calls are less runtime-local.
Filesystem stat and directory listing host calls now route through per-OS
adapters too.
Filesystem mutation host calls (remove-file, remove-dir, mkdir, rename)
now route through per-OS adapters as well.
Sandbox symlink metadata reads now also route through per-OS adapters.
Sandbox-root and parent-path canonicalization now also route through per-OS
adapters.
Filesystem open host calls now also route through per-OS adapters.
Runtime component-file reads and sandbox-root directory creation now also route
through per-OS adapters.
File-handle read, write, seek, and metadata host calls now also route through
per-OS adapters.
Stdin reads and stderr write or flush host calls now also route through per-OS
adapters.
Plain HTTP request writes and response reads now also route through per-OS
adapters.
Shared stdout print, write, and flush paths now also route through per-OS
adapters.
There is also a first smoke app under test/integration/phase2-smoke. It is not
one of the final sample apps yet. Its job is smaller: prove that a real Phase 2
component can read a file, call time and locale, and print through the UAPI
path. CI builds that component and runs it through krate run on the host
test matrix. The same smoke app now has a missing-grant test too: without
fs.read, the host returns permission denied and the component exits with a
clear stderr message.
The first named sample app has started too. apps/krate-clock is a Rust
component that reads time and locale through UAPI, then prints through UAPI
stdout. The CLI now has a hidden --test-time flag, so tests can freeze the
clock and check stable output. It now also has hidden --test-locale and
--test-timezone flags, so fixture tests can pin all clock output fields and
assert one exact snapshot across hosts.
The second sample path has started as well. apps/krate-cat reads app
arguments through krate:io/args.raw, opens files through krate:fs/files,
and writes bytes to UAPI stdout. The tests prove both sides: it reads files with
the right fs.read grant, and gets permission denied without that grant. It
also denies a file outside the granted glob with exit code 5, matching the
CLI's permission-denied convention. In this phase, raw app-argument transport
is intentionally conservative: empty arguments, newline/NUL delimiter
characters, too many argument entries, and oversized raw payloads are rejected
before they reach guest argument parsing. Krate CLI now also does the same
check as a preflight step, so these invalid argument shapes fail before runtime
startup.
The third sample path has started now too. apps/krate-curl reads a URL from
Krate app args, calls krate:net/http-client.get, and writes the response
body through UAPI stdout. Its first tests use a local HTTP server: with
net.connect:127.0.0.1:PORT it fetches, without that grant it exits before the
runtime opens a socket. Permission denial also exits with code 5.
Oversized responses, timeouts, and malformed HTTP responses now print specific
curl messages too.
The generated UAPI reference has also grown past a raw signature list. It now pulls capability strings from the manifest crate and adds short behavior notes under each function and resource method, so the docs explain both the call shape and the permission model in one place. The WIT files now carry first-pass contract comments for the Phase 2 world, interfaces, records, variants, enum cases, resource methods, and functions. Those comments flow into the generated reference, so the published API page is closer to a real freeze candidate instead of only showing type signatures.
The UAPI gate is stricter now too. scripts/check-uapi.sh first runs
wasm-tools component wit across the Phase 2 world and all dependency
packages (io, fs, net, time, locale), then runs the contract-shape
checks in krate-tools --bin check-uapi. That checker now also fails if
public Phase 2 WIT items are missing contract docs. Hosted and self-hosted CI
both run this before UAPI reference regeneration.
The Rust SDK publish-readiness smoke is stricter too. It still packages the
krate crate and compiles a tiny outside-workspace component against that
packaged crate, and now it also checks that the packaged crate contains the
public README, SDK root, and generated bindings files before passing.
There is now a Rust SDK Evidence page too.
scripts/record-phase2-rust-sdk-evidence.sh records package smoke results,
SDK doc-build results, and packaged-file presence in one report. Normal hosted
CI uploads that report as a rust-sdk-evidence artifact, which gives P2E-03
a concrete proof source while crates.io publishing waits for UAPI freeze.
There is now a dedicated UAPI Freeze Review
page. It turns the remaining freeze work into a clear checklist for contract
shape, runtime behavior, samples, language tracks, and evidence.
There is also a generated UAPI Freeze Evidence
page now. It records the current package set, imported interface set, world
shape, and contract checks in one place, so freeze review has a concrete
snapshot instead of only a checklist. Hosted CI and self-hosted CI now
regenerate that page and fail if it is stale.
There is also a generated UAPI Freeze Lock.
It records SHA-256 hashes for the exact Phase 2 WIT files under review. CI
regenerates it and fails if the committed lock is stale, so UAPI contract drift
has to be intentional and visible.
The UAPI Freeze Review Evidence
page now explains the local review recorder. That recorder checks the WIT
contract, generated reference, freeze evidence, freeze lock, adapter-boundary
guard, and exit ledger in one report. It gives the freeze review a concrete
artifact without pretending the final decision is already made.
There is now a Phase 2 Exit Evidence ledger too.
It tracks all 15 exit gates with a status, proof source, and next step, and CI
checks the page shape so the list cannot silently drift from the plan.
The quick local review path is now Exit Bundle.
scripts/record-phase2-exit-bundle.sh runs the core local exit checks and
writes one report with command results, the current gate snapshot, working tree
state, and log tails. This is not a completion stamp. It is a cleaner handoff
file for review before the final Phase 2 decision.
For the one human proof gate, there is now a
Timed Walkthrough Evidence page and a
template generator. This keeps the outside Rust walkthrough tied to a commit,
with one place for timing, step results, and notes.
The adapter split now has a guard too. The
Adapter Boundary page explains the rule in
plain terms: the runtime checks policy, then the OS adapter touches the host.
scripts/check-adapter-boundary.sh checks 34 runtime wrappers and verifies that
the Linux, macOS, and Windows adapter crates expose the matching functions.
Hosted CI and the self-hosted full gate run that check so direct host-call
backsliding is easier to catch.
There is now an Adapter Evidence flow too:
scripts/record-phase2-adapter-evidence.sh records a host report and
scripts/compare-phase2-adapter-evidence.sh compares Linux/macOS/Windows
reports for one commit. Hosted full CI now publishes per-OS adapter evidence
artifacts and runs the compare gate automatically.
The adapter report now includes more than boundary shape: it also runs shared
adapter behavior tests and the native adapter crate tests for that host. This
keeps P2E-02 pointed at real behavior proof instead of only checking that
function names exist.
Sample evidence has a repeatable recorder now too. The
Sample Evidence page explains how to run
scripts/record-phase2-sample-evidence.sh on each desktop host. The recorder
builds the CLI and Rust clock/cat/curl components, runs fixed fixtures, and
writes a markdown report with host metadata, commands, exit codes, stdout
hashes, and output snapshots. That gives the Phase 2 exit review a practical
way to compare Linux, macOS, and Windows sample behavior.
There is also a companion comparator now:
scripts/compare-phase2-sample-evidence.sh wraps
krate-tools --bin compare-phase2-sample-evidence so three host reports can
be checked in one run. It fails fast when clock/cat/curl stdout hashes drift
across hosts and supports a temporary --allow-blocked-curl exception for
restricted localhost environments. That exception is now narrow: if two hosts
run curl successfully and one host is blocked, the successful curl hashes must
still match.
Hosted full CI now also records per-OS sample evidence artifacts and runs a
sample-evidence-compare job automatically. That compare step currently uses
the curl blocked exception, while still enforcing strict cross-host hash parity
for clock and cat outputs plus any successful curl outputs.
Language-variant evidence now has a recorder as well:
Language Variant Evidence explains
how to run scripts/record-phase2-language-variant-evidence.sh. That report
captures fixture readiness plus test outcomes for Rust, Go, and TypeScript in
one markdown artifact, with fixture hashes and log tails for review.
There is now a companion comparator too:
scripts/compare-phase2-language-variant-evidence.sh checks three host reports
in one run, verifies same commit metadata, enforces passed build/test steps,
and fails when fixture availability is not aligned or a present fixture is
missing its hash. It does not require TypeScript fixtures generated by jco on
different operating systems to be byte-identical. This lane proves portable
behavior through Krate, not reproducible compiler output.
Hosted full CI now runs that comparator automatically after Linux, macOS, and
Windows full-test lanes upload their evidence artifacts.
UCap deny evidence now follows the same pattern:
UCap Enforcement Evidence shows how
to record one report per host with scripts/record-phase2-ucap-evidence.sh and
compare those reports with scripts/compare-phase2-ucap-evidence.sh.
The report now includes a named dispatcher matrix that proves every non-default
filesystem and network boundary returns permission denied before an adapter can
touch the host.
Hosted full CI now uploads per-host UCap evidence artifacts and runs a compare
job so deny-path regressions are caught as a cross-host gate, not only in local
checks.
Performance evidence now has the same repeatable flow:
Benchmark Evidence shows how to record
startup and dispatch benchmark results, run baseline regression checks, and
compare Linux/macOS/Windows benchmark reports for commit and gate consistency.
That report now also includes full external CLI startup evidence for
krate run krate-clock, so P2E-10 is tracking the command users will
actually run, not only the in-process runtime path.
The benchmark comparator also enforces per-host threshold bounds from the
metric table, so a host can no longer pass this lane with an over-threshold
metric hidden behind warning-only regression mode.
This gives P2E-10 and P2E-11 clearer cross-host proof tracking even when
raw timing numbers differ by hardware.
Dependency evidence is tracked beside those performance checks:
Dependency Evidence records the
cargo-deny wrapper output, tool versions, and any advisory-database warning
so dependency signoff is explicit instead of buried in CI logs.
The first terminal grant prompt exists too. krate run --prompt app.wasm
shows the app identity, lists missing manifest capabilities, accepts all or a
numbered subset, and stores the approved caps only for that run. In a normal
terminal the same prompt can appear automatically when required capabilities
are missing. In non-interactive runs, Krate keeps the safer behavior and
fails with a clear permission message.
There is now a small manifest trust check as well. If the sidecar manifest says
the app entry is app.wasm, then krate run must be pointed at that same
file. Running a different component with that manifest is rejected before any
grant prompt or runtime execution.
For debugging, krate run --dump-caps app.wasm now prints the effective
session capabilities and exits before the component starts. It is a simple way
to see what the current grant resolution actually produced.
The proof apps are:
krate-curlkrate-catkrate-clock
The Go and TypeScript SDK tracks now also include matching clock/cat/curl sample
sources with CI shape checks. The CLI test harness now also has optional
fixture assertions for Go and TypeScript variants behind KRATE_GO_* and
KRATE_TS_* WASM env vars. We now also run a pre-test fixture build step
(scripts/build-phase2-language-variant-fixtures.sh) in hosted full CI and
self-hosted CI. Hosted full CI now runs that step in ts mode by default and
allows npx install for jco (with a pinned package version). The full-test
matrix also pins Node 22 for this lane, so TypeScript runtime fixtures stay
active without manual runner setup. Go lane status remains explicit.
Hosted CI now also uses Node 24-ready core actions (actions/checkout@v5 and
actions/setup-node@v5) to remove Node 20 deprecation warnings in daily runs.
The same fixture build step now also auto-attempts Go runtime fixture promotion
when go, tinygo, and wasm-tools are available, so both language tracks
use one orchestration entry point.
When fixture mode is set to go or both, that Go promotion path now runs in
strict required mode so missing or non-pure Go fixtures fail that run clearly.
The shared fixture build step now also prints per-language readiness reasons
and includes those reasons in strict mode failures, so CI triage is faster when
language fixture lanes fail on toolchain or fixture-state issues.
TypeScript fixture generation now enforces Krate-only imports and uses the
real WIT variant shape for filesystem open mode ({ tag: "read" }), which made
the TypeScript cat fixture runtime path stable in local tests.
TypeScript curl fixture coverage now also includes non-localhost denial and
unresolved-host error checks, so restricted runners still prove key curl failure
paths even when localhost fixture sockets are unavailable.
TypeScript curl error classification is now aligned with Rust for two critical
failure paths: missing net grant now exits with code 5 and invalid URL now
exits with code 20. The CLI harness now includes explicit Rust versus
TypeScript parity checks for both of those paths.
Go curl fixture coverage now includes matching non-localhost denial and
unresolved-host checks with stable stderr markers.
The Go curl sample now also uses a stable, case-insensitive error classifier
with unit tests for key Krate net error classes, so common failure paths
map to predictable messages and exit codes (5, 20, 21).
The dedicated Go curl fixture tests now assert those same contracts directly,
instead of allowing broad fallback statuses, so parity regressions fail earlier.
The language-variant parity tests now check curl error paths across Rust, Go,
and TypeScript too: missing grant, invalid URL, and unresolved-host cases are
validated together without requiring localhost fixture sockets.
The Go fixture promotion diagnostics are also clearer now. When TinyGo outputs
are not Krate-import pure, the import checker reports every failing Go
artifact instead of stopping at the first one. Current local smoke artifacts
build successfully, but still import WASI host APIs, so they are correctly not
promoted into runtime fixtures yet.
There is now a Go readiness evidence recorder as well. It builds the TinyGo
smoke artifacts, records Go, TinyGo, and wasm-tools versions, hashes the
artifacts, and stores the import-purity log in one review file. This does not
make Go complete. It makes the remaining Go blocker visible enough for a clean
Phase 2 exit decision.
When both Go and TypeScript fixture sets are available, the CLI harness now
also runs cross-language parity checks for krate-clock, krate-cat, and
krate-curl. Those checks run Rust, Go, and TypeScript samples with the same
inputs and assert byte-identical stdout. The curl parity check uses a local
fixture server and skips cleanly on restricted runners that cannot use local
socket fixtures.
Self-hosted CI now runs a TinyGo WASI Preview 2 build-smoke lane for Go
clock/cat/curl samples and then tries to promote those outputs into
test/integration/language-variants/krate_go_*.wasm.
Promotion only happens when import-purity checks pass (krate:* imports
only). If current TinyGo outputs still import host wasi:*, the promotion step
prints a clear skip message and keeps Go runtime fixtures disabled.
Krate-runtime fixture proof for Go therefore remains pending, but it is now
guarded by an explicit readiness gate.
We now also have three language walkthroughs in the docs: Rust, Go, and
TypeScript, so contributors can onboard per language without guessing the
current phase boundaries.
We also now have the first fuzz-harness set for manifest parsing, shared path
normalization, and policy matching. Nightly multi-hour fuzz runs are still a
remaining exit item, but the run path is now configurable without editing
scripts. scripts/run-phase2-fuzz-smoke.sh accepts
KRATE_FUZZ_MAX_TOTAL_TIME (seconds per target), and self-hosted CI now
accepts a matching manual input. There is also a dedicated
Self-hosted Fuzz Nightly workflow that defaults to long runs on local
runners.
The first self-hosted fuzz smoke already paid off by finding and helping fix a
real non-ASCII path-parsing panic in shared path handling.
Timezone fallback coverage also now includes extra Unix timezone file paths
(/etc/TIMEZONE and /var/db/timezone/timezone) in addition to /etc/timezone,
which helps hosts that do not expose timezone only through /etc/localtime.
The closeout material is also drafted now. The Phase 2 retrospective draft and
Phase 3 kickoff issue draft exist, and a small checker keeps both in draft form
until the final exit review is actually done. That checker now runs in hosted
CI, self-hosted CI, and the exit bundle.
If Phase 2 works, those apps should produce the same output on Linux, macOS, and Windows while running through the same Krate runtime model.
If you used the Phase 1 proof app, read Migrating From Phase 1 To Phase 2.
See Plan/Phase-2-Plan.md.
Phase 2 Benchmarks
Phase 2 adds UAPI, so the runtime does more work than Phase 1. Every app call now passes through a policy check before the host adapter touches files, network, time, locale, or streams. That check needs to be cheap.
These numbers are an early local read, not a release promise. They tell us whether the current design is in the right range before we freeze UAPI v0.1.
Reference Machine (Current Baseline)
| Field | Value |
|---|---|
| Date | 2026-05-05 |
| CPU | Apple M4 |
| OS | macOS |
| Architecture | arm64 |
| Rust | rustc 1.91.1 |
Commands
cargo bench -p krate-runtime --bench uapi_dispatch
cargo bench -p krate-runtime --bench startup
scripts/record-phase2-benchmark-baseline.sh
scripts/check-benchmark-regression.sh
The benchmark uses a no-op host adapter. That means it measures Krate dispatcher and policy overhead, not disk speed, terminal speed, or network speed.
The current Phase 2 regression baseline is stored in:
docs/book/src/phase2/benchmark-baseline.json
The checker supports:
BENCH_REGRESSION_MODE=warnfor warning-only reportsBENCH_REGRESSION_MODE=failfor hard gate modeBENCH_REGRESSION_THRESHOLD_PCT=<n>for threshold tuningBENCH_BASELINE_FILES=<path[:path...]>for custom baseline sets
Current CI usage:
- Hosted full benchmark job uses
warnmode against the Phase 2 baseline file. - Self-hosted CI can run the same check in
failmode for strict local gating.
Dispatch Baseline (2026-05-05)
| Path | Local result | Phase 2 target | Notes |
|---|---|---|---|
| Default stdout grant | ~192 ns | < 1 us | Default low-risk IO capability. |
| Filesystem open with read grant | ~1.16 us | track | Path grant check plus adapter call. |
| File handle read with grant re-check | ~1.20 us | track | Re-checks the opened file path before read. |
| File handle write with grant re-check | ~1.19 us | track | Re-checks the opened file path before write. |
| Missing filesystem read grant | ~579 ns | < 1 us | Denial path stops before adapter work. |
| HTTP fetch grant check | ~954 ns | < 1 us | URL endpoint parsing plus net.connect check. |
What This Means
The first dispatcher path is fast enough for the Phase 2 target on the reference machine. The harder work is still ahead: measuring full component startup, cross-host variance, real adapter cost, and regressions over time.
For now, this gives us a useful line in the sand. UAPI checks are not free, but they are small enough that safety is not fighting the basic CLI performance goal.
Runtime Startup Benchmarks
The startup benchmark now measures real Phase 2 components too. This is still an in-process runtime benchmark, not a full shell command benchmark. It measures Wasmtime setup, component loading, UAPI linking, policy checks, and adapter calls without including terminal process startup time.
Build the fixtures first:
scripts/build-phase1-components.sh
scripts/build-phase2-smoke-component.sh
scripts/build-krate-clock-component.sh
cargo bench -p krate-runtime --bench startup
Startup Baseline (2026-05-05)
| Path | Local result | Phase 2 target | Notes |
|---|---|---|---|
| Compile Phase 2 smoke component from bytes | ~2.86 ms | track | Wasmtime component compile path. |
| Cold runtime + run Phase 2 smoke app | ~3.16 ms | < 150 ms | Reads a granted file, uses time, locale, and stdout. |
| Run preloaded Phase 2 smoke app | ~81.05 us | track | UAPI calls with component already loaded. |
Run preloaded krate-clock with fixed time | ~49.92 us | track | Time, locale, and stdout path. |
The first cold runtime number is comfortably below the Phase 2 startup target on
the reference machine. We still need a full CLI benchmark with hyperfine,
cross-host numbers, and warning-only regression tracking before this exit gate
can be marked done.
Phase 2 Threat Model v0.2
Phase 2 is where Krate starts doing real host work: file reads and writes, network requests, clock access, locale formatting, and manifest-based grants.
So this threat model is about one thing: do we enforce capability checks before host access, every time, in every path?
Scope
In scope:
krate runwith Phase 2krate:app/cli@0.1.0components- UAPI modules:
io,fs,net,time,locale - Manifest parsing and capability validation
- Session grants (
--grant,--auto-grant, prompt flow) - Runtime dispatcher and host adapter boundary checks
- Current local adapter behavior and guardrails
Out of scope:
- GUI and input stack (Phase 3)
- Mobile host adapters (Phase 4)
- Bundle signing and marketplace trust (Phase 6)
- Full policy database and revocation UX (later UCap phases)
Assets We Protect
| Asset | Why it matters |
|---|---|
| Host filesystem | Prevent reads and writes outside granted paths. |
| Host network | Prevent unauthorized outbound requests. |
| Runtime process stability | Avoid crashes, panics, and unbounded memory or handle growth. |
| Capability intent | Manifest and launch grants must match actual runtime behavior. |
| User trust | App output and grant prompts must stay clear and predictable. |
Trust Boundaries
flowchart LR
subgraph App["Untrusted app component"]
WASM["WASM component"]
end
subgraph Runtime["Trusted Krate runtime"]
CLI["CLI preflight and grant resolution"]
POL["Session policy and capability matcher"]
DISP["UAPI dispatcher"]
HOST["Generated host bridge and resource table"]
end
subgraph Adapter["Host adapter layer"]
FS["Filesystem adapter calls"]
NET["HTTP adapter calls"]
LOC["Clock and locale adapter calls"]
end
subgraph OS["Host operating system"]
SYS["Kernel, files, sockets, locale data"]
end
WASM --> DISP
CLI --> POL --> DISP --> HOST
HOST --> FS --> SYS
HOST --> NET --> SYS
HOST --> LOC --> SYS
Main Attack Surfaces
- Manifest capability parsing and normalization
- Grant matching rules for paths and
net.connectresources - Resource handles reused across calls after open
- URL parsing and HTTP request framing
- Localhost fixture and test behavior on restricted runners
- Runtime limits for reads, writes, list sizes, and resource table growth
STRIDE Summary
| Type | Example in Phase 2 | Current controls | Residual risk |
|---|---|---|---|
| Spoofing | A random component claims to be a trusted app | Manifest identity is explicit during grant flows | No signing chain yet |
| Tampering | Bypass policy and reach adapter directly | Dispatcher enforces checks before adapter calls | New call paths can drift if not tested |
| Repudiation | App denies requested or effective grants | --dump-caps and grant logging paths exist | No central audit backend yet |
| Information disclosure | Read outside granted sandbox path | Path normalization and grant checks happen before adapter calls | Symlink and host edge cases still need continued hardening |
| Denial of service | Oversized reads, writes, response bodies, or handles | Explicit bounds on reads/writes/listings, response caps, resource caps | Benchmark and fuzz evidence still growing |
| Elevation of privilege | Escape WASM and execute host behavior outside UAPI | Wasmtime isolation + narrow host surface | Depends on upstream engine correctness |
Required Security Invariants
These are non-negotiable in Phase 2:
- Every UAPI call that can touch host resources must pass capability checks first.
- Handle-based methods (
fileand stream resources) must re-check grants before use. - Denials must happen before adapter and OS calls.
- Manifest parsing must reject malformed capability resources early.
- Runtime limits must fail safely with clear errors, not panic.
What Is Already True
- Capability strings are validated and normalized at parse time.
- Runtime policy checks gate filesystem and network operations.
- File and stdio handle methods do capability re-checks.
- Multiple guardrails exist for large reads, writes, and listings.
- Resource tables are bounded and released handles can be reused.
- URL and host parsing is stricter than the initial draft.
What Still Needs Work
- Full cross-host proof over long CI windows
- TinyGo runtime fixture lane completion
- Always-on TypeScript runtime fixture lane in hosted CI
- Longer fuzz soak evidence and additional target depth
- Security review pass before UAPI v0.1 freeze
Review Triggers
Update this model whenever:
- a new UAPI module is added
- capability semantics change
- host adapter behavior changes for path, network, clock, or locale
- resource model or table limits change
- a new CI signal reveals a security or safety drift
Migrating From Phase 1 To Phase 2
Phase 1 proved that Krate could load one WebAssembly component and call a tiny host interface. Phase 2 starts the real app model.
This page is for anyone who tried the Phase 1 hello-world component and now
wants to understand what changed.
The Short Version
Phase 1 was a runtime proof.
Phase 2 is the first app platform slice.
| Area | Phase 1 | Phase 2 |
|---|---|---|
| App shape | one proof component | CLI component world |
| Host API | krate:phase1/host.print and exit | io, fs, net, time, locale |
| Permissions | none | manifest capabilities plus run-session grants |
| App metadata | none | sidecar manifest.toml |
| Samples | hello-world | krate-clock, krate-cat, krate-curl |
| SDK | direct generated bindings | first Rust SDK facade |
What Replaces print
Phase 1:
#![allow(unused)] fn main() { bindings::krate::phase1::host::print("Hello, Krate!"); }
Phase 2:
#![allow(unused)] fn main() { use krate::io::{stdio, streams::OutputStreamExt}; let out = stdio::stdout(); out.write_line("Hello, Krate")?; }
The Phase 2 version is slightly longer because it is real I/O. stdout is a
host resource, and writing can fail.
What Replaces exit
Phase 1 used a host import:
#![allow(unused)] fn main() { bindings::krate::phase1::host::exit(0); }
Phase 2 apps return an integer from run:
#![allow(unused)] fn main() { impl krate::Guest for Component { fn run() -> i32 { 0 } } }
The runtime maps that return value to the process exit code.
What Replaces "No Permissions"
Phase 1 had no file or network access, so it did not need permissions.
Phase 2 apps can ask for useful host access, so they must declare it:
[app]
id = "dev.krate.cat"
name = "krate-cat"
version = "0.1.0-dev"
entry = "target/wasm32-wasip1/release/krate_cat.wasm"
world = "krate:app/cli@0.1.0"
[[capabilities]]
cap = "io.stdout"
rationale = "Print file contents"
required = true
[[capabilities]]
cap = "fs.read:./fixtures/**"
rationale = "Read test fixture files"
required = true
You can generate this shape instead of writing it by hand:
cargo run -p krate-cli -- manifest init \
--id dev.krate.cat \
--name krate-cat \
--entry target/wasm32-wasip1/release/krate_cat.wasm \
--cap io.stdout \
--cap 'fs.read:./fixtures/**'
And you can inspect what grants it will need:
cargo run -p krate-cli -- manifest explain apps/krate-cat/manifest.toml
For CI checks or editor tooling, use the JSON form:
cargo run -p krate-cli -- manifest check \
--format json \
apps/krate-cat/manifest.toml
cargo run -p krate-cli -- manifest explain \
--format json \
apps/krate-cat/manifest.toml
If you need a local audit trail while testing, run with --log-grants:
cargo run -p krate-cli -- run \
--manifest apps/krate-cat/manifest.toml \
--auto-grant \
--log-grants krate-grants.log \
apps/krate-cat/target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
What Replaces The Phase 1 World
Phase 1 component metadata pointed at:
[package.metadata.component.target]
path = "../../../wit/krate/phase1.wit"
world = "app"
Phase 2 samples point at the CLI world:
[package.metadata.component.target]
path = "../../wit/krate/phase2"
world = "cli"
That cli world imports Krate UAPI modules and exports run.
What To Change In A Rust App
- Replace Phase 1 generated host calls with the Rust SDK.
- Return an exit code from
Guest::run. - Add
manifest.toml. - Declare every non-default capability.
- Run with explicit grants,
--prompt, or--auto-grant.
For a full walkthrough, read Your First UAPI App In Rust.
Old Command, New Command
Phase 1:
cargo run -p krate-cli -- run test/integration/hello-world/target/wasm32-wasip1/release/hello_world.wasm
Phase 2:
cargo run -p krate-cli -- run \
--manifest apps/krate-cat/manifest.toml \
--auto-grant \
apps/krate-cat/target/wasm32-wasip1/release/krate_cat.wasm \
-- ./fixtures/hello.txt
The -- separates Krate runner arguments from app arguments.
Keep In Mind
- Phase 1 is still supported so the original proof keeps working.
- Phase 2 is the path for new work.
- Phase 2 manifests are not signed yet.
- Grants are session-only.
- The UAPI is not frozen as a compatibility promise yet.
In simple words: Phase 1 proved the engine starts. Phase 2 teaches it useful app behavior.
Adapter Boundary
Phase 2 has one rule for host work:
The runtime decides policy. The adapter touches the host.
That means the runtime can check grants, normalize paths, choose limits, and map errors. When it needs the real machine, it must go through the host adapter crate for the current OS.
Current Shape
Krate app
|
v
Phase 2 UAPI call
|
v
Runtime policy check
|
v
Runtime host wrapper
|
v
Linux, macOS, or Windows adapter crate
|
v
Native OS call
The wrapper step matters. It gives us one clear place to keep Phase 2 behavior stable while each host can do the native work in its own crate.
What Is Guarded
The current boundary check covers 34 runtime wrappers:
| Area | Count | Examples |
|---|---|---|
| Filesystem | 16 | open, read, write, stat, list, remove, rename, canonicalize |
| Network | 5 | resolve address, connect TCP, apply timeouts, write request, read response |
| IO | 6 | stdin, stdout, stderr, stream writes, stream flushes |
| Time | 2 | clock discovery, sleep |
| Locale | 5 | locale discovery, timezone, date format, number format |
For each wrapper, check-adapter-boundary verifies two things:
- the runtime wrapper calls
host_os_adapter::*on supported desktop hosts - Linux, macOS, and Windows adapter crates expose the matching function
This is not a security proof by itself. It is a drift guard. If future runtime work accidentally bypasses the adapter split, CI should catch it early.
Commands
Run this from repo root:
scripts/check-adapter-boundary.sh
The same check runs in hosted CI and in the self-hosted full gate.
Why This Helps Phase 2
The final Krate goal is one app contract across many systems. We do not get there by hiding every OS difference. We get there by keeping OS-specific work behind a small boundary and making the shared contract clear.
This guard keeps that direction honest for the CLI UAPI slice.
Adapter Evidence
This page tracks repeatable cross-host proof for the Phase 2 adapter boundary gate.
The rule is simple:
- runtime policy checks stay in runtime code
- host-facing calls go through per-OS adapter crates
- shared adapter behavior is tested before the host report is accepted
- the native adapter crate for that host is tested in the same report
Record One Host Report
Run this on Linux, macOS, and Windows:
scripts/record-phase2-adapter-evidence.sh --strict
Default output path:
target/phase2-adapter-evidence/adapter-evidence.md
Optional custom output:
scripts/record-phase2-adapter-evidence.sh --strict --output /tmp/adapter-linux.md
Compare Three Host Reports
After collecting one report per host:
scripts/compare-phase2-adapter-evidence.sh /tmp/adapter-linux.md /tmp/adapter-macos.md /tmp/adapter-windows.md
The compare step checks:
- same commit metadata across all reports
- host labels match expected OS lanes
scripts/check-adapter-boundary.shpassed on each hostcargo test -p krate-adapter-commonpassed on each host- the native adapter crate test passed for that host:
- Linux:
krate-adapter-linux - macOS:
krate-adapter-macos - Windows:
krate-adapter-windows
- Linux:
The adapter evidence is still not a full hardware lab. It proves the current Phase 2 adapter contract, shared path/net/time/locale behavior, and native adapter crate surface on each runner.
Hosted CI Evidence
Full hosted CI now uploads one adapter evidence artifact per OS:
adapter-evidence-ubuntu-latestadapter-evidence-macos-latestadapter-evidence-windows-latest
Then CI runs:
Adapter evidence compare
This gives a direct cross-host evidence path for P2E-02.
Sample Evidence
This page explains how to record Phase 2 sample evidence.
The goal is simple: run the same three Rust sample apps on each desktop host and compare the stdout hashes.
krate-clockkrate-catkrate-curl
If Linux, macOS, and Windows produce the same stdout for these fixed fixtures, we have much better evidence that the UAPI behaves the same across hosts.
Recorder
Run this from the repo root:
scripts/record-phase2-sample-evidence.sh
By default, it writes:
target/phase2-sample-evidence/sample-evidence.md
You can choose another output path:
scripts/record-phase2-sample-evidence.sh target/phase2-sample-evidence/macos-arm64.md
For three-host comparison after collecting Linux, macOS, and Windows reports:
scripts/compare-phase2-sample-evidence.sh \
target/phase2-sample-evidence/linux.md \
target/phase2-sample-evidence/macos.md \
target/phase2-sample-evidence/windows.md
If curl is blocked only because localhost binding is restricted on one host, you can use:
scripts/compare-phase2-sample-evidence.sh \
target/phase2-sample-evidence/linux.md \
target/phase2-sample-evidence/macos.md \
target/phase2-sample-evidence/windows.md \
--allow-blocked-curl
What It Runs
The recorder builds the local CLI and the three Rust sample components. Then it runs:
krate-clockwith fixed time, locale, and timezonekrate-catagainst two small fixture fileskrate-curlagainst a local HTTP fixture server
The output file records:
- git commit
- host OS and CPU architecture
- command text
- process exit code
- result status
- stdout SHA-256
- stderr SHA-256
- exact stdout snapshot
Exit Use
For Phase 2 exit, collect one report from each required desktop host.
The important comparison is the stdout hash for each sample. Host metadata will be different. The sample stdout hashes should match.
If a host cannot run the curl local-server fixture, record that as a blocker instead of treating it as cross-host proof. The recorder does this automatically when localhost binding is blocked: it keeps clock and cat evidence, and marks curl as blocked. The comparator can then enforce exact hash matches and fail fast on drift. It also verifies two safety checks before hash comparison:
- each report matches the flag you pass (
--linux,--macos,--windows) - all three reports were recorded from the same git commit
Hosted CI Evidence
Full hosted CI now records one sample evidence report per OS lane and uploads:
sample-evidence-ubuntu-latestsample-evidence-macos-latestsample-evidence-windows-latest
Then CI runs:
Sample evidence compare
The compare step currently uses --allow-blocked-curl so the lane can stay
useful when one hosted runner blocks localhost fixture sockets. Clock and cat
still require strict cross-host hash alignment.
When curl is blocked on one host, the comparator still checks the curl stdout
hashes from any hosts that did run it. The exception only covers the blocked
host; it does not hide drift between successful curl runs.
Rust SDK Evidence
This page explains how we record Phase 2 evidence for the Rust SDK.
The Rust SDK is not published yet. Until UAPI v0.1 is frozen, the useful proof is:
- the crate can be packaged
- the package contains the public files a developer needs
- an outside-workspace app can compile against that package
- the SDK docs build without pulling in unrelated workspace docs
Record One Evidence Report
Run this from the repo root:
scripts/record-phase2-rust-sdk-evidence.sh --strict
Default output path:
target/phase2-rust-sdk-evidence/rust-sdk-evidence.md
Choose a custom output path:
scripts/record-phase2-rust-sdk-evidence.sh --strict --output /tmp/rust-sdk-evidence.md
What It Checks
The recorder runs:
scripts/smoke-rust-sdk.shcargo doc -p krate --no-deps
It also records whether the packaged SDK contains:
Cargo.tomlREADME.mdsrc/lib.rssrc/bindings.rs
Hosted CI Evidence
Normal hosted CI now uploads:
rust-sdk-evidence
That artifact gives P2E-03 a concrete proof source while crates.io publishing
stays blocked on the final UAPI freeze decision.
Language Variant Evidence
This page explains how to record one evidence file for Phase 2 language variants.
The goal is simple: keep one markdown report that shows fixture readiness and test outcomes for Rust, Go, and TypeScript language paths.
Run The Recorder
From the repo root:
scripts/record-phase2-language-variant-evidence.sh
Default output:
target/phase2-language-variant-evidence/language-variant-evidence.md
You can pass a custom output path:
scripts/record-phase2-language-variant-evidence.sh \
--output target/phase2-language-variant-evidence/macos-arm64.md
For three-host comparison after collecting Linux, macOS, and Windows reports:
scripts/compare-phase2-language-variant-evidence.sh \
target/phase2-language-variant-evidence/linux.md \
target/phase2-language-variant-evidence/macos.md \
target/phase2-language-variant-evidence/windows.md
In hosted full CI, each OS lane uploads one language-variant evidence artifact
(language-variant-evidence-<os>). You can download those three files and run
the comparator locally to check the proof set.
Hosted full CI also runs this comparator automatically after the full matrix, so drift is caught in CI as part of the same run.
What It Records
The report includes:
- git commit, host OS, host architecture, and UTC timestamp
- fixture mode (
optional,any,both,go,ts) - exit codes for:
scripts/build-phase2-language-variant-fixtures.shscripts/test-phase2-language-variants.sh
- fixture presence and SHA-256 hashes for:
krate_go_clock.wasmkrate_go_cat.wasmkrate_go_curl.wasmkrate_ts_clock.wasmkrate_ts_cat.wasmkrate_ts_curl.wasm
- tail logs for build and test steps
What The Comparator Checks
The comparator checks that:
- all three reports came from the same git commit
- the Linux, macOS, and Windows files are labelled as the right host
- the build and runtime test steps passed on every host
- each fixture row is present in every report
- a fixture is either present on all hosts or missing on all hosts
- every present fixture has a SHA-256 hash recorded
It does not require independently generated TypeScript fixtures to have the same hash on every operating system. The useful Phase 2 proof is that the same source fixtures build, pass import checks, and run through Krate on Linux, macOS, and Windows. Byte-for-byte reproducible jco output is a different promise and is not needed for this phase.
Strict Mode
By default, the recorder writes a report even when one command fails.
For CI-style behavior, use strict mode:
scripts/record-phase2-language-variant-evidence.sh --strict
In strict mode, the script exits non-zero when build or test fails.
You can also choose a fixture mode directly:
scripts/record-phase2-language-variant-evidence.sh --mode ts --strict
Why This Helps Phase 2
This gives one repeatable artifact for language-variant progress.
It does not replace cross-host sample evidence for Rust clock/cat/curl, but it makes language-variant status and cross-host behavior easier to review before Phase 2 exit.
Go Readiness Evidence
This page records the current Phase 2 state for Go.
The short version: the Go examples build with TinyGo, but they are not runtime
fixtures yet. They still import some WASI host APIs directly. Phase 2 keeps them
out of the runtime fixture set until those imports are replaced by krate:*
UAPI imports.
Why This Matters
Krate is trying to make apps portable by keeping host access behind one small runtime boundary.
For Go, that means a compiled component should call Krate UAPI packages such
as krate:io, krate:fs, and krate:net. It should not reach around
the runtime and call wasi:filesystem, wasi:stdio, or other host APIs
directly.
That rule keeps the security model simple:
flowchart LR
GO["Go app"] --> L36["krate:* UAPI"]
L36 --> POLICY["Krate policy checks"]
POLICY --> ADAPTER["Host adapter"]
ADAPTER --> OS["Operating system"]
GO -.blocked for runtime fixtures.-> WASI["wasi:* host API"]
WASI -.bypasses Krate policy.-> OS
Run The Recorder
From the repo root:
scripts/record-phase2-go-readiness-evidence.sh
Default output:
target/phase2-go-readiness-evidence/go-readiness-evidence.md
For an exit-style check that fails when Go is not import-pure:
scripts/record-phase2-go-readiness-evidence.sh --strict
What It Records
The report includes:
- git commit, host OS, host architecture, and timestamp
- Go, TinyGo, and
wasm-toolsversions - TinyGo smoke build result for clock, cat, and curl
- import-purity result for the three compiled components
- SHA-256 hashes for the smoke artifacts
- the full import-purity log tail
Current Decision
Go is still a Phase 2 binding track, but not a promoted runtime fixture track.
The accepted Phase 2 decision is:
- keep Go SDK source, examples, shape checks, and TinyGo build smoke
- keep Go runtime fixture promotion gated by import purity
- do not claim Go runtime parity until the compiled artifacts import only
krate:* - mark Go runtime parity as experimental for this phase
- carry the import-pure runtime proof into the next phase if it is not ready before the Phase 2 freeze
That is not a failure of direction. It is the correct boundary doing its job.
The fuller decision note is here: Go Phase 2 Decision.
Go Phase 2 Decision
Go stays in Phase 2, but it is experimental for runtime parity.
That is the current decision.
What Works
The Go SDK source is in the repo. The examples build with TinyGo:
krate-clockkrate-catkrate-curl
The readiness recorder also captures tool versions, artifact hashes, and the import check result:
scripts/record-phase2-go-readiness-evidence.sh
What Does Not Work Yet
The compiled Go components still import wasi:* host APIs directly.
That means they are not promoted into the runtime fixture set yet. Krate Phase
2 requires promoted runtime fixtures to import krate:* UAPI packages, so
policy checks stay in front of host access.
Why We Are Not Forcing It
Forcing Go promotion now would weaken the boundary we are trying to prove.
The better decision is simple:
- keep Go examples and shape checks
- keep TinyGo smoke builds
- keep import-purity checks strict
- mark Go runtime parity as experimental for Phase 2
- carry import-pure Go runtime fixtures into the next phase if TinyGo tooling or our binding path becomes ready
This keeps Phase 2 honest. Rust and TypeScript continue to carry the current runtime proof. Go remains visible, tested, and ready to advance without blocking the UAPI freeze.
Exit Meaning
For Phase 2 exit, Go is considered:
- usable as an SDK source and TinyGo build-smoke track
- not yet a runtime parity track
- explicitly experimental until compiled artifacts pass the Krate import check
The command that decides promotion is still:
scripts/promote-phase2-go-runtime-fixtures.sh
If it promotes the fixtures later, this decision can be revisited.
UCap Enforcement Evidence
This page shows how we collect repeatable proof that Phase 2 permission checks are really enforced at runtime boundaries.
What This Covers
The current evidence run checks six deny paths:
- Runtime deny matrix for non-default capabilities
- Dispatcher deny-before-adapter matrix for every non-default Phase 2 boundary
krate-catdenies missingfs.readgrantkrate-curldenies missingnet.connectgrant- Manifest-required capability deny path
- Rust Go TypeScript curl missing-grant parity test
The second check is important. It proves the runtime returns permission denied before the host adapter can open a file, list a directory, remove a path, create a directory, rename a path, or start a network fetch.
Record One Host Report
Run this on Linux, macOS, and Windows:
scripts/record-phase2-ucap-evidence.sh --strict
By default, the report is written to:
target/phase2-ucap-evidence/ucap-enforcement-evidence.md
You can choose a custom output path:
scripts/record-phase2-ucap-evidence.sh --strict --output /tmp/ucap-linux.md
Compare Three Host Reports
After you have one report from each host:
scripts/compare-phase2-ucap-evidence.sh /tmp/ucap-linux.md /tmp/ucap-macos.md /tmp/ucap-windows.md
The comparator fails when:
- commit metadata does not match across reports
- host labels do not match the expected OS lane
- any required deny check failed on any host
Hosted CI Evidence
Full hosted CI now records one UCap evidence report per OS lane and uploads:
ucap-enforcement-evidence-ubuntu-latestucap-enforcement-evidence-macos-latestucap-enforcement-evidence-windows-latest
Then CI runs an evidence compare job:
UCap enforcement evidence compare
This gives us one strict cross-host gate for P2E-09.
Timed Walkthrough Evidence
Phase 2 has one human proof gate: a Rust developer should be able to write and run a small UAPI CLI app in 30 minutes or less.
The code cannot prove this by itself. We need one outside reviewer to follow the Rust walkthrough and record what happened.
Rehearse The Walkthrough Locally
Before asking an outside reviewer, run the local rehearsal:
scripts/check-phase2-rust-walkthrough-rehearsal.sh
It runs the same core path the reviewer will use:
krate doctor- CLI build
- Rust cat component build
- manifest generation
- manifest explanation
- granted file read
- denied missing-grant path
The rehearsal writes:
target/phase2-walkthrough/rehearsal.md
This does not close the human gate. It only proves the current walkthrough is ready for a reviewer.
Generate The Packet
Run this from the repo root:
scripts/record-phase2-walkthrough-template.sh
It writes:
target/phase2-walkthrough/walkthrough-template.md
The template records the commit under review and gives the reviewer one place to fill in timing, step results, notes, and the final pass or fail.
Check The Filled Packet
After the reviewer fills the packet, run:
scripts/check-phase2-walkthrough-evidence.sh target/phase2-walkthrough/walkthrough-template.md
The checker does not replace the human review. It only checks that the packet is usable evidence:
- metadata fields are filled
- result is
passorfail - total minutes is a number
- a passing run is 30 minutes or less
- every walkthrough step has a reviewer result
What The Reviewer Does
The reviewer follows:
They start a timer before checking tools and stop it after the missing-grant denial path works.
The pass rule is simple:
Rust developer, new to Krate, completes the walkthrough in 30 minutes or less.
Private help from us does not count. Docs fixes after the run are welcome, but the original timing result should stay honest.
What To Save
Save:
- the filled walkthrough template
- the checker output
- the terminal transcript or log
- the commit hash used for the run
- any notes about confusing wording or missing setup steps
Once that evidence exists, P2E-12 can move from pending to done or partial,
depending on the result.
CI Stability Evidence
Phase 2 needs more than one green run before exit.
This page explains the recorder we use to capture hosted CI and GitHub Pages history in one reviewable file.
scripts/record-phase2-ci-stability-evidence.sh
Default output:
target/phase2-ci-stability-evidence/ci-stability-evidence.md
For final exit review, require both hosted workflows to have at least one completed green run in the inspected history:
scripts/record-phase2-ci-stability-evidence.sh --require-success
When the final candidate commit is ready, narrow the report to the review window too:
scripts/record-phase2-ci-stability-evidence.sh --created '>=2026-05-18' --require-success
You can also include this report in the Phase 2 exit bundle:
scripts/record-phase2-exit-bundle.sh --strict --include-ci-stability
The exit bundle uses the stricter success check. If hosted CI or GitHub Pages does not show a completed green run, the strict bundle fails.
What It Records
The report includes:
- repository and branch
- git commit at recording time
- optional GitHub creation-date filter
- latest hosted CI run
- latest Pages deploy run
- recent run history for both workflows
- completed success streak for each workflow
- required success streak when strict checking is enabled
It uses GitHub CLI, so it needs a logged-in gh session with repository access.
Why This Helps Phase 2
The Phase 2 exit criteria include CI stability over time. A screenshot or a memory of green checks is not enough.
This recorder turns the GitHub run list into a plain markdown artifact. That lets us attach a concrete report to the final Phase 2 review.
What It Does Not Prove
This is hosted CI and Pages evidence only.
It does not replace:
- self-hosted full gate evidence
- long fuzz soak evidence
- benchmark evidence
- dependency evidence
- cross-host sample and adapter evidence
Those stay as separate proof tracks because each one answers a different question.
Self-hosted run history is recorded with
scripts/record-phase2-self-hosted-evidence.sh.
Current Reading
The latest local report showed recent hosted CI and Pages runs green on main.
That is a good signal, but Phase 2 exit still needs the final UAPI candidate and
the remaining evidence bundle to line up.
Hosted Full CI Evidence
Normal push CI is intentionally small. It checks the fast Linux path, docs, UAPI freshness, the Rust SDK package shape, formatting, and linting.
Phase 2 exit also needs the heavier hosted full CI path. That path runs the Linux, macOS, and Windows lanes and records the cross-host evidence artifacts.
Latest checked full run:
- Run
26069665276 - Commit
3f1a219 - Result: passed
- Passed compare jobs: language variants, UCap enforcement, adapters, samples
Use this recorder to prove that a recent hosted CI run was a full run, not only the fast push run:
scripts/record-phase2-hosted-full-ci-evidence.sh
Default output:
target/phase2-hosted-full-ci-evidence/hosted-full-ci-evidence.md
For final review, require a completed full run with every required full job green:
scripts/record-phase2-hosted-full-ci-evidence.sh --require-success
If a full run is cancelled or fails, the report can still record it for triage,
but --require-success will reject it. The selected-run summary shows the
workflow conclusion separately from the required job table, so a cancelled run
cannot be mistaken for Phase 2 proof.
To limit the report to the final review window:
scripts/record-phase2-hosted-full-ci-evidence.sh \
--created '>=2026-05-18' \
--require-success
You can include it in the exit bundle:
scripts/record-phase2-exit-bundle.sh --strict --include-hosted-full-ci
The final review shortcut includes it too:
scripts/record-phase2-exit-bundle.sh --final-review
Required Full Jobs
The recorder checks these hosted CI jobs:
- Phase 2 bindings
- Build shared component fixtures
- Full test on Linux
- Full test on macOS
- Full test on Windows
- Language variant evidence compare
- UCap enforcement evidence compare
- Adapter evidence compare
- Sample evidence compare
- Phase 2 benchmark check
- Dependency audit
If those jobs are missing or skipped, the run is not counted as hosted full CI proof.
How To Run Hosted Full CI
Use one of these paths:
gh workflow run CI --ref main -f full=true -f language_variants_mode=ts
or include [full-ci] in a commit message.
The manual workflow path is cleaner for final review because it does not require a documentation-only commit just to trigger the heavy matrix.
Shared Fixtures
Hosted full CI builds the Rust component fixtures once on Linux, uploads them, then downloads the same files into each full-test lane.
The full-test lanes also copy those downloaded files into the app target paths named by the sample manifests:
apps/krate-clock/target/wasm32-wasip1/release/krate_clock.wasm
apps/krate-cat/target/wasm32-wasip1/release/krate_cat.wasm
apps/krate-curl/target/wasm32-wasip1/release/krate_curl.wasm
That keeps two checks true at the same time: each host runs the same shared fixture bytes, and the sample manifest tests still use the exact entry paths shown in the example apps.
The sample evidence recorder follows the same rule. In hosted full CI it reuses
the downloaded fixture files already placed at those app target paths. On a
local machine, if those files are missing, it can still build the fixtures with
cargo-component.
Windows CLI Binary Path
Cargo writes the Krate CLI to target/debug/krate on Linux and macOS, and
to target/debug/krate.exe on Windows.
The sample evidence recorder now resolves that host difference before it runs.
In Git Bash on Windows, it chooses the .exe path directly after cargo build.
It also accepts KRATE_BIN when a caller wants to point at a specific binary.
This keeps the evidence command portable instead of making each workflow lane
know the executable suffix by hand.
Language Variant Evidence Hashes
The language-variant evidence lane builds TypeScript fixtures on each host with jco, runs the Krate import checks, and runs the TypeScript runtime tests.
The comparator requires matching commit metadata, matching host labels, passing build/test rows, aligned fixture presence, and a recorded hash for every present fixture. It does not require the jco-built TypeScript component bytes to be identical across Linux, macOS, and Windows. That lane proves portable behavior, not reproducible compiler output.
On Windows, the recorder uses sha256sum when available and falls back to
shasum on hosts that provide it. This keeps the fixture hash column filled in
Git Bash and still works on macOS.
Windows Command-Line Limit
One guard test sends more than 64 KiB of app arguments to prove that Krate rejects the payload before the runtime starts. Linux and macOS can launch that test command and Krate rejects it.
Windows has a lower process command-line limit for this shape of argument. The OS rejects the process before Krate can run, so the Windows lane records this case as a host-limit skip. The related count-limit, empty-argument, newline, and NUL checks still run on Windows.
Local HTTP Fixtures
Some curl tests use a tiny local HTTP server inside the test process. The
response-limit test asks Krate to stop after a very small number of response
bytes. On Windows, that early close can surface in the fixture thread as a
connection-aborted write. The fixture treats that as an accepted connection and
lets the test check the real Krate result: exit code 21 and a
response too large message.
Sandboxed Logical Paths
Krate filesystem paths are logical paths, not direct host paths. A component
may ask for /fixtures/public/note.txt, but the runtime must resolve that as
fixtures/public/note.txt under the sandbox root.
This matters on Windows because a leading slash can be interpreted as a rooted host path before the sandbox root is joined. The runtime now trims the leading slash from the normalized Krate path string first, then builds host path segments. That keeps the same sandbox behavior on Linux, macOS, and Windows.
What This Does Not Prove
This is hosted full CI proof only.
It does not replace:
- normal hosted CI and Pages stability history
- self-hosted macOS ARM64 full-gate proof
- long fuzz soak proof
- the outside Rust walkthrough
- the final UAPI freeze decision
Each track answers a different question, so the final Phase 2 packet should keep them separate.
Self-Hosted Evidence
Phase 2 needs one recent local full-gate run before exit.
Hosted CI tells us the normal push checks are healthy. The self-hosted full gate answers a different question: can the heavier local path still build fixtures, run language checks, run benchmarks, run fuzz smoke, build docs, and record UAPI freeze evidence on the macOS ARM64 runner.
scripts/record-phase2-self-hosted-evidence.sh
Default output:
target/phase2-self-hosted-evidence/self-hosted-evidence.md
For final exit review, require at least one completed green self-hosted run in the inspected history:
scripts/record-phase2-self-hosted-evidence.sh --require-success
When the final candidate commit is ready, narrow the report to the review window too:
scripts/record-phase2-self-hosted-evidence.sh --created '>=2026-05-18' --require-success
You can also include this report in the Phase 2 exit bundle:
scripts/record-phase2-exit-bundle.sh --strict --include-self-hosted
The exit bundle uses the stricter success check. If the latest completed self-hosted history does not show a green run, the strict bundle fails instead of hiding the problem in a report.
What It Records
The report includes:
- repository and branch
- workflow file or workflow name
- git commit at recording time
- optional GitHub creation-date filter
- latest completed self-hosted run
- recent run history
- completed success streak
- required success streak when strict checking is enabled
It uses GitHub CLI, so it needs a logged-in gh session with repository access.
What It Does Not Prove
This report records GitHub workflow history. It does not replace the uploaded artifacts from the workflow itself.
For final Phase 2 review, read it beside:
- the self-hosted workflow run page
- the UAPI freeze review artifact
- benchmark output
- fuzz smoke output
- the exit bundle
Current Reading
A recent green self-hosted full gate is still one of the final proof items for Phase 2. This recorder gives that proof a stable markdown shape so the final review does not rely on a screenshot.
Fuzz Evidence
Phase 2 needs fuzz proof before exit because the UAPI boundary handles paths, manifests, and capability patterns.
The fuzz evidence recorder runs the Phase 2 fuzz smoke command and writes one markdown report with the commit, host, target list, duration, tool versions, result, and log tail.
scripts/record-phase2-fuzz-evidence.sh --strict
Default output:
target/phase2-fuzz-evidence/fuzz-evidence.md
For a quick local check, use a short run:
scripts/record-phase2-fuzz-evidence.sh --strict --max-total-time 5
For final Phase 2 review, run the same recorder on the final candidate with a larger time value on the self-hosted runner:
scripts/record-phase2-fuzz-evidence.sh --strict --max-total-time 14400
That example means four hours per fuzz target.
What It Records
The report includes:
- git commit
- host operating system and architecture
- fuzz targets
- seconds per target
- dry-run flag
- cargo-fuzz, rustup, and nightly rustc details
- command exit code
- fuzz log tail
Include It In The Exit Bundle
scripts/record-phase2-exit-bundle.sh --strict --include-fuzz
The final review bundle includes fuzz evidence too:
scripts/record-phase2-exit-bundle.sh --final-review
Set KRATE_FUZZ_MAX_TOTAL_TIME first if the final review should use a longer
soak window.
Current Reading
A short fuzz smoke is useful during normal development. A longer self-hosted soak remains one of the final Phase 2 proof items.
Benchmark Evidence
This page shows the repeatable way to record Phase 2 performance evidence for:
- startup path checks
- full external
krate runstartup checks - UAPI dispatch checks
- baseline regression checks
The goal is simple. We want proof that performance checks pass on Linux, macOS, and Windows for the same commit.
Record One Host Report
Run this on each host:
scripts/record-phase2-benchmark-evidence.sh --strict
This runs:
cargo bench -p krate-runtime --bench startupcargo bench -p krate-runtime --bench uapi_dispatchscripts/check-benchmark-regression.shcargo build -p krate-cli --releasescripts/build-krate-clock-component.sh- a measured external
krate runloop forkrate-clock
Default output path:
target/phase2-benchmark-evidence/benchmark-evidence.md
Useful options:
scripts/record-phase2-benchmark-evidence.sh --strict --mode fail --threshold 10 --output /tmp/bench-linux.md
scripts/record-phase2-benchmark-evidence.sh --skip-bench --output /tmp/bench-reuse.md
scripts/record-phase2-benchmark-evidence.sh --skip-cli-startup --output /tmp/bench-no-cli.md
The full CLI startup check is intentionally separate from the Criterion runtime benchmarks. It measures the real command path: process start, CLI argument parsing, manifest loading, grant resolution, runtime setup, component execution, and stdout collection.
Compare Three Host Reports
After recording one report per host:
scripts/compare-phase2-benchmark-evidence.sh /tmp/bench-linux.md /tmp/bench-macos.md /tmp/bench-windows.md
The compare step checks:
- commit metadata matches across all three reports
- host label matches the expected OS lane
- startup, dispatch, and regression steps passed on all three reports
- full external CLI startup evidence passed on all three reports
- required metric rows exist with current values
- baseline and threshold metadata is consistent across hosts
- each metric stays within its baseline threshold on each host report
Notes
- Runtime numbers are expected to differ across host hardware.
- This compare gate does not force numeric equality across hosts.
- It does enforce per-host threshold bounds from the recorded baseline table.
- It proves shape and pass state consistency for the same code revision.
- The full CLI startup report is evidence, not a strict numeric threshold yet. We should set a threshold only after Linux, macOS, and Windows reports are collected for the same commit.
Dependency Evidence
This page records how Phase 2 checks Rust dependency risk before exit.
The dependency audit is not only a CI checkbox. Krate puts Wasmtime, Cranelift, parser crates, SDK helpers, and test tools in the trusted build path. If one of those dependencies brings an unsafe license, an unexpected source, or a known advisory, we need to see it before freezing the phase.
What We Check
The local command is:
scripts/record-phase2-dependency-evidence.sh --strict
It runs:
scripts/check-dependencies.sh
That wrapper checks:
- advisories
- licenses
- banned crates and duplicate rules
- allowed source registries
The wrapper is deliberately plain. It keeps licenses, bans, and sources as hard
gates. Advisory database parser or lock-path failures are written as warnings
because current cargo-deny and RustSec advisory data can drift before the tool
is updated.
Output
Default output path:
target/phase2-dependency-evidence/dependency-evidence.md
The report includes:
- git commit
- host and architecture
- tool versions
- dependency audit exit code
- whether advisories were checked or skipped with a warning
- whether licenses, bans, and sources passed
- the tail of the dependency log
How To Use It For Exit
For Phase 2 exit, keep one dependency evidence report for the final commit.
The preferred result is:
- dependency audit passed
- advisories checked
- licenses, bans, and sources passed
If a local machine cannot take the advisory database lock, use the hosted CI audit as the final advisory proof and keep the local report as supporting evidence. That keeps the review honest without blocking on a local cache path.
Commands
scripts/record-phase2-dependency-evidence.sh --strict
scripts/check-dependencies.sh
UAPI Freeze Review
This page is the checklist we use before calling Phase 2 UAPI v0.1 frozen.
Freezing does not mean Krate is finished. It means the Phase 2 contract is stable enough that apps can depend on it without us casually changing function names, error shapes, resource behavior, or capability strings.
Current State
The Phase 2 UAPI is a strong draft.
What is already in place:
- WIT packages exist for
io,fs,net,time, andlocale wasm-toolsvalidates the world and each dependency packagecheck-uapichecks package names, world shape, imports, naming, permission error variants, and WIT docs- the generated reference is published in the book
- the current contract evidence snapshot is published in UAPI Freeze Evidence
- the current WIT file hash lock is published in UAPI Freeze Lock
- the freeze-review recorder writes one local report that checks the contract, generated reference, freeze evidence, freeze lock, freeze decision packet, adapter-boundary guard, and exit ledger together
- the freeze decision packet keeps the final human decision explicit instead of letting a passing script imply that the UAPI is frozen
- the full Phase 2 gate list is tracked in Phase 2 Exit Evidence
- hosted CI and self-hosted CI fail if that evidence page is stale
- Rust sample apps use the current SDK facade
- Rust sample app evidence can be recorded with
scripts/record-phase2-sample-evidence.sh - TypeScript fixtures build through jco and pass local runtime checks
- Go TinyGo artifacts build, but are not runtime fixtures yet because they still import WASI host APIs
What is not frozen yet:
- the final v0.1 wording for every function and error case
- cross-host evidence for Linux, macOS, and Windows over a stable window
- final decision on whether Go is required for the formal Phase 2 exit or marked experimental for the first UAPI slice
Freeze Rules
After freeze:
- no breaking changes inside
krate:*@0.1.0 - no removing functions, records, variants, or enum cases
- no changing parameter order or result shape
- no changing capability string meaning
- no changing error meaning in a way that breaks existing apps
If we need a breaking change later, we publish a new package version such as
krate:fs@0.2.0 beside the old one.
Review Checklist
Before checking the Phase 2 WIT freeze box, all items below should be true.
Contract Shape
-
scripts/check-uapi.shpasses on a clean checkout -
scripts/generate-uapi-freeze-evidence.shrefreshes the published evidence page without manual edits -
scripts/generate-uapi-freeze-lock.shrefreshes the published WIT hash lock without manual edits -
scripts/check-phase2-freeze-decision.shpasses - generated UAPI reference is current
- every public WIT item has clear docs
- every function has a plain behavior note in the reference or the WIT docs
- every typed error case has a clear meaning
- default grants and explicit grants are documented
Runtime Behavior
- every current UAPI entry reaches UCap before host adapter work
- denied calls fail before native file or network access
- file and stdio resources re-check grants on resource methods
- path normalization rules match policy matching rules
-
HTTP URL endpoint parsing matches
net.connectgrant parsing
Samples
-
krate-clockhas deterministic fixed-time output -
krate-catreads granted files and denies missing grants -
krate-curlfetches granted HTTP fixtures and denies missing grants -
sample components import only
krate:* - Rust and TypeScript sample behavior matches for covered paths
Language Tracks
- Rust SDK package smoke passes from the packaged crate
- Rust docs build without broken links
- TypeScript jco fixture build is reproducible
- Go TinyGo smoke artifacts build
- Go runtime fixture promotion decision is explicit: either import-pure runtime proof is ready, or Go is marked experimental for Phase 2 exit
Evidence
- hosted CI is green after the freeze commit
- self-hosted full gate has a recent green run
- dependency evidence is recorded and clean, or has a documented temporary exception
- benchmark gate has a recent Phase 2 baseline check
- fuzz smoke has passed after the last WIT or parser change
Commands
Run these from repo root:
scripts/check-uapi.sh
scripts/generate-uapi-freeze-evidence.sh
scripts/generate-uapi-freeze-lock.sh
scripts/check-uapi-freeze-lock.sh
scripts/check-phase2-freeze-decision.sh
scripts/record-phase2-uapi-freeze-review.sh --strict
cargo test -p krate-tools
env PATH="$HOME/.cargo/bin:$PATH" mdbook build docs/book
scripts/record-phase2-dependency-evidence.sh --strict
scripts/smoke-rust-sdk.sh
scripts/test-phase2-language-variants.sh
For local runner evidence:
gh workflow run self-hosted-ci.yml
For Go readiness:
scripts/build-phase2-go-variant-smoke.sh
KRATE_GO_RUNTIME_FIXTURE_MODE=optional scripts/promote-phase2-go-runtime-fixtures.sh
scripts/record-phase2-go-readiness-evidence.sh
The promotion command should only copy fixtures when all Go artifacts import
krate:* APIs only. The recorder keeps the build result and the current
import-purity log in one review file.
The freeze-review recorder writes
target/phase2-uapi-freeze-review/uapi-freeze-review.md. Read it beside the
UAPI Freeze Review Evidence page and the
UAPI Freeze Decision Packet before making the final
freeze decision.
Freeze Decision
The freeze decision should be recorded in the Phase 2 plan and in an ADR if it changes a rule that future phases depend on.
Until that happens, treat the current WIT as a serious draft: stable enough to test and document, not stable enough to promise forever.
UAPI Freeze Decision Packet
Status: Draft. UAPI v0.1 is not frozen yet.
This page is the decision packet for freezing the Phase 2 UAPI.
The freeze decision is small but serious. It says the krate:*@0.1.0
contracts are stable enough for apps and SDKs to depend on them. It does not
mean Krate is complete. It only means this first CLI contract is no longer a
casual draft.
Decision State
Decision: Not frozen yet.
Current recommendation: keep the UAPI as a freeze candidate while the last Phase 2 proof work finishes.
The current WIT shape is strong enough for samples, docs, SDK smoke tests, and cross-host evidence. The final freeze should wait until the exit review has a fresh evidence bundle and the outside Rust walkthrough is filled.
Scope
The freeze covers these Phase 2 packages:
krate:io@0.1.0krate:fs@0.1.0krate:net@0.1.0krate:time@0.1.0krate:locale@0.1.0
The freeze also covers the krate:platform/cli@0.1.0 world shape, because it
defines how CLI components import those packages and export run.
What Must Be True
Before the decision changes from Not frozen yet to Frozen, the reviewer
should see:
scripts/check-uapi.shpassesscripts/check-uapi-freeze-lock.shpassesscripts/record-phase2-uapi-freeze-review.sh --strictpassesscripts/record-phase2-exit-bundle.sh --strictpasses- hosted CI is green for the freeze commit
- the self-hosted full gate is green for the freeze commit
- the timed Rust walkthrough packet is filled and accepted
- the Phase 2 exit ledger has no pending gate
The Go track may remain experimental for runtime parity in Phase 2. That is allowed only because the decision is recorded in Go Phase 2 Decision and guarded by import-purity checks.
No-Go Conditions
Do not freeze if any of these are true:
- a WIT file changed but the freeze lock was not regenerated
- generated reference docs are stale
- UCap deny-before-adapter coverage is missing for a current UAPI entry
- a sample app relies on direct host APIs instead of
krate:*imports, except for the documented experimental Go runtime path - the outside walkthrough has not been completed
- CI or self-hosted evidence is failing on the freeze commit
Reviewer Signoff
Fill this section during the final review.
| Field | Value |
|---|---|
| Reviewer | Pending |
| Freeze commit | Pending |
| Hosted CI run | Pending |
| Self-hosted full gate run | Pending |
| Exit bundle path or artifact | Pending |
| Walkthrough packet | Pending |
| Decision date | Pending |
If Accepted Later
When the freeze is accepted:
- Replace the decision line with the frozen wording.
- Fill the reviewer signoff table.
- Update the Phase 2 exit ledger gate
P2E-01. - Record the change in
Plan/Phase-2-Plan.md. - Add an ADR only if the final review changes a rule that future phases depend on.
After freeze, breaking changes need a new package version such as
krate:fs@0.2.0. Additive changes can be considered only if they do not
change existing names, parameter order, result shape, capability meaning, or
error meaning.
UAPI Freeze Review Evidence
This page explains the repeatable review report for the Phase 2 UAPI freeze candidate.
The freeze candidate is the current set of WIT files under wit/krate/phase2.
Those files define the public contract for io, fs, net, time, and
locale. Once we call that contract frozen, app authors should be able to build
against it without names, error shapes, resources, or capability meanings
changing underneath them.
What The Recorder Checks
Run:
scripts/record-phase2-uapi-freeze-review.sh --strict
The recorder writes:
target/phase2-uapi-freeze-review/uapi-freeze-review.md
It checks:
- the Phase 2 WIT package shape
- the generated UAPI reference freshness
- the freeze evidence page freshness
- the freeze lock freshness
- the freeze lock checker
- the adapter-boundary guard
- the Phase 2 exit-ledger guard
This is useful because a freeze review can drift if people only read a checklist. The recorder makes the current contract state concrete. If the WIT files change, the generated reference, evidence page, and hash lock must move together.
What A Passing Report Means
A passing report means the current UAPI candidate is internally consistent.
It does not mean Phase 2 is complete. The remaining exit work still includes cross-host evidence, language-track evidence, benchmark and fuzz evidence, and one outside walkthrough.
Self Hosted CI
The manual self-hosted full gate records this report too. That gives us a local runner proof for the exact same freeze candidate we review in the docs.
The report should be read beside:
How We Use It
Before a final freeze decision:
- Run the recorder in strict mode.
- Confirm the working tree only contains expected review output.
- Check the exit ledger for anything still partial, pending, or blocked.
- Record the freeze decision in the Phase 2 plan.
- Add an ADR only if the review changes a rule that future phases depend on.
This keeps the freeze decision honest. We are not freezing because the code feels ready. We freeze only when the contract, generated docs, adapter boundary, and review evidence agree.
UAPI Freeze Evidence
Generated by
cargo run -p krate-tools --bin check-uapi -- --format markdown.
Status: serious draft, not frozen yet.
See also: UAPI Freeze Lock. The lock records the exact WIT file hashes for the current freeze candidate.
Contract Summary
- App package:
krate:app@0.1.0 - World:
cli - Imported interfaces: 14
- Packages: 6
Checks Passed
- expected package set is present
cliworld shape is stable- imported interface set matches the Phase 2 plan
- world export is
run: func() -> s32 - public names use kebab case
fs-errorandnet-errorincludepermission-denied- public WIT items have contract docs
Packages
krate:app@0.1.0krate:fs@0.1.0krate:io@0.1.0krate:locale@0.1.0krate:net@0.1.0krate:time@0.1.0
Imported Interfaces
krate:fs/files@0.1.0krate:fs/types@0.1.0krate:io/args@0.1.0krate:io/log@0.1.0krate:io/stdio@0.1.0krate:io/streams@0.1.0krate:io/types@0.1.0krate:locale/format@0.1.0krate:locale/info@0.1.0krate:locale/types@0.1.0krate:net/http-client@0.1.0krate:net/types@0.1.0krate:time/clock@0.1.0krate:time/sleep@0.1.0
Freeze Note
This evidence proves the current UAPI contract shape is internally consistent. It does not freeze v0.1 by itself. The freeze lock records the exact WIT file hashes for this candidate. The remaining freeze decision still needs cross-host evidence, language-track evidence, and a final human review.
UAPI Freeze Lock
Generated by
scripts/generate-uapi-freeze-lock.sh.
Status: freeze candidate lock.
This file records the exact Phase 2 WIT files being reviewed for UAPI v0.1. It does not replace the human freeze decision. It makes contract drift visible before that decision and after it.
Summary
- Package root:
wit/krate/phase2 - WIT files: 6
- Aggregate SHA-256:
2067782f444d0831fb8633212795dc2b18e57466d4948ce04b2f94228742ed58
Files
| File | Lines | Bytes | SHA-256 |
|---|---|---|---|
wit/krate/phase2/deps/fs/fs.wit | 78 | 2611 | b0fb112d9f3aa7f1ab9cf4f16710a3b67fdbaac931107e4602af077b28f96610 |
wit/krate/phase2/deps/io/io.wit | 92 | 2599 | 61efe9abd8f86899aed66d3cc01c959a8a3e8982004af09ac0edac853cdb40a2 |
wit/krate/phase2/deps/locale/locale.wit | 63 | 1467 | 3278729cfe72935fe4621750214cdd024740e8f07e63e9fc1c9390385d2e423c |
wit/krate/phase2/deps/net/net.wit | 86 | 2280 | 720ecc62ba371ca3faf7e241a3aae28326b0eeb5ee63360c96ceffe0cda53066 |
wit/krate/phase2/deps/time/time.wit | 17 | 482 | 5a8f5bb2f2967ae817b166f22b0865f98171c25acfc7475f66b0ebb210bc2770 |
wit/krate/phase2/world.wit | 19 | 523 | 6a7cc928a9ffc0d3af2a9c00ba50932c91670b08a742240edee6d8089c287f2b |
Review Rule
If any hash changes, the Phase 2 UAPI changed. Review the WIT diff, regenerate this lock intentionally, regenerate the UAPI reference and freeze evidence, then record the reason in the Phase 2 plan.
Exit Bundle
The exit bundle is a local review report for Phase 2.
It does not say Phase 2 is finished. It collects the checks we want to see green before a final exit review:
- UAPI contract shape
- UAPI freeze lock
- UAPI freeze decision packet
- adapter boundary shape
- exit ledger coverage
- full exit readiness snapshot
- closeout docs draft guard
- Rust walkthrough rehearsal
- docs build
- dependency evidence
- Go readiness evidence
- optional hosted CI and Pages stability evidence
- optional hosted full cross-host CI evidence
- optional self-hosted full-gate evidence
- optional fuzz evidence
- optional Rust SDK package evidence
This is useful because Phase 2 now has many separate proof files. The bundle puts the basic review state in one place.
Record A Quick Bundle
Run this from the repo root:
scripts/record-phase2-exit-bundle.sh --strict
Default output path:
target/phase2-exit-bundle/exit-bundle.md
Choose a custom output path:
scripts/record-phase2-exit-bundle.sh --strict --output /tmp/krate-exit-bundle.md
Record A Final Review Bundle
When the final candidate is ready, run the fuller review packet:
scripts/record-phase2-exit-bundle.sh --final-review
That is shorthand for strict mode plus Rust SDK proof, hosted CI stability
proof, hosted full CI proof, self-hosted full-gate proof, and fuzz evidence.
Set KRATE_CI_STABILITY_CREATED, KRATE_HOSTED_FULL_CI_CREATED, and
KRATE_SELF_HOSTED_CREATED first if you want GitHub run history limited to a
review window. Set KRATE_FUZZ_MAX_TOTAL_TIME first if the final packet
should record a longer fuzz soak.
Include Rust SDK Proof
The Rust SDK package proof can touch the crates.io index, so it is optional in the bundle. Include it when you want a fuller local review report:
scripts/record-phase2-exit-bundle.sh --strict --include-rust-sdk
Normal hosted CI already uploads the Rust SDK report as rust-sdk-evidence.
Include Hosted CI Stability Proof
Hosted CI stability uses GitHub CLI, so it is optional in the bundle. Include it
when you are preparing a final review packet and have a logged-in gh session:
scripts/record-phase2-exit-bundle.sh --strict --include-ci-stability
This adds the recent hosted CI and GitHub Pages run history from
scripts/record-phase2-ci-stability-evidence.sh. The bundle calls that
recorder with --require-success, so a strict review bundle fails if hosted CI
or GitHub Pages does not show a completed green run.
For the final candidate, set KRATE_CI_STABILITY_CREATED to the review
window you want, for example >=2026-05-18, before running the bundle. That
keeps older green hosted runs from being mistaken for final proof.
Include Hosted Full CI Proof
Hosted full CI evidence is separate from normal hosted CI stability. Include it when a recent full cross-host run has completed:
scripts/record-phase2-exit-bundle.sh --strict --include-hosted-full-ci
This adds scripts/record-phase2-hosted-full-ci-evidence.sh --require-success.
The strict bundle fails if the inspected history does not contain a completed
full CI run where the Linux, macOS, Windows, language, UCap, adapter, sample,
benchmark, and dependency jobs all ran and passed.
For the final candidate, set KRATE_HOSTED_FULL_CI_CREATED to the review
window you want, for example >=2026-05-18.
Include Self-Hosted Full-Gate Proof
The self-hosted evidence recorder uses GitHub CLI too. Include it when the local runner has produced a recent full-gate run:
scripts/record-phase2-exit-bundle.sh --strict --include-self-hosted
This adds recent Self-hosted CI run history from
scripts/record-phase2-self-hosted-evidence.sh. The bundle calls that recorder
with --require-success, so a strict review bundle fails if the inspected
self-hosted history does not contain a completed green full-gate run.
For the final candidate, set KRATE_SELF_HOSTED_CREATED to the review window
you want, for example >=2026-05-18, before running the bundle. That keeps old
green local runs from being mistaken for final proof.
Include Fuzz Proof
Fuzz proof uses the same target list as scripts/run-phase2-fuzz-smoke.sh.
Include it when you want the bundle to record a fuzz smoke or longer soak:
scripts/record-phase2-exit-bundle.sh --strict --include-fuzz
The default is a short smoke. For final review, set KRATE_FUZZ_MAX_TOTAL_TIME
to the per-target soak window first.
Dependency evidence is included by default because it is one of the final Phase 2 signoff checks. If local advisory lookup is blocked by a cache lock, the bundle records that warning and still shows whether licenses, bans, and sources passed.
Go readiness evidence is also included by default, but it is not treated as a completion stamp. It records whether the TinyGo smoke artifacts build and whether they are Krate import-pure. Today this helps reviewers see the exact reason Go runtime fixtures are still blocked.
What The Bundle Shows
The report includes:
- host and commit metadata
- pass or fail status for each included command
- the freeze candidate lock check result
- the UAPI freeze decision packet result
- the closeout docs draft guard result
- the Rust walkthrough rehearsal result
- the dependency audit evidence result
- the Go readiness result and current import-purity status
- the hosted CI stability result when included
- the hosted full CI result when included
- the self-hosted full-gate result when included
- the fuzz result when included
- the current
P2E-*gate snapshot from the exit ledger - the full
scripts/phase2-exit-readiness.sh --alloutput - the current working tree state
- short log tails for each check
The bundle is meant for review and handoff. It should make a new session or a human reviewer understand the current Phase 2 proof state without opening every separate log first.
Phase 2 Exit Readiness
This page explains the quick readiness command for Phase 2.
The command does not decide that Phase 2 is complete. It reads the exit ledger and gives a plain summary of what is done, what has proof in progress, and what is still blocked.
scripts/phase2-exit-readiness.sh
For the full review view, including every open proof item and its next step:
scripts/phase2-exit-readiness.sh --all
It reports:
- how many exit gates are tracked
- how many are fully done
- how many are strong drafts
- how many have proof in progress
- how many are pending or blocked
- which gates still need final proof or a decision
- with
--all, the next step for every open gate
Why This Exists
Phase 2 has many proof files now. That is good, but it can become hard to see the whole picture from one page.
The readiness command keeps the answer repeatable. Instead of guessing from
memory, it reads docs/book/src/phase2/exit-evidence.md and prints the current
gate state.
The default output stays short so daily progress checks are easy to read. The
--all output is for exit review and handoff, where hiding the lower-priority
open gates would make the status less clear.
Current Shape
The important split is:
- Fully done gates are complete for the current Phase 2 scope.
- Strong draft and partial gates have real work behind them, but still need a final review, cross-host proof, or a human decision.
- Pending and blocked gates are the hard remaining items.
This matches the current state of Phase 2: the runtime and UAPI shape are
strong, while formal exit still needs proof from more than one machine and one
outside walkthrough. Go now has an explicit Phase 2 decision: it stays tested,
but runtime parity is experimental until its compiled components import only
krate:*.
When To Run It
Run it before:
- deciding whether Phase 2 is ready to freeze
- asking what is left in Phase 2
- creating a handoff status file
- opening the Phase 3 kickoff issue
- updating the Phase 2 retrospective draft after final review
For deeper review, also run:
scripts/record-phase2-exit-bundle.sh --strict
scripts/check-phase2-exit-evidence.sh
scripts/check-phase2-freeze-decision.sh
scripts/phase2-exit-readiness.sh --all
The readiness command is the map. The exit bundle is the evidence packet. The freeze decision packet keeps the final human decision separate from the automated checks.
Phase 2 Exit Evidence
This page tracks the evidence needed before we say Phase 2 is complete.
Phase 2 is not finished just because the code runs on one machine. It is finished when the UAPI contract, host adapters, language paths, samples, performance checks, docs, and CI evidence all line up.
How To Read This Page
Status meanings:
- Done means the gate is complete for the current Phase 2 scope.
- Strong draft means the design and local checks are solid, but the final freeze or external proof is still pending.
- Partial means useful work exists, but at least one planned proof is still missing.
- Pending means the gate still needs its first real proof.
- Blocked means we know what is needed, but another decision or toolchain step must happen first.
Exit Gate Ledger
| Gate | Criterion | Status | Evidence | Next step |
|---|---|---|---|---|
| P2E-01 | UAPI modules frozen | Strong draft | scripts/check-uapi.sh, scripts/check-uapi-freeze-lock.sh, scripts/check-phase2-freeze-decision.sh, scripts/record-phase2-uapi-freeze-review.sh, UAPI Freeze Evidence, UAPI Freeze Lock, UAPI Freeze Review Evidence, UAPI Freeze Decision Packet | Run the final freeze review and fill the decision packet after cross-host and language evidence are ready. |
| P2E-02 | Desktop host adapters | Partial | scripts/check-adapter-boundary.sh, scripts/record-phase2-adapter-evidence.sh, scripts/compare-phase2-adapter-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts adapter-evidence-<os>, hosted full CI Adapter evidence compare job, Adapter Boundary, Adapter Evidence, Hosted Full CI Evidence | Keep collecting Linux macOS Windows evidence; adapter reports now include boundary, shared adapter behavior tests, and native adapter crate tests. |
| P2E-03 | Rust bindings usable | Partial | scripts/smoke-rust-sdk.sh, scripts/record-phase2-rust-sdk-evidence.sh, hosted CI artifact rust-sdk-evidence, Rust SDK Evidence, First Rust CLI | Keep package and outside-workspace smoke evidence green; publish only after UAPI v0.1 is frozen. |
| P2E-04 | Go bindings usable | Partial | scripts/build-phase2-go-variant-smoke.sh, scripts/promote-phase2-go-runtime-fixtures.sh, scripts/record-phase2-go-readiness-evidence.sh, Go Readiness Evidence, Go Phase 2 Decision | Go is experimental for runtime parity in Phase 2; keep TinyGo smoke and import-purity checks, and revisit promotion when artifacts import only krate:*. |
| P2E-05 | TypeScript bindings usable | Partial | scripts/build-phase2-language-variant-fixtures.sh, scripts/test-phase2-language-variants.sh, scripts/record-phase2-language-variant-evidence.sh, scripts/compare-phase2-language-variant-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts language-variant-evidence-<os>, hosted full CI language-variant-evidence-compare job, Language Variant Evidence, Hosted Full CI Evidence, language_variants_curl_* CLI tests | Keep TS curl success evidence stable across restricted runners and keep cross-language curl error parity green (5, 20, 21 classes). |
| P2E-06 | curl cross-host | Partial | scripts/record-phase2-sample-evidence.sh, scripts/compare-phase2-sample-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts sample-evidence-<os>, hosted full CI Sample evidence compare job, Sample Evidence, Hosted Full CI Evidence | Move curl from temporary blocked-host exception to strict all-host hash parity once hosted localhost fixture behavior is stable. The comparator still checks matching curl hashes across hosts that did run curl. |
| P2E-07 | cat cross-host | Partial | scripts/record-phase2-sample-evidence.sh, scripts/compare-phase2-sample-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts sample-evidence-<os>, hosted full CI Sample evidence compare job, Sample Evidence, Hosted Full CI Evidence | Keep strict cross-host cat hash parity green in hosted and self-hosted evidence runs. |
| P2E-08 | clock cross-host | Partial | scripts/record-phase2-sample-evidence.sh, scripts/compare-phase2-sample-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts sample-evidence-<os>, hosted full CI Sample evidence compare job, Sample Evidence, Hosted Full CI Evidence | Keep strict fixed-time clock hash parity green in hosted and self-hosted evidence runs. |
| P2E-09 | UCap enforcement | Partial | crates/policy, crates/runtime, dispatcher_denies_all_non_default_boundaries_before_adapter, tests/cli.rs (language_variants_curl_permission_denied_matches_rust_go_ts), scripts/record-phase2-ucap-evidence.sh, scripts/compare-phase2-ucap-evidence.sh, scripts/record-phase2-hosted-full-ci-evidence.sh, hosted full CI artifacts ucap-enforcement-evidence-<os>, hosted full CI UCap enforcement evidence compare job, UCap Enforcement Evidence, Hosted Full CI Evidence | Keep collecting clean Linux macOS Windows deny evidence for the full deny-before-adapter matrix. |
| P2E-10 | Startup performance | Partial | cargo bench -p krate-runtime --bench startup, record-phase2-cli-startup, scripts/record-phase2-benchmark-evidence.sh, scripts/compare-phase2-benchmark-evidence.sh, Dispatch Benchmarks, Benchmark Evidence | Collect Linux macOS Windows benchmark reports for the same commit, then set a full-CLI startup threshold from that evidence. |
| P2E-11 | Dispatch performance | Partial | cargo bench -p krate-runtime --bench uapi_dispatch, scripts/check-benchmark-regression.sh, scripts/record-phase2-benchmark-evidence.sh, scripts/compare-phase2-benchmark-evidence.sh, Benchmark Evidence, Dependency Evidence | Keep stable regression checks, cross-host benchmark evidence, and dependency-audit signoff linked to the final Phase 2 commit. |
| P2E-12 | Timed developer walkthrough | Pending | scripts/check-phase2-rust-walkthrough-rehearsal.sh, scripts/record-phase2-walkthrough-template.sh, scripts/check-phase2-walkthrough-evidence.sh, First Rust CLI, Timed Walkthrough Evidence | Ask an outside Rust developer to fill the walkthrough packet, time the run, and pass the filled-packet checker. |
| P2E-13 | Generated UAPI reference | Done | scripts/generate-uapi-reference.sh, Generated Reference | Keep CI freshness checks enabled. |
| P2E-14 | WIT style guide | Done | WIT Style Guide | Keep using it during freeze review. |
| P2E-15 | ADR set | Done | docs/adr/0006-wit-versioning.md through docs/adr/0012-adapter-crate-split.md | Add a freeze ADR only if the final review changes a rule. |
What Is Already Strong
The current direction is still right.
Krate has the correct shape for a universal software layer: app code calls a portable UAPI, the runtime checks policy, and host adapters translate approved calls to the native operating system. That is the right path for the larger 6 by 6 goal because it avoids hardcoding one host model into every app.
The strongest Phase 2 pieces today are:
- the UAPI contract shape for
io,fs,net,time, andlocale - capability parsing, launch grants, and runtime boundary checks
- Rust sample apps for clock, cat, and curl
- generated docs and CI freshness checks
- adapter split structure across Linux, macOS, and Windows crates
What Still Blocks Phase 2 Exit
The remaining work is mostly proof:
- freeze UAPI v0.1 after review
- collect clean Linux, macOS, and Windows evidence for the same samples
- keep the Go track experimental until import purity passes
- run longer fuzz and benchmark evidence
- do one timed external walkthrough
- record the retrospective and Phase 3 kickoff
Quick Readiness Command
To get the current gate count in one terminal command:
scripts/phase2-exit-readiness.sh
This does not replace the ledger. It reads this page and prints the current
number of done, strong-draft, partial, pending, and blocked gates. It also
prints the hard blockers so a returning session can start from the right place.
Run it with --all when preparing a handoff or exit review packet; that mode
prints every open proof item with its next step.
Local Evidence Commands
Run these before a Phase 2 exit review:
scripts/record-phase2-exit-bundle.sh --strict
scripts/check-uapi.sh
scripts/generate-uapi-freeze-evidence.sh
scripts/generate-uapi-freeze-lock.sh
scripts/check-uapi-freeze-lock.sh
scripts/check-phase2-freeze-decision.sh
scripts/check-adapter-boundary.sh
scripts/record-phase2-adapter-evidence.sh
scripts/compare-phase2-adapter-evidence.sh
scripts/check-phase2-exit-evidence.sh
scripts/record-phase2-sample-evidence.sh
scripts/compare-phase2-sample-evidence.sh
scripts/record-phase2-rust-sdk-evidence.sh
scripts/record-phase2-language-variant-evidence.sh
scripts/compare-phase2-language-variant-evidence.sh
scripts/record-phase2-go-readiness-evidence.sh
scripts/record-phase2-ucap-evidence.sh
scripts/compare-phase2-ucap-evidence.sh
scripts/record-phase2-benchmark-evidence.sh
scripts/compare-phase2-benchmark-evidence.sh
scripts/record-phase2-dependency-evidence.sh
scripts/record-phase2-fuzz-evidence.sh
scripts/check-phase2-rust-walkthrough-rehearsal.sh
scripts/record-phase2-walkthrough-template.sh
scripts/check-phase2-walkthrough-evidence.sh <filled-walkthrough.md>
scripts/record-phase2-ci-stability-evidence.sh
scripts/record-phase2-hosted-full-ci-evidence.sh
scripts/record-phase2-self-hosted-evidence.sh
scripts/check-phase2-closeout-docs.sh
scripts/smoke-rust-sdk.sh
scripts/build-phase2-language-variant-fixtures.sh
scripts/test-phase2-language-variants.sh
scripts/phase2-exit-readiness.sh
The Exit Bundle command gives the quick local review report. The other commands are still useful when you need the detailed per-area reports. The exit bundle now includes the closeout docs check and can optionally include hosted CI stability and self-hosted full-gate evidence, so the final review packet can carry current GitHub run history beside the local proof.
For performance and soak checks:
cargo bench -p krate-runtime --bench startup
cargo bench -p krate-runtime --bench uapi_dispatch
scripts/check-benchmark-regression.sh
scripts/check-dependencies.sh
scripts/record-phase2-fuzz-evidence.sh --strict
scripts/run-phase2-fuzz-smoke.sh
CI Evidence We Still Need
For formal exit, save the run links in the Phase 2 plan:
- one recent hosted CI green run
- one hosted CI stability report from
scripts/record-phase2-ci-stability-evidence.sh - one hosted full CI report from
scripts/record-phase2-hosted-full-ci-evidence.sh - one recent self-hosted full gate green run on macOS ARM64
- Linux and Windows hosted or trusted runner evidence for the sample outputs
- one longer self-hosted fuzz run after the final UAPI freeze candidate
- one benchmark baseline check after the final sample set is fixed
- one dependency evidence report for the final commit, with hosted advisory proof if local advisory DB locking is unavailable
- one final pass of
scripts/check-phase2-closeout-docs.shafter the retrospective and Phase 3 kickoff issue are updated from draft to final
That is enough to move Phase 2 from strong engineering progress to a clean phase exit.
The hosted CI stability recorder is documented in CI Stability Evidence.
Phase 2 Retrospective Draft
Status: Draft until Phase 2 exit review passes. Written: May 17, 2026 Author: incyashraj
This page is the working retrospective for Phase 2. It is not the final retrospective yet. The final version should be written after the UAPI freeze review, the outside walkthrough, and the last evidence bundle are complete.
What Shipped
Phase 2 turned Krate from a runtime proof into a useful CLI platform slice.
The main shipped pieces are:
- a Phase 2 UAPI for
io,fs,net,time, andlocale - manifest parsing and capability strings
- launch grants through
--grant,--auto-grant, prompts, and cap dumps - runtime policy checks before host adapter calls
- host adapter split across Linux, macOS, and Windows crates
- Rust sample apps for clock, cat, and curl
- Rust SDK packaging and smoke evidence
- TypeScript and Go SDK scaffolds with fixture evidence paths
- cross-host evidence recorders for samples, adapters, UCap, benchmarks, and language variants
- CI freshness checks for generated UAPI docs, freeze evidence, and freeze lock
- GitHub Pages docs for the current Phase 2 state
What Did Not Ship Yet
These are still open before Phase 2 can close.
| Area | Current state | Next step |
|---|---|---|
| UAPI freeze | Strong draft | Run final freeze review after the remaining proof is collected. |
| Cross-host evidence | Partial | Keep Linux, macOS, and Windows reports aligned for one commit. |
| Go runtime parity | Experimental | Keep TinyGo smoke builds, but wait for import-pure Krate components before promotion. |
| Long fuzz soak | Partial | Run a longer self-hosted fuzz pass on the final candidate. |
| Timed outside walkthrough | Pending | Ask a Rust developer new to Krate to run the walkthrough and pass the filled-packet checker. |
UAPI Lessons
The UAPI direction is still right. Apps should call a small Krate surface, not direct host APIs. That keeps the later 6 by 6 host matrix possible.
The main lesson is that the WIT contract must be treated like a product surface, not an internal file. We now generate reference docs, freeze evidence, and a freeze lock because manual review alone is too easy to miss.
Adapter Lessons
The adapter split is useful even before Phase 3. Moving filesystem, network, time, locale, and stdio host calls behind adapter crates made the runtime less host-shaped and easier to review.
The next lesson for Phase 3 is to keep this boundary strict. Windowing and input will have more host differences than CLI calls, so host-specific behavior should stay in adapter crates as early as possible.
Binding Lessons
Rust is the strongest path today. It has working samples, SDK smoke evidence, and the cleanest runtime path.
TypeScript is useful and should stay in the fixture lane. Its value is that many app developers already know the language, but the generated binding shape needs careful testing.
Go should stay experimental for runtime parity in Phase 2. The current TinyGo artifacts build, but still import WASI host APIs. That is acceptable only because the project now records the limitation clearly and keeps import-purity checks in place.
UCap Lessons
Capability checks are already doing real work. The deny-before-adapter matrix is especially important because it proves denied calls stop before native host work can happen.
The prompt flow is enough for CLI apps. Phase 3 should not reuse terminal-style prompts for GUI apps. It needs a system UI grant flow with clear wording and a better place to explain why an app wants access.
Performance Lessons
The current dispatch path is small enough for Phase 2. Startup and dispatch benchmarks exist, but the final threshold should be based on same-commit cross-host evidence, not one local machine.
Phase 3 should start with frame-time and first-paint measurement from day one. GUI performance problems become expensive if measurement arrives late.
Documentation Lessons
The docs are now part of the product. The public site explains what works, what is experimental, and what remains. That is important for a project this early because honest docs prevent confusion.
The next improvement is to keep final review pages short and direct. Long plans are useful for implementation, but exit pages should tell a reviewer what to run, what passed, and what still needs judgment.
The closeout docs check now runs in the normal hosted UAPI proof path and the self-hosted full gate. That keeps this retrospective draft and the Phase 3 kickoff draft from drifting away from the evidence ledger.
Build Plan Changes To Carry Forward
- Keep evidence recorders for every formal phase gate.
- Keep generated reference pages under CI freshness checks.
- Keep Go runtime parity experimental until import-pure components exist.
- Keep Phase 3 blocked until Phase 2 has one filled outside walkthrough packet.
- Keep normal hosted CI cheap, with full proof runs manual or self-hosted.
Phase 3 Notes Before Start
Before Phase 3 starts, confirm:
- Phase 2 exit evidence is green for the final commit.
- UAPI v0.1 is frozen and no Phase 3 work changes it.
- The Phase 3 kickoff issue links to this retrospective and the final exit bundle.
- Phase 3 starts with WIT and ADR work for UI, graphics, input, and accessibility before implementation.
Phase 3: UI + Graphics
Status: About half done — real windows open from one portable artifact on all three desktop OSes (human-verified native widgets and click round trip on macOS; CI-proven winit windows with the first drawn-widget pass on Linux and Windows); the agent-embedding track (embed API, krate run --json, MCP server) is complete. Remaining: the vello renderer, richer widgets, text input, accessibility, and krate-notes.
Estimate: est. 6 to 10 weeks
Goal: Run one windowed app on Windows, macOS, and Linux.
Phase 3 has started with the contract layer. That means we are defining what a desktop app is allowed to ask Krate for before we build the host adapters and sample app behind it.
Phase 2 is still waiting on the outside developer review before we call it formally closed. The engineering proof is strong enough to begin the Phase 3 draft contracts, as long as Phase 2's existing API remains unchanged.
Phase 3 adds the first visual app surface:
- windows
- layout
- buttons and text
- keyboard and pointer input
- 2D drawing
- a small notes app
The target is not a web view hidden in a desktop shell. The target is a Krate app that feels close to native on each desktop host.
Current Slice
The first Phase 3 slice is now in the repo:
krate:app@0.2.0with aguiworldkrate:ui@0.1.0for windows, widget trees, events, dialogs, clipboard, and menuskrate:gfx@0.1.0for 2D canvas and a small future 3D surfacekrate:audio@0.1.0for playback and capture shapescripts/check-phase3-uapi.shso CI can reject broken WIT before runtime code depends on it- manifest and policy support for the first Phase 3 permission names
adapter-common::ui, an in-memory draft window registry for IDs, title and size validation, lifecycle state, redraw requests, and eventsadapter-common::ui, the first host-neutral widget tree model for stable widget IDs, widget kinds, labels, roles, style hints, and parent linksadapter-common::ui::WindowAdapter, the named lower boundary for window lifecycle and host event-loop signalsadapter-common::ui::UiAdapter, the shared trait that native UI adapters will implementruntime::phase3_ui, a runtime dispatcher scaffold that checks UCap before calling the shared UI adapter- draft widget-tree dispatch for setting a root widget, upserting nodes, removing nodes, moving focus, and inspecting widget state
krate-layout, the first Taffy-backed layout wrapper, which turns the shared widget tree into stable rectangles by widget ID- runtime layout dispatch, so the Phase 3 dispatcher can compute layout for a stored draft widget tree after the same UI capability check
- generated layout tests across 100 tree shapes, plus a 1k/10k-node Criterion benchmark target
- a prepared repeated-layout path, so future event loops can reuse the same layout tree instead of rebuilding it each frame
- first layout-based hit-test helper for finding the deepest widget under a point
- draft pointer event routing, so the runtime can turn a logical pointer position into a queued event with the hit widget ID
- draft key and text input routing, so focused widgets can receive portable key events and committed text before native IME work starts
- FIFO event polling, so future
events.poll()calls can consume one queued UI event at a time - draft host window event routing for close requests, resize, and focus changes
- draft theme and scale event routing, so dark mode and DPI changes have a stable path before native windows land
- headless draft UI adapter entry points in the macOS, Linux, and Windows adapter crates, each with a blank-window smoke test
- active and planned window backend info for each host, with AppKit planned for macOS and winit planned for Linux and Windows
- native window handle handoff in
WindowAdapter, so a real host window can be bound to a stable KrateWindowId - the first macOS AppKit handoff method, ready for the native AppKit window prototype while the default adapter remains headless draft
- an opt-in macOS
AppKitWindowBackendthat creates an ownedNSWindowon the main thread, attaches its native handle to a Krate window id, and shows it through the shared window path - AppKit event bridge targets for close, resize, focus, and display scale, plus a native window snapshot helper for the coming delegate wiring
AppKitWindowSession, a small state object that owns the native window, remembers the last snapshot, and refreshes changed native state into the shared event queueAppKitWindowNativeEventandAppKitWindowEventState, a tested Rust callback surface for the real AppKit delegate to call next- an AppKit redraw bridge, so the first native drawing surface can request paint through the shared window event queue
AppKitWindowDelegateCallbackandAppKitWindowDelegateBridge, so the coming Objective-C delegate can stay thin and hand event translation to tested Rust codeAppKitDrawSurfaceState, which tracks size, scale, clear color, redraw requests, and frame metadata before the real AppKit view paints pixelsAppKitDrawViewSurface, an opt-in AppKitNSViewattachment path that sets a visible clear color, marks the view dirty, and records the first frame snapshotAppKitWindowNativeDelegate, a retained AppKitNSWindowDelegateobject that records native close, resize, focus, and backing-scale callbacks for the Rust session to drainAppKitWindowEventLoopDriver, a non-blocking AppKit tick proof that refreshes native state, drains delegate callbacks, and queues redraw requests through the shared event streamPhase3UiRuntime::with_host_adapter, which selects the current host UI adapter and reports whether it is still headless or nativePhase3UiRuntime::try_with_host_adapter_mode, which keeps the default headless path stable while allowing macOS to explicitly request the AppKit prototype adapterUiEventLoopTickandPhase3UiDispatcher::pump_event_loop_once, so every host adapter has the same non-blocking event-loop pump shape- a local runtime smoke command that asks for the AppKit prototype path, opens the native window, pumps one shared tick, checks the report, and closes the window
- Linux and Windows Winit prototype boundaries, with tested native-handle handoff helpers and guarded discovery paths that stay off until real native windows land
- a shared Winit session owner scaffold, so Linux and Windows can track a native session, refresh snapshot state, pump prepared native events, and remove that session on close before real OS windows are connected
- a Winit callback collector bridge, so Linux and Windows can record future native callbacks in order and let the normal event-loop pump drain them into the shared UI queue
- ADR-0013 and RFC-0003 now record the widget lowering strategy: native controls where the host has a semantic match, drawn fallback where it does not
- ADR-0014 records the layout engine choice: Taffy, with a small flexbox-style subset first
This is a draft contract, not a frozen API. The macOS side can now create and
show one AppKit window through an opt-in prototype, and it has checked bridge
methods plus session state for the main host-window events. The callback-shaped
Rust event state is now in place too, including redraw requests for the first
paint path. AppKit-style delegate callbacks now have a tested Rust translator
too. AppKit now also has draw-surface state and an opt-in AppKit draw view
surface. The view path can attach an NSView, set a visible clear color, mark
the view dirty, and record the first frame snapshot. The real AppKit delegate
object now exists too. It records native window callbacks into a small queue
that the Rust session drains through the same bridge. There is now a small
event-loop driver that can process one native tick without blocking. The next
runtime work has started too: the AppKit prototype can be selected explicitly,
while the normal runtime path still stays headless for CI. The runtime can now
ask any adapter to process one non-blocking UI tick. Headless adapters return
no native work; AppKit maps its prototype tick into the shared report. There is
also a local smoke command for that full runtime path, so a macOS developer can
prove create, show, pump, inspect, and close on the main process thread without
making normal CI open a window. Linux and Windows now have a Winit session
scaffold and a callback collector bridge. That means the future real Winit
event handlers have a small tested place to record resize, focus, scale,
redraw, and close callbacks before the shared pump forwards them to the app.
See Widget Protocol for the plain-language version of this Phase 3 direction. See Layout for the current geometry path.
Phase 3 UAPI Draft
This page explains the first Phase 3 contract in simple terms.
Phase 2 proved that a portable app can ask Krate for files, network, time, locale, and terminal input or output. Phase 3 adds the first desktop app shape. The app still does not call macOS, Windows, or Linux directly. It asks Krate for a window, input events, drawing, and audio. The host adapter translates those requests.
flowchart LR
APP["Portable app component"] --> GUI["krate:app gui world"]
GUI --> UDISP["Runtime UI dispatcher"]
UDISP --> ADAPTER["Shared UiAdapter trait"]
ADAPTER --> UI["UI<br/>window, tree, events"]
GUI --> GFX["Graphics<br/>2D canvas, GPU surface"]
GUI --> AUD["Audio<br/>playback, capture"]
UI --> HOST["Host adapter"]
GFX --> HOST
AUD --> HOST
HOST --> OS["Native desktop OS"]
classDef built fill:#d9fbe3,stroke:#16833a,color:#102a17,stroke-width:2px;
classDef draft fill:#fff3bf,stroke:#b7791f,color:#2d2100,stroke-width:2px;
classDef pending fill:#eeeeee,stroke:#999999,color:#777777,stroke-width:1px;
class APP,GUI,UDISP,ADAPTER draft;
class UI,GFX,AUD draft;
class HOST,OS pending;
What Exists Now
The first draft lives under wit/krate/phase3.
It defines:
krate:app@0.2.0with aguiworldkrate:ui@0.1.0for windows, widget trees, events, dialogs, clipboard, and menuskrate:gfx@0.1.0for simple 2D drawing and an early 3D surface shapekrate:audio@0.1.0for playback and capture stream shape
The checker is:
scripts/check-phase3-uapi.sh
That checker proves the WIT parses, the expected packages are present, the
gui world imports the intended interfaces, the world exports run, public
names follow the repo style, and the new error types include
permission-denied.
The CLI also recognizes this manifest world now:
[app]
id = "com.example.notes"
name = "Notes"
version = "0.1.0"
entry = "notes.wasm"
world = "krate:app/gui@0.2.0"
krate manifest check accepts that world and labels it as a Phase 3 GUI
draft. krate run does not launch it yet. It exits early with a clear message
that the GUI runtime is not implemented. This is intentional. We want the
manifest and tooling path to be real before we add windows.
The first Phase 3 capability names are also wired into the manifest and policy layer:
| Capability | Meaning today |
|---|---|
ui.window:create | A GUI app may ask for a window. This is a default grant. |
ui.dialog:* | File dialogs are allowed by default for the draft GUI shape. |
ui.clipboard:read and ui.clipboard:write | Clipboard access is sensitive and must be granted explicitly. |
ui.dropzone:<mime-type> | Drag and drop can be scoped by MIME type. |
gfx.gpu:basic | Basic GPU-backed drawing is a default grant. |
gfx.gpu:compute | Compute access is explicit. |
audio.playback | Playback is defined, but not default yet. |
audio.capture | Microphone capture is explicit. |
This does not grant real desktop access yet. It means Phase 3 manifests can name the same permissions the future runtime will enforce.
The repo also has a small shared UI model now: adapter-common::ui. It has a
UiAdapter trait and an in-memory DraftUiAdapter. The draft adapter can
create window records, validate titles and sizes, track show or close state,
and collect window events. This gives the runtime one stable shape to call
while native adapters are still being built. It is not AppKit, Win32, GTK, or a
real event loop yet.
The runtime now has a first UI dispatcher scaffold too: runtime::phase3_ui.
It checks the same capability policy before it calls the shared UI adapter.
Window create, show, resize, redraw, and close all pass through that boundary.
Clipboard read and write are shaped through the adapter too, but the draft
implementation still returns unsupported after the permission check. That lets
us test the security path before native clipboard integration exists.
The macOS, Linux, and Windows adapter crates also expose Phase 3 window and UI adapter entry points now. Each one currently uses the same headless draft backend and has a blank-window smoke test. The adapter info says that clearly. It also records the native backend each host is aiming at: AppKit on macOS, and winit on Linux and Windows. That means the host crates are wired into the UI contract, but they still do not open real OS windows.
The runtime can now discover the current host UI adapter too. Phase3UiRuntime
owns the session guard and the selected adapter, then hands out a dispatcher
that checks permissions before calling that adapter. It also reports adapter
capability info such as host family, backend name, and whether native windows
or a native event loop are enabled. Today those values still say headless draft.
The Rust side now has a named WindowAdapter boundary too. Window lifecycle,
redraw, event polling, close requests, resize, focus, theme, and scale changes
sit in that lower layer. UiAdapter builds on it with widget trees, input, and
clipboard. This keeps the first native window backend smaller than the full
widget bridge.
That window boundary now has the first native handle handoff. In plain terms,
Krate has a stable WindowId, while the host has a real object such as an
AppKit NSWindow, a winit window, or a Win32 window. The new
NativeWindowHandle token lets a host adapter attach that native object to the
Krate id, look it up later, and detach it. macOS has the first AppKit handoff
method. The default backend is still headless draft, so this does not open a
real window yet.
macOS now has the first opt-in native window prototype behind that handoff. The
AppKitWindowBackend can create an owned AppKit NSWindow on the main thread,
attach its raw handle to the Krate window id, and show it through the shared
window path. The normal adapter still reports headless draft because native
event capture, drawing, and app-facing loop wiring are not finished yet.
The same prototype now has a small event bridge. It can queue close requests,
resize events, focus changes, and display-scale changes through the shared
WindowAdapter path. It can also read a native snapshot from AppKit: content
size, focus, visibility, and backing scale. That snapshot is not a full event
loop yet. It is the checked handoff point that real AppKit delegates can call
next.
The prototype also has AppKitWindowSession now. This is the first event-loop
state object for macOS. It owns the native window, remembers the last snapshot,
and refreshes only changed state into the shared event queue. That keeps the
next delegate work small: callbacks can update the session instead of reaching
into loose helper methods.
The macOS adapter now exports AppKitWindowNativeEvent and
AppKitWindowEventState too. These are not Objective-C delegates yet. They are
the Rust shape those delegates will call: close request, resize, focus, display
scale, or a full snapshot. This lets us test the event behavior before adding
the unsafe AppKit callback object.
Redraw requests are part of that path now. In plain terms, when the future AppKit view needs to paint again, it can ask Krate for a redraw through the same shared window event queue. That keeps drawing requests beside resize, focus, scale, and close events instead of making a separate one-off path.
There is also a small Rust delegate bridge now. It uses AppKit-style callback names such as resize, become key, resign key, backing-scale change, display needed, and should close. The bridge translates those callbacks into the tested native event state. That means the real Objective-C delegate can stay small: call into Rust and let Rust handle the event rules.
The first real AppKit delegate object is now in place. It implements
NSWindowDelegate, stays retained by the AppKit window session, records native
close, resize, focus, and backing-scale callbacks into a FIFO queue, then lets
the Rust session drain those callbacks through the tested bridge. This is still
an opt-in prototype path, not the default runtime event loop.
There is also a small AppKit event-loop driver now. It does one non-blocking tick: refresh the native snapshot, drain delegate callbacks, and optionally queue a redraw request. That is enough to prove the native path can feed the same event stream as the headless draft adapter. It is not yet the default runtime path.
The runtime can select that prototype path explicitly now. The default
Phase3UiRuntime::with_host_adapter path remains headless. A caller that is
ready for native macOS rules can ask for Phase3HostUiMode::NativePrototype,
which selects the AppKit prototype adapter and reports native window plus
native event-loop support.
The event-loop step has a shared runtime boundary now too. UiAdapter exposes
one non-blocking pump method, and Phase3UiDispatcher checks the same UI grant
before it calls that method. Headless adapters return no native tick. The
AppKit prototype maps its native step report into a small common
UiEventLoopTick report. This keeps the next Linux and Windows window work on
the same shape instead of making macOS a one-off path.
Linux and Windows now have the first Winit callback collector bridge. The prototype adapters can record Winit-shaped callbacks in FIFO order, count what is waiting, and let the normal event-loop pump drain those callbacks through the shared UI event queue. This still does not create a real Linux or Windows window. It gives the coming real Winit event handlers a small tested place to send resize, focus, scale, redraw, and close callbacks.
There is also a local smoke command for the full selectable AppKit runtime
path. On macOS, a developer can run it to ask the runtime for
Phase3HostUiMode::NativePrototype, create and show the AppKit window, pump
one shared event-loop tick, inspect the tick report, and close the window. It
runs as a command rather than a unit test because AppKit needs the main process
thread. Normal CI should not open native desktop windows.
AppKit now has draw-surface state too. It records the Krate window id, logical
size, display scale, clear color, redraw count, and frame number. A redraw
request from that surface goes through the same delegate bridge as a future
native AppKit view. This still does not paint pixels. It is the small state
object the next NSView painter will use.
The first AppKit draw view surface is in place now. It attaches an owned
NSView to the opt-in prototype window, sets the window background from the
surface clear color, marks the view as needing display, and records the first
frame snapshot. This is a local native smoke path. The normal runtime still
uses the headless draft adapter until native event-loop wiring is ready.
ADR-0013 and RFC-0003 now define how widgets lower once a native backend exists. A widget should become a native control when the host has a semantic match. If it does not, Krate uses a drawn fallback with the same layout, input, accessibility, and permission rules.
The shared Rust model has started too. adapter-common::ui now has stable
widget IDs, widget kinds, labels, role hints, small style hints, and parent-link
validation. That is the local model the next layout and native-lowering code
can use before the WIT bindings are wired end to end.
The runtime can now move draft widget trees through the adapter boundary. The dispatcher checks the current GUI grant, then calls the selected adapter to set a root widget, upsert child nodes, remove nodes, move focus, and inspect draft widget state. This is still headless, but it proves the runtime path that later native widgets will use.
The first layout path is wired too. crates/layout maps that shared widget
tree into Taffy and returns a LayoutSnapshot: one logical rectangle per stable
WidgetId. The Phase 3 dispatcher can request that snapshot for a draft window
after the same UI capability check. This does not draw anything yet. It gives
native widgets, drawn fallback, hit testing, and accessibility one geometry
answer to share.
There is also a prepared layout path now. The runtime can prepare the window's widget tree once, then recompute layout for repeated viewport changes without rebuilding the engine tree each time. That is the shape the future event loop should use between widget mutations.
Layout has its first input-facing helper too. The layout crate can hit-test a point against the computed rectangles and return the deepest widget under that point. The runtime now has a draft pointer route that uses that helper. It takes a window, viewport, logical x and y, button state, and modifiers, then queues a pointer event with the widget ID if a hit was found. This is still not a native mouse or touch event loop, but the routing shape is now in code.
Keyboard and text input have a draft route too. The runtime can take a key name, button state, and modifiers, look up the window's focused widget, and queue a portable key event. It can also queue committed text input for that same focused widget. This is not IME support yet. It is the route that native keyboard and IME commit events will use.
Theme and scale changes have draft routes too. A native host can later tell the runtime that the system moved between light and dark mode, or that a window changed scale because it moved between displays. Scale values are checked before they enter the queue, so a bad host value cannot silently reach app code.
The event queue now has a single-event poll path as well as a batch drain path.
That matters because the planned UI API exposes events.poll(). Native host
event loops can feed the queue first, then app code can consume events one by
one in stable FIFO order.
Basic host window events have draft routes now too. A future native backend can queue "close requested", "window resized", and "window focus changed" without closing the app window by itself. The app still decides how to respond.
What It Does Not Mean Yet
This is not a finished desktop UI layer.
There is an opt-in AppKit window prototype on macOS, but the default runtime is still headless. The AppKit view path is available only through the native prototype and ignored local smoke tests. The shared event-loop pump gives that prototype a real runtime handoff point, but it does not mean the API is frozen. It is the first contract and adapter shape that lets us build the runtime and host work in the right direction.
Why Start Here
For Krate, the contract is the platform boundary. If the contract is vague, each host adapter will drift. Starting with WIT gives us one shared language for the app, runtime, SDKs, and host adapters.
The next proof should stay small and visible:
- Record prepared and cold layout benchmark numbers on the target hosts.
- Create the first real Linux and Windows
winitwindow and feed its actual callbacks into the Winit collector. - Connect real host input events to the draft pointer, key, and text routes.
- Add a small notes app skeleton that uses the same path.
- Keep capability checks at the dispatcher boundary as native code is added.
Widget Protocol
Krate does not want every desktop app to look like the same painted surface. It also does not want every app developer to write three different UIs for Windows, macOS, and Linux.
The Phase 3 answer is a small widget tree. The app says what it wants. The host adapter decides how to make it feel right on that machine.
The Basic Idea
For common controls, Krate should use real host widgets. A button should be a real button. A text field should use the host text system where possible. A menu should follow host menu rules.
For surfaces that do not have a good native match, Krate draws them itself. That gives apps room for canvas, custom lists, charts, and later richer graphics.
flowchart LR
A["App asks for widgets"] --> B["Krate runtime"]
B --> C["Layout and permission checks"]
C --> D{"Host has a real match?"}
D -->|yes| E["Native widget"]
D -->|no| F["Drawn fallback"]
E --> G["User sees the app"]
F --> G
Why This Direction
There are three common ways to build cross platform desktop UI:
| Approach | What it means | Why Krate is not using it as the main path |
|---|---|---|
| Draw everything | The framework paints every control itself | Easier to match pixels, but often feels less native |
| Use only native controls | Every widget is a host widget | Good feel, but too rigid for custom app surfaces |
| Embed a browser | The app is a web UI in a desktop shell | Useful elsewhere, but not the Krate desktop goal |
Krate uses a mixed path. Native where the host has the right control. Drawn where the app needs its own surface.
Per-Host Lowering In v0.1
The mixed path is a rule about the protocol, not a promise that every host lowers every widget natively. For v0.1 (ADR-0015):
| Host | Windowing | Widget lowering |
|---|---|---|
| macOS | AppKit (own bridge) | Native AppKit controls, drawn fallback for the long tail |
| Windows | winit | Native Win32 common controls as child windows, drawn fallback for the long tail |
| Linux | winit | Drawn fallback for every widget, styled with theme tokens |
Linux uses the drawn path everywhere in v0.1 because GTK4 widgets cannot be embedded inside winit-owned windows — GTK4 removed foreign-window embedding, so its widgets only live in GTK-owned windows with GTK's own event loop. Rather than fork the window model for one host, Linux v0.1 exercises the drawn fallback that every host needs anyway. Native GTK lowering can return later as a separate GTK-owned-window backend if demand justifies it.
The Native Three Of Five Rule
A widget belongs in the core protocol only when at least three of these hosts have a native control with the same meaning:
- Windows
- macOS
- Linux
- iOS
- Android
This keeps the core set small. It also keeps the API close to what real platforms already know how to do.
First Widget Set
The first set is intentionally small:
| Widget | First use |
|---|---|
| Window root | Put the app in a host window |
| Stack | Arrange controls vertically or horizontally |
| Text | Show labels and short text |
| Button | Trigger actions |
| Text field | Edit one line |
| Text area | Edit note content |
| List | Show notes |
| Scroll | Move through long content |
| Checkbox | Basic on or off state |
| Menu | App and window commands |
| Canvas | Custom drawing later |
That is enough to build the first krate-notes app without turning Phase 3
into a full design system.
How Events Move
The host creates raw events. Krate turns those into stable events that the app can understand.
sequenceDiagram
participant Host
participant Adapter
participant Runtime
participant App
Host->>Adapter: click, key, text, pointer, close
Adapter->>Runtime: Krate UI event
Runtime->>App: event with widget or window id
App->>Runtime: next widget tree
The first code path already handles draft window lifecycle events. It also has a routed pointer event path now: the runtime can take a logical pointer position, run layout hit testing, and queue an event with the target widget ID. It also has key and text input routing through the focused widget. The next steps are a real native window, a host event loop, then a tiny widget tree with text and a button.
Current Status
Done now:
- Phase 3 UI, graphics, and audio WIT drafts exist.
- GUI manifests are recognized.
- Phase 3 permission names exist.
adapter-common::uihas the first host-neutral widget tree types:WidgetId,WidgetKind,WidgetNode,WidgetStyle, andWidgetTree.- The shared UI adapter and runtime dispatcher can now set a root widget, update child nodes, remove nodes, move focus, and inspect draft widget state.
krate-layoutcan compute Taffy-backed rectangles for the shared widget tree and return them by stable widget ID.- The runtime dispatcher can ask for a layout snapshot for the widget tree stored on a draft window.
- The runtime dispatcher can also prepare a layout tree for repeated passes, which is the path future event loops should use between widget mutations.
- The layout crate has a first hit-test helper. It can use the layout snapshot to find the deepest widget under a point.
- The runtime can queue a routed pointer event after hit testing, so a future native mouse or touch event can already become a stable Krate event with a window ID and optional widget ID.
- The runtime can queue routed key events and committed text input for the focused widget, which gives native keyboard and IME commit events a stable place to land later.
- The adapter and runtime can poll one queued UI event at a time in FIFO order,
which matches the planned
events.poll()app-facing shape. - Host window events for close request, resize, and focus change have draft routes through the same queue.
- Theme and scale change events have draft routes too, so dark mode and DPI changes can use the same queue once real native windows exist.
WindowAdapternow names the lower window/event-loop boundary.UiAdapterbuilds on it for widget trees, input, and clipboard.- Native window handles now have a shared handoff point. A host backend can attach an AppKit, winit, or Win32 handle to a stable Krate window id, then look it up or detach it later.
- macOS has the first opt-in AppKit window prototype. It creates an owned
NSWindow, binds it to the Krate window id, and can show it through the shared window path. This starts the real native window backend work. Native events and drawing are still pending. - The AppKit prototype now has bridge targets for close requests, resize, focus, and display-scale changes. It also has a snapshot helper that reads native size, focus, visibility, and scale from the real window.
- AppKit now has a window session object. It owns the native window, remembers the last snapshot, refreshes changed state into the queue, and gives future delegates a clear object to call.
- AppKit now has a native event state object. It accepts delegate-shaped events for close, resize, focus, display scale, and full snapshots, then queues them through the same shared path.
- AppKit redraw requests now use that same path, so the first native drawing surface has a tested way to ask Krate for another paint.
- AppKit delegate callbacks now have a Rust bridge. The future Objective-C delegate can translate native method calls into a small enum, then the Rust bridge handles resize, focus, scale, redraw, close, and snapshot routing.
- AppKit now has draw-surface state. It tracks the window size, display scale,
clear color, redraw count, and frame number. It still does not paint pixels.
It gives the next
NSViewstep a small state object to use. - AppKit now has a first draw view surface too. It can attach an owned
NSViewto the prototype window, set a visible clear color, mark the view dirty, and record the first frame snapshot. This is still opt-in and not the default runtime path. - AppKit now has a first real window delegate object. It implements
NSWindowDelegate, keeps the delegate retained for the window session, and records close, resize, focus, and backing-scale callbacks into a FIFO queue. The Rust session drains that queue through the same tested bridge. - AppKit now has a first event-loop step driver. It can refresh native state, drain delegate callbacks, and queue a redraw request without blocking the process.
- The runtime can now select the AppKit prototype mode explicitly on macOS. The default runtime path still stays headless.
- The runtime and shared
UiAdapternow have one common event-loop pump method. Headless adapters return no native tick, and the AppKit prototype maps its native step into the sharedUiEventLoopTickreport. - There is now a local smoke command for the selectable AppKit runtime path. It creates, shows, pumps, inspects, and closes the native prototype through the runtime dispatcher on the main process thread.
- The runtime has a UI dispatcher scaffold.
- macOS, Linux, and Windows adapters expose headless draft window and UI entry points, plus the planned native backend for each host. macOS also exposes the first AppKit handle handoff method.
- Linux and Windows own real winit windows now: thread-locally pumped event loops feed native close, resize, focus, and scale events into the shared stream, and the full CI matrix proves the hello-gui component opens real windows on both hosts.
- macOS lowers
ButtonandTextFieldto real native controls; a human-verified click round trip flows fromNSButtonback into the component. Linux and Windows paint lowered placements through the first drawn-widget pass (CPU framebuffer; vello replaces the painter behind the same contract). - Linux and Windows also have a shared Winit session owner scaffold. It can hold a tracked session, apply prepared resize, focus, scale, redraw, and close events, and remove the session on close.
- Linux and Windows now have a Winit callback collector bridge. Future native Winit event handlers can record callbacks into a small FIFO queue, and the normal event-loop pump drains that queue through the shared UI event stream.
- The runtime can choose the current host adapter.
- ADR-0013 and RFC-0003 now describe the widget lowering rule.
- ADR-0015 now records the v0.1 per-host lowering decision: Linux draws every widget inside winit windows because GTK4 cannot embed in foreign windows.
Pending:
- the real drawn renderer (vello/wgpu) with labels, styling, and text
- pointer, key, and text events from the winit backends (window-level events flow today; input routing into widgets is next)
- richer widget lowering beyond Button/TextField on macOS and drawn rectangles elsewhere
- larger layout style coverage and recorded large-tree benchmark results on all target hosts
- IME composition events
- accessibility tree
krate-notes
This is the right direction for the universal platform goal. We are building the contract first, then the runtime boundary, then the host adapters. That keeps the platform from becoming one app demo with no reusable core.
Layout
Layout is the step that turns a widget tree into rectangles.
For example, the app may say:
- here is a window
- inside it is a vertical stack
- inside the stack are a title, a note list, and an editor
The layout engine decides where each part sits and how much space it gets.
Why It Matters
Krate has two UI paths:
- native widgets, when the host has a real matching control
- drawn fallback widgets, when it does not
Both paths need the same layout answer. If a native button and a drawn canvas do not agree on positions and sizes, input, accessibility, screenshots, and redraws will drift.
flowchart LR
A["Widget tree"] --> B["Krate layout"]
B --> C["Rectangles by widget id"]
C --> D["Native widgets"]
C --> E["Drawn fallback"]
C --> F["Hit testing"]
C --> G["Accessibility bounds"]
What Exists Now
The first layout crate exists:
crates/layout/
It does these things today:
- takes a validated
WidgetTree - maps it into Taffy
- computes logical rectangles for each
WidgetId - returns a
LayoutSnapshotkeyed by stable widget IDs - returns absolute rectangles when code needs root-window coordinates
- can hit-test a point and return the deepest widget under that point
- has generated tests for 100 different layout shapes
- can prepare a layout tree once and reuse it for repeated layout passes
The runtime can now ask for a layout snapshot for a stored draft widget tree. That means the path is already connected to the Phase 3 dispatcher, not only a standalone library.
The runtime can also ask for a PreparedLayoutTree. That is the path future
event loops should use when a widget tree stays mostly the same across many
frames.
What This Does Not Mean Yet
This does not draw a UI.
This does not open a native window.
This does not freeze the Phase 3 style model.
It means the shared geometry step has started. Native widgets, drawn fallback, hit testing, and accessibility can now build on one rectangle map.
Current Shape
The first style block is intentionally small:
| Field | Meaning |
|---|---|
width | optional logical width |
height | optional logical height |
grow | flex grow factor |
padding | same padding on all sides |
The first layout pass treats stack-like widgets as vertical flex containers. That is enough for early notes app screens and for testing the runtime boundary.
Hit Testing
Layout rectangles are also the base for input routing. The first helper can take a point, walk the computed layout, and return the deepest widget that contains that point.
That is still a headless helper. It is not connected to mouse, touch, or pointer events yet. The important part is that future input routing will use the same rectangles as native widgets and drawn fallback rendering.
Benchmark Shape
The repo now has a Criterion benchmark target for layout:
cargo bench -p krate-layout --bench layout
It includes 1,000-node and 10,000-node stack-like trees. The benchmark is a measurement tool today, not a Phase 3 exit proof yet. Before exit, we still need recorded numbers on the target hosts and a pass or fail decision against the Phase 3 budget. The first local run shows optimization is still needed before the 10,000-node target can be treated as ready.
The prepared layout path is the first optimization step. Locally, repeated 10,000-node layout passes are now under the Phase 3 budget, while cold rebuilds are still above it. That fits the planned reconciler model: build or update the engine tree when widgets change, then reuse it for frame layout.
Next Steps
- add more style fields only when the notes app needs them
- record prepared and cold layout benchmark numbers on Linux, macOS, and Windows
- connect hit testing to real input events
- connect layout rectangles to accessibility bounds
- use the same rectangles when native window work starts
Embedding and Machine-Readable Runs
Krate's wedge is safe execution of software that programs — including AI
agents — produce and run on a user's behalf. That needs two surfaces beyond
the interactive CLI: a library API other programs can embed, and a
machine-readable form of krate run.
The embedding API
krate_runtime::embed runs a component inside the capability sandbox with
no terminal: grants are supplied programmatically through SessionPolicy,
nothing prompts, and the app's stdout comes back captured next to a
classified exit.
use krate_policy::SessionPolicy; use krate_runtime::{embed, Config}; fn main() -> Result<(), Box<dyn std::error::Error>> { let component = std::fs::read("app.wasm")?; let mut config = Config::default(); config.session_policy = SessionPolicy::from_cli_grants(&[ "fs.read:./data/**".to_string(), ])?; let outcome = embed::run_component(&component, &config)?; println!("class: {}", outcome.exit_class().as_str()); println!("stdout: {}", outcome.stdout_lossy()); Ok(()) }
EmbedOutcome reports the exit code, an EmbedExitClass
(success, permission-denied, app-error, limit-exceeded), the captured
stdout bytes, and the run duration. Runtime-level failures (invalid
component, trap) surface as errors; a capability denial inside the app is a
classified outcome, not an error — policy-aware callers decide what to do
with it.
Grants never come from prompts here. SessionPolicy::from_cli_grants parses
explicit capability strings, and SessionPolicy::allow_all_declared grants
everything a manifest declares — the embedding caller owns that decision the
way a human owns the terminal prompt.
krate run --json (schema krate.run.v1)
With --json, the CLI prints exactly one JSON object on stdout describing
the run, and the app's own stdout is captured into that object instead of
streaming. Process exit codes stay identical to the normal mode, so existing
scripts keep working.
{
"schema": "krate.run.v1",
"app": {
"id": "dev.krate.clock",
"name": "krate-clock",
"version": "0.1.0-dev",
"world": "krate:app/cli@0.1.0"
},
"capabilities": {
"granted": ["io.stdout", "time.clock"],
"denied": []
},
"exit": {
"code": 0,
"class": "success",
"message": null
},
"duration_ms": 87,
"stdout": "app=krate-clock\n..."
}
Field notes:
appisnullwhen no manifest was provided.capabilities.grantedlists the effective session grants, with boundaries (fs.read:data/**,net.connect:host:port).capabilities.deniedis non-empty only when required capabilities were missing and the run was refused before the component started; the process exits5in that case, matching the interactive flow.exit.classis one ofsuccess,permission-denied(app exit code 5 by Krate convention, or a refusal before the run),app-error,limit-exceeded,invalid-component, ortrap.exit.codeisnullwhen the runtime stopped the component (limit-exceeded,invalid-component,trap).duration_msisnullwhen the run was refused before starting.stdoutholds the app's captured stdout as lossy UTF-8.
Current status
Done now:
Runtime::run_bytes_captured/run_file_capturedcapture app stdout for embedding callers.krate_runtime::embed::run_componentwithEmbedOutcomeandEmbedExitClass, doc-tested.krate run --jsonemittingkrate.run.v1, covered by CLI integration tests for the success, denied-before-run, and invalid-component paths.
The MCP server
krate-mcp-server (a crates/tools binary) exposes one MCP tool,
run_component, over newline-delimited JSON-RPC on stdio. Any MCP-capable
agent framework can execute components inside the sandbox without linking
Rust:
cargo build -p krate-tools --bin krate-mcp-server
claude mcp add krate -- target/debug/krate-mcp-server
The tool takes component_path, optional manifest_path, grants,
auto_grant, app_args, and sandbox_root, and returns the
krate.run.v1 report as the tool result (with isError set for anything
but success). Denials surface as data: calling without grants returns
permission-denied plus the exact missing capabilities, and the same call
with auto_grant succeeds — the agent observes the capability wall
directly.
Still pending on this track:
- richer per-call deny logs inside a run (today the denial signal is the app's own exit code plus the refusal-before-run path).
Hello GUI: Testing the Vertical Slice
krate-hello-gui is the first GUI component: one portable .wasm file
that opens a real native window with a real native button and text field.
This page is the test manual for it.
The one-command demo
sh scripts/demo-hello-gui.sh
On macOS this opens a native window front and center. Click the "Click me" button within 30 seconds: the text field flips to "clicked!", the window closes itself a second later, and the script reports the result. On Linux and Windows the same portable file runs headless for about two seconds (those window backends are the next milestone), which still exercises the full component → runtime → permission → adapter pipeline.
Exit codes are the test assertions
The app reports what it observed, so scripts and CI can assert behavior:
| Exit | Meaning |
|---|---|
0 | A click on the native button reached the component (the full round trip) |
1 | Clean bounded run with no click — the normal headless outcome |
2 | The user closed the window before clicking |
30–32 | Window creation, show, or widget-tree calls failed |
Manual commands
Interactive native window (macOS):
target/debug/krate run --auto-grant --native-window \
--manifest apps/krate-hello-gui/manifest.toml \
apps/krate-hello-gui/target/wasm32-wasip1/release/krate_hello_gui.wasm
Headless, fast, any OS (the quick app arg shortens the wait loop):
target/debug/krate run --auto-grant \
--manifest apps/krate-hello-gui/manifest.toml \
apps/krate-hello-gui/target/wasm32-wasip1/release/krate_hello_gui.wasm \
-- quick
Machine-readable report of the same run (schema krate.run.v1):
target/debug/krate run --json --auto-grant \
--manifest apps/krate-hello-gui/manifest.toml \
apps/krate-hello-gui/target/wasm32-wasip1/release/krate_hello_gui.wasm \
-- quick
Automated regression smoke (builds, checks import purity, asserts the headless exit):
sh scripts/smoke-phase3-gui-app.sh
How the three-OS claim is proven today
Honestly, in two layers:
- The artifact is proven portable on all three OSes on every full CI run. The hosted full-test matrix builds the hello-gui fixture once, then runs the byte-identical file headless on Linux, macOS, and Windows and asserts the clean bounded exit. Same bytes, three operating systems, machine-checked.
- The visible window is proven on macOS only, by a human clicking the
native button (exit
0), because macOS (AppKit) is the only native window backend so far. The Linux and Windows winit backends are the next Phase 3 milestone; when they land, layer 1's runs graduate into visible windows there too — the component itself will not change at all.
That last sentence is the point of the whole design: the app is already finished for all three OSes. Only host adapters remain.
What to look for when it misbehaves
- No window appears on macOS: the process must promote itself to a
regular app and pump the NSApplication event queue — both were real bugs
caught by a human eye and fixed in the adapter; if a regression appears,
check
show_window(activation policy + activate) andpump_app_eventsincrates/adapter-macos/src/appkit.rs. - Window appears but clicks do nothing: the event-queue pump is not running — same place.
- Exit
32headless: a widget-tree call failed; run with--jsonto see the classified error.
Phase 4: Mobile Hosts
Status: Planned Estimate: est. 8 to 12 weeks Goal: Run the same app on iOS and Android.
Phase 4 brings the desktop proof to phones:
- iOS host shell
- Android host shell
- touch input
- app lifecycle
- sensors where allowed
- mobile packaging experiments
This phase will be shaped by app store rules and device testing. The estimate is therefore rough.
Phase 5: Developer SDK
Status: Planned Estimate: est. 6 to 10 weeks Goal: Make Krate pleasant to build with.
This phase turns the runtime into a developer workflow:
krate new- templates for first class languages
- debug support
- hot reload
- profiling
- editor support
- better errors
The target is simple: a developer should go from nothing to a running app in about a minute.
Phase 6: Distribution
Status: Planned Estimate: est. 8 to 12 weeks Goal: Make apps installable, signed, updated, and tied to identity.
This phase adds the platform pieces around the runtime:
.kratebundle format- signing
- update flow
- developer identity
- user identity
- marketplace backend
- marketplace app
Security review becomes much more important here because users will be running apps from other people.
Phase 7: v1.0 Hardening
Status: Planned Estimate: after Phase 6 Goal: Prove Krate with a real product and prepare for public launch.
This is where the project stops being only a runtime project.
The work includes:
- migrating a real app
- security audit
- performance cleanup
- accessibility pass
- full API docs
- launch site
- public release
The timing depends on how much real product work has already happened in earlier phases.
Announcing Krate
Status: Draft Target: Refresh before public launch
Krate is an experiment in making native application development portable again. The goal is direct: write an app once, package it once, and run it natively on the devices people actually use.
The platform is built around WebAssembly and the Component Model. Applications compile to a portable component, call a Universal API defined in WIT, and run inside a host runtime that enforces capability-based permissions before mapping those calls onto native operating-system APIs.
This is not a new kernel and it is not an emulator. It is a meta-platform: a runtime, standard library, permission model, bundle format, and distribution layer that sit above existing operating systems.
Krate is still pre-alpha, but the repo is now past the original foundation work. Phase 1 proved the base runtime path. Phase 2 is building the first useful app-platform slice: UAPI modules for files, network, time, locale, and I/O, plus manifest-declared capabilities and runtime permission checks.
The current milestone is Phase 2 exit: freeze the first UAPI contract, collect clean cross-host evidence, and complete an outside developer walkthrough before starting the desktop UI phase.
Follow the roadmap in the docs, read the build plan, and open an issue if there is a small piece you want to help with.
Every AI sandbox in 2026 runs in someone else's cloud
Published: 2026-07-22
Modal published a comparison of the best code execution sandboxes for AI agents. Seven providers: Modal, E2B, Northflank, Daytona, Blaxel, Vercel Sandbox, Cloudflare Sandboxes.
Every single one is cloud-hosted. Every single one isolates with containers or microVMs. Not one of them uses a capability-based permission model, and not one runs on the machine the developer is already sitting at.
That is not a criticism of those products. They are good at what they do, and for most agent workloads the cloud is the right answer. But it means an entire half of the problem has no serious entrant, and I think that half matters more than it looks.
The assumption nobody states
Every one of those tools starts from the same premise: untrusted code should run far away from the user, in an environment that can be destroyed.
That premise is correct for scale. It is wrong for a large and growing class of work, because the interesting things an agent does are usually about your stuff. Your files. Your local database. Your credentials. The application you are building right now. Ship that work to a remote microVM and you have to ship your context with it, which is either a security problem or a synchronization problem, and usually both.
So you end up with two bad options. Run agent-written code in the cloud and lose access to the local context that made it useful. Or run it locally with ambient authority, which means a script that was supposed to rename some files can read your SSH keys, because on every mainstream operating system a process inherits everything you can do.
Isolation solved the first problem. Nobody solved the second one, because the second one is not an isolation problem. It is a permission problem.
Container isolation and capability permission are different things
A container answers: what machine is this code on?
A capability answers: what is this specific code allowed to touch, right now, and who decided that?
You can have excellent isolation and terrible permissions. A microVM with a container full of your credentials is a strong wall around a room where everything is unlocked. The wall stops the code escaping to the host. It does nothing about what the code does with what is already inside.
The capability model inverts the default. Code starts with nothing. Not a restricted filesystem, not a read-only mount. Nothing. Then a specific grant is made for a specific resource, deliberately, by a human or a policy:
$ krate run --grant fs.read:./secrets.txt --manifest app.toml cat.wasm -- ./secrets.txt
Without that grant the same binary running the same code cannot open the file. It does not crash on a permission error deep in a syscall. It gets a structured denial: this capability was requested, it was not granted, here is the identity of the thing that was denied.
That last part is the piece I think is underrated for agents. A generic EACCES
tells an agent that something went wrong. A structured denial tells it exactly
which authority it lacks, which means it can ask for that specific thing, or
route around it, or report to the user precisely what it needs and why. Failure
becomes information instead of a dead end.
What I built
Krate is a runtime for that model. A program is one WebAssembly component in a single file. It runs natively on macOS, Windows and Linux, opening a real desktop window, not a bundled browser. It starts with zero ambient permissions and receives capabilities only through explicit grants.
The demo application is 26 kilobytes. The same file, unmodified, runs on all three operating systems.
I want to be precise about what is and is not proven, because this space is full of claims.
Every push to the repository triggers CI across all three operating systems. On all three, the component is built and executed and opens a real window. On Linux, CI goes further: it runs the app under Xvfb, uses xdotool to click the button, type into the text field and scroll the list, then photographs the screen and uploads the image as a build artifact. A machine I have never touched exercised the interface and produced the evidence, before I wrote this sentence.

The click-and-photograph step is Linux only today. macOS and Windows run the component and open real windows in the same CI run, but are not yet driven synthetically. Extending that is on the list.
The other honest caveat: on macOS, widgets are real AppKit controls, genuine
NSButton and NSTextField. On Linux and Windows the widgets are currently
drawn through a shared rendering path rather than being native OS controls. The
window is real on all three. The controls inside it are not equally native yet.
Why this shape, and why now
Two things had to become true for this to be buildable at all.
The WebAssembly Component Model matured to the point where one compiled artifact can carry typed interfaces across languages and hosts. That is what makes a single file legitimately portable rather than portable-if-you-squint.
And AI started generating far more software than humans can review. That changes the economics of trust. When a person writes code and runs it, they have some idea what it does. When an agent writes it, nobody has read it, and the honest position is that you do not know. Ambient authority was always a bad default. It becomes an untenable one at machine-generated volume.
Neither curve produces this on its own. The intersection does.
What I am not claiming
Krate is not a replacement for cloud sandboxes. If you are running ten thousand concurrent agent tasks, you want microVMs and a control plane, and you should use one of the seven products above.
It is also early. There is no SDK yet, the developer experience is rough, and the honest state of it is in STATUS.md rather than in a marketing page.
What I am claiming is narrower: local execution with a real permission boundary is a missing primitive, the entire market has been building the other half, and a portable component format with capabilities attached is a plausible shape for the missing piece.
If you build agents
The question I actually want answered: if you were routing agent-generated code through something like this, what capability would you need first that does not exist yet?
That answer is worth more to me than a star. The repo is public, the CI evidence is in the Actions log, and my DMs are open.
- Repo: https://github.com/incyashraj/krate
- Four minute demo: https://www.youtube.com/watch?v=RFefANqu0fc
Glossary
| Term | Definition |
|---|---|
| ABI | Application Binary Interface: the contract between caller and callee in compiled code. |
| ADR | Architecture Decision Record: a short document capturing a decision and its context. |
| AOT | Ahead-Of-Time compilation. |
| App Bundle | The .krate distributable package. |
| Capability | An unforgeable token granting the right to perform an operation on a resource. |
| Component Model | WebAssembly specification adding typed interfaces and composition to WASM modules. |
| DID | Decentralized Identifier: a W3C standard for self-sovereign identity. |
| Host Adapter | The per-OS module inside the Krate runtime that maps UAPI calls to native OS calls. |
| JIT | Just-In-Time compilation. |
| Manifest | manifest.toml: describes a Krate app's metadata and required capabilities. |
| Krate | The universal application platform described here. |
| UAPI | Universal API: the standard library exposed to every Krate app. |
| UCap | Universal Capabilities: the permission model. |
| UIR | Universal Intermediate Representation: the bytecode every app compiles to (= WASM + Component Model). |
| WASI | WebAssembly System Interface: a standard set of host interfaces for WASM. |
| WASM | WebAssembly. |
| WIT | WebAssembly Interface Types: the IDL for Component Model interfaces. |
References
Specifications
- WebAssembly Core Specification
- WebAssembly Component Model
- WASI Preview 2
- W3C DID Core
- WebGPU Specification
Runtimes
Languages / Toolchains
UI frameworks (prior art)
- Xilem: Rust reactive UI
- Slint: cross-platform, declarative
- Flutter architecture
- React Native Fabric
Capability systems (prior art)
- seL4: capability-kernel
- Fuchsia Zircon handles
- OpenBSD pledge/unveil